/*
 * 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"),
    convert = require("./convert");

var _generator;


// 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;
};

// Create an 1x1 px pixmap for the empty new documents
function get1x1Pixmap() {
    return {
        bitsPerChannel: 8,
        bounds: {bottom: 1, left: 0, right: 1, top: 0},
        bytesPerPixel: 4,
        channelCount: 4,
        colorMode: 1,
        format: 2,
        height: 1,
        padding: 0,
        pixels: new Buffer(_.fill(new Array(8), 255)),
        rowBytes: 4,
        width: 1
    };
}

// 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;
}

function getDefaultSavePixmapSettings() {
    return {
        format: "png",
        quality: 24,
        ppi: 72
    };
}

function getArtboard(document, id) {
    return _.find(getArtboards(document), function(artboard) {
        return artboard.id === id;
    });
}

function getLayerBounds(artboard) {
    var allLayers = [];

    _generator.visitAllLayers(artboard, function (layer) {
        // don't let the last layer be a group or PS gets confused...
        if (layer.type !== "layerSection" && layer.visible === true) {
            allLayers.push(layer);
        }
    });

    function joinBounds(joinedBounds, layer) {
        return joinedBounds.join(layer.bounds);
    }

    return _.reduce(
        allLayers,
        joinBounds,
        new Bounds({top: artboard.bounds.bottom,
                    left: artboard.bounds.right,
                    bottom: artboard.bounds.top,
                    right: artboard.bounds.left}
        )
    );
}

/*
    This function will modify the given settings and make some adjustments based on the canvasWidth, canvasHeight,
    x and y offset, pixmapWidth, pixmapHeight and canvasScale.

    The updated settings will be used to generate a pixmap with correct padding and cropping.
    This function was borrowed from crema.
 */
function updateSettingsWithNewValues(canvasWidth, canvasHeight, canvasOffsetX, canvasOffsetY, pixmapWidth, pixmapHeight, settings, canvasScale) {
    var canvasScaledWidth = canvasWidth ? canvasWidth * canvasScale : 0,
        canvasScaledHeight = canvasHeight ? canvasHeight * canvasScale : 0,
        padding = settings.padding || {top: 0, left: 0, right: 0, bottom: 0},
        extract = settings.extract || {x: 0, y: 0, width: pixmapWidth, height: pixmapHeight},
        targetImageWidth = extract.width,
        targetImageHeight = extract.height,
        dx = canvasWidth ? canvasScaledWidth - targetImageWidth : 0,
        dy = canvasHeight ? canvasScaledHeight - targetImageHeight : 0;

    if (dx > 0) {
        if (canvasOffsetX === undefined) {
            padding.left += Math.floor(dx / 2);
            padding.right += Math.ceil(dx / 2);
        } else {
            padding.left = -1 * canvasOffsetX;
            padding.right = dx + canvasOffsetX;
        }
    } else if (dx < 0) {
        extract.x -= Math.floor(dx / 2);
        extract.width = canvasScaledWidth;
    }

    if (dy > 0) {
        if (canvasOffsetY === undefined) {
            padding.top += Math.floor(dy / 2);
            padding.bottom += Math.ceil(dy / 2);
        } else {
            padding.top = -1 * canvasOffsetY;
            padding.bottom = dy + canvasOffsetY;
        }
    } else if (dy < 0) {
        extract.y -= Math.floor(dy / 2);
        extract.height = canvasScaledHeight;
    }

    if (extract.width !== pixmapWidth || extract.height !== pixmapHeight) {
        settings.extract = extract;
    } else {
        settings.extract = null;
    }

    settings.padding = padding;
}

