/*
 * 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.artemis.jms.client;

import javax.jms.BytesMessage;
import javax.jms.CompletionListener;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.IllegalStateException;
import javax.jms.InvalidDestinationException;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageProducer;
import javax.jms.ObjectMessage;
import javax.jms.Queue;
import javax.jms.QueueSender;
import javax.jms.StreamMessage;
import javax.jms.TextMessage;
import javax.jms.Topic;
import javax.jms.TopicPublisher;

import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQInterruptedException;
import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException;
import org.apache.activemq.artemis.api.core.QueueAttributes;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.client.ClientMessage;
import org.apache.activemq.artemis.api.core.client.ClientProducer;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.SendAcknowledgementHandler;
import org.apache.activemq.artemis.api.core.RoutingType;
import org.apache.activemq.artemis.utils.UUID;
import org.apache.activemq.artemis.utils.UUIDGenerator;

/**
 * ActiveMQ Artemis implementation of a JMS MessageProducer.
 */
public class ActiveMQMessageProducer implements MessageProducer, QueueSender, TopicPublisher {

   private final ConnectionFactoryOptions options;

   private final ActiveMQConnection connection;

   private final SimpleString connID;

   private final ClientProducer clientProducer;
   private final ClientSession clientSession;

   private boolean disableMessageID = false;

   private boolean disableMessageTimestamp = false;

   private int defaultPriority = Message.DEFAULT_PRIORITY;
   private long defaultTimeToLive = Message.DEFAULT_TIME_TO_LIVE;
   private int defaultDeliveryMode = Message.DEFAULT_DELIVERY_MODE;
   private long defaultDeliveryDelay = Message.DEFAULT_DELIVERY_DELAY;

   private final ActiveMQDestination defaultDestination;
   // Constructors --------------------------------------------------

   protected ActiveMQMessageProducer(final ActiveMQConnection connection,
                                     final ClientProducer producer,
                                     final ActiveMQDestination defaultDestination,
                                     final ClientSession clientSession,
                                     final ConnectionFactoryOptions options) throws JMSException {
      this.options = options;
      this.connection = connection;

      connID = connection.getClientID() != null ? new SimpleString(connection.getClientID()) : connection.getUID();

      this.clientProducer = producer;

      this.defaultDestination = defaultDestination;

      this.clientSession = clientSession;
   }

   // MessageProducer implementation --------------------------------

   @Override
   public void setDisableMessageID(final boolean value) throws JMSException {
      checkClosed();

      disableMessageID = value;
   }

   @Override
   public boolean getDisableMessageID() throws JMSException {
      checkClosed();

      return disableMessageID;
   }

   @Override
   public void setDisableMessageTimestamp(final boolean value) throws JMSException {
      checkClosed();

      disableMessageTimestamp = value;
   }

   @Override
   public boolean getDisableMessageTimestamp() throws JMSException {
      checkClosed();

      return disableMessageTimestamp;
   }

   @Override
   public void setDeliveryMode(final int deliveryMode) throws JMSException {
      checkClosed();
      if (deliveryMode != DeliveryMode.NON_PERSISTENT && deliveryMode != DeliveryMode.PERSISTENT) {
         throw ActiveMQJMSClientBundle.BUNDLE.illegalDeliveryMode(deliveryMode);
      }

      defaultDeliveryMode = deliveryMode;
   }

   @Override
   public int getDeliveryMode() throws JMSException {
      checkClosed();

      return defaultDeliveryMode;
   }

   @Override
   public void setPriority(final int defaultPriority) throws JMSException {
      checkClosed();

      if (defaultPriority < 0 || defaultPriority > 9) {
         throw new JMSException("Illegal priority value: " + defaultPriority);
      }

      this.defaultPriority = defaultPriority;
   }

   @Override
   public int getPriority() throws JMSException {
      checkClosed();

      return defaultPriority;
   }

   @Override
   public void setTimeToLive(final long timeToLive) throws JMSException {
      checkClosed();

      defaultTimeToLive = timeToLive;
   }

   @Override
   public long getTimeToLive() throws JMSException {
      checkClosed();

      return defaultTimeToLive;
   }

   @Override
   public Destination getDestination() throws JMSException {
      checkClosed();

      return defaultDestination;
   }

   @Override
   public void close() throws JMSException {
      connection.getThreadAwareContext().assertNotCompletionListenerThread();
      try {
         clientProducer.close();
      } catch (ActiveMQException e) {
         throw JMSExceptionHelper.convertFromActiveMQException(e);
      }
   }

   @Override
   public void send(final Message message) throws JMSException {
      checkDefaultDestination();
      doSendx(defaultDestination, message, defaultDeliveryMode, defaultPriority, defaultTimeToLive, null);
   }

