"use strict";

var simplesmtp = require("../index"),
    EventEmitter = require('events').EventEmitter,
    utillib = require("util"),
    xoauth2 = require("xoauth2");

// expose to the world
module.exports = function(port, host, options){
    var pool = new SMTPConnectionPool(port, host, options);
    return pool;
};

/**
 * <p>Creates a SMTP connection pool</p>
 *
 * <p>Optional options object takes the following possible properties:</p>
 * <ul>
 *     <li><b>secureConnection</b> - use SSL</li>
 *     <li><b>name</b> - the name of the client server</li>
 *     <li><b>auth</b> - authentication object <code>{user:"...", pass:"..."}</code>
 *     <li><b>ignoreTLS</b> - ignore server support for STARTTLS</li>
 *     <li><b>tls</b> - options for createCredentials</li>
 *     <li><b>debug</b> - output client and server messages to console</li>
 *     <li><b>maxConnections</b> - how many connections to keep in the pool</li>
 * </ul>
 *
 * @constructor
 * @namespace SMTP Client Pool module
 * @param {Number} [port=25] The port number to connecto to
 * @param {String} [host="localhost"] THe hostname to connect to
 * @param {Object} [options] optional options object
 */
function SMTPConnectionPool(port, host, options){
    EventEmitter.call(this);

    /**
     * Port number to connect to
     * @public
     */
    this.port = port || 25;

    /**
     * Hostname to connect to
     * @public
     */
    this.host = host || "localhost";

    /**
     * Options object
     * @public
     */
    this.options = options || {};
    this.options.maxConnections = this.options.maxConnections || 5;
    this.options.maxMessages = this.options.maxMessages || Infinity;

    /**
     * An array of connections that are currently idle
     * @private
     */
    this._connectionsAvailable = [];

    /**
     * An array of connections that are currently in use
     * @private
     */
    this._connectionsInUse = [];

    /**
     * Message queue (FIFO)
     * @private
     */
    this._messageQueue = [];

    /**
     * Counter for generating ID values for debugging
     * @private
     */
    this._idgen = 0;

    // Initialize XOAUTH2 if needed
    if(this.options.auth && typeof this.options.auth.XOAuth2 == "object"){
        if(!this.options.auth.XOAuth2.user && this.options.auth.user){
            this.options.auth.XOAuth2.user = this.options.auth.user;
        }
        this.options.auth.XOAuth2 = xoauth2.createXOAuth2Generator(this.options.auth.XOAuth2);
    }
}
utillib.inherits(SMTPConnectionPool, EventEmitter);

/**
 * <p>Sends a message. If there's any idling connections available
 * use one to send the message immediatelly, otherwise add to queue.</p>
 *
 * @param {Object} message MailComposer object
 * @param {Function} callback Callback function to run on finish, gets an
 *        <code>error</code> object as a parameter if the sending failed
 *        and on success an object with <code>failedRecipients</code> array as
 *        a list of addresses that were rejected (if any) and
 *        <code>message</code> which indicates the last message received from
 *        the server
 */
SMTPConnectionPool.prototype.sendMail = function(message, callback){
    var connection;

    message.returnCallback = callback;

    if(this._connectionsAvailable.length){
        // if available connections pick one
        connection = this._connectionsAvailable.pop();
        this._connectionsInUse.push(connection);
        this._processMessage(message, connection);
    }else{
        this._messageQueue.push(message);
        if(this._connectionsAvailable.length + this._connectionsInUse.length < this.options.maxConnections){
            this._createConnection();
        }
    }
};

/**
 * <p>Closes all connections</p>
 */
SMTPConnectionPool.prototype.close = function(callback){
    var connection;

    // for some reason destroying the connections seem to be the only way :S
    while(this._connectionsAvailable.length){
        connection = this._connectionsAvailable.pop();
        if(connection.socket){
            connection.socket.destroy();
        }
    }

    while(this._connectionsInUse.length){
        connection = this._connectionsInUse.pop();
        if(connection.socket){
            connection.socket.destroy();
        }
    }

    if(callback){
        if(typeof setImmediate == "function"){
            setImmediate(callback);
        }else{
            process.nextTick(callback);
        }
    }
};

/**
 * <p>Initiates a connection to the SMTP server and adds it to the pool</p>
 */
SMTPConnectionPool.prototype._createConnection = function(){

    var connectionOptions = {
            instanceId: ++this._idgen,
            debug: !!this.options.debug,
            logFile: this.options.logFile,
            ignoreTLS: !!this.options.ignoreTLS,
            tls: this.options.tls || false,
            auth: this.options.auth || false,
            authMethod: this.options.authMethod,
            name: this.options.name || false,
            secureConnection: !!this.options.secureConnection
        },
        connection = simplesmtp.connect(this.port, this.host, connectionOptions);

    if("greetingTimeout" in this.options){
        connectionOptions.greetingTimeout = this.options.greetingTimeout;
    }

    if("connectionTimeout" in this.options){
        connectionOptions.connectionTimeout = this.options.connectionTimeout;
    }

    if("rejectUnathorized" in this.options){
        connectionOptions.rejectUnathorized = this.options.rejectUnathorized;
    }

    if("localAddress" in this.options){
        connectionOptions.localAddress = this.options.localAddress;
    }

    connection._messagesProcessed = 0;

    connection.on("idle", this._onConnectionIdle.bind(this, connection));
    connection.on("message", this._onConnectionMessage.bind(this, connection));
    connection.on("ready", this._onConnectionReady.bind(this, connection));
    connection.on("error", this._onConnectionError.bind(this, connection));
    connection.on("end", this._onConnectionEnd.bind(this, connection));
    connection.on("rcptFailed", this._onConnectionRCPTFailed.bind(this, connection));

    this.emit('connectionCreated', connection);

    // as the connection is not ready yet, add to "in use" queue
    this._connectionsInUse.push(connection);
};