function getPixmapForArtboard(generator, documentId, artboardId, settings) {
    if (documentId === undefined || artboardId === undefined) {
        return Q.reject("Document ID and artboard Name are required");
    } else {
        var document = Model.docInfo.raw;
        var artboard = getArtboard(document, artboardId);
        var pixmapTimer = logger.timerStart("getPixmapForArtboard getPixmap"),
            adjustedPixmapTimer = logger.timerStart("getPixmapForArtboard getAdjustedPixmap");

        var contentBounds = getLayerBounds(artboard);
        contentBounds = contentBounds.intersect(new Bounds(artboard.artboard.artboardRect));

        var paddedBounds = artboard.artboard.artboardRect,
            clipToBounds = document.bounds;

        var pixmapParams = generator.getPixmapParams(settings,
            artboard.artboard.artboardRect,
            artboard.artboard.artboardRect,
            paddedBounds,
            clipToBounds);

        return generator.getPixmap(documentId, artboard.id, pixmapParams).then(function (pixmap) {
            logger.timerEnd(pixmapTimer);
            return pixmap;
        }).then(function (pixmap) {
            var padding = pixmapParams.hasOwnProperty("getPadding") ?
                    pixmapParams.getPadding(pixmap.width, pixmap.height) : undefined,
                extract =  pixmapParams.hasOwnProperty("getExtractParamsForDocBounds") ?
                    pixmapParams.getExtractParamsForDocBounds(pixmap.width, pixmap.height) : undefined;

            // padding required?
            var artboardBounds = new Bounds(artboard.bounds),
                canvasWidth = artboardBounds.width(),
                canvasHeight = artboardBounds.height(),
                canvasOffsetX = Math.min(artboardBounds.left - contentBounds.left, 0),
                canvasOffsetY = Math.min(artboardBounds.top - contentBounds.top, 0),
                SCALING_FACTOR = 1;

            // prepare the settings to create the final PNG
            var conversionSettings = getDefaultSavePixmapSettings();
            conversionSettings.ppi = document.resolution;
            conversionSettings.padding = padding;
            conversionSettings.extract = extract;

            // this handles the empty Artboard issue
            if (pixmap.width === 0 && pixmap.height === 0) {
                pixmap = get1x1Pixmap();
            }

            updateSettingsWithNewValues(canvasWidth,
                canvasHeight, canvasOffsetX, canvasOffsetY, pixmap.width, pixmap.height, conversionSettings, SCALING_FACTOR);

            logger.timerEnd(adjustedPixmapTimer);
            return {pixmap: pixmap, settings: conversionSettings};
        }).catch(function (err) {
            logger.timerEnd(pixmapTimer);
            logger.timerEnd(adjustedPixmapTimer);
            logger.logMessage("Unexpected error generating pixmap: " + err, logger.LOGLEVEL_ERROR);
        });
    }
}

function savePixmap(generator, pixmap, path, settings) {
    if (settings._scale) {
        settings._scale = parseFloat(settings._scale);
    }

    // check that arguments are of the correct type
    pixmap.width          = parseInt(pixmap.width, 10);
    pixmap.height         = parseInt(pixmap.height, 10);
    pixmap.bitsPerChannel = parseInt(pixmap.bitsPerChannel, 10);
    if (settings.hasOwnProperty("quality")) {
        settings.quality  = parseInt(settings.quality, 10);
    }
    if (settings.hasOwnProperty("ppi")) {
        settings.ppi      = parseFloat(settings.ppi);
    }

    return convert.savePixmap(generator._paths, pixmap, path, settings);
}

function getPNG(generator, pixmap, savePixmapSettings) {
    var defaultSettings = getDefaultSavePixmapSettings();

    var settings = savePixmapSettings || defaultSettings;

    return savePixmap(generator, pixmap, null, settings);
}
// end generator-core bits

/**
 * 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 Air with 2048. However, designers create
 * assets for the iPhone 6+ with 2208 pixels on the longest side even though the device only
 * has 1920 physical pixels on that side. So, 2208 is the number we need to go with.
 */
function getScalingFactor(document) {
    var width = document.bounds.right - document.bounds.left,
        height = document.bounds.bottom - document.bounds.top,
        smallerSide = width > height ? height : width;

    if (smallerSide > 2208) {
        return 2208 / smallerSide;
    }

    return 1;
}

/*
 * 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,
        compToGenerate = _.find(document.comps, function (comp) {
                return !!comp.applied;
            });


    function generatePNG(pixmap, conversionSettings) {
        var getPNGTimerId = logger.timerStart("generatePreviewPNG getPNG");
        getPNG(_generator, pixmap, conversionSettings).then(
            function (buffer) {
                logger.timerEnd(getPNGTimerId);
                logger.timerEnd(generatePreviewPNGTimerId);
                result.resolve(buffer);
            },
            function (err) {
                logger.timerEnd(getPNGTimerId);
                logger.timerEnd(generatePreviewPNGTimerId);
                logger.logMessage("Error generating PNG: " + err, logger.LOGLEVEL_ERROR);
                result.reject(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;
            var pixmapTimerId = logger.timerStart("generatePreviewPNG getPixmapForArtboard");

            getPixmapForArtboard(_generator, docId, artboard.id, {
                clipToDocumentBounds: true,
                compId: compToGenerate && compToGenerate.id
            }).then(
                function (pixmapResult) {
                    logger.timerEnd(pixmapTimerId);
                    generatePNG(pixmapResult.pixmap, pixmapResult.settings);
                },
                function (err) {
                    logger.timerEnd(pixmapTimerId);
                    result.reject("ERROR: getPixmapForArtboard: " + err);
                }
            );
        }
    }

    if (useDocumentForComposition) {
        var scale = getScalingFactor(document);

        _generator.getDocumentPixmap(docId, {
            clipToDocumentBounds: true,
            compId: compToGenerate && compToGenerate.id,
            scaleX: scale,
            scaleY: scale
        }).then(
            function (pixmap) {
                generatePNG(pixmap);
            },
            function (err) {
                result.reject("ERROR: getDocumentPixmap: " + err);
            }
        );
    }
    return result.promise;
}

function init(generator) {
    _generator = generator;

    /*
     * 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.getScalingFactor = getScalingFactor;
exports.getLayerBounds = getLayerBounds;
exports.Bounds = Bounds;