   @Override
   public void send(final Message message,
                    final int deliveryMode,
                    final int priority,
                    final long timeToLive) throws JMSException {
      checkDefaultDestination();
      doSendx(defaultDestination, message, deliveryMode, priority, timeToLive, null);
   }

   @Override
   public void send(final Destination destination, final Message message) throws JMSException {
      send(destination, message, defaultDeliveryMode, defaultPriority, defaultTimeToLive);
   }

   @Override
   public void send(final Destination destination,
                    final Message message,
                    final int deliveryMode,
                    final int priority,
                    final long timeToLive) throws JMSException {
      checkClosed();

      checkDestination(destination);

      doSendx((ActiveMQDestination) destination, message, deliveryMode, priority, timeToLive, null);
   }

   @Override
   public void setDeliveryDelay(long deliveryDelay) throws JMSException {
      this.defaultDeliveryDelay = deliveryDelay;
   }

   @Override
   public long getDeliveryDelay() throws JMSException {
      return defaultDeliveryDelay;
   }

   @Override
   public void send(Message message, CompletionListener completionListener) throws JMSException {
      send(message, defaultDeliveryMode, defaultPriority, defaultTimeToLive, completionListener);
   }

   @Override
   public void send(Message message,
                    int deliveryMode,
                    int priority,
                    long timeToLive,
                    CompletionListener completionListener) throws JMSException {
      checkCompletionListener(completionListener);
      checkDefaultDestination();
      doSendx(defaultDestination, message, deliveryMode, priority, timeToLive, completionListener);
   }

   @Override
   public void send(Destination destination,
                    Message message,
                    CompletionListener completionListener) throws JMSException {
      send(destination, message, defaultDeliveryMode, defaultPriority, defaultTimeToLive, completionListener);
   }

   @Override
   public void send(Destination destination,
                    Message message,
                    int deliveryMode,
                    int priority,
                    long timeToLive,
                    CompletionListener completionListener) throws JMSException {
      checkClosed();

      checkCompletionListener(completionListener);

      checkDestination(destination);

      doSendx((ActiveMQDestination) destination, message, deliveryMode, priority, timeToLive, completionListener);
   }

   // TopicPublisher Implementation ---------------------------------

   @Override
   public Topic getTopic() throws JMSException {
      return (Topic) getDestination();
   }

   @Override
   public void publish(final Message message) throws JMSException {
      send(message);
   }

   @Override
   public void publish(final Topic topic, final Message message) throws JMSException {
      send(topic, message);
   }

   @Override
   public void publish(final Message message,
                       final int deliveryMode,
                       final int priority,
                       final long timeToLive) throws JMSException {
      send(message, deliveryMode, priority, timeToLive);
   }

   @Override
   public void publish(final Topic topic,
                       final Message message,
                       final int deliveryMode,
                       final int priority,
                       final long timeToLive) throws JMSException {
      checkDestination(topic);
      doSendx((ActiveMQDestination) topic, message, deliveryMode, priority, timeToLive, null);
   }

   // QueueSender Implementation ------------------------------------

   @Override
   public void send(final Queue queue, final Message message) throws JMSException {
      send((Destination) queue, message);
   }

   @Override
   public void send(final Queue queue,
                    final Message message,
                    final int deliveryMode,
                    final int priority,
                    final long timeToLive) throws JMSException {
      checkDestination(queue);
      doSendx((ActiveMQDestination) queue, message, deliveryMode, priority, timeToLive, null);
   }

   @Override
   public Queue getQueue() throws JMSException {
      return (Queue) getDestination();
   }

   // Public --------------------------------------------------------

   @Override
   public String toString() {
      return "ActiveMQMessageProducer->" + clientProducer;
   }

   /**
    * Check if the default destination has been set
    */
   private void checkDefaultDestination() {
      if (defaultDestination == null) {
         throw new UnsupportedOperationException("Producer does not have a default destination");
      }
   }

   /**
    * Check if the destination is sent correctly
    */
   private void checkDestination(Destination destination) throws InvalidDestinationException {
      if (destination != null && !(destination instanceof ActiveMQDestination)) {
         throw new InvalidDestinationException("Foreign destination:" + destination);
      }
      if (destination != null && defaultDestination != null) {
         throw new UnsupportedOperationException("Cannot specify destination if producer has a default destination");
      }
      if (destination == null) {
         throw ActiveMQJMSClientBundle.BUNDLE.nullTopic();
      }
   }

   private void checkCompletionListener(CompletionListener completionListener) {
      if (completionListener == null) {
         throw ActiveMQJMSClientBundle.BUNDLE.nullArgumentNotAllowed("CompletionListener");
      }
   }

