/*
 * 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.qpid.jms.provider.amqp;

import java.io.IOException;
import java.util.Map;

import javax.jms.JMSException;

import org.apache.qpid.jms.JmsDestination;
import org.apache.qpid.jms.message.JmsOutboundMessageDispatch;
import org.apache.qpid.jms.meta.JmsProducerId;
import org.apache.qpid.jms.meta.JmsProducerInfo;
import org.apache.qpid.jms.provider.AsyncResult;
import org.apache.qpid.jms.provider.WrappedAsyncResult;
import org.apache.qpid.jms.provider.amqp.builders.AmqpProducerBuilder;
import org.apache.qpid.jms.util.IdGenerator;
import org.apache.qpid.jms.util.LRUCache;
import org.apache.qpid.proton.engine.EndpointState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Handles the case of anonymous JMS MessageProducers.
 *
 * In order to simulate the anonymous producer we must create a sender for each message
 * send attempt and close it following a successful send.
 */
public class AmqpAnonymousFallbackProducer extends AmqpProducer {

    private static final Logger LOG = LoggerFactory.getLogger(AmqpAnonymousFallbackProducer.class);
    private static final IdGenerator producerIdGenerator = new IdGenerator();

    private final AnonymousProducerCache producerCache;
    private final String producerIdKey = producerIdGenerator.generateId();
    private long producerIdCount;

    /**
     * Creates the Anonymous Producer object.
     *
     * @param session
     *        the session that owns this producer
     * @param info
     *        the JmsProducerInfo for this producer.
     */
    public AmqpAnonymousFallbackProducer(AmqpSession session, JmsProducerInfo info) {
        super(session, info);

        if (connection.isAnonymousProducerCache()) {
            producerCache = new AnonymousProducerCache(10);
            producerCache.setMaxCacheSize(connection.getAnonymousProducerCacheSize());
        } else {
            producerCache = null;
        }
    }

    @Override
    public void send(JmsOutboundMessageDispatch envelope, AsyncResult request) throws IOException, JMSException {
        LOG.trace("Started send chain for anonymous producer: {}", getProducerId());

        // Force sends marked as asynchronous to be sent synchronous so that the temporary
        // producer instance can handle failures and perform necessary completion work on
        // the send.
        envelope.setSendAsync(false);

        AmqpProducer producer = null;
        if (connection.isAnonymousProducerCache()) {
            producer = producerCache.get(envelope.getDestination());
        }

        if (producer == null) {
            // Create a new ProducerInfo for the short lived producer that's created to perform the
            // send to the given AMQP target.
            JmsProducerInfo info = new JmsProducerInfo(getNextProducerId());
            info.setDestination(envelope.getDestination());
            info.setPresettle(this.getResourceInfo().isPresettle());

            // We open a Fixed Producer instance with the target destination.  Once it opens
            // it will trigger the open event which will in turn trigger the send event.
            // If caching is disabled the created producer will be closed immediately after
            // the entire send chain has finished and the delivery has been acknowledged.
            AmqpProducerBuilder builder = new AmqpProducerBuilder(session, info);
            builder.buildResource(new AnonymousSendRequest(request, builder, envelope));

            if (connection.isAnonymousProducerCache()) {
                // Cache it in hopes of not needing to create large numbers of producers.
                producerCache.put(envelope.getDestination(), builder.getResource());
            }

            getParent().getProvider().pumpToProtonTransport(request);
        } else {
            producer.send(envelope, request);
        }
    }

    @Override
    public void close(AsyncResult request) {
        // Trigger an immediate close, the internal producers that are currently in the cache
        // if the cache is enabled.
        if (connection.isAnonymousProducerCache()) {
            for (AmqpProducer producer : producerCache.values()) {
                producer.close(new CloseRequest(producer));
            }
        }

        request.onSuccess();
    }

    @Override
    public boolean isAnonymous() {
        return true;
    }

    @Override
    public EndpointState getLocalState() {
        return EndpointState.ACTIVE;
    }

    @Override
    public EndpointState getRemoteState() {
        return EndpointState.ACTIVE;
    }

    private JmsProducerId getNextProducerId() {
        return new JmsProducerId(producerIdKey, -1, producerIdCount++);
    }

