/*
 * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 * WSO2 Inc. 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.wso2.mb.integration.common.clients;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.mb.integration.common.clients.configurations.AndesJMSPublisherClientConfiguration;
import org.wso2.mb.integration.common.clients.configurations.JMSHeaderProperty;
import org.wso2.mb.integration.common.clients.configurations.JMSHeaderPropertyType;
import org.wso2.mb.integration.common.clients.exceptions.AndesClientException;
import org.wso2.mb.integration.common.clients.operations.utils.AndesClientConstants;
import org.wso2.mb.integration.common.clients.operations.utils.AndesClientUtils;
import org.wso2.mb.integration.common.clients.operations.utils.JMSMessageType;

import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.DeliveryMode;
import javax.jms.Destination;
import javax.jms.JMSException;
import javax.jms.MapMessage;
import javax.jms.Message;
import javax.jms.MessageProducer;
import javax.jms.Session;
import javax.jms.TextMessage;
import javax.naming.NamingException;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.List;

/**
 * The JMS message publisher used for creating a publisher and for publishing JMS messages.
 */
public class AndesJMSPublisher extends AndesJMSBase implements Runnable {
    /**
     * The logger used in logging information, warnings, errors and etc.
     */
    private static Log log = LogFactory.getLog(AndesJMSPublisher.class);

    /**
     * The configuration for the publisher
     */
    private AndesJMSPublisherClientConfiguration publisherConfig;

    /**
     * The amount of messages sent by the publisher
     */
    private long sentMessageCount;

    /**
     * The timestamp at which the first message was published
     */
    private long firstMessagePublishTimestamp;

    /**
     * The timestamp at which the last message was published
     */
    private long lastMessagePublishTimestamp;

    /**
     * The connection which is used to create the JMS session
     */
    private Connection connection;

    /**
     * The session which is used to create the JMS message producer
     */
    private Session session;

    /**
     * The message producer which produces/sends messages
     */
    private MessageProducer sender;

    /**
     * Message content which is needed to be published. The value will depend on the configuration.
     */
    private String messageContent = null;

