/*
 * 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.server.cluster.impl;

import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.activemq.artemis.api.core.SimpleString;
import org.apache.activemq.artemis.core.filter.Filter;
import org.apache.activemq.artemis.core.filter.impl.FilterImpl;
import org.apache.activemq.artemis.core.message.impl.MessageImpl;
import org.apache.activemq.artemis.core.postoffice.BindingType;
import org.apache.activemq.artemis.core.server.Bindable;
import org.apache.activemq.artemis.core.server.Queue;
import org.apache.activemq.artemis.core.server.RoutingContext;
import org.apache.activemq.artemis.core.server.ServerMessage;
import org.apache.activemq.artemis.core.server.cluster.RemoteQueueBinding;
import org.jboss.logging.Logger;

public class RemoteQueueBindingImpl implements RemoteQueueBinding {

   private static final Logger logger = Logger.getLogger(RemoteQueueBindingImpl.class);

   private final SimpleString address;

   private final Queue storeAndForwardQueue;

   private final SimpleString uniqueName;

   private final SimpleString routingName;

   private final long remoteQueueID;

   private final Filter queueFilter;

   private final Set<Filter> filters = new HashSet<>();

   private final Map<SimpleString, Integer> filterCounts = new HashMap<>();

   private int consumerCount;

   private final SimpleString idsHeaderName;

   private final long id;

   private final int distance;

   private boolean connected = true;

   public RemoteQueueBindingImpl(final long id,
                                 final SimpleString address,
                                 final SimpleString uniqueName,
                                 final SimpleString routingName,
                                 final Long remoteQueueID,
                                 final SimpleString filterString,
                                 final Queue storeAndForwardQueue,
                                 final SimpleString bridgeName,
                                 final int distance) throws Exception {
      this.id = id;

      this.address = address;

      this.storeAndForwardQueue = storeAndForwardQueue;

      this.uniqueName = uniqueName;

      this.routingName = routingName;

      this.remoteQueueID = remoteQueueID;

      queueFilter = FilterImpl.createFilter(filterString);

      idsHeaderName = MessageImpl.HDR_ROUTE_TO_IDS.concat(bridgeName);

      this.distance = distance;
   }

   @Override
   public long getID() {
      return id;
   }

   @Override
   public SimpleString getAddress() {
      return address;
   }

   @Override
   public Bindable getBindable() {
      return storeAndForwardQueue;
   }

   @Override
   public Queue getQueue() {
      return storeAndForwardQueue;
   }

   @Override
   public SimpleString getRoutingName() {
      return routingName;
   }

   @Override
   public SimpleString getUniqueName() {
      return uniqueName;
   }

   @Override
   public SimpleString getClusterName() {
      return uniqueName;
   }

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

   @Override
   public BindingType getType() {
      return BindingType.REMOTE_QUEUE;
   }

   @Override
   public Filter getFilter() {
      return queueFilter;
   }

   @Override
   public int getDistance() {
      return distance;
   }

   @Override
   public synchronized boolean isHighAcceptPriority(final ServerMessage message) {
      if (consumerCount == 0) {
         return false;
      }

      if (filters.isEmpty()) {
         return true;
      } else {
         for (Filter filter : filters) {
            if (filter.match(message)) {
               return true;
            }
         }
      }

      return false;
   }

   @Override
   public void unproposed(SimpleString groupID) {
   }

   @Override
   public void route(final ServerMessage message, final RoutingContext context) {
      addRouteContextToMessage(message);

      List<Queue> durableQueuesOnContext = context.getDurableQueues(storeAndForwardQueue.getAddress());

      if (!durableQueuesOnContext.contains(storeAndForwardQueue)) {
         // There can be many remote bindings for the same node, we only want to add the message once to
         // the s & f queue for that node
         context.addQueue(storeAndForwardQueue.getAddress(), storeAndForwardQueue);
      }
   }

   @Override
   public void routeWithAck(ServerMessage message, RoutingContext context) {
      addRouteContextToMessage(message);

      List<Queue> durableQueuesOnContext = context.getDurableQueues(storeAndForwardQueue.getAddress());

      if (!durableQueuesOnContext.contains(storeAndForwardQueue)) {
         // There can be many remote bindings for the same node, we only want to add the message once to
         // the s & f queue for that node
         context.addQueueWithAck(storeAndForwardQueue.getAddress(), storeAndForwardQueue);
      }
   }

   @Override
   public synchronized void addConsumer(final SimpleString filterString) throws Exception {
      if (filterString != null) {
         // There can actually be many consumers on the same queue with the same filter, so we need to maintain a ref
         // count

         Integer i = filterCounts.get(filterString);

         if (i == null) {
            filterCounts.put(filterString, 1);

            filters.add(FilterImpl.createFilter(filterString));
         } else {
            filterCounts.put(filterString, i + 1);
         }
      }

      consumerCount++;
   }

   @Override
   public synchronized void removeConsumer(final SimpleString filterString) throws Exception {
      if (filterString != null) {
         Integer i = filterCounts.get(filterString);

         if (i != null) {
            int ii = i - 1;

            if (ii == 0) {
               filterCounts.remove(filterString);

               filters.remove(FilterImpl.createFilter(filterString));
            } else {
               filterCounts.put(filterString, ii);
            }
         }
      }

      consumerCount--;
   }

   @Override
   public void reset() {
      consumerCount = 0;
      filterCounts.clear();
      filters.clear();
   }

   @Override
   public synchronized int consumerCount() {
      return consumerCount;
   }

   @Override
   public String toString() {
      return "RemoteQueueBindingImpl(" +
         (connected ? "connected" : "disconnected") + ")[address=" + address +
         ", consumerCount=" +
         consumerCount +
         ", distance=" +
         distance +
         ", filters=" +
         filters +
         ", id=" +
         id +
         ", idsHeaderName=" +
         idsHeaderName +
         ", queueFilter=" +
         queueFilter +
         ", remoteQueueID=" +
         remoteQueueID +
         ", routingName=" +
         routingName +
         ", storeAndForwardQueue=" +
         storeAndForwardQueue +
         ", uniqueName=" +
         uniqueName +
         "]";
   }

   @Override
   public String toManagementString() {
      return "RemoteQueueBindingImpl [address=" + address +
         ", storeAndForwardQueue=" + storeAndForwardQueue.getName() +
         ", remoteQueueID=" +
         remoteQueueID + "]";
   }

   @Override
   public void disconnect() {
      connected = false;
   }

   @Override
   public boolean isConnected() {
      return connected;
   }

   @Override
   public void connect() {
      connected = true;
   }

   public Set<Filter> getFilters() {
      return filters;
   }

   @Override
   public void close() throws Exception {
      storeAndForwardQueue.close();
   }

   /**
    * This will add routing information to the message.
    * This will be later processed during the delivery between the nodes. Because of that this has to be persisted as a property on the message.
    *
    * @param message
    */
   private void addRouteContextToMessage(final ServerMessage message) {
      byte[] ids = message.getBytesProperty(idsHeaderName);

      if (ids == null) {
         ids = new byte[8];
      } else {
         byte[] newIds = new byte[ids.length + 8];

         System.arraycopy(ids, 0, newIds, 8, ids.length);

         ids = newIds;
      }

      ByteBuffer buff = ByteBuffer.wrap(ids);

      buff.putLong(remoteQueueID);

      message.putBytesProperty(idsHeaderName, ids);

      if (logger.isTraceEnabled()) {
         logger.trace("Adding remoteQueue ID = " + remoteQueueID + " into message=" + message + " store-forward-queue=" + storeAndForwardQueue);
      }
   }

   @Override
   public long getRemoteQueueID() {
      return remoteQueueID;
   }
}
