/*
 * 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.camel.component.sjms;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

import javax.jms.Connection;
import javax.jms.MessageProducer;
import javax.jms.Session;

import org.apache.camel.AsyncCallback;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.component.sjms.jms.ConnectionResource;
import org.apache.camel.component.sjms.tx.SessionTransactionSynchronization;
import org.apache.camel.support.DefaultAsyncProducer;
import org.apache.commons.pool.BasePoolableObjectFactory;
import org.apache.commons.pool.impl.GenericObjectPool;

/**
 * Base SjmsProducer class.
 */
public abstract class SjmsProducer extends DefaultAsyncProducer {

    /**
     * The {@link MessageProducerResources} pool for all {@link SjmsProducer}
     * classes.
     */
    protected class MessageProducerResourcesFactory extends BasePoolableObjectFactory<MessageProducerResources> {

        @Override
        public MessageProducerResources makeObject() throws Exception {
            return doCreateProducerModel(createSession());
        }

        @Override
        public void destroyObject(MessageProducerResources model) throws Exception {
            if (model.getMessageProducer() != null) {
                model.getMessageProducer().close();
            }

            if (model.getSession() != null) {
                try {
                    if (model.getSession().getTransacted()) {
                        try {
                            model.getSession().rollback();
                        } catch (Exception e) {
                            // Do nothing. Just make sure we are cleaned up
                        }
                    }
                    model.getSession().close();
                } catch (Exception e) {
                    // TODO why is the session closed already?
                }
            }
        }
    }

    private GenericObjectPool<MessageProducerResources> producers;
    private ExecutorService executor;
    private Future<?> asyncStart;

    public SjmsProducer(Endpoint endpoint) {
        super(endpoint);
    }

    @Override
    protected void doStart() throws Exception {
        super.doStart();

        this.executor = getEndpoint().getCamelContext().getExecutorServiceManager().newDefaultThreadPool(this, "SjmsProducer");
        if (getProducers() == null) {
            setProducers(new GenericObjectPool<>(new MessageProducerResourcesFactory()));
            getProducers().setMaxActive(getProducerCount());
            getProducers().setMaxIdle(getProducerCount());
            getProducers().setLifo(false);
            if (getEndpoint().isPrefillPool()) {
                if (getEndpoint().isAsyncStartListener()) {
                    asyncStart = getEndpoint().getComponent().getAsyncStartStopExecutorService().submit(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                fillProducersPool();
                            } catch (Throwable e) {
                                log.warn("Error filling producer pool for destination: " + getDestinationName() + ". This exception will be ignored.", e);
                            }
                        }

                        @Override
                        public String toString() {
                            return "AsyncStartListenerTask[" + getDestinationName() + "]";
                        }
                    });
                } else {
                    fillProducersPool();
                }
            }
        }
    }

    private void fillProducersPool() throws Exception {
        while (producers.getNumIdle() < producers.getMaxIdle()) {
            producers.addObject();
        }
    }

    @Override
    protected void doStop() throws Exception {
        super.doStop();
        if (asyncStart != null && !asyncStart.isDone()) {
            asyncStart.cancel(true);
        }
        if (getProducers() != null) {
            if (getEndpoint().isAsyncStopListener()) {
                getEndpoint().getComponent().getAsyncStartStopExecutorService().submit(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            getProducers().close();
                            setProducers(null);
                        } catch (Throwable e) {
                            log.warn("Error closing producers on destination: " + getDestinationName() + ". This exception will be ignored.", e);
                        }
                    }

                    @Override
                    public String toString() {
                        return "AsyncStopListenerTask[" + getDestinationName() + "]";
                    }
                });
            } else {
                getProducers().close();
                setProducers(null);
            }
        }
        if (this.executor != null) {
            getEndpoint().getCamelContext().getExecutorServiceManager().shutdownGraceful(this.executor);
        }
    }

    @Override
    public SjmsEndpoint getEndpoint() {
        return (SjmsEndpoint) super.getEndpoint();
    }

    protected MessageProducerResources doCreateProducerModel(Session session) throws Exception {
        MessageProducerResources answer;
        try {
            MessageProducer messageProducer = getEndpoint().getJmsObjectFactory().createMessageProducer(session, getEndpoint());

            answer = new MessageProducerResources(session, messageProducer, getCommitStrategy());

        } catch (Exception e) {
            log.error("Unable to create the MessageProducer", e);
            throw e;
        }
        return answer;
    }

    protected Session createSession() throws Exception {
        ConnectionResource connectionResource = getOrCreateConnectionResource();
        Connection conn = connectionResource.borrowConnection();
        try {
            return conn.createSession(isEndpointTransacted(), getAcknowledgeMode());
        } catch (Exception e) {
            log.error("Unable to create the Session", e);
            throw e;
        } finally {
            connectionResource.returnConnection(conn);
        }
    }

    protected interface ReleaseProducerCallback {
        void release(MessageProducerResources producer) throws Exception;
    }

    protected class CloseProducerCallback implements ReleaseProducerCallback {
        @Override
        public void release(MessageProducerResources producer) throws Exception {
            producer.getMessageProducer().close();
        }
    }

    protected class ReturnProducerCallback implements ReleaseProducerCallback {
        @Override
        public void release(MessageProducerResources producer) throws Exception {
            getProducers().returnObject(producer);
        }
    }

    public abstract void sendMessage(Exchange exchange, AsyncCallback callback, MessageProducerResources producer, ReleaseProducerCallback releaseProducerCallback) throws Exception;

    @Override
    public boolean process(final Exchange exchange, final AsyncCallback callback) {
        if (log.isDebugEnabled()) {
            log.debug("Processing Exchange.id:{}", exchange.getExchangeId());
        }

        try {
            final MessageProducerResources producer;
            final ReleaseProducerCallback releaseProducerCallback;
            if (isEndpointTransacted() && isSharedJMSSession()) {
                Session session = exchange.getIn().getHeader(SjmsConstants.JMS_SESSION, Session.class);
                if (session != null && session.getTransacted()) {
                    // Join existing transacted session - Synchronization must have been added
                    // by the session initiator
                    producer = doCreateProducerModel(session);
                    releaseProducerCallback = new CloseProducerCallback();
                } else {
                    // Propagate JMS session and register Synchronization as an initiator
                    producer = getProducers().borrowObject();
                    releaseProducerCallback = new ReturnProducerCallback();
                    exchange.getIn().setHeader(SjmsConstants.JMS_SESSION, producer.getSession());
                    exchange.getUnitOfWork().addSynchronization(new SessionTransactionSynchronization(producer.getSession(), producer.getCommitStrategy()));
                }
            } else {
                producer = getProducers().borrowObject();
                releaseProducerCallback = new ReturnProducerCallback();
                if (isEndpointTransacted()) {
                    exchange.getUnitOfWork().addSynchronization(new SessionTransactionSynchronization(producer.getSession(), producer.getCommitStrategy()));
                }
            }
            
            if (producer == null) {
                exchange.setException(new Exception("Unable to send message: connection not available"));
            } else {
                if (!isSynchronous()) {
                    if (log.isDebugEnabled()) {
                        log.debug("  Sending message asynchronously: {}", exchange.getIn().getBody());
                    }
                    getExecutor().execute(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                sendMessage(exchange, callback, producer, releaseProducerCallback);
                            } catch (Exception e) {
                                RuntimeCamelException.wrapRuntimeCamelException(e);
                            }
                        }
                    });
                } else {
                    if (log.isDebugEnabled()) {
                        log.debug("  Sending message synchronously: {}", exchange.getIn().getBody());
                    }
                    sendMessage(exchange, callback, producer, releaseProducerCallback);
                }
            }
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.debug("Processing Exchange.id:{}", exchange.getExchangeId() + " - FAILED");
            }
            if (log.isDebugEnabled()) {
                log.trace("Exception: {}", e.getLocalizedMessage(), e);
            }
            exchange.setException(e);
        }
        log.debug("Processing Exchange.id:{}", exchange.getExchangeId() + " - SUCCESS");

        return isSynchronous();
    }

    /**
     * @deprecated use {@link #getOrCreateConnectionResource()}
     */
    @Deprecated
    protected ConnectionResource getConnectionResource() {
        return getEndpoint().getConnectionResource();
    }

    protected ConnectionResource getOrCreateConnectionResource() {
        ConnectionResource answer = getEndpoint().getConnectionResource();
        if (answer == null) {
            answer = getEndpoint().createConnectionResource(this);
        }
        return answer;
    }

    /**
     * Gets the acknowledgment mode for this instance of DestinationProducer.
     *
     * @return int
     */
    public int getAcknowledgeMode() {
        return getEndpoint().getAcknowledgementMode().intValue();
    }

    /**
     * Gets the synchronous value for this instance of DestinationProducer.
     *
     * @return true if synchronous, otherwise false
     */
    public boolean isSynchronous() {
        return getEndpoint().isSynchronous();
    }

    /**
     * Gets the replyTo for this instance of DestinationProducer.
     *
     * @return String
     */
    public String getReplyTo() {
        return getEndpoint().getNamedReplyTo();
    }

    /**
     * Gets the destinationName for this instance of DestinationProducer.
     *
     * @return String
     */
    public String getDestinationName() {
        return getEndpoint().getDestinationName();
    }

    /**
     * Sets the producer pool for this instance of SjmsProducer.
     *
     * @param producers A MessageProducerPool
     */
    public void setProducers(GenericObjectPool<MessageProducerResources> producers) {
        this.producers = producers;
    }

    /**
     * Gets the MessageProducerPool value of producers for this instance of
     * SjmsProducer.
     *
     * @return the producers
     */
    public GenericObjectPool<MessageProducerResources> getProducers() {
        return producers;
    }

    /**
     * Test to verify if this endpoint is a JMS Topic or Queue.
     *
     * @return true if it is a Topic, otherwise it is a Queue
     */
    public boolean isTopic() {
        return getEndpoint().isTopic();
    }

    /**
     * Test to determine if this endpoint should use a JMS Transaction.
     *
     * @return true if transacted, otherwise false
     */
    public boolean isEndpointTransacted() {
        return getEndpoint().isTransacted();
    }

    /**
     * Test to determine if this endpoint should share a JMS Session with other SJMS endpoints.
     *
     * @return true if shared, otherwise false
     */
    public boolean isSharedJMSSession() {
        return getEndpoint().isSharedJMSSession();
    }

    /**
     * Returns the named reply to value for this producer
     *
     * @return true if it is a Topic, otherwise it is a Queue
     */
    public String getNamedReplyTo() {
        return getEndpoint().getNamedReplyTo();
    }

    /**
     * Gets the producerCount for this instance of SjmsProducer.
     *
     * @return int
     */
    public int getProducerCount() {
        return getEndpoint().getProducerCount();
    }

    /**
     * Gets consumerCount for this instance of SjmsProducer.
     *
     * @return int
     */
    public int getConsumerCount() {
        return getEndpoint().getConsumerCount();
    }

    /**
     * Gets the executor for this instance of SjmsProducer.
     *
     * @return ExecutorService
     */
    public ExecutorService getExecutor() {
        return executor;
    }

    /**
     * Gets the ttl for this instance of SjmsProducer.
     *
     * @return long
     */
    public long getTtl() {
        return getEndpoint().getTtl();
    }

    /**
     * Gets the boolean value of persistent for this instance of SjmsProducer.
     *
     * @return true if persistent, otherwise false
     */
    public boolean isPersistent() {
        return getEndpoint().isPersistent();
    }

    /**
     * Gets responseTimeOut for this instance of SjmsProducer.
     *
     * @return long
     */
    public long getResponseTimeOut() {
        return getEndpoint().getResponseTimeOut();
    }

    /**
     * Gets commitStrategy for this instance of SjmsProducer.
     *
     * @return TransactionCommitStrategy
     */
    protected TransactionCommitStrategy getCommitStrategy() {
        if (isEndpointTransacted()) {
            return getEndpoint().getTransactionCommitStrategy();
        }
        return null;
    }

}