   private void doSendx(ActiveMQDestination destination,
                        final Message jmsMessage,
                        final int deliveryMode,
                        final int priority,
                        final long timeToLive,
                        CompletionListener completionListener) throws JMSException {

      jmsMessage.setJMSDeliveryMode(deliveryMode);

      jmsMessage.setJMSPriority(priority);

      if (timeToLive == 0) {
         jmsMessage.setJMSExpiration(0);
      } else {
         jmsMessage.setJMSExpiration(System.currentTimeMillis() + timeToLive);
      }

      if (!disableMessageTimestamp) {
         jmsMessage.setJMSTimestamp(System.currentTimeMillis());
      } else {
         jmsMessage.setJMSTimestamp(0);
      }

      SimpleString address = null;

      if (destination == null) {
         if (defaultDestination == null) {
            throw new UnsupportedOperationException("Destination must be specified on send with an anonymous producer");
         }

         destination = defaultDestination;
      } else {
         if (defaultDestination != null) {
            if (!destination.equals(defaultDestination)) {
               throw new UnsupportedOperationException("Where a default destination is specified " + "for the sender and a destination is " + "specified in the arguments to the send, " + "these destinations must be equal");
            }
         }

         address = destination.getSimpleAddress();

         if (!connection.containsKnownDestination(address)) {
            try {
               ClientSession.AddressQuery query = clientSession.addressQuery(address);

               if (!query.isExists()) {
                  if (destination.isQueue() && query.isAutoCreateQueues()) {
                     clientSession.createAddress(address, RoutingType.ANYCAST, true);
                     if (destination.isTemporary()) {
                        // TODO is it right to use the address for the queue name here?
                        clientSession.createTemporaryQueue(address, RoutingType.ANYCAST, address);
                     } else {
                        createQueue(destination, RoutingType.ANYCAST, address, null, true, true, query.getDefaultMaxConsumers(), query.isDefaultPurgeOnNoConsumers(), query.isDefaultExclusive(), query.isDefaultLastValueQueue());
                     }
                  } else if (!destination.isQueue() && query.isAutoCreateAddresses()) {
                     clientSession.createAddress(address, RoutingType.MULTICAST, true);
                  } else if ((destination.isQueue() && !query.isAutoCreateQueues()) || (!destination.isQueue() && !query.isAutoCreateAddresses())) {
                     throw new InvalidDestinationException("Destination " + address + " does not exist");
                  }
               } else {
                  if (destination.isQueue()) {
                     ClientSession.QueueQuery queueQuery = clientSession.queueQuery(address);
                     if (!queueQuery.isExists()) {
                        if (destination.isTemporary()) {
                           clientSession.createTemporaryQueue(address, RoutingType.ANYCAST, address);
                        } else {
                           createQueue(destination, RoutingType.ANYCAST, address, null, true, true, query.getDefaultMaxConsumers(), query.isDefaultPurgeOnNoConsumers(), query.isDefaultExclusive(), query.isDefaultLastValueQueue());
                        }
                     }
                  }

                  connection.addKnownDestination(address);
               }
            } catch (ActiveMQQueueExistsException e) {
               // The queue was created by another client/admin between the query check and send create queue packet
            } catch (ActiveMQException e) {
               throw JMSExceptionHelper.convertFromActiveMQException(e);
            }
         }
      }

      ActiveMQMessage activeMQJmsMessage;

      boolean foreign = false;

      // First convert from foreign message if appropriate
      if (!(jmsMessage instanceof ActiveMQMessage)) {
         // JMS 1.1 Sect. 3.11.4: A provider must be prepared to accept, from a client,
         // a message whose implementation is not one of its own.

         if (jmsMessage instanceof BytesMessage) {
            activeMQJmsMessage = new ActiveMQBytesMessage((BytesMessage) jmsMessage, clientSession);
         } else if (jmsMessage instanceof MapMessage) {
            activeMQJmsMessage = new ActiveMQMapMessage((MapMessage) jmsMessage, clientSession);
         } else if (jmsMessage instanceof ObjectMessage) {
            activeMQJmsMessage = new ActiveMQObjectMessage((ObjectMessage) jmsMessage, clientSession, options);
         } else if (jmsMessage instanceof StreamMessage) {
            activeMQJmsMessage = new ActiveMQStreamMessage((StreamMessage) jmsMessage, clientSession);
         } else if (jmsMessage instanceof TextMessage) {
            activeMQJmsMessage = new ActiveMQTextMessage((TextMessage) jmsMessage, clientSession);
         } else {
            activeMQJmsMessage = new ActiveMQMessage(jmsMessage, clientSession);
         }

         // Set the destination on the original message
         jmsMessage.setJMSDestination(destination);

         foreign = true;
      } else {
         activeMQJmsMessage = (ActiveMQMessage) jmsMessage;
      }

      if (!disableMessageID) {
         // Generate a JMS id

         UUID uid = UUIDGenerator.getInstance().generateUUID();

         activeMQJmsMessage.getCoreMessage().setUserID(uid);

         activeMQJmsMessage.resetMessageID(null);
      }

      if (foreign) {
         jmsMessage.setJMSMessageID(activeMQJmsMessage.getJMSMessageID());
      }

      activeMQJmsMessage.setJMSDestination(destination);

      try {
         activeMQJmsMessage.doBeforeSend();
      } catch (Exception e) {
         JMSException je = new JMSException(e.getMessage());

         je.initCause(e);

         throw je;
      }

      if (defaultDeliveryDelay > 0) {
         activeMQJmsMessage.setJMSDeliveryTime(System.currentTimeMillis() + defaultDeliveryDelay);
      }

      ClientMessage coreMessage = activeMQJmsMessage.getCoreMessage();
      coreMessage.putStringProperty(ActiveMQConnection.CONNECTION_ID_PROPERTY_NAME, connID);

      coreMessage.setRoutingType(destination.isQueue() ? RoutingType.ANYCAST : RoutingType.MULTICAST);

      try {
         /**
          * Using a completionListener requires wrapping using a {@link CompletionListenerWrapper},
          * so we avoid it if we can.
          */
         if (completionListener != null) {
            clientProducer.send(address, coreMessage, new CompletionListenerWrapper(completionListener, jmsMessage, this));
         } else {
            clientProducer.send(address, coreMessage);
         }
      } catch (ActiveMQInterruptedException e) {
         JMSException jmsException = new JMSException(e.getMessage());
         jmsException.initCause(e);
         throw jmsException;
      } catch (ActiveMQException e) {
         throw JMSExceptionHelper.convertFromActiveMQException(e);
      } catch (java.lang.IllegalStateException e) {
         JMSException je = new IllegalStateException(e.getMessage());
         je.setStackTrace(e.getStackTrace());
         je.initCause(e);
         throw je;
      }
   }

