/*!
 * psd2fgui
 * @license [MIT]
 * @copyright http://www.fairygui.com/
 */

"use strict";

const PSD = require('psd');
const fs = require('fs-extra');
const path = require('path');
const crypto = require('crypto');
const archiver = require('archiver');
const xmlbuilder = require('xmlbuilder');

//The group name prefix identified as a component.
const componentPrefix = 'Com';

//The group name prefix identified as a common button.
const commonButtonPrefix = 'Button';

//The group name prefix identified as a checkbox button.
const checkButtonPrefix = 'CheckButton';

//The group name prefix identified as a radio button.
const radioButtonPrefix = 'RadioButton';

//The layer name suffix of each status of the button.
const buttonStatusSuffix = ['@up', '@down', '@over', '@selectedOver'];

exports.constants = {
    NO_PACK: 1,
    IGNORE_FONT: 2
};

var targetPackage;

/**
 * Convert a PSD file to a fairygui package.
 * @param {string} psdFile path of the psd file.
 * @param {string} outputFile optional. output file path.
 * @param {integer} option psd2fgui.constants.
 * @param {string} buildId optinal. You can use same build id to keep resource ids unchanged during multiple converting for a psd file.
 * @return {string} output file path.
 */
exports.convert = function (psdFile, outputFile, option, buildId) {
    return new Promise(function (resolve, reject) {
        if (!option)
            option = 0;
        if (!buildId)
            buildId = genBuildId();

        var pathInfo = path.parse(psdFile);
        var outputDirectory;

        if (option & exports.constants.NO_PACK) {
            outputDirectory = outputFile;
            if (!outputDirectory)
                outputDirectory = path.join(pathInfo.dir, pathInfo.name + '-fairypackage');
        }
        else {
            outputDirectory = path.join(pathInfo.dir, pathInfo.name + '~temp');
            fs.emptyDirSync(outputDirectory);

            if (!outputFile)
                outputFile = path.join(pathInfo.dir, pathInfo.name + '.fairypackage');
        }

        var psd = PSD.fromFile(psdFile);
        psd.parse();

        targetPackage = new UIPackage(outputDirectory, buildId);
        targetPackage.exportOption = option;

        createComponent(psd.tree(), pathInfo.name);

        var pkgDesc = xmlbuilder.create('packageDescription');
        pkgDesc.att('id', targetPackage.id);
        var resourcesNode = pkgDesc.ele('resources');
        var savePromises = [];

        targetPackage.resources.forEach(function (item) {
            var resNode = resourcesNode.ele(item.type);
            resNode.att('id', item.id).att('name', item.name).att('path', '/');
            if (item.type == 'image') {
                if (item.scale9Grid) {
                    resNode.att('scale', item.scale);
                    resNode.att('scale9Grid', item.scale9Grid);
                }
            }

            if (item.type == 'image')
                savePromises.push(item.data.saveAsPng(path.join(targetPackage.basePath, item.name)));
            else
                savePromises.push(fs.writeFile(path.join(targetPackage.basePath, item.name), item.data));
        });

        savePromises.push(fs.writeFile(path.join(targetPackage.basePath, 'package.xml'),
            pkgDesc.end({ pretty: true })));

        var pa = Promise.all(savePromises);
        if (option & exports.constants.NO_PACK) {
            pa.then(function () {
                console.log(psdFile + '->' + outputDirectory);
                resolve(buildId);
            }).catch(function (reason) {
                reject(reason);
            });
        }
        else {
            pa.then(function () {
                return fs.readdir(outputDirectory);
            }).then(function (files) {
                var output = fs.createWriteStream(outputFile);
                output.on('close', function () {
                    fs.emptyDirSync(outputDirectory);
                    fs.rmdirSync(outputDirectory);

                    console.log(psdFile + '->' + outputFile);
                    resolve(buildId);
                });

                var zipArchiver = archiver('zip');
                zipArchiver.pipe(output);
                files.forEach(function (ff) {
                    zipArchiver.file(path.join(outputDirectory, ff), { 'name': ff });
                });
                zipArchiver.finalize();
            }).catch(function (reason) {
                reject(reason);
            });
        }
    });
}

//=====================================================================================
function UIPackage(basePath, buildId) {
    this.id = buildId.substr(0, 8);
    this.itemIdBase = buildId.substr(8);
    this.nextItemIndex = 0;
    this.getNextItemId = function () {
        return this.itemIdBase + (this.nextItemIndex++).toString(36);
    };

    this.basePath = basePath;
    fs.ensureDirSync(basePath);

    this.resources = [];
    this.sameDataTestHelper = {};
    this.sameNameTestHelper = {};
}

function createImage(aNode, scale9Grid) {
    var packageItem = createPackageItem('image', aNode.get('name') + '.png', aNode);
    if (scale9Grid) {
        packageItem.scale = '9grid';
        packageItem.scale9Grid = scale9Grid;
    }

    return packageItem;
}

