/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.activemq.transport.amqp.client;

import java.io.IOException;
import java.nio.BufferOverflowException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.jms.IllegalStateException;
import javax.jms.JMSException;
import javax.jms.TransactionRolledBackException;

import org.apache.activemq.transport.amqp.client.util.AsyncResult;
import org.apache.activemq.transport.amqp.client.util.IOExceptionSupport;
import org.apache.qpid.proton.amqp.messaging.AmqpValue;
import org.apache.qpid.proton.amqp.messaging.Rejected;
import org.apache.qpid.proton.amqp.messaging.Source;
import org.apache.qpid.proton.amqp.transaction.Coordinator;
import org.apache.qpid.proton.amqp.transaction.Declare;
import org.apache.qpid.proton.amqp.transaction.Declared;
import org.apache.qpid.proton.amqp.transaction.Discharge;
import org.apache.qpid.proton.amqp.transaction.TxnCapability;
import org.apache.qpid.proton.amqp.transport.DeliveryState;
import org.apache.qpid.proton.amqp.transport.ReceiverSettleMode;
import org.apache.qpid.proton.amqp.transport.SenderSettleMode;
import org.apache.qpid.proton.engine.Delivery;
import org.apache.qpid.proton.engine.Sender;
import org.apache.qpid.proton.message.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.invoke.MethodHandles;

/**
 * Represents the AMQP Transaction coordinator link used by the transaction context of a session to control the lifetime
 * of a given transaction.
 */
public class AmqpTransactionCoordinator extends AmqpAbstractResource<Sender> {

