/*
 * ADOBE CONFIDENTIAL
 *
 * Copyright (c) 2015 Adobe Systems Incorporated. All rights reserved.
 *
 * NOTICE:  All information contained herein is, and remains
 * the property of Adobe Systems Incorporated and its suppliers,
 * if any.  The intellectual and technical concepts contained
 * herein are proprietary to Adobe Systems Incorporated and its
 * suppliers and are protected by trade secret or copyright law.
 * Dissemination of this information or reproduction of this material
 * is strictly forbidden unless prior written permission is obtained
 * from Adobe Systems Incorporated.
 */

"use strict";

var Q              = require("q"),
    _              = require("lodash"),
    logger         = require("../logger"),
    Model          = require("./Model"),
    AssetExtractor = require("./asset-extractor"),
    spawn          = require("child_process").spawn,
    Stream         = require("stream");

var _generator;

var LARGEST_DEVICE_DIMENSION_NO_SCALING = 2732;

// Object to keep bounds
function Bounds(raw) {
    Object.defineProperties(this, {
        "top": {
            get: function () { return raw.top; },
            enumerable: true
        },
        "right": {
            get: function () { return raw.right; },
            enumerable: true
        },
        "bottom": {
            get: function () { return raw.bottom; },
            enumerable: true
        },
        "left": {
            get: function () { return raw.left; },
            enumerable: true
        }
    });
}

Bounds.prototype.toJSON = function () {
    return {top: this.top, left: this.left, bottom: this.bottom, right: this.right};
};

Bounds.prototype.isEmpty = function () {
    var height = this.bottom - this.top,
        width = this.right - this.left;

    return !(Number.isFinite(height) && Number.isFinite(width) &&
        width > 0 && height > 0);
};

Bounds.prototype.join = function (otherBounds) {
    return new Bounds({
        top: Math.min(this.top, otherBounds.top),
        left: Math.min(this.left, otherBounds.left),
        bottom: Math.max(this.bottom, otherBounds.bottom),
        right: Math.max(this.right, otherBounds.right)
    });
};

Bounds.prototype.intersect = function (otherBounds) {
    var intersectBounds = new Bounds({
        top: Math.max(this.top, otherBounds.top),
        left: Math.max(this.left, otherBounds.left),
        bottom: Math.min(this.bottom, otherBounds.bottom),
        right: Math.min(this.right, otherBounds.right)
    });

    if (intersectBounds.isEmpty()) {
        intersectBounds = new Bounds({top: 0, left: 0, bottom: 0, right: 0});
    }

    return intersectBounds;
};

Bounds.prototype.width = function () {
    return this.right - this.left;
};

Bounds.prototype.height = function () {
    return this.bottom - this.top;
};

// The following section ideally would go in generator-core.
// Or, maybe in the document model
function getArtboards(document) {
    var result = [];

    _generator.visitAllLayers(document, function (layer) {
        if (layer.hasOwnProperty("artboard") && layer.type === "layerSection") {
            result.push(layer);
        }
    });

    return result;
}

// end generator-core bits

/*
 * We want to scale smaller side of large documents down to match the largest iOS device resolution.
 * This keeps the file sizes smaller while still enabling vertical and horizontal scrolling.
 *
 * The current iOS device with most pixels is iPad Pro with 2732x2048, so we'll scale down for
 * documents with the smaller side greater than 2732.
 *
 * This function gets the required size, along with the new scaled width and height.
 */
function getNewSize(document) {
    var width = document.bounds.right - document.bounds.left,
        height = document.bounds.bottom - document.bounds.top,
        smallerSide = width > height ? height : width,
        size = {
            scale: 1,
            width: width,
            height: height
            };

    if (smallerSide > LARGEST_DEVICE_DIMENSION_NO_SCALING) {
        size.scale = LARGEST_DEVICE_DIMENSION_NO_SCALING / smallerSide;
        size.width = Math.floor(width * size.scale);
        size.height = Math.floor(height * size.scale);
    }

    return size;
}

function createComponent(docId, layerId, scale) {
    var component = {
            documentId: docId,
            layerId: layerId,
            scale: scale,
            extension: "png",
            quality: 24,
            interpolationType: "bicubic",
            metadataType: "none",
            useICCProfile: "sRGB IEC61966-2.1"
        };

    return component;
}

function makeEmptyPNG(bounds, stream) {
    var convertDeferred = Q.defer(),
        args = [
            "-size", bounds.width() + "x" + bounds.height(),
            "canvas:white",
            "png:-"
        ];

    // Launch convert
    var convertProc = spawn(_generator._paths.convert, args);

    function onStreamError(err) {
        try {
            stream.close();
        } catch (e) {
            logger.logMessage("Error when closing file stream: " + e, logger.LOGLEVEL_ERROR);
        }
        convertDeferred.reject(err);
    }

    // Handle errors
    convertProc.on("error", function (err) { onStreamError("Error with convert: " + err); });
    convertProc.stdin.on("error",  function (err) { onStreamError("Error with convert's STDIN: "  + err); });
    convertProc.stdout.on("error", function (err) { onStreamError("Error with convert's STDOUT: " + err); });
    convertProc.stderr.on("error", function (err) { onStreamError("Error with convert's STDERR: " + err); });

    // Pipe convert's output (the produced image) into the file stream
    convertProc.stdout.pipe(stream);
    // Wait until convert is done (pipe from the last utility will close the stream)
    stream.on("close", function () {
        convertDeferred.resolve();
    });

    return convertDeferred.promise;
}