function createComponent(aNode, name) {
    var component = xmlbuilder.create('component');
    component.att('size', aNode.get('width') + ',' + aNode.get('height'));
    var displayList = component.ele('displayList');

    var cnt = aNode.children().length;
    for (var i = cnt - 1; i >= 0; i--) {
        parseNode(aNode.children()[i], aNode, displayList);
    }

    return createPackageItem('component', (name ? name : aNode.get('name')) + '.xml', component.end({ pretty: true }));
}

function createButton(aNode, instProps) {
    var component = xmlbuilder.create('component');
    component.att('size', aNode.get('width') + ',' + aNode.get('height'));
    component.att('extention', 'Button');

    var images = [];
    var imagePages = [];
    var imageCnt = 0;
    aNode.descendants().forEach(function (childNode) {
        var nodeName = childNode.get('name');
        for (var i in buttonStatusSuffix) {
            if (nodeName.indexOf(buttonStatusSuffix[i]) != -1) {
                images[i] = childNode;
                imageCnt++;
            }
        };
    });
    for (var i in buttonStatusSuffix) {
        imagePages[i] = [];
        if (!images[i]) {
            if (i == 3 && images[1]) //if no 'selectedOver', use 'down'
                imagePages[1].push(i);
            else //or else, use 'up'
                imagePages[0].push(i);
        }
        else {
            imagePages[i].push(i);
        }
    }

    var onElementCallback = function (child, node) {
        var nodeName = node.get('name');
        var j = images.indexOf(node);
        if (j != -1) {
            var gear = child.ele('gearDisplay');
            gear.att('controller', 'button');
            gear.att('pages', imagePages[j].join(','));
        }

        if (nodeName.indexOf('@title') != -1) {
            if (child.attributes['text']) {
                instProps['@title'] = child.attributes['text'].value;
                child.removeAttribute('text');
            }
        }
        else if (nodeName.indexOf('@icon') != -1) {
            if (child.attributes['url']) {
                instProps['@icon'] = child.attributes['url'].value;
                child.removeAttribute('url');
            }
        }
    };

    var controller = component.ele('controller');
    controller.att('name', 'button');
    controller.att('pages', '0,up,1,down,2,over,3,selectedOver');

    var displayList = component.ele('displayList');
    var cnt = aNode.children().length;
    for (i = cnt - 1; i >= 0; i--) {
        parseNode(aNode.children()[i], aNode, displayList, onElementCallback);
    }

    var extension = component.ele('Button');
    if (aNode.get('name').indexOf(checkButtonPrefix) == 0) {
        extension.att('mode', 'Check');
        instProps['@checked'] = 'true';
    }
    else if (aNode.get('name').indexOf(radioButtonPrefix) == 0)
        extension.att('mode', 'Radio');

    if (imageCnt == 1) {
        extension.att('downEffect', 'scale');
        extension.att('downEffectValue', '1.1');
    }

    return createPackageItem('component', aNode.get('name') + '.xml', component.end({ pretty: true }));
}

