/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *  
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"), 
 * to deal in the Software without restriction, including without limitation 
 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 
 * and/or sell copies of the Software, and to permit persons to whom the 
 * Software is furnished to do so, subject to the following conditions:
 *  
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *  
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 * DEALINGS IN THE SOFTWARE.
 * 
 */

(function () {
    "use strict";

    // Dependencies
    // ------------
    
    var util = require("util"),
        EventEmitter = require("events").EventEmitter,
        net = require("net"),
        Q = require("q"),
        psCrypto = require("./ps_crypto");
    
    // Constants
    // ---------
    
    // Connection constants
    var DEFAULT_PASSWORD       = "password",
        DEFAULT_PORT           = 49494,
        DEFAULT_HOSTNAME       = "127.0.0.1",
        SOCKET_KEEPALIVE_DELAY = 1000; //milliseconds

    var CONNECTION_STATES = {
        NONE           : 0,
        CONNECTING     : 1,
        OPEN           : 2,
        CLOSING        : 3,
        CLOSED         : 4,
        DESTROYED      : 5
    };
    
    // Protocol constants
    var MESSAGE_LENGTH_OFFSET     = 0,
        MESSAGE_STATUS_OFFSET     = 4,
        MESSAGE_STATUS_LENGTH     = 4,
        MESSAGE_PAYLOAD_OFFSET    = MESSAGE_STATUS_OFFSET + MESSAGE_STATUS_LENGTH,
        PAYLOAD_HEADER_LENGTH     = 12,
        PAYLOAD_PROTOCOL_OFFSET   = 0,
        PAYLOAD_ID_OFFSET         = 4,
        PAYLOAD_TYPE_OFFSET       = 8,
        MAX_MESSAGE_ID            = 256 * 256 * 256,
        PROTOCOL_VERSION          = 1,
        MESSAGE_TYPE_ERROR        = 1,
        MESSAGE_TYPE_JAVASCRIPT   = 2,  // ExtendScript that requires refreshed scripting engine
        MESSAGE_TYPE_JAVASCRIPT_S = 10, // ExtendScript that can reuse same scripting engine
        MESSAGE_TYPE_PIXMAP       = 3,
        MESSAGE_TYPE_ICC_PROFILE  = 4,
        MESSAGE_TYPE_KEEPALIVE    = 6,
        STATUS_NO_COMM_ERROR      = 0;

    // 64 kB (maximum size of the TCP packages on the Mac)
    var INITIAL_MESSAGE_BUFFER_SIZE = 65536,
        // Known strings to be sent as messages, that should not be parsed as JSON
        KNOWN_LITERAL_STRING_MESSAGES = ["", "[ActionDescriptor]", "Yep, still alive"];

    // Helper Functions
    // ----------------

    function ensureBufferSize(buffer, minimumSize, contentSize) {
        if (buffer.length >= minimumSize) {
            return buffer;
        }
        var newBuffer = new Buffer(minimumSize);
        if (contentSize) {
            buffer.copy(newBuffer, 0, 0, contentSize);
        } else {
            buffer.copy(newBuffer);
        }
        return newBuffer;
    }

    function round(value, decimals) {
        var factor = Math.pow(10, decimals);
        return Math.round(factor * value) / factor;
    }

    /**
     * PhotoshopClient Class
     * 
     * @constructor
     */
    function PhotoshopClient(options, connectListener, logger) {
        var self = this;
        
        if (!(self instanceof PhotoshopClient)) {
            return new PhotoshopClient(options, connectListener, logger);
        } else {
            var password = DEFAULT_PASSWORD;
            
            if (options.hasOwnProperty("password")) { password = options.password; }
            if (options.hasOwnProperty("port")) { self._port = options.port; }
            if (options.hasOwnProperty("hostname")) { self._hostname = options.hostname; }
            if (options.hasOwnProperty("inputFd")) { self._inputFd = options.inputFd; }
            if (options.hasOwnProperty("outputFd")) { self._outputFd = options.outputFd; }
            
            self._logger = logger;
            self._receiveBuffer = new Buffer(INITIAL_MESSAGE_BUFFER_SIZE);
            self._receivedBytes = 0;
            self._writeQueue = [];
            
            if (connectListener) {
                this.once("connect", connectListener);
            }
            
            var connectionPromise = null;
            var cryptoPromise = null;
            
            if ((typeof self._inputFd === "number" && typeof self._outputFd === "number") ||
                (typeof self._inputFd === "string" && typeof self._outputFd === "string")) {
                connectionPromise = self._connectPipe();
                var cryptoDeferred = Q.defer();
                cryptoDeferred.resolve();
                cryptoPromise = cryptoDeferred.promise;
            } else if (self._hostname && typeof self._port === "number") {
                connectionPromise = self._connectSocket();
                cryptoPromise = self._initCrypto(password);
            } else {
                var connectionDeferred = Q.defer();
                connectionDeferred.reject();
                connectionPromise = connectionDeferred.promise;
            }
            
            Q.all([
                connectionPromise,
                cryptoPromise
            ]).done(
                function () {
                    self.emit("connect");
                },
                function (err) {
                    self.emit("error", err);
                    self.disconnect();
                }
            );
        }
    }
    util.inherits(PhotoshopClient, EventEmitter);

    // Member Variables
    // ----------------
    
    PhotoshopClient.prototype._port = DEFAULT_PORT;
    PhotoshopClient.prototype._hostname = DEFAULT_HOSTNAME;
    PhotoshopClient.prototype._connectionState = CONNECTION_STATES.NONE;
    PhotoshopClient.prototype._lastMessageID = 0;
    PhotoshopClient.prototype._crypto = null;
    PhotoshopClient.prototype._receiveBuffer = null;
    PhotoshopClient.prototype._receivedBytes = 0;
    PhotoshopClient.prototype._writeQueue = null;
    PhotoshopClient.prototype._commandDeferreds = null;
    
    // Methods
    // -------
    
    PhotoshopClient.prototype._initCrypto = function (password) {
        var self = this;
        var cryptoDeferred = Q.defer();
        psCrypto.createPSCrypto(password, function (err, crypto) {
            if (err) {
                cryptoDeferred.reject(err);
            } else {
                self._crypto = crypto;
                cryptoDeferred.resolve();
            }
        });
        return cryptoDeferred.promise;
    };

    PhotoshopClient.prototype._connectPipe = function () {
        var self = this;
        var fs = require("fs");
        var result = false;
        
        try {
            self._connectionState = CONNECTION_STATES.CONNECTING;
            
            if (typeof self._inputFd === "number") {
                self._readStream = fs.createReadStream(null, {fd: self._inputFd});
            } else {
                // named pipe
                self.emit("info", "using named read pipe " + self._inputFd);
                self._readStream = fs.createReadStream(self._inputFd);
            }

            self._readStream.on("data", function (incomingBuffer) {
                var isNewMessage = self._receivedBytes === 0,
                    minimumSize  = incomingBuffer.length + self._receivedBytes;
                
                self._receiveBuffer = ensureBufferSize(self._receiveBuffer, minimumSize);
                incomingBuffer.copy(self._receiveBuffer, self._receivedBytes);
                self._receivedBytes += incomingBuffer.length;

                self._processReceiveBuffer(isNewMessage);
            });
            
            self._readStream.on("error", function (err) {
                self.emit("error", "Pipe read error: " + err);
                self.disconnect();
            });
                    
            self._readStream.on("close", function () {
                self._connectionState = CONNECTION_STATES.CLOSED;
                self.emit("close");
            });
            
            self._readStream.resume();

            if (typeof self._outputFd === "number") {
                self._writeStream = fs.createWriteStream(null, {fd: self._outputFd});
            } else {
                // named pipe
                self.emit("info", "using named write pipe " + self._outputFd);
                self._writeStream = fs.createWriteStream(self._outputFd);
            }

            self._writeStream.on("error", function (err) {
                self.emit("error", "Pipe write error: " + err);
                self.disconnect();
            });
                    
            self._writeStream.on("close", function () {
                self._connectionState = CONNECTION_STATES.CLOSED;
                self.emit("close");
            });
            
            self._writeStream.on("drain", function () {
                self._doPendingWrites();
            });

            result = true;
        } catch (e) {
            self.emit("error", "Connect Pipe error: " + e);
            self.disconnect();
        }
        
        return result;
    };
    
    PhotoshopClient.prototype._connectSocket = function () {
        var self = this;
        var connectSocketDeferred = Q.defer();
        
        self._socket = new net.Socket();
        self._connectionState = CONNECTION_STATES.CONNECTING;
        
        self._socket.connect(self._port, self._hostname);
        
        // register event handlers for socket
        
        self._socket.on("connect", function () {
            self._socket.setKeepAlive(true, SOCKET_KEEPALIVE_DELAY);
            self._socket.setNoDelay();
            connectSocketDeferred.resolve(self);
            self._writeStream = self._socket;
        });

        self._socket.on("error", function (err) {
            if (connectSocketDeferred.promise.isPending()) {
                connectSocketDeferred.reject(err);
            }
            self.emit("error", "Socket error: " + err);
            self.disconnect();
        });
                
        self._socket.on("close", function () {
            self._connectionState = CONNECTION_STATES.CLOSED;
            self.emit("close");
        });
        
        self._socket.on("data", function (incomingBuffer) {
            var isNewMessage = self._receivedBytes === 0,
                minimumSize  = incomingBuffer.length + self._receivedBytes;
            
            self._receiveBuffer = ensureBufferSize(self._receiveBuffer, minimumSize);
            incomingBuffer.copy(self._receiveBuffer, self._receivedBytes);
            self._receivedBytes += incomingBuffer.length;

            self._processReceiveBuffer(isNewMessage);
        });

        self._socket.on("drain", function () {
            self._doPendingWrites();
        });
        
        return connectSocketDeferred.promise;
    };

    PhotoshopClient.prototype._writeWhenFree  = function (buffer) {
        var self = this;
        self._writeQueue.push(buffer);
        process.nextTick(function () { self._doPendingWrites(); });
    };
    
    PhotoshopClient.prototype._doPendingWrites = function () {
        if (this._writeStream && this._writeQueue.length > 0 && !this._writeStream.busy) {
            var bufferToWrite = this._writeQueue.shift();
            var writeStatus = this._writeStream.write(bufferToWrite);
            if (writeStatus && this._writeQueue.length > 0) {
                this._doPendingWrites();
            }
        }
        // Continue to try writing as long as there is something left to write
        if (this._writeStream && this._writeQueue.length > 0) {
            process.nextTick(this._doPendingWrites.bind(this));
        }
    };

    PhotoshopClient.prototype.isConnected = function () {
        return (this._connectionState === CONNECTION_STATES.OPEN);
    };
    
    PhotoshopClient.prototype.disconnect = function (listener) {
        // NOTE: This method *must* do any actual cleanup of os resources
        // (e.g. sockets, pipes) synchronously. Listeners may be called asynchronously
        // after those resources actually close. In cases where disconnect is called
        // at process.exit, listeners may not get called.
        if (!listener) {
            listener = function () { };
        }

        if (this._socket) {
            var currentState = this._connectionState;
            switch (currentState) {
                case CONNECTION_STATES.NONE:
                    this._connectionState = CONNECTION_STATES.CLOSED;
                    process.nextTick(listener);
                    return true;
                case CONNECTION_STATES.CONNECTING:
                    this._connectionState = CONNECTION_STATES.CLOSING;
                    this._socket.once("close", listener);
                    this._socket.end();
                    return true;
                case CONNECTION_STATES.OPEN:
                    this._connectionState = CONNECTION_STATES.CLOSING;
                    this._socket.once("close", listener);
                    this._socket.end();
                    return true;
                case CONNECTION_STATES.CLOSING:
                    this._socket.once("close", listener);
                    return true;
                case CONNECTION_STATES.CLOSED:
                    process.nextTick(listener);
                    return true;
                case CONNECTION_STATES.DESTROYED:
                    process.nextTick(listener);
                    return true;
            }
        } else {
            if (this._readStream) {
                try {
                    this._readStream.close();
                } catch (readCloseError) {
                    // do nothing
                }
                this._readStream = null;
            }

            if (this._writeStream) {
                try {
                    this._writeStream.close();
                } catch (writeCloseError) {
                    // do nothing
                }
                this._writeStream = null;
            }
            
            this._connectionState = CONNECTION_STATES.CLOSED;
            process.nextTick(listener);
            return true;
        }
    };
    
    PhotoshopClient.prototype.destroy = function () {
        this._connectionState = CONNECTION_STATES.DESTROYED;
        if (this._socket) {
            this._socket.destroy();
        }
    };

    PhotoshopClient.prototype._sendMessage = function (payloadBuffer) {
        var cipheredPayloadBuffer = this._crypto ? this._crypto.cipher(payloadBuffer) : payloadBuffer;
        var headerBuffer = new Buffer(MESSAGE_PAYLOAD_OFFSET);

        // message length includes status and payload, but not the UInt32 specifying message length
        var messageLength = cipheredPayloadBuffer.length + MESSAGE_STATUS_LENGTH;
        headerBuffer.writeUInt32BE(messageLength, MESSAGE_LENGTH_OFFSET);
        headerBuffer.writeInt32BE(STATUS_NO_COMM_ERROR, MESSAGE_STATUS_OFFSET);
        
        this._writeWhenFree(headerBuffer);
        this._writeWhenFree(cipheredPayloadBuffer);
    };

    PhotoshopClient.prototype.sendKeepAlive = function () {
        var id = this._lastMessageID = (this._lastMessageID + 1) % MAX_MESSAGE_ID,
            payloadBuffer = new Buffer(PAYLOAD_HEADER_LENGTH);
        
        payloadBuffer.writeUInt32BE(PROTOCOL_VERSION, PAYLOAD_PROTOCOL_OFFSET);
        payloadBuffer.writeUInt32BE(id, PAYLOAD_ID_OFFSET);
        payloadBuffer.writeUInt32BE(MESSAGE_TYPE_KEEPALIVE, PAYLOAD_TYPE_OFFSET);
        
        this._sendMessage(payloadBuffer);

        return id;
    };

    /**
     * Send a JSX command to photoshop.
     * Use a special message type in the message header
     * if this script is designated safe for use with a scripting engine
     *
     * @param {string} javascript
     * @param {boolean=} sharedEngineSafe
     * @return {number} command ID
     */
    PhotoshopClient.prototype.sendCommand = function (javascript, sharedEngineSafe) {
        var id = this._lastMessageID = (this._lastMessageID + 1) % MAX_MESSAGE_ID,
            codeBuffer = new Buffer(javascript, "utf8"),
            payloadBuffer = new Buffer(codeBuffer.length + PAYLOAD_HEADER_LENGTH),
            messageType = sharedEngineSafe ? MESSAGE_TYPE_JAVASCRIPT_S : MESSAGE_TYPE_JAVASCRIPT;

        this._logger.debug("sendCommand " + id + "with type " + messageType + ": " + javascript.substr(0, 50));

        payloadBuffer.writeUInt32BE(PROTOCOL_VERSION, PAYLOAD_PROTOCOL_OFFSET);
        payloadBuffer.writeUInt32BE(id, PAYLOAD_ID_OFFSET);
        payloadBuffer.writeUInt32BE(messageType, PAYLOAD_TYPE_OFFSET);
        codeBuffer.copy(payloadBuffer, PAYLOAD_HEADER_LENGTH);

        this._sendMessage(payloadBuffer);
        
        return id;
    };

    PhotoshopClient.prototype._processReceiveBuffer = function (isNewMessage) {
        // Log when the message started to come in so we can analyze the performance
        if (isNewMessage && this._receivedBytes > 0) {
            this.messageStartTime = new Date().getTime();
        }

        // The header hasn't arrived yet: stop here
        if (this._receivedBytes < MESSAGE_PAYLOAD_OFFSET) { return; }

        // Evaluate communication status
        var commStatus = this._receiveBuffer.readInt32BE(MESSAGE_STATUS_OFFSET);
        if (commStatus !== STATUS_NO_COMM_ERROR) {
            this._logger.error("Communication error: " + commStatus);
            this.emit("communicationsError", "Photoshop communication error: " + commStatus);
            this.disconnect();
            return;
        }

        // Message length includes status and payload, but not the UInt32 specifying message length
        var messageLength = this._receiveBuffer.readUInt32BE(MESSAGE_LENGTH_OFFSET),
            totalLength   = messageLength + MESSAGE_STATUS_OFFSET;

        // Make sure the buffer is large enough to contain the whole message
        this._receiveBuffer = ensureBufferSize(this._receiveBuffer, totalLength, this._receivedBytes);

        // Unless the entire message has already been received, stop here
        if (this._receivedBytes < totalLength) { return; }

        // Performance evaluation
        var duration = new Date().getTime() - this.messageStartTime;
        if (duration > 10) {
            var size  = this._receivedBytes / 1024,
                speed = size / (duration / 1000);
            this._logger.info(duration + "ms to receive " + round(size, 1) + " kB (" + round(speed, 1) + " kB/s)");
        }
        
        // Extract the message
        var cipheredBody = this._receiveBuffer.slice(MESSAGE_PAYLOAD_OFFSET, totalLength),
            remainingBytes = this._receivedBytes - totalLength,
            preferredBufferSize = Math.max(INITIAL_MESSAGE_BUFFER_SIZE, remainingBytes);

        // Prepare the buffer for the next message
        var oldBuffer = this._receiveBuffer;
        this._receiveBuffer = new Buffer(preferredBufferSize);
        if (remainingBytes > 0) {
            oldBuffer.copy(this._receiveBuffer, 0, totalLength, this._receivedBytes);
        }
        this._receivedBytes = remainingBytes;

        // Decrypt the message
        var startTime = new Date().getTime(),
            bodyBuffer = this._crypto ? this._crypto.decipher(cipheredBody) : cipheredBody;
        duration = new Date().getTime() - startTime;
        if (duration > 10) {
            this._logger.info(duration + "ms to decrypt buffer");
        }

        // Process message
        this._processMessage(bodyBuffer);
        
        // Receive buffer might have more than one message, so process again
        this._processReceiveBuffer(true);
    };
    
    PhotoshopClient.prototype._processMessage = function (bodyBuffer) {
        var protocolVersion = bodyBuffer.readUInt32BE(PAYLOAD_PROTOCOL_OFFSET),
            messageID = bodyBuffer.readUInt32BE(PAYLOAD_ID_OFFSET),
            messageType = bodyBuffer.readUInt32BE(PAYLOAD_TYPE_OFFSET),
            messageBody = bodyBuffer.slice(PAYLOAD_HEADER_LENGTH);
        
        var rawMessage = {
            protocolVersion: protocolVersion,
            id: messageID,
            type: messageType,
            body: messageBody
        };

        if (protocolVersion !== PROTOCOL_VERSION) {
            this.emit("communicationsError", "unknown protocol version", protocolVersion);
        } else if (messageType === MESSAGE_TYPE_JAVASCRIPT) {
            var messageBodyString = messageBody.toString("utf8");
            var messageBodyParts = messageBodyString.split("\r");
            var eventName = null;
            var parsedValue = null;
            
            if (messageBodyParts.length === 2) {
                eventName = messageBodyParts[0];
            }

            parsedValue = messageBodyParts[messageBodyParts.length - 1];

            // Most commands pass JSON back. However, some pass strings that result from
            // toStrings of un-JSON-ifiable data (e.g. "[ActionDescriptor]"). Still others
            // pass actual strings (that are not JSON) that we want to use (e.g. requests
            // for the Photoshop path). So, we try to parse as JSON, and if we fail, we just
            // use the string as the parsedValue. For some known strings, we don't try,
            // in order to avoid uninformative error messages for the console and the logs.
            //
            // TODO: In the future, it might make more sense to have a different slot in
            // the message that gives parsed data (if available) and unparsed string (always)
            if (KNOWN_LITERAL_STRING_MESSAGES.indexOf(parsedValue) === -1) {
                try {
                    parsedValue = JSON.parse(parsedValue);
                } catch (jsonParseException) {
                    // do nothing; this is not unexpected
                }
            }
    
            if (eventName) {
                this.emit("event", messageID, eventName, parsedValue, rawMessage);
            } else {
                this.emit("message", messageID, parsedValue, rawMessage);
            }
        } else if (messageType === MESSAGE_TYPE_PIXMAP) {
            this.emit("pixmap", messageID, messageBody, rawMessage);
        } else if (messageType === MESSAGE_TYPE_ICC_PROFILE) {
            this.emit("iccProfile", messageID, messageBody, rawMessage);
        } else if (messageType === MESSAGE_TYPE_ERROR) {
            this.emit("error", { id: messageID, body: messageBody.toString("utf8") });
        } else {
            this.emit("communicationsError", "unknown message type", messageType);
        }
    };
        
    // Factory Functions
    // -----------------    
    
    function createClient(options, connectListener, logger) {
        return new PhotoshopClient(options, connectListener, logger);
    }

    // Public Interface
    // =================================

    exports.PhotoshopClient = PhotoshopClient;
    exports.createClient = createClient;

}());