/**
 * <p>Processes a message by assigning it to a connection object and initiating
 * the sending process by setting the envelope</p>
 *
 * @param {Object} message MailComposer message object
 * @param {Object} connection <code>simplesmtp.connect</code> connection
 */
SMTPConnectionPool.prototype._processMessage = function(message, connection){
    connection.currentMessage = message;
    message.currentConnection = connection;

    connection._messagesProcessed++;

    // send envelope
    connection.useEnvelope(message.getEnvelope());
};

/**
 * <p>Will be fired on <code>'idle'</code> events by the connection, if
 * there's a message currently in queue</p>
 *
 * @event
 * @param {Object} connection Connection object that fired the event
 */
SMTPConnectionPool.prototype._onConnectionIdle = function(connection){
    var message = this._messageQueue.shift();

    if(message){
        this._processMessage(message, connection);
    }else{
        for(var i=0, len = this._connectionsInUse.length; i<len; i++){
            if(this._connectionsInUse[i] == connection){
                this._connectionsInUse.splice(i,1); // remove from list
                break;
            }
        }
        this._connectionsAvailable.push(connection);
    }
};

/**
 * <p>Will be called when not all recipients were accepted</p>
 *
 * @event
 * @param {Object} connection Connection object that fired the event
 * @param {Array} addresses Failed addresses as an array of strings
 */
SMTPConnectionPool.prototype._onConnectionRCPTFailed = function(connection, addresses){
    if(connection.currentMessage){
        connection.currentMessage.failedRecipients = addresses;
    }
};

/**
 * <p>Will be called when the client is waiting for a message to deliver</p>
 *
 * @event
 * @param {Object} connection Connection object that fired the event
 */
SMTPConnectionPool.prototype._onConnectionMessage = function(connection){
    if(connection.currentMessage){
        connection.currentMessage.streamMessage();
        connection.currentMessage.pipe(connection);
    }
};

/**
 * <p>Will be called when a message has been delivered</p>
 *
 * @event
 * @param {Object} connection Connection object that fired the event
 * @param {Boolean} success True if the message was queued by the SMTP server
 * @param {String} message Last message received from the server
 */
SMTPConnectionPool.prototype._onConnectionReady = function(connection, success, message){
    var error, responseObj = {};

    if(connection._messagesProcessed >= this.options.maxMessages && connection.socket){

        connection.emit("end");
        connection.removeAllListeners();
        if(connection.socket){
            connection.socket.destroy();
        }

        this.emit('released', connection);
    }

    if(connection.currentMessage && connection.currentMessage.returnCallback){
        if(success){

            if(connection.currentMessage.failedRecipients){
                responseObj.failedRecipients = connection.currentMessage.failedRecipients;
            }

            if(message){
                responseObj.message = message;
            }

            if(connection.currentMessage._messageId){
                responseObj.messageId = connection.currentMessage._messageId;
            }

            connection.currentMessage.returnCallback(null, responseObj);

        }else{
            error = new Error("Message delivery failed" + (message?": "+message:""));
            error.name = "DeliveryError";
            error.data = message;
            connection.currentMessage.returnCallback(error);
        }
    }
    connection.currentMessage = false;
};

/**
 * <p>Will be called when an error occurs</p>
 *
 * @event
 * @param {Object} connection Connection object that fired the event
 * @param {Object} error Error object
 */
SMTPConnectionPool.prototype._onConnectionError = function(connection, error){
    var message = connection.currentMessage;
    connection.currentMessage = false;

    // clear a first message from the list, otherwise an infinite loop will emerge
    if(!message){
        message = this._messageQueue.shift();
    }

    if(message && message.returnCallback){
        message.returnCallback(error);
    }
};

/**
 * <p>Will be called when a connection to the client is closed</p>
 *
 * @event
 * @param {Object} connection Connection object that fired the event
 */
SMTPConnectionPool.prototype._onConnectionEnd = function(connection){
    var removed = false, i, len;

    // if in "available" list, remove
    for(i=0, len = this._connectionsAvailable.length; i<len; i++){
        if(this._connectionsAvailable[i] == connection){
            this._connectionsAvailable.splice(i,1); // remove from list
            removed = true;
            break;
        }
    }

    if(!removed){
        // if in "in use" list, remove
        for(i=0, len = this._connectionsInUse.length; i<len; i++){
            if(this._connectionsInUse[i] == connection){
                this._connectionsInUse.splice(i,1); // remove from list
                removed = true;
                break;
            }
        }
    }

    // if there's still unprocessed mail and available connection slots, create
    // a new connection
    if(this._messageQueue.length &&
      this._connectionsInUse.length + this._connectionsAvailable.length < this.options.maxConnections){
        this._createConnection();
    }
};