   private static final Logger logger = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());

   private final byte[] OUTBOUND_BUFFER = new byte[64];

   private final AmqpSession session;
   private final AmqpTransferTagGenerator tagGenerator = new AmqpTransferTagGenerator();

   private List<Delivery> pendingDeliveries = new LinkedList<>();
   private Map<AmqpTransactionId, AsyncResult> pendingRequests = new HashMap<>();

   public AmqpTransactionCoordinator(AmqpSession session) {
      this.session = session;
   }

   @Override
   public void processDeliveryUpdates(AmqpConnection connection, Delivery delivery) throws IOException {
      try {
         Iterator<Delivery> deliveries = pendingDeliveries.iterator();
         while (deliveries.hasNext()) {
            Delivery pendingDelivery = deliveries.next();
            if (!pendingDelivery.remotelySettled()) {
               continue;
            }

            DeliveryState state = pendingDelivery.getRemoteState();
            AmqpTransactionId txId = (AmqpTransactionId) pendingDelivery.getContext();
            AsyncResult pendingRequest = pendingRequests.get(txId);

            if (pendingRequest == null) {
               throw new IllegalStateException("Pending tx operation with no pending request");
            }

            if (state instanceof Declared declared) {
               logger.debug("New TX started: {}", txId.getTxId());
               txId.setRemoteTxId(declared.getTxnId());
               pendingRequest.onSuccess();
            } else if (state instanceof Rejected rejected) {
               logger.debug("Last TX request failed: {}", txId.getTxId());
               Exception cause = AmqpSupport.convertToException(rejected.getError());
               JMSException failureCause = null;
               if (txId.isCommit()) {
                  failureCause = new TransactionRolledBackException(cause.getMessage());
               } else {
                  failureCause = new JMSException(cause.getMessage());
               }

               pendingRequest.onFailure(failureCause);
            } else {
               logger.debug("Last TX request succeeded: {}", txId.getTxId());
               pendingRequest.onSuccess();
            }

            // Clear state data
            pendingDelivery.settle();
            pendingRequests.remove(txId);
            deliveries.remove();
         }

         super.processDeliveryUpdates(connection, delivery);
      } catch (Exception e) {
         throw IOExceptionSupport.create(e);
      }
   }

   public void declare(AmqpTransactionId txId, AsyncResult request) throws Exception {
      if (txId.getRemoteTxId() != null) {
         throw new IllegalStateException("Declare called while a TX is still Active.");
      }

      if (isClosed()) {
         request.onFailure(new JMSException("Cannot start new transaction: Coordinator remotely closed"));
         return;
      }

      Message message = Message.Factory.create();
      Declare declare = new Declare();
      message.setBody(new AmqpValue(declare));

      Delivery pendingDelivery = getEndpoint().delivery(tagGenerator.getNextTag());
      pendingDelivery.setContext(txId);

      // Store away for completion
      pendingDeliveries.add(pendingDelivery);
      pendingRequests.put(txId, request);

      sendTxCommand(message);
   }

   public void discharge(AmqpTransactionId txId, AsyncResult request, boolean commit) throws Exception {

      if (isClosed()) {
         Exception failureCause = null;

         if (commit) {
            failureCause = new TransactionRolledBackException("Transaction inbout: Coordinator remotely closed");
         } else {
            failureCause = new JMSException("Rollback cannot complete: Coordinator remotely closed");
         }

         request.onFailure(failureCause);
         return;
      }

      // Store the context of this action in the transaction ID for later completion.
      txId.setState(commit ? AmqpTransactionId.COMMIT_MARKER : AmqpTransactionId.ROLLBACK_MARKER);

      Message message = Message.Factory.create();
      Discharge discharge = new Discharge();
      discharge.setFail(!commit);
      discharge.setTxnId(txId.getRemoteTxId());
      message.setBody(new AmqpValue(discharge));

      Delivery pendingDelivery = getEndpoint().delivery(tagGenerator.getNextTag());
      pendingDelivery.setContext(txId);

      // Store away for completion
      pendingDeliveries.add(pendingDelivery);
      pendingRequests.put(txId, request);

      sendTxCommand(message);
   }

   @Override
   public void remotelyClosed(AmqpConnection connection) {

      Exception txnError = AmqpSupport.convertToException(getEndpoint().getRemoteCondition());

      // Alert any pending operation that the link failed to complete the pending
      // begin / commit / rollback operation.
      for (AsyncResult pendingRequest : pendingRequests.values()) {
         pendingRequest.onFailure(txnError);
      }

      // Purge linkages to pending operations.
      pendingDeliveries.clear();
      pendingRequests.clear();

      // Override the base class version because we do not want to propagate
      // an error up to the client if remote close happens as that is an
      // acceptable way for the remote to indicate the discharge could not
      // be applied.

      if (getEndpoint() != null) {
         getEndpoint().close();
         getEndpoint().free();
      }

      logger.debug("Transaction Coordinator link {} was remotely closed", getEndpoint());
   }

   //----- Internal implementation ------------------------------------------//

   private void sendTxCommand(Message message) throws IOException {
      int encodedSize = 0;
      byte[] buffer = OUTBOUND_BUFFER;
      while (true) {
         try {
            encodedSize = message.encode(buffer, 0, buffer.length);
            break;
         } catch (BufferOverflowException e) {
            buffer = new byte[buffer.length * 2];
         }
      }

      Sender sender = getEndpoint();
      sender.send(buffer, 0, encodedSize);
      sender.advance();
   }

   @Override
   protected void doOpen() {
      Coordinator coordinator = new Coordinator();
      coordinator.setCapabilities(TxnCapability.LOCAL_TXN);
      Source source = new Source();

      String coordinatorName = "qpid-jms:coordinator:" + session.getConnection().getConnectionId();

      Sender sender = session.getEndpoint().sender(coordinatorName);
      sender.setSource(source);
      sender.setTarget(coordinator);
      sender.setSenderSettleMode(SenderSettleMode.UNSETTLED);
      sender.setReceiverSettleMode(ReceiverSettleMode.FIRST);

      setEndpoint(sender);

      super.doOpen();
   }

   @Override
   protected void doOpenInspection() {
      // TODO
   }

   @Override
   protected void doClosedInspection() {
      // TODO
   }
}