    //----- AsyncResult objects used to complete the sends -------------------//

    private abstract class AnonymousRequest extends WrappedAsyncResult {

        protected final JmsOutboundMessageDispatch envelope;

        public AnonymousRequest(AsyncResult sendResult, JmsOutboundMessageDispatch envelope) {
            super(sendResult);
            this.envelope = envelope;
        }

        /**
         * In all cases of the chain of events that make up the send for an anonymous
         * producer a failure will trigger the original send request to fail.
         */
        @Override
        public void onFailure(Throwable result) {
            LOG.debug("Send failed during {} step in chain: {}", this.getClass().getName(), getProducerId());
            super.onFailure(result);
        }

        public abstract AmqpProducer getProducer();
    }

    private final class AnonymousSendRequest extends AnonymousRequest {

        private final AmqpProducerBuilder producerBuilder;

        public AnonymousSendRequest(AsyncResult sendResult, AmqpProducerBuilder producerBuilder, JmsOutboundMessageDispatch envelope) {
            super(sendResult, envelope);

            this.producerBuilder = producerBuilder;
        }

        @Override
        public void onSuccess() {
            LOG.trace("Open phase of anonymous send complete: {} ", getProducerId());
            AnonymousSendCompleteRequest send = new AnonymousSendCompleteRequest(this);
            try {
                getProducer().send(envelope, send);
            } catch (Exception e) {
                super.onFailure(e);
            }
        }

        @Override
        public AmqpProducer getProducer() {
            return producerBuilder.getResource();
        }
    }

    private final class AnonymousSendCompleteRequest extends AnonymousRequest {

        private final AmqpProducer producer;

        public AnonymousSendCompleteRequest(AnonymousSendRequest open) {
            super(open.getWrappedRequest(), open.envelope);

            this.producer = open.getProducer();
        }

        @Override
        public void onFailure(Throwable result) {
            LOG.trace("Send phase of anonymous send failed: {} ", getProducerId());
            if (!connection.isAnonymousProducerCache()) {
                AnonymousCloseRequest close = new AnonymousCloseRequest(this);
                producer.close(close);
            }
            super.onFailure(result);
        }

        @Override
        public void onSuccess() {
            LOG.trace("Send phase of anonymous send complete: {} ", getProducerId());
            if (!connection.isAnonymousProducerCache()) {
                AnonymousCloseRequest close = new AnonymousCloseRequest(this);
                producer.close(close);
            } else {
                super.onSuccess();
            }
        }

        @Override
        public AmqpProducer getProducer() {
            return producer;
        }
    }

    private final class AnonymousCloseRequest extends AnonymousRequest {

        private final AmqpProducer producer;

        public AnonymousCloseRequest(AnonymousSendCompleteRequest sendComplete) {
            super(sendComplete.getWrappedRequest(), sendComplete.envelope);

            this.producer = sendComplete.getProducer();
        }

        @Override
        public void onSuccess() {
            LOG.trace("Close phase of anonymous send complete: {} ", getProducerId());
            super.onSuccess();
        }

        @Override
        public AmqpProducer getProducer() {
            return producer;
        }
    }

    private final class CloseRequest implements AsyncResult {

        private final AmqpProducer producer;

        public CloseRequest(AmqpProducer producer) {
            this.producer = producer;
        }

        @Override
        public void onFailure(Throwable result) {
            AmqpAnonymousFallbackProducer.this.connection.getProvider().fireProviderException(result);
        }

        @Override
        public void onSuccess() {
            LOG.trace("Close of anonymous producer {} complete", producer);
        }

        @Override
        public boolean isComplete() {
            return producer.isClosed();
        }
    }

    private final class AnonymousProducerCache extends LRUCache<JmsDestination, AmqpProducer> {

        private static final long serialVersionUID = 1L;

        public AnonymousProducerCache(int cacheSize) {
            super(cacheSize);
        }

        @Override
        protected void onCacheEviction(Map.Entry<JmsDestination, AmqpProducer> cached) {
            LOG.trace("Producer: {} evicted from producer cache", cached.getValue());
            cached.getValue().close(new CloseRequest(cached.getValue()));
        }
    }
}
