/*
 * 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.core.protocol.stomp;

import javax.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledExecutorService;

import org.apache.activemq.artemis.api.core.ActiveMQBuffer;
import org.apache.activemq.artemis.api.core.ActiveMQBuffers;
import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.ActiveMQQueueExistsException;
import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.api.core.client.ActiveMQClient;
import org.apache.activemq.artemis.core.protocol.stomp.v10.StompFrameHandlerV10;
import org.apache.activemq.artemis.core.protocol.stomp.v12.StompFrameHandlerV12;
import org.apache.activemq.artemis.core.remoting.CloseListener;
import org.apache.activemq.artemis.core.remoting.FailureListener;
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants;
import org.apache.activemq.artemis.core.server.ActiveMQServerLogger;
import org.apache.activemq.artemis.core.server.RoutingType;
import org.apache.activemq.artemis.core.server.ServerMessage;
import org.apache.activemq.artemis.core.server.ServerSession;
import org.apache.activemq.artemis.core.server.impl.ServerMessageImpl;
import org.apache.activemq.artemis.core.settings.impl.AddressSettings;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.remoting.Acceptor;
import org.apache.activemq.artemis.spi.core.remoting.Connection;
import org.apache.activemq.artemis.spi.core.remoting.ReadyListener;
import org.apache.activemq.artemis.utils.ConfigurationHelper;
import org.apache.activemq.artemis.utils.ExecutorFactory;
import org.apache.activemq.artemis.utils.VersionLoader;

import static org.apache.activemq.artemis.core.protocol.stomp.ActiveMQStompProtocolMessageBundle.BUNDLE;

public final class StompConnection implements RemotingConnection {

   protected static final String CONNECTION_ID_PROP = "__AMQ_CID";
   private static final String SERVER_NAME = "ActiveMQ-Artemis/" + VersionLoader.getVersion().getFullVersion() +
      " ActiveMQ Artemis Messaging Engine";

   private final StompProtocolManager manager;

   private final Connection transportConnection;

   private String login;

   private String passcode;

   private String clientID;

   //this means login is valid. (stomp connection ok)
   private boolean valid;

   private boolean destroyed = false;

   private final long creationTime;

   private final Acceptor acceptorUsed;

   private final List<FailureListener> failureListeners = new CopyOnWriteArrayList<>();

   private final List<CloseListener> closeListeners = new CopyOnWriteArrayList<>();

   private final Object failLock = new Object();

   private boolean dataReceived;

   private final boolean enableMessageID;

   private final int minLargeMessageSize;

   private StompVersions version;

   private VersionedStompFrameHandler frameHandler;

   //this means the version negotiation done.
   private boolean initialized;

   private FrameEventListener stompListener;

   private final Object sendLock = new Object();

   private final ScheduledExecutorService scheduledExecutorService;

   private final ExecutorFactory factory;

   @Override
   public boolean isSupportReconnect() {
      return false;
   }

   public StompFrame decode(ActiveMQBuffer buffer) throws ActiveMQStompException {
      StompFrame frame = null;
      try {
         frame = frameHandler.decode(buffer);
      } catch (ActiveMQStompException e) {
         switch (e.getCode()) {
            case ActiveMQStompException.INVALID_EOL_V10:
               if (version != null)
                  throw e;
               frameHandler = new StompFrameHandlerV12(this, scheduledExecutorService, factory);
               buffer.resetReaderIndex();
               frame = decode(buffer);
               break;
            case ActiveMQStompException.INVALID_COMMAND:
            case ActiveMQStompException.UNDEFINED_ESCAPE:
               frameHandler.onError(e);
               break;
            default:
               throw e;
         }
      }
      return frame;
   }

   @Override
   public boolean isWritable(ReadyListener callback) {
      return transportConnection.isWritable(callback);
   }

   public boolean hasBytes() {
      return frameHandler.hasBytes();
   }

   StompConnection(final Acceptor acceptorUsed,
                   final Connection transportConnection,
                   final StompProtocolManager manager,
                   final ScheduledExecutorService scheduledExecutorService,
                   final ExecutorFactory factory) {
      this.scheduledExecutorService = scheduledExecutorService;

      this.factory = factory;

      this.transportConnection = transportConnection;

      this.manager = manager;

      this.frameHandler = new StompFrameHandlerV10(this, scheduledExecutorService, factory);

      this.creationTime = System.currentTimeMillis();

      this.acceptorUsed = acceptorUsed;

      this.enableMessageID = ConfigurationHelper.getBooleanProperty(TransportConstants.STOMP_ENABLE_MESSAGE_ID, false, acceptorUsed.getConfiguration());
      this.minLargeMessageSize = ConfigurationHelper.getIntProperty(TransportConstants.STOMP_MIN_LARGE_MESSAGE_SIZE, ActiveMQClient.DEFAULT_MIN_LARGE_MESSAGE_SIZE, acceptorUsed.getConfiguration());
   }

   @Override
   public void addFailureListener(final FailureListener listener) {
      if (listener == null) {
         throw new IllegalStateException("FailureListener cannot be null");
      }

      failureListeners.add(listener);
   }

   @Override
   public boolean removeFailureListener(final FailureListener listener) {
      if (listener == null) {
         throw new IllegalStateException("FailureListener cannot be null");
      }

      return failureListeners.remove(listener);
   }

   @Override
   public void addCloseListener(final CloseListener listener) {
      if (listener == null) {
         throw new IllegalStateException("CloseListener cannot be null");
      }

      closeListeners.add(listener);
   }

   @Override
   public boolean removeCloseListener(final CloseListener listener) {
      if (listener == null) {
         throw new IllegalStateException("CloseListener cannot be null");
      }

      return closeListeners.remove(listener);
   }

   @Override
   public List<CloseListener> removeCloseListeners() {
      List<CloseListener> ret = new ArrayList<>(closeListeners);

      closeListeners.clear();

      return ret;
   }

   @Override
   public List<FailureListener> removeFailureListeners() {
      List<FailureListener> ret = new ArrayList<>(failureListeners);

      failureListeners.clear();

      return ret;
   }

   @Override
   public void setCloseListeners(List<CloseListener> listeners) {
      closeListeners.clear();

      closeListeners.addAll(listeners);
   }

   @Override
   public void setFailureListeners(final List<FailureListener> listeners) {
      failureListeners.clear();

      failureListeners.addAll(listeners);
   }

   protected synchronized void setDataReceived() {
      dataReceived = true;
   }

   @Override
   public synchronized boolean checkDataReceived() {
      boolean res = dataReceived;

      dataReceived = false;

      return res;
   }

   // TODO this should take a type - send or receive so it knows whether to check the address or the queue
   public void checkDestination(String destination) throws ActiveMQStompException {
      if (!manager.destinationExists(getSession().getCoreSession().removePrefix(SimpleString.toSimpleString(destination)).toString())) {
         throw BUNDLE.destinationNotExist(destination).setHandler(frameHandler);
      }
   }

   public boolean autoCreateDestinationIfPossible(String queue, RoutingType routingType) throws ActiveMQStompException {
      boolean result = false;
      ServerSession session = getSession().getCoreSession();

      try {
         if (manager.getServer().getAddressInfo(SimpleString.toSimpleString(queue)) == null) {
            AddressSettings addressSettings = manager.getServer().getAddressSettingsRepository().getMatch(queue);
            if (routingType != null && routingType == RoutingType.MULTICAST && addressSettings.isAutoCreateAddresses()) {
               session.createAddress(SimpleString.toSimpleString(queue), RoutingType.MULTICAST, true);
               result = true;
            } else {
               if (addressSettings.isAutoCreateAddresses()) {
                  session.createAddress(SimpleString.toSimpleString(queue), RoutingType.ANYCAST, true);
                  result = true;
               }
               if (addressSettings.isAutoCreateQueues()) {
                  session.createQueue(SimpleString.toSimpleString(queue), SimpleString.toSimpleString(queue), RoutingType.ANYCAST, null, false, true, true);
                  result = true;
               }
            }
         }
      } catch (ActiveMQQueueExistsException e) {
         // ignore
      } catch (Exception e) {
         throw new ActiveMQStompException(e.getMessage(), e).setHandler(frameHandler);
      }

      return result;
   }

   public void checkRoutingSemantics(String destination, RoutingType routingType) throws ActiveMQStompException {
      Set<RoutingType> actualDeliveryModesOfAddress = manager.getServer().getAddressInfo(getSession().getCoreSession().removePrefix(SimpleString.toSimpleString(destination))).getRoutingTypes();
      if (routingType != null && !actualDeliveryModesOfAddress.contains(routingType)) {
         throw BUNDLE.illegalSemantics(routingType.toString(), actualDeliveryModesOfAddress.toString());
      }
   }

   @Override
   public ActiveMQBuffer createTransportBuffer(int size) {
      return ActiveMQBuffers.dynamicBuffer(size);
   }

   @Override
   public void destroy() {
      synchronized (failLock) {
         if (destroyed) {
            return;
         }
      }

      destroyed = true;

      internalClose();

      synchronized (sendLock) {
         callClosingListeners();
      }
   }

   public Acceptor getAcceptorUsed() {
      return acceptorUsed;
   }

   private void internalClose() {
      transportConnection.close();

      manager.cleanup(this);
   }

   @Override
   public void fail(final ActiveMQException me) {
      synchronized (failLock) {
         if (destroyed) {
            return;
         }

         StompFrame frame = frameHandler.createStompFrame(Stomp.Responses.ERROR);
         frame.addHeader(Stomp.Headers.Error.MESSAGE, me.getMessage());
         sendFrame(frame);

         destroyed = true;
      }

      ActiveMQServerLogger.LOGGER.connectionFailureDetected(me.getMessage(), me.getType());
      // Then call the listeners
      callFailureListeners(me);

      callClosingListeners();

      internalClose();
   }

   @Override
   public void fail(final ActiveMQException me, String scaleDownTargetNodeID) {
      fail(me);
   }

   @Override
   public void flush() {
   }

   @Override
   public List<FailureListener> getFailureListeners() {
      // we do not return the listeners otherwise the remoting service
      // would NOT destroy the connection.
      return Collections.emptyList();
   }

   @Override
   public Object getID() {
      return transportConnection.getID();
   }

   @Override
   public String getRemoteAddress() {
      return transportConnection.getRemoteAddress();
   }

   @Override
   public long getCreationTime() {
      return creationTime;
   }

   @Override
   public Connection getTransportConnection() {
      return transportConnection;
   }

   @Override
   public boolean isClient() {
      return false;
   }

   @Override
   public boolean isDestroyed() {
      return destroyed;
   }

   @Override
   public void bufferReceived(Object connectionID, ActiveMQBuffer buffer) {
      manager.handleBuffer(this, buffer);
   }

   public String getLogin() {
      return login;
   }

   public String getPasscode() {
      return passcode;
   }

   public void setClientID(String clientID) {
      this.clientID = clientID;
   }

   public String getClientID() {
      return clientID;
   }

   public boolean isValid() {
      return valid;
   }

   public void setValid(boolean valid) {
      this.valid = valid;
   }

   private void callFailureListeners(final ActiveMQException me) {
      final List<FailureListener> listenersClone = new ArrayList<>(failureListeners);

      for (final FailureListener listener : listenersClone) {
         try {
            listener.connectionFailed(me, false);
         } catch (final Throwable t) {
            // Failure of one listener to execute shouldn't prevent others
            // from
            // executing
            ActiveMQServerLogger.LOGGER.errorCallingFailureListener(t);
         }
      }
   }

   private void callClosingListeners() {
      final List<CloseListener> listenersClone = new ArrayList<>(closeListeners);

      for (final CloseListener listener : listenersClone) {
         try {
            listener.connectionClosed();
         } catch (final Throwable t) {
            // Failure of one listener to execute shouldn't prevent others
            // from
            // executing
            ActiveMQServerLogger.LOGGER.errorCallingFailureListener(t);
         }
      }
   }

   /*
    * accept-version value takes form of "v1,v2,v3..."
    * we need to return the highest supported version
    */
   public void negotiateVersion(StompFrame frame) throws ActiveMQStompException {
      String acceptVersion = frame.getHeader(Stomp.Headers.ACCEPT_VERSION);

      if (acceptVersion == null) {
         this.version = StompVersions.V1_0;
      } else {
         StringTokenizer tokenizer = new StringTokenizer(acceptVersion, ",");
         Set<String> requestVersions = new HashSet<>(tokenizer.countTokens());
         while (tokenizer.hasMoreTokens()) {
            requestVersions.add(tokenizer.nextToken());
         }

         if (requestVersions.contains(StompVersions.V1_2.toString())) {
            this.version = StompVersions.V1_2;
         } else if (requestVersions.contains(StompVersions.V1_1.toString())) {
            this.version = StompVersions.V1_1;
         } else if (requestVersions.contains(StompVersions.V1_0.toString())) {
            this.version = StompVersions.V1_0;
         } else {
            //not a supported version!
            ActiveMQStompException error = BUNDLE.versionNotSupported(acceptVersion).setHandler(frameHandler);
            error.addHeader(Stomp.Headers.Error.VERSION, manager.getSupportedVersionsAsErrorVersion());
            error.addHeader(Stomp.Headers.CONTENT_TYPE, "text/plain");
            error.setBody("Supported protocol versions are " + manager.getSupportedVersionsAsString());
            error.setDisconnect(true);
            throw error;
         }
      }

      if (this.version != (StompVersions.V1_0)) {
         VersionedStompFrameHandler newHandler = VersionedStompFrameHandler.getHandler(this, this.version, scheduledExecutorService, factory);
         newHandler.initDecoder(this.frameHandler);
         this.frameHandler = newHandler;
      }
      this.initialized = true;
   }

   //reject if the host doesn't match
   public void setHost(String host) throws ActiveMQStompException {
      if (host == null) {
         ActiveMQStompException error = BUNDLE.nullHostHeader().setHandler(frameHandler);
         error.setBody(BUNDLE.hostCannotBeNull());
         throw error;
      }

      String localHost = manager.getVirtualHostName();
      if (!host.equals(localHost)) {
         ActiveMQStompException error = BUNDLE.hostNotMatch().setHandler(frameHandler);
         error.setBody(BUNDLE.hostNotMatchDetails(host));
         throw error;
      }
   }

   public void handleFrame(StompFrame request) {
      StompFrame reply = null;

      if (stompListener != null) {
         stompListener.requestAccepted(request);
      }

      String cmd = request.getCommand();
      try {
         if (isDestroyed()) {
            throw BUNDLE.connectionDestroyed().setHandler(frameHandler);
         }
         if (!initialized) {
            if (!(Stomp.Commands.CONNECT.equals(cmd) || Stomp.Commands.STOMP.equals(cmd))) {
               throw BUNDLE.connectionNotEstablished().setHandler(frameHandler);
            }
            //decide version
            negotiateVersion(request);
         }

         reply = frameHandler.handleFrame(request);
      } catch (ActiveMQStompException e) {
         reply = e.getFrame();
      }

      if (reply != null) {
         sendFrame(reply);
      }

      if (Stomp.Commands.DISCONNECT.equals(cmd)) {
         this.disconnect(false);
      }
   }

   public void sendFrame(StompFrame frame) {
      manager.sendReply(this, frame);
   }

   public boolean validateUser(final String login, final String pass, final X509Certificate[] certificates) {
      this.valid = manager.validateUser(login, pass, certificates);
      if (valid) {
         this.login = login;
         this.passcode = pass;
      }
      return valid;
   }

   public ServerMessageImpl createServerMessage() {
      return manager.createServerMessage();
   }

   public StompSession getSession() throws ActiveMQStompException {
      return getSession(null);
   }

   public StompSession getSession(String txID) throws ActiveMQStompException {
      StompSession session = null;
      try {
         if (txID == null) {
            session = manager.getSession(this);
         } else {
            session = manager.getTransactedSession(this, txID);
         }
      } catch (Exception e) {
         throw BUNDLE.errorGetSession(e).setHandler(frameHandler);
      }

      return session;
   }

   protected void validate() throws ActiveMQStompException {
      if (!this.valid) {
         throw BUNDLE.invalidConnection().setHandler(frameHandler);
      }
   }

   protected void sendServerMessage(ServerMessageImpl message, String txID) throws ActiveMQStompException {
      StompSession stompSession = getSession(txID);

      if (stompSession.isNoLocal()) {
         message.putStringProperty(CONNECTION_ID_PROP, getID().toString());
      }
      if (isEnableMessageID()) {
         message.putStringProperty("amqMessageId", "STOMP" + message.getMessageID());
      }
      try {
         if (minLargeMessageSize == -1 || (message.getBodyBuffer().writerIndex() < minLargeMessageSize)) {
            stompSession.sendInternal(message, false);
         } else {
            stompSession.sendInternalLarge(message, false);
         }
      } catch (Exception e) {
         throw BUNDLE.errorSendMessage(message, e).setHandler(frameHandler);
      }
   }

   @Override
   public void disconnect(final boolean criticalError) {
      disconnect(null, criticalError);
   }

   @Override
   public void disconnect(String scaleDownNodeID, final boolean criticalError) {
      destroy();
   }

   protected void beginTransaction(String txID) throws ActiveMQStompException {
      try {
         manager.beginTransaction(this, txID);
      } catch (ActiveMQStompException e) {
         throw e;
      } catch (Exception e) {
         throw BUNDLE.errorBeginTx(txID, e).setHandler(frameHandler);
      }
   }

   public void commitTransaction(String txID) throws ActiveMQStompException {
      try {
         manager.commitTransaction(this, txID);
      } catch (Exception e) {
         throw BUNDLE.errorCommitTx(txID, e).setHandler(frameHandler);
      }
   }

   public void abortTransaction(String txID) throws ActiveMQStompException {
      try {
         manager.abortTransaction(this, txID);
      } catch (ActiveMQStompException e) {
         throw e;
      } catch (Exception e) {
         throw BUNDLE.errorAbortTx(txID, e).setHandler(frameHandler);
      }
   }

   void subscribe(String destination,
                  String selector,
                  String ack,
                  String id,
                  String durableSubscriptionName,
                  boolean noLocal,
                  RoutingType subscriptionType) throws ActiveMQStompException {
      autoCreateDestinationIfPossible(destination, subscriptionType);
      checkDestination(destination);
      checkRoutingSemantics(destination, subscriptionType);
      if (noLocal) {
         String noLocalFilter = CONNECTION_ID_PROP + " <> '" + getID().toString() + "'";
         if (selector == null) {
            selector = noLocalFilter;
         } else {
            selector += " AND " + noLocalFilter;
         }
      }

      if (ack == null) {
         ack = Stomp.Headers.Subscribe.AckModeValues.AUTO;
      }

      String subscriptionID = null;
      if (id != null) {
         subscriptionID = id;
      } else {
         if (destination == null) {
            throw BUNDLE.noDestination().setHandler(frameHandler);
         }
         subscriptionID = "subscription/" + destination;
      }

      try {
         manager.subscribe(this, subscriptionID, durableSubscriptionName, destination, selector, ack, noLocal);
      } catch (ActiveMQStompException e) {
         throw e;
      } catch (Exception e) {
         throw BUNDLE.errorCreatingSubscription(subscriptionID, e).setHandler(frameHandler);
      }
   }

   public void unsubscribe(String subscriptionID, String durableSubscriptionName) throws ActiveMQStompException {
      try {
         manager.unsubscribe(this, subscriptionID, durableSubscriptionName);
      } catch (ActiveMQStompException e) {
         throw e;
      } catch (Exception e) {
         throw BUNDLE.errorUnsubscribing(subscriptionID, e).setHandler(frameHandler);
      }
   }

   public void acknowledge(String messageID, String subscriptionID) throws ActiveMQStompException {
      try {
         manager.acknowledge(this, messageID, subscriptionID);
      } catch (ActiveMQStompException e) {
         throw e;
      } catch (Exception e) {
         throw BUNDLE.errorAck(messageID, e).setHandler(frameHandler);
      }
   }

   public String getVersion() {
      return String.valueOf(version);
   }

   public String getActiveMQServerName() {
      return SERVER_NAME;
   }

   public StompFrame createStompMessage(ServerMessage serverMessage,
                                        StompSubscription subscription,
                                        int deliveryCount) throws Exception {
      return frameHandler.createMessageFrame(serverMessage, subscription, deliveryCount);
   }

   public void addStompEventListener(FrameEventListener listener) {
      this.stompListener = listener;
   }

   //send a ping stomp frame
   public void ping(StompFrame pingFrame) {
      manager.sendReply(this, pingFrame);
   }

   public void physicalSend(StompFrame frame) throws Exception {
      ActiveMQBuffer buffer = frame.toActiveMQBuffer();
      synchronized (sendLock) {
         getTransportConnection().write(buffer, false, false);
      }

      if (stompListener != null) {
         stompListener.replySent(frame);
      }

   }

   public VersionedStompFrameHandler getFrameHandler() {
      return this.frameHandler;
   }

   public boolean isEnableMessageID() {
      return enableMessageID;
   }

   public int getMinLargeMessageSize() {
      return minLargeMessageSize;
   }

   public StompProtocolManager getManager() {
      return manager;
   }

   @Override
   public void killMessage(SimpleString nodeID) {
      //unsupported
   }

   @Override
   public boolean isSupportsFlowControl() {
      return false;
   }
}