   private void checkClosed() throws JMSException {
      if (clientProducer.isClosed() || clientSession.isClosed()) {
         throw new IllegalStateException("Producer is closed");
      }
   }

   private void createQueue(ActiveMQDestination destination, RoutingType routingType, SimpleString queueName, SimpleString filter, boolean durable, boolean autoCreated, int maxConsumers, boolean purgeOnNoConsumers, Boolean exclusive, Boolean lastValue) throws ActiveMQException {
      QueueAttributes queueAttributes = destination.getQueueAttributes();
      if (queueAttributes == null) {
         clientSession.createQueue(destination.getSimpleAddress(), routingType, queueName, filter, durable, autoCreated, maxConsumers, purgeOnNoConsumers, exclusive, lastValue);
      } else {
         clientSession.createQueue(
            destination.getSimpleAddress(),
            routingType,
            queueName,
            filter,
            durable,
            autoCreated,
            queueAttributes.getMaxConsumers() == null ? maxConsumers : queueAttributes.getMaxConsumers(),
            queueAttributes.getPurgeOnNoConsumers() == null ? purgeOnNoConsumers : queueAttributes.getPurgeOnNoConsumers(),
            queueAttributes.getExclusive() == null ? exclusive : queueAttributes.getExclusive(),
            queueAttributes.getLastValue() == null ? lastValue : queueAttributes.getLastValue()
         );
      }
   }


   private static final class CompletionListenerWrapper implements SendAcknowledgementHandler {

      private final CompletionListener completionListener;
      private final Message jmsMessage;
      private final ActiveMQMessageProducer producer;

      /**
       * @param jmsMessage
       * @param producer
       */
      private CompletionListenerWrapper(CompletionListener listener,
                                        Message jmsMessage,
                                        ActiveMQMessageProducer producer) {
         this.completionListener = listener;
         this.jmsMessage = jmsMessage;
         this.producer = producer;
      }

      @Override
      public void sendAcknowledged(org.apache.activemq.artemis.api.core.Message clientMessage) {
         if (jmsMessage instanceof StreamMessage) {
            try {
               ((StreamMessage) jmsMessage).reset();
            } catch (JMSException e) {
               // HORNETQ-1209 XXX ignore?
            }
         }
         if (jmsMessage instanceof BytesMessage) {
            try {
               ((BytesMessage) jmsMessage).reset();
            } catch (JMSException e) {
               // HORNETQ-1209 XXX ignore?
            }
         }

         try {
            producer.connection.getThreadAwareContext().setCurrentThread(true);
            completionListener.onCompletion(jmsMessage);
         } finally {
            producer.connection.getThreadAwareContext().clearCurrentThread(true);
         }
      }

      @Override
      public String toString() {
         return CompletionListenerWrapper.class.getSimpleName() + "( completionListener=" + completionListener + ")";
      }
   }
}