function createPackageItem(type, fileName, data) {
    var dataForHash;
    if (type == 'image') //data should a psd layer
        dataForHash = Buffer.from(data.get('image').pixelData);
    else
        dataForHash = data;
    var hash = crypto.createHash('md5').update(dataForHash).digest('hex');
    var item = targetPackage.sameDataTestHelper[hash];
    if (!item) {
        item = {};
        item.type = type;
        item.id = targetPackage.getNextItemId();

        var i = fileName.lastIndexOf('.');
        var basename = fileName.substr(0, i);
        var ext = fileName.substr(i);
        basename = basename.replace(/[\@\'\"\\\/\b\f\n\r\t\$\%\*\:\?\<\>\|]/g, '_');
        while (true) {
            var j = targetPackage.sameNameTestHelper[basename];
            if (j == undefined) {
                targetPackage.sameNameTestHelper[basename] = 1;
                break;
            }
            else {
                targetPackage.sameNameTestHelper[basename] = j + 1;
                basename = basename + '_' + j;
            }
        }
        fileName = basename + ext;
        item.name = fileName;
        item.data = data;
        targetPackage.resources.push(item);
        targetPackage.sameDataTestHelper[hash] = item;
    }

    return item;
}

function parseNode(aNode, rootNode, displayList, onElementCallback) {
    var child;
    var packageItem;
    var instProps;
    var str;

    var nodeName = aNode.get('name');
    var specialUsage;
    if (nodeName.indexOf('@title') != -1)
        specialUsage = 'title';
    else if (nodeName.indexOf('@icon') != -1)
        specialUsage = 'icon';

    if (aNode.isGroup()) {
        if (nodeName.indexOf(componentPrefix) == 0) {
            packageItem = createComponent(aNode);
            child = xmlbuilder.create('component');
            str = 'n' + (displayList.children.length + 1);
            child.att('id', str + '_' + targetPackage.itemIdBase);
            child.att('name', specialUsage ? specialUsage : str);
            child.att('src', packageItem.id);
            child.att('fileName', packageItem.name);
            child.att('xy', (aNode.left - rootNode.left) + ',' + (aNode.top - rootNode.top));
        }
        else if (nodeName.indexOf(commonButtonPrefix) == 0 || nodeName.indexOf(checkButtonPrefix) == 0 || nodeName.indexOf(radioButtonPrefix) == 0) {
            instProps = {};
            packageItem = createButton(aNode, instProps);
            child = xmlbuilder.create('component');
            str = 'n' + (displayList.children.length + 1);
            child.att('id', str + '_' + targetPackage.itemIdBase);
            child.att('name', specialUsage ? specialUsage : str);
            child.att('src', packageItem.id);
            child.att('fileName', packageItem.name);
            child.att('xy', (aNode.left - rootNode.left) + ',' + (aNode.top - rootNode.top));
            child.ele({ Button: instProps });
        }
        else {
            var cnt = aNode.children().length;
            for (var i = cnt - 1; i >= 0; i--)
                parseNode(aNode.children()[i], rootNode, displayList, onElementCallback);
        }
    }
    else {
        var typeTool = aNode.get('typeTool');
        if (typeTool) {
            child = xmlbuilder.create('text');
            str = 'n' + (displayList.children.length + 1);
            child.att('id', str + '_' + targetPackage.itemIdBase);
            child.att('name', specialUsage ? specialUsage : str);
            child.att('text', typeTool.textValue);
            if (specialUsage == 'title') {
                child.att('xy', '0,' + (aNode.top - rootNode.top - 4));
                child.att('size', rootNode.width + ',' + (aNode.height + 8));
                child.att('align', 'center');
            }
            else {
                child.att('xy', (aNode.left - rootNode.left - 4) + ',' + (aNode.top - rootNode.top - 4));
                child.att('size', (aNode.width + 8) + ',' + (aNode.height + 8));
                str = typeTool.alignment()[0];
                if (str != 'left')
                    child.att('align', str);
            }
            child.att('vAlign', 'middle');
            child.att('autoSize', 'none');
            if (!(targetPackage.exportOption & exports.constants.IGNORE_FONT))
                child.att('font', typeTool.fonts()[0]);
            child.att('fontSize', typeTool.sizes()[0]);
            child.att('color', convertToHtmlColor(typeTool.colors()[0]));
        }
        else if (!aNode.isEmpty()) {
            packageItem = createImage(aNode);
            if (specialUsage == 'icon')
                child = xmlbuilder.create('loader');
            else
                child = xmlbuilder.create('image');
            str = 'n' + (displayList.children.length + 1);
            child.att('id', str + '_' + targetPackage.itemIdBase);
            child.att('name', specialUsage ? specialUsage : str);
            child.att('xy', (aNode.left - rootNode.left) + ',' + (aNode.top - rootNode.top));
            if (specialUsage == 'icon') {
                child.att('size', aNode.width + ',' + aNode.height);
                child.att('url', 'ui://' + targetPackage.id + packageItem.id);
            }
            else
                child.att('src', packageItem.id);
            child.att('fileName', packageItem.name);
        }
    }

    if (child) {
        var opacity = aNode.get('opacity');
        if (opacity < 255)
            child.att('alpha', (opacity / 255).toFixed(2));

        if (onElementCallback)
            onElementCallback(child, aNode);

        displayList.importDocument(child);
    }

    return child;
}

//=====================================================================================
function genBuildId() {
    var magicNumber = Math.floor(Math.random() * 36).toString(36).substr(0, 1);
    var s1 = '0000' + Math.floor(Math.random() * 1679616).toString(36);
    var s2 = '000' + Math.floor(Math.random() * 46656).toString(36);
    var count = 0;
    for (var i = 0; i < 4; i++) {
        var c = Math.floor(Math.random() * 26);
        count += Math.pow(26, i) * (c + 10);
    }
    count += Math.floor(Math.random() * 1000000) + Math.floor(Math.random() * 222640);

    return magicNumber + s1.substr(s1.length - 4) + s2.substr(s2.length - 3) + count.toString(36);
}

function convertToHtmlColor(rgbaArray, includingAlpha) {
    var result = '#';
    var str;
    if (includingAlpha) {
        str = rgbaArray[3].toString(16);
        if (str.length == 1)
            str = '0' + str;
        result += str;
    }

    for (var i = 0; i < 3; i++) {
        str = rgbaArray[i].toString(16);
        if (str.length == 1)
            str = '0' + str;
        result += str;
    }

    return result;
}