    /**
     * Creates a new JMS publisher with a given configuration.
     *
     * @param config          The configuration
     * @param createPublisher Creates connection, session and sender.
     * @throws NamingException
     * @throws JMSException
     */
    public AndesJMSPublisher(AndesJMSPublisherClientConfiguration config, boolean createPublisher)
            throws NamingException, JMSException {
        super(config);

        // Sets the configuration
        this.publisherConfig = config;

        if (null != config.getMessagesContentToSet()) {
            this.messageContent = config.getMessagesContentToSet();
        }

        // Creates a JMS connection, sessions and sender
        if (createPublisher) {
            ConnectionFactory connFactory = (ConnectionFactory) super.getInitialContext()
                    .lookup(AndesClientConstants.CF_NAME);
            connection = connFactory.createConnection();
            connection.start();
            if(config.isTransactionalSession()) {
                this.session = connection.createSession(true, 0);
            } else {
                this.session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            }

            Destination destination = (Destination) super.getInitialContext()
                    .lookup(this.publisherConfig.getDestinationName());
            this.sender = this.session.createProducer(destination);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void startClient() throws AndesClientException, IOException {
        if (null != connection && null != session && null != sender) {
            //reading message content from file
            if (null != this.publisherConfig.getReadMessagesFromFilePath()) {
                this.getMessageContentFromFile();
            }

            Thread subscriberThread = new Thread(this);
            subscriberThread.start();
        } else {
            throw new AndesClientException("The connection, session and message sender is not assigned.");
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void stopClient() throws JMSException {
        if (null != connection && null != session && null != sender) {
            long threadID = Thread.currentThread().getId();
            log.info("Closing publisher | ThreadID : " + threadID);
            this.sender.close();
            this.session.close();
            this.connection.close();
            this.sender = null;
            this.session = null;
            this.connection = null;
            log.info("Publisher closed | ThreadID : " + threadID);
        }
    }

    /**
     * Reads message content from a file which is used as the message content to when publishing
     * messages.
     *
     * @throws IOException
     */
    public void getMessageContentFromFile() throws IOException {
        if (null != this.publisherConfig.getReadMessagesFromFilePath()) {
            BufferedReader br = new BufferedReader(new FileReader(this.publisherConfig
                                                                          .getReadMessagesFromFilePath()));
            try {
                StringBuilder sb = new StringBuilder();
                String line = br.readLine();

                while (line != null) {
                    sb.append(line);
                    sb.append('\n');
                    line = br.readLine();
                }

                // Remove the last appended next line since there is no next line.
                sb.replace(sb.length() - 1, sb.length() + 1, "");
                messageContent = sb.toString();
            } finally {
                br.close();
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void run() {
        try {
            Message message = null;
            long threadID = Thread.currentThread().getId();
            while (this.sentMessageCount < this.publisherConfig.getNumberOfMessagesToSend()) {
                // Creating a JMS message
                if (JMSMessageType.TEXT == this.publisherConfig.getJMSMessageType()) {
                    if ((null != this.publisherConfig.getReadMessagesFromFilePath()) || (null != this.messageContent)) {
                        message = this.session.createTextMessage(this.messageContent);
                    } else {
                        message = this.session.createTextMessage(MessageFormat
                                .format(AndesClientConstants.PUBLISH_MESSAGE_FORMAT, this.sentMessageCount, threadID));
                    }
                } else if (JMSMessageType.BYTE == this.publisherConfig.getJMSMessageType()) {
                    message = this.session.createBytesMessage();
                } else if (JMSMessageType.MAP == this.publisherConfig.getJMSMessageType()) {
                    MapMessage mapMessage = this.session.createMapMessage();
                    if (null != this.publisherConfig.getReadMessagesFromFilePath()) {
                        String[] entries = this.messageContent.split(System.getProperty("line.separator"));
                        for (int i = 0; i < entries.length; i++) {
                            mapMessage.setString("key" + i, entries[i]);
                        }
                    }
                    message = mapMessage;
                } else if (JMSMessageType.OBJECT == this.publisherConfig.getJMSMessageType()) {
                    message = this.session.createObjectMessage();
                } else if (JMSMessageType.STREAM == this.publisherConfig.getJMSMessageType()) {
                    message = this.session.createStreamMessage();
                }

                //set JMS message type
                String jmsType = publisherConfig.getJMSType();
                if(message!= null && null != jmsType && !jmsType.isEmpty()) {
                    message.setJMSType(jmsType);
                }

                //set JMS header properties
                setMessageProperties(message);

                if (null != message) {
                    this.sender.send(message, DeliveryMode.PERSISTENT, 0, this.publisherConfig
                            .getJMSMessageExpiryTime());
                    // need to commit if transactional
                    if(getConfig().isTransactionalSession()) {
                        session.commit();
                    }
                    if (message instanceof TextMessage && null != this.publisherConfig.getFilePathToWritePublishedMessages()){
                        AndesClientUtils.writePublishedMessagesToFile(((TextMessage) message)
                              .getText(), this.publisherConfig.getFilePathToWritePublishedMessages());
                    }

                    this.sentMessageCount++;

                    // TPS calculation
                    long currentTimeStamp = System.currentTimeMillis();
                    if (0 == this.firstMessagePublishTimestamp) {
                        this.firstMessagePublishTimestamp = currentTimeStamp;
                    }

                    this.lastMessagePublishTimestamp = currentTimeStamp;
                    if (0 == this.sentMessageCount % this.publisherConfig.getPrintsPerMessageCount()) {
                        // Logging the sent message details.
                        if (null != this.publisherConfig.getReadMessagesFromFilePath()) {
                            log.info("[SEND]" + " (FROM FILE) ThreadID:" +
                                     threadID + " Destination(" + this.publisherConfig
                                    .getExchangeType().getType() + "):" +
                                     this.publisherConfig
                                             .getDestinationName() + " SentMessageCount:" +
                                     this.sentMessageCount + " CountToSend:" +
                                     this.publisherConfig.getNumberOfMessagesToSend());
                        } else {
                            log.info("[SEND]" + " (INBUILT MESSAGE) ThreadID:" +
                                     threadID + " Destination(" + this.publisherConfig
                                    .getExchangeType().getType() + "):" +
                                     this.publisherConfig
                                             .getDestinationName() + " SentMessageCount:" +
                                     this.sentMessageCount + " CountToSend:" +
                                     this.publisherConfig.getNumberOfMessagesToSend());
                        }
                    }
                    // Writing statistics
                    if (null != this.publisherConfig.getFilePathToWriteStatistics()) {
                        String statisticsString =
                                ",,,," + Long.toString(currentTimeStamp) + "," + Double
                                        .toString(this.getPublisherTPS());
                        AndesClientUtils
                                .writeStatisticsToFile(statisticsString, this.publisherConfig
                                        .getFilePathToWriteStatistics());
                    }

                    // Delaying the publishing of messages
                    if (0 < this.publisherConfig.getRunningDelay()) {
                        try {
                            Thread.sleep(this.publisherConfig.getRunningDelay());
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                }
            }

            this.stopClient();
        } catch (JMSException e) {
            throw new RuntimeException("Error while publishing messages", e);
        } catch (IOException e) {
            throw new RuntimeException("Error while writing statistics", e);
        }
    }

    /**
     * Set JMS Headers to the message according to publisher configuration
     *
     * @param message message to set properties
     */
    private void setMessageProperties(Message message) throws JMSException {

        List<JMSHeaderProperty> headerPropertyList = publisherConfig.getJMSHeaderProperties();

        for (JMSHeaderProperty jmsHeaderProperty : headerPropertyList) {
            JMSHeaderPropertyType type = jmsHeaderProperty.getType();
            String propertyKey = jmsHeaderProperty.getKey();
            Object propertyValue = jmsHeaderProperty.getValue();
            switch (type) {
                case OBJECT:
                    message.setObjectProperty(propertyKey, propertyValue);
                    break;
                case BYTE:
                    message.setByteProperty(propertyKey, (Byte) propertyValue);
                    break;
                case BOOLEAN:
                    message.setBooleanProperty(propertyKey, (Boolean) propertyValue);
                    break;
                case DOUBLE:
                    message.setDoubleProperty(propertyKey, (Double) propertyValue);
                    break;
                case FLOAT:
                    message.setFloatProperty(propertyKey, (Float) propertyValue);
                    break;
                case SHORT:
                    message.setShortProperty(propertyKey, (Short) propertyValue);
                    break;
                case STRING:
                    message.setStringProperty(propertyKey, (String) propertyValue);
                    break;
                case INTEGER:
                    message.setIntProperty(propertyKey, (Integer) propertyValue);
                    break;
                case LONG:
                    message.setLongProperty(propertyKey, (Long) propertyValue);
                    break;
            }
        }
    }

    /**
     * Gets the published message count.
     *
     * @return The published message count.
     */
    public long getSentMessageCount() {
        return this.sentMessageCount;
    }

    /**
     * Gets the transactions per seconds for publisher.
     *
     * @return The transactions per second.
     */
    public double getPublisherTPS() {
        if (0 == this.lastMessagePublishTimestamp - this.firstMessagePublishTimestamp) {
            return ((double) this.sentMessageCount) / (1D / 1000);
        } else {
            return ((double) this.sentMessageCount) / (((double) (this.lastMessagePublishTimestamp - this.firstMessagePublishTimestamp)) / 1000);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AndesJMSPublisherClientConfiguration getConfig() {
        return this.publisherConfig;
    }

    /**
     * Gets the JMS message sending connection ({@link javax.jms.Connection}).
     *
     * @return A {@link javax.jms.Connection}
     */
    public Connection getConnection() {
        return this.connection;
    }

    /**
     * Sets the JMS message sending connection ({@link javax.jms.Connection}).
     *
     * @param connection A {@link javax.jms.Connection}.
     */
    public void setConnection(Connection connection) {
        this.connection = connection;
    }

    /**
     * Gets the JMS message sending session ({@link javax.jms.Session}).
     *
     * @return A {@link javax.jms.Session}.
     */
    public Session getSession() {
        return this.session;
    }

    /**
     * Sets the JMS message sending session ({@link javax.jms.Session}).
     *
     * @param session A {@link javax.jms.Session}.
     */
    public void setSession(Session session) {
        this.session = session;
    }

    /**
     * Gets the JMS message producer ({@link javax.jms.MessageProducer}).
     *
     * @return A {@link javax.jms.MessageProducer}.
     */
    public MessageProducer getSender() {
        return this.sender;
    }

    /**
     * Sets the JMS message producer ({@link javax.jms.MessageProducer}). Suppressing
     * "UnusedDeclaration" as the client acts as a service.
     *
     * @param sender A {@link javax.jms.MessageProducer}.
     */
    @SuppressWarnings("UnusedDeclaration")
    public void setSender(MessageProducer sender) {
        this.sender = sender;
    }
}