package org.skyscreamer.nevado.jms;


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.skyscreamer.nevado.jms.connector.SQSConnector;
import org.skyscreamer.nevado.jms.destination.*;

import javax.jms.*;
import javax.jms.IllegalStateException;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Nevado's implementation of JMS Connection.
 *
 * @author Carter Page <carter@skyscreamer.org>
 */
public class NevadoConnection implements Connection {
    public static final int DEFAULT_MAX_POLL_WAIT_MS = 5000;
    private final Log _log = LogFactory.getLog(getClass());

    private final AtomicBoolean _closed = new AtomicBoolean(false);
    private final AtomicBoolean _running = new AtomicBoolean(false);

    protected volatile boolean _inUse = false;
    private final SQSConnector _sqsConnector;
    private volatile String _clientID;
    private volatile String _connectionID = UUID.randomUUID().toString();
    private volatile Integer _jmsDeliveryMode;
    private volatile Long _jmsTTL;
    private volatile Integer _jmsPriority;
    private volatile ExceptionListener _exceptionListener;
    private final List<NevadoSession> _sessions = new CopyOnWriteArrayList<NevadoSession>();
    private final Set<NevadoDestination> _temporaryDestinations = new CopyOnWriteArraySet<NevadoDestination>();
    private String _temporaryQueueSuffix = "";
    private String _temporaryTopicSuffix = "";
    private long _maxPollWaitMs = DEFAULT_MAX_POLL_WAIT_MS;
    private String _durableSubcriptionPrefixOveride;

    public NevadoConnection(SQSConnector sqsConnector) throws JMSException {
        _sqsConnector = sqsConnector;
        _sqsConnector.test();
    }

    @Override
    public NevadoSession createSession(boolean transacted, int acknowledgeMode) throws JMSException {
        checkClosed();
        _inUse = true;
        NevadoSession nevadoSession = new NevadoSession(this, transacted, acknowledgeMode);
        initializeSession(nevadoSession);
        return nevadoSession;
    }

    protected void initializeSession(NevadoSession nevadoSession) {
        nevadoSession.setOverrideJMSDeliveryMode(_jmsDeliveryMode);
        nevadoSession.setOverrideJMSTTL(_jmsTTL);
        nevadoSession.setOverrideJMSPriority(_jmsPriority);
        synchronized (_running) {
            _sessions.add(nevadoSession);
            if (_running.get())
            {
                nevadoSession.start();
            }
        }
    }

    @Override
    public NevadoConnectionMetaData getMetaData() throws JMSException {
        return NevadoConnectionMetaData.getInstance();
    }

    @Override
    public ExceptionListener getExceptionListener() {
        return _exceptionListener;
    }

    @Override
    public void setExceptionListener(ExceptionListener exceptionListener) throws IllegalStateException {
        checkClosed();
        _exceptionListener = exceptionListener;
    }

    @Override
    public void start() throws JMSException
    {
        checkClosed();
        _inUse = true;
        synchronized (_running) {
            _running.set(true);
            for(NevadoSession session : _sessions)
            {
                session.start();
            }
        }
    }

    @Override
    public void stop() throws JMSException
    {
        checkClosed();
        synchronized (_running) {
            _running.set(false);
            for(NevadoSession session : _sessions)
            {
                session.stop();
            }
        }
    }

    @Override
    public void close() throws JMSException {
        synchronized (_closed) {
            if (!_closed.get()) {
                stop();
                List<JMSException> sessionExceptions = new ArrayList<JMSException>();
                for(NevadoSession session : _sessions)
                {
                    try {
                        session.close();
                    } catch (JMSException e) {
                        sessionExceptions.add(e);
                        _log.warn("Caught exception closing a session.  Will continue trying to clean up, then will " +
                                "throw it up the stack.  (First one if multiple.)", e);
                    }
                }
                for(NevadoDestination temporaryDestination : new ArrayList<NevadoDestination>(_temporaryDestinations)) {
                    try {
                        if (temporaryDestination instanceof NevadoTemporaryQueue)
                        {
                            deleteTemporaryQueue((NevadoTemporaryQueue)temporaryDestination);
                        }
                        else if (temporaryDestination instanceof NevadoTemporaryTopic)
                        {
                            deleteTemporaryTopic((NevadoTemporaryTopic) temporaryDestination);
                        }
                        else
                        {
                            throw new IllegalStateException("Unexpected temporary destination of type: "
                                    + temporaryDestination.getClass().getName());
                        }

                    } catch (JMSException e) {
                        // Log but continue
                        _log.error("Unable to delete temporary destination " + temporaryDestination, e);
                    }
                }
                _temporaryDestinations.clear();
                _closed.set(true);
                if (sessionExceptions.size() > 0) {
                    throw sessionExceptions.get(0);
                }
            }
        }
    }

    @Override
    public ConnectionConsumer createConnectionConsumer(Destination destination, String s, ServerSessionPool serverSessionPool, int i) throws JMSException {
        checkClosed();
        _inUse = true;
        return null;  // TODO
    }

    @Override
    public ConnectionConsumer createDurableConnectionConsumer(Topic topic, String s, String s1, ServerSessionPool serverSessionPool, int i) throws JMSException {
        checkClosed();
        _inUse = true;
        return null;  // TODO
    }

    protected NevadoTemporaryTopic createTemporaryTopic() throws JMSException {
        checkClosed();
        String tempTopicName = "" + NevadoProviderQueuePrefix.TEMPORARY_DESTINATION_PREFIX
                + new BigInteger(64, new Random()).toString(Character.MAX_RADIX) + _temporaryTopicSuffix;
        NevadoTopic topic = getSQSConnector().createTopic(tempTopicName);
        _log.info("Created temporary topic " + tempTopicName);
        NevadoTemporaryTopic temporaryTopic = new NevadoTemporaryTopic(this, topic);
        _temporaryDestinations.add(temporaryTopic);
        return temporaryTopic;
    }

    public void deleteTemporaryTopic(NevadoTemporaryTopic temporaryTopic) throws JMSException
    {
        checkClosed();
        deleteTopic(temporaryTopic);
        _temporaryDestinations.remove(temporaryTopic);
    }

    public void deleteTopic(NevadoTopic topic) throws JMSException {
        getSQSConnector().deleteTopic(topic);
        topic.setDeleted(true);
    }

    protected NevadoTemporaryQueue createTemporaryQueue() throws JMSException
    {
        checkClosed();
        String tempQueueName = "" + NevadoProviderQueuePrefix.TEMPORARY_DESTINATION_PREFIX
                + new BigInteger(64, new Random()).toString(Character.MAX_RADIX) + _temporaryQueueSuffix;
        NevadoQueue queue = getSQSConnector().createQueue(tempQueueName);
        _log.info("Created temporary queue " + tempQueueName);
        NevadoTemporaryQueue temporaryQueue = new NevadoTemporaryQueue(this, queue);
        _temporaryDestinations.add(temporaryQueue);
        return temporaryQueue;
    }

    public void deleteTemporaryQueue(NevadoTemporaryQueue temporaryQueue) throws JMSException
    {
        checkClosed();
        deleteQueue(temporaryQueue);
        _temporaryDestinations.remove(temporaryQueue);
    }

    protected void deleteQueue(NevadoQueue queue) throws JMSException {
        _sqsConnector.deleteQueue(queue);
        queue.setDeleted(true);
    }

    protected boolean ownsTemporaryDestination(Destination temporaryDestination)
    {
        return _temporaryDestinations.contains(temporaryDestination);
    }

    public Collection<NevadoTemporaryQueue> listAllTemporaryQueues() throws JMSException {
        Collection<NevadoQueue> queues = getSQSConnector()
                .listQueues(NevadoProviderQueuePrefix.TEMPORARY_DESTINATION_PREFIX + "");
        Collection<NevadoTemporaryQueue> temporaryQueues = new HashSet<NevadoTemporaryQueue>(queues.size());
        for(NevadoQueue queue : queues) {
            temporaryQueues.add(new NevadoTemporaryQueue(this, queue));
        }
        return temporaryQueues;
    }

    public Collection<NevadoTemporaryTopic> listAllTemporaryTopics() throws JMSException {
        Collection<NevadoTopic> topics = getSQSConnector().listTopics();
        Collection<NevadoTemporaryTopic> temporaryTopics = new HashSet<NevadoTemporaryTopic>(topics.size());
        for(NevadoTopic topic : topics) {
            if (topic.getTopicName().startsWith(NevadoProviderQueuePrefix.TEMPORARY_DESTINATION_PREFIX + ""))
                temporaryTopics.add(new NevadoTemporaryTopic(this, topic));
        }
        return temporaryTopics;
    }

    public Collection<NevadoTemporaryQueue> deleteUnusedTemporaryQueues(String suffix) throws JMSException {
        if (suffix == null)
            suffix = "";
        Collection<NevadoTemporaryQueue> allQueues = listAllTemporaryQueues();
        List<NevadoTemporaryQueue> deletedQueues = new ArrayList<NevadoTemporaryQueue>();
        for (NevadoTemporaryQueue queue : allQueues) {
            if (queue.getQueueName().endsWith(suffix) && !_temporaryDestinations.contains(queue)) {
                deleteTemporaryQueue(queue);
                deletedQueues.add(queue);
            }
        }
        return deletedQueues;
    }

    public Collection<NevadoTemporaryTopic> deleteUnusedTemporaryTopics(String suffix) throws JMSException {
        if (suffix == null)
            suffix = "";
        Collection<NevadoTemporaryTopic> allTopics = listAllTemporaryTopics();
        List<NevadoTemporaryTopic> deletedTopics = new ArrayList<NevadoTemporaryTopic>();
        for (NevadoTemporaryTopic topic : allTopics) {
            if (topic.getTopicName().endsWith(suffix) && !_temporaryDestinations.contains(topic)) {
                deleteTemporaryTopic(topic);
                deletedTopics.add(topic);
            }
        }
        return deletedTopics;
    }

    public String subscribe(NevadoTopic topic, NevadoQueue topicEndpoint) throws JMSException {
        return getSQSConnector().subscribe(topic, topicEndpoint);
    }

    public void unsubscribe(NevadoTopic topic) throws JMSException {
        getSQSConnector().unsubscribe(topic);
    }

    // Getters & Setters
    public SQSConnector getSQSConnector() {
        return _sqsConnector;
    }

    @Override
    public String getClientID() {
        return _clientID;
    }

    @Override
    public void setClientID(String clientID) throws JMSException {
        checkClosed();
        if (clientID == null || clientID.trim().length() == 0)
        {
            throw new InvalidClientIDException("Client ID is empty");
        }
        if (_clientID != null) {
            throw new IllegalStateException("Client ID has already been set");
        }
        if (_inUse) {
            throw new IllegalStateException("Client ID cannot be set after the connection is in use");
        }
        if (clientID != null && !clientID.matches("^[\\w\\-_]+$"))
        {
            throw new InvalidClientIDException("Client ID can only include alphanumeric characters, hyphens, or underscores");
        }
        _clientID = clientID;
        _connectionID = clientID;
    }

    public void setOverrideJMSDeliveryMode(Integer jmsDeliveryMode) throws IllegalStateException {
        checkClosed();
        _jmsDeliveryMode = jmsDeliveryMode;
    }

    public void setOverrideJMSPriority(Integer jmsPriority) throws IllegalStateException {
        checkClosed();
        _jmsPriority = jmsPriority;
    }

    public void setOverrideJMSTTL(Long jmsTTL) throws IllegalStateException {
        checkClosed();
        _jmsTTL = jmsTTL;
    }

    public void setTemporaryQueueSuffix(String temporaryQueueSuffix) {
        _temporaryQueueSuffix = temporaryQueueSuffix;
    }

    public void setTemporaryTopicSuffix(String temporaryTopicSuffix) {
        _temporaryTopicSuffix = temporaryTopicSuffix;
    }

    public void setMaxPollWaitMs(long maxPollWaitMs) {
        _maxPollWaitMs = maxPollWaitMs;
    }

    protected long getMaxPollWaitMs() {
        return _maxPollWaitMs;
    }

    public boolean isRunning() {
        return _running.get();
    }

    /**
     * Tell whether the connection is closed.  If the connection is in the process of closing, this
     * will return true until complete.
     *
     * @return true if closed, otherwise false
     */
    public boolean isClosed() {
        return _closed.get();
    }

    protected void checkClosed() throws IllegalStateException {
        if (_closed.get())
        {
            throw new IllegalStateException("Connection is closed");
        }
    }

    public String getConnectionID() {
        return _connectionID;
    }
    
    public void setDurableSubcriptionPrefixOveride(String durableSubcriptionPrefixOveride) {
        _durableSubcriptionPrefixOveride = durableSubcriptionPrefixOveride;
    }
    
    public String getDurableSubcriptionPrefixOveride() {
        return _durableSubcriptionPrefixOveride;
    }
}