/*
 * Generates a PNG in memory for the current document
 * @param {Object=} opts options
 */
function generatePreviewPNG(opts) {
    var result = Q.defer(),
        options = opts || {};

    var generatePreviewPNGTimerId = logger.timerStart("generatePreviewPNG");

    var useDocumentForComposition = true,
        document = Model.docInfo.raw,
        docId = document.id,
        arrayOfChunks = [],
        totalLength = 0,
        stream = new Stream.Writable();

    if (options.length) {
        totalLength = options.length;
    }

    stream._write = function (chunk, encoding, next) {
        arrayOfChunks.push(chunk);
        totalLength += chunk.length;
        next();
    };

    stream.on("finish", function () {
        // Emulate fs stream so streamPixmap() resolves deferred object
        stream.emit("close");
    });

    function resolveWithBuffer(arrayBuffer) {
        arrayBuffer = Buffer.concat(arrayOfChunks, totalLength);
        arrayOfChunks = [];
        result.resolve(arrayBuffer);
    }
    
    function generateAssetPreview(component) {
        AssetExtractor.generatePreview(component)
            .then(
                function (comp) {
                    logger.timerEnd(generatePreviewPNGTimerId);
                    logger.logMessage("after generatePreview, total buffer length is " + totalLength);
                    var arrayBuffer;

                    if (totalLength === 0) {
                        // We didn't get anything back, so assume it would be an empty image
                        // and make that ourselves.
                        var bounds = comp.layer ? comp.layer.bounds : comp.document.bounds;
                        this.makeEmptyPNG(bounds, stream)
                            .then(function () {
                                logger.logMessage("after makeEmptyPNG, totalLength is " + totalLength);
                                resolveWithBuffer(arrayBuffer);
                            });
                    } else {
                        resolveWithBuffer(arrayBuffer);
                    }
                }.bind(this),
                function (err) {
                    logger.timerEnd(generatePreviewPNGTimerId);
                    result.reject("ERROR: generatePreview: " + err);
                }
            );
    }

    if (options.generateForArtboard) {
        var artboards = getArtboards(document),
            artboard = _.find(artboards, function(board) {
                if (options.artboardId) {
                    return board.id === options.artboardId;
                } else {
                    return board.name === options.artboardName;
                }
            });

        if (artboard) {
            useDocumentForComposition = false;
            logger.logMessage("generating pixmap for Artboard id: " + artboard.id);
            
            var artboardScale = 1,
                artboardComponent = createComponent(docId, artboard.id, artboardScale);

            artboardComponent.stream = stream;

            generateAssetPreview.call(this, artboardComponent);
        }
    }

    if (useDocumentForComposition) {
        var docComponent = createComponent(docId, 0),
            newSize = getNewSize(document);
        
        docComponent.clipToDocumentBounds = newSize.scale === 1;  // only clip if no scaling; see Watson bug #4049210
        docComponent.scale = newSize.scale;        
        docComponent.canvasWidth = newSize.width;
        docComponent.canvasHeight = newSize.height;
        docComponent.stream = stream;
        generateAssetPreview.call(this, docComponent);
    }
    return result.promise;
}

function init(generator, genLogger) {
    _generator = generator;

    // init AssetExtractor, etc.
    var genConfig = {};

    if (generator._config && generator._config["generator-assets"]) {
        genConfig = _.clone(generator._config["generator-assets"]);
    }

    genConfig["meta-data-driven"] = false;
    genConfig["expand-max-dimensions"] = true;
    if (!_.has(genConfig, "use-flite")) {
        genConfig["use-flite"] = true;
    }

    AssetExtractor.init(generator, genConfig, genLogger);

    /*
     * Another reusable generator function,
     *   this would be used to refactor _computeHiddenLayers
     */
    _generator.visitAllLayers = function (parent, cb) {
        if (parent && parent.layers) {
            parent.layers.forEach(function (layer) {
                cb(layer);
                if (layer.type === "layerSection" && layer.layers && layer.layers.length) {
                    _generator.visitAllLayers(layer, cb);
                }
            });
        }
    };
}

// TODO: refactor this to be in the document model
function getArtboardsForConnectMessage(document) {
    return getArtboards(document).map(function(artboard) {
        return {
            name: artboard.name,
            id: artboard.id,
            height: artboard.bounds.bottom - artboard.bounds.top,
            width: artboard.bounds.right - artboard.bounds.left,
            top: artboard.bounds.top,
            left: artboard.bounds.left,
            visible: artboard.visible
        };
    }).filter(function(artboard) {
        return artboard.visible;    // filter out hidden artboards
    });
}

exports.init = init;
exports.generatePreviewPNG = generatePreviewPNG;
exports.getArtboards = getArtboards;
exports.getArtboardsForConnectMessage = getArtboardsForConnectMessage;
exports.getNewSize = getNewSize;
exports.createComponent = createComponent;
exports.Bounds = Bounds;
exports.makeEmptyPNG = makeEmptyPNG;
