/*
 * 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.rest.queue;

import javax.ws.rs.DefaultValue;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;

import org.apache.activemq.artemis.api.core.ActiveMQException;
import org.apache.activemq.artemis.api.core.client.ClientConsumer;
import org.apache.activemq.artemis.api.core.client.ClientMessage;
import org.apache.activemq.artemis.api.core.client.ClientSession;
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory;
import org.apache.activemq.artemis.jms.client.ConnectionFactoryOptions;
import org.apache.activemq.artemis.rest.ActiveMQRestLogger;
import org.apache.activemq.artemis.rest.util.Constants;
import org.apache.activemq.artemis.rest.util.LinkStrategy;
import org.apache.activemq.artemis.jms.client.SelectorTranslator;

/**
 * Auto-acknowleged consumer
 */
public class QueueConsumer {

   protected ClientSessionFactory factory;
   protected ClientSession session;
   protected ClientConsumer consumer;
   protected String destination;
   protected boolean closed;
   protected String id;
   protected long lastPing = System.currentTimeMillis();
   protected DestinationServiceManager serviceManager;
   protected boolean autoAck = true;
   protected String selector;

   /**
    * token used to create consume-next links
    */
   protected long previousIndex = -1;
   protected ConsumedMessage lastConsumed;

   public long getConsumeIndex() {
      if (lastConsumed == null)
         return -1;
      return lastConsumed.getMessageID();
   }

   public DestinationServiceManager getServiceManager() {
      return serviceManager;
   }

   public void setServiceManager(DestinationServiceManager serviceManager) {
      this.serviceManager = serviceManager;
   }

   public long getLastPingTime() {
      return lastPing;
   }

   protected void ping(long offsetSecs) {
      lastPing = System.currentTimeMillis() + (offsetSecs * 1000);
   }

   public QueueConsumer(ClientSessionFactory factory,
                        String destination,
                        String id,
                        DestinationServiceManager serviceManager,
                        String selector) throws ActiveMQException {
      this.factory = factory;
      this.destination = destination;
      this.id = id;
      this.serviceManager = serviceManager;
      this.selector = selector;

      createSession();
   }

   public String getId() {
      return id;
   }

   public boolean isClosed() {
      return closed;
   }

   public synchronized void shutdown() {
      if (closed)
         return;
      closed = true;
      lastConsumed = null;
      previousIndex = -2;
      try {
         consumer.close();
         ActiveMQRestLogger.LOGGER.debug("Closed consumer: " + consumer);
      }
      catch (Exception e) {
      }

      try {
         session.close();
         ActiveMQRestLogger.LOGGER.debug("Closed session: " + session);
      }
      catch (Exception e) {
      }
      session = null;
      consumer = null;
   }

   @Path("consume-next{index}")
   @POST
   public synchronized Response poll(@HeaderParam(Constants.WAIT_HEADER) @DefaultValue("0") long wait,
                                     @PathParam("index") long index,
                                     @Context UriInfo info) {
      ActiveMQRestLogger.LOGGER.debug("Handling POST request for \"" + info.getRequestUri() + "\"");

      if (closed) {
         UriBuilder builder = info.getBaseUriBuilder();
         builder.path(info.getMatchedURIs().get(1)).path("consume-next");
         String uri = builder.build().toString();

         // redirect to another consume-next

         return Response.status(307).location(URI.create(uri)).build();
      }
      return checkIndexAndPoll(wait, info, info.getMatchedURIs().get(1), index);
   }

   protected Response checkIndexAndPoll(long wait, UriInfo info, String basePath, long index) {
      ping(wait);

      if (lastConsumed == null && index > 0) {
         return Response.status(412).entity("You are using an old consume-next link and are out of sync with the JMS session on the server").type("text/plain").build();
      }
      if (lastConsumed != null) {
         if (index == previousIndex) {
            String token = Long.toString(lastConsumed.getMessageID());
            return getMessageResponse(lastConsumed, info, basePath, token).build();
         }
         if (index != lastConsumed.getMessageID()) {
            return Response.status(412).entity("You are using an old consume-next link and are out of sync with the JMS session on the server").type("text/plain").build();
         }
      }

      try {
         return pollWithIndex(wait, info, basePath, index);
      }
      finally {
         ping(0); // ping again as we don't want wait time included in timeout.
      }
   }

   protected Response pollWithIndex(long wait, UriInfo info, String basePath, long index) {
      try {
         ClientMessage message = receive(wait);
         if (message == null) {
            Response.ResponseBuilder builder = Response.status(503).entity("Timed out waiting for message receive.").type("text/plain");
            setPollTimeoutLinks(info, basePath, builder, Long.toString(index));
            return builder.build();
         }
         previousIndex = index;
         lastConsumed = ConsumedMessage.createConsumedMessage(message, this.getJmsOptions());
         String token = Long.toString(lastConsumed.getMessageID());
         Response response = getMessageResponse(lastConsumed, info, basePath, token).build();
         if (autoAck)
            message.acknowledge();
         return response;
      }
      catch (Exception e) {
         Response errorResponse = Response.serverError().entity(e.getMessage())
                 .status(Response.Status.INTERNAL_SERVER_ERROR).build();
         return errorResponse;
      }
   }

   protected void createSession() throws ActiveMQException {
      session = factory.createSession(true, true, 0);
      ActiveMQRestLogger.LOGGER.debug("Created session: " + session);
      if (selector == null) {
         consumer = session.createConsumer(destination);
      }
      else {
         consumer = session.createConsumer(destination, SelectorTranslator.convertToActiveMQFilterString(selector));
      }
      ActiveMQRestLogger.LOGGER.debug("Created consumer: " + consumer);
      session.start();
   }

   protected ClientMessage receiveFromConsumer(long timeoutSecs) throws Exception {
      ClientMessage m = null;
      if (timeoutSecs <= 0) {
         m = consumer.receive(1);
      }
      else {
         m = consumer.receive(timeoutSecs * 1000);
      }

      ActiveMQRestLogger.LOGGER.debug("Returning message " + m + " from consumer: " + consumer);

      return m;
   }

   protected ClientMessage receive(long timeoutSecs) throws Exception {
      return receiveFromConsumer(timeoutSecs);
   }

   protected void setPollTimeoutLinks(UriInfo info, String basePath, Response.ResponseBuilder builder, String index) {
      setSessionLink(builder, info, basePath);
      setConsumeNextLink(serviceManager.getLinkStrategy(), builder, info, basePath, index);
   }

   protected Response.ResponseBuilder getMessageResponse(ConsumedMessage msg,
                                                         UriInfo info,
                                                         String basePath,
                                                         String index) {
      Response.ResponseBuilder responseBuilder = Response.ok();
      setMessageResponseLinks(info, basePath, responseBuilder, index);
      msg.build(responseBuilder);
      return responseBuilder;
   }

   protected void setMessageResponseLinks(UriInfo info,
                                          String basePath,
                                          Response.ResponseBuilder responseBuilder,
                                          String index) {
      setConsumeNextLink(serviceManager.getLinkStrategy(), responseBuilder, info, basePath, index);
      setSessionLink(responseBuilder, info, basePath);
   }

   public static void setConsumeNextLink(LinkStrategy linkStrategy,
                                         Response.ResponseBuilder response,
                                         UriInfo info,
                                         String basePath,
                                         String index) {
      if (index == null)
         throw new IllegalArgumentException("index cannot be null");
      UriBuilder builder = info.getBaseUriBuilder();
      builder.path(basePath).path("consume-next" + index);
      String uri = builder.build().toString();
      linkStrategy.setLinkHeader(response, "consume-next", "consume-next", uri, MediaType.APPLICATION_FORM_URLENCODED);
   }

   public void setSessionLink(Response.ResponseBuilder response, UriInfo info, String basePath) {
      UriBuilder builder = info.getBaseUriBuilder();
      builder.path(basePath);
      String uri = builder.build().toString();
      serviceManager.getLinkStrategy().setLinkHeader(response, "consumer", "consumer", uri, MediaType.APPLICATION_XML);
   }

   public ConnectionFactoryOptions getJmsOptions() {
      return serviceManager.getJmsOptions();
   }
}
