'use strict';

var Lib = require('../lib');
var isPlainObject = Lib.isPlainObject;
var PlotSchema = require('./plot_schema');
var Plots = require('../plots/plots');
var plotAttributes = require('../plots/attributes');
var Template = require('./plot_template');
var dfltConfig = require('./plot_config').dfltConfig;

/**
 * Plotly.makeTemplate: create a template off an existing figure to reuse
 * style attributes on other figures.
 *
 * Note: separated from the rest of templates because otherwise we get circular
 * references due to PlotSchema.
 *
 * @param {object|DOM element|string} figure: The figure to base the template on
 *     should contain a trace array `figure.data`
 *     and a layout object `figure.layout`
 * @returns {object} template: the extracted template - can then be used as
 *     `layout.template` in another figure.
 */
exports.makeTemplate = function(figure) {
    figure = Lib.isPlainObject(figure) ? figure : Lib.getGraphDiv(figure);
    figure = Lib.extendDeep({_context: dfltConfig}, {data: figure.data, layout: figure.layout});
    Plots.supplyDefaults(figure);
    var data = figure.data || [];
    var layout = figure.layout || {};
    // copy over a few items to help follow the schema
    layout._basePlotModules = figure._fullLayout._basePlotModules;
    layout._modules = figure._fullLayout._modules;

    var template = {
        data: {},
        layout: {}
    };

    /*
     * Note: we do NOT validate template values, we just take what's in the
     * user inputs data and layout, not the validated values in fullData and
     * fullLayout. Even if we were to validate here, there's no guarantee that
     * these values would still be valid when applied to a new figure, which
     * may contain different trace modes, different axes, etc. So it's
     * important that when applying a template we still validate the template
     * values, rather than just using them as defaults.
     */

    data.forEach(function(trace) {
        // TODO: What if no style info is extracted for this trace. We may
        // not want an empty object as the null value.
        // TODO: allow transforms to contribute to templates?
        // as it stands they are ignored, which may be for the best...

        var traceTemplate = {};
        walkStyleKeys(trace, traceTemplate, getTraceInfo.bind(null, trace));

        var traceType = Lib.coerce(trace, {}, plotAttributes, 'type');
        var typeTemplates = template.data[traceType];
        if(!typeTemplates) typeTemplates = template.data[traceType] = [];
        typeTemplates.push(traceTemplate);
    });

    walkStyleKeys(layout, template.layout, getLayoutInfo.bind(null, layout));

    /*
     * Compose the new template with an existing one to the same effect
     *
     * NOTE: there's a possibility of slightly different behavior: if the plot
     * has an invalid value and the old template has a valid value for the same
     * attribute, the plot will use the old template value but this routine
     * will pull the invalid value (resulting in the original default).
     * In the general case it's not possible to solve this with a single value,
     * since valid options can be context-dependent. It could be solved with
     * a *list* of values, but that would be huge complexity for little gain.
     */
    delete template.layout.template;
    var oldTemplate = layout.template;
    if(isPlainObject(oldTemplate)) {
        var oldLayoutTemplate = oldTemplate.layout;

        var i, traceType, oldTypeTemplates, oldTypeLen, typeTemplates, typeLen;

        if(isPlainObject(oldLayoutTemplate)) {
            mergeTemplates(oldLayoutTemplate, template.layout);
        }
        var oldDataTemplate = oldTemplate.data;
        if(isPlainObject(oldDataTemplate)) {
            for(traceType in template.data) {
                oldTypeTemplates = oldDataTemplate[traceType];
                if(Array.isArray(oldTypeTemplates)) {
                    typeTemplates = template.data[traceType];
                    typeLen = typeTemplates.length;
                    oldTypeLen = oldTypeTemplates.length;
                    for(i = 0; i < typeLen; i++) {
                        mergeTemplates(oldTypeTemplates[i % oldTypeLen], typeTemplates[i]);
                    }
                    for(i = typeLen; i < oldTypeLen; i++) {
                        typeTemplates.push(Lib.extendDeep({}, oldTypeTemplates[i]));
                    }
                }
            }
            for(traceType in oldDataTemplate) {
                if(!(traceType in template.data)) {
                    template.data[traceType] = Lib.extendDeep([], oldDataTemplate[traceType]);
                }
            }
        }
    }

    return template;
};

function mergeTemplates(oldTemplate, newTemplate) {
    // we don't care about speed here, just make sure we have a totally
    // distinct object from the previous template
    oldTemplate = Lib.extendDeep({}, oldTemplate);

    // sort keys so we always get annotationdefaults before annotations etc
    // so arrayTemplater will work right
    var oldKeys = Object.keys(oldTemplate).sort();
    var i, j;

    function mergeOne(oldVal, newVal, key) {
        if(isPlainObject(newVal) && isPlainObject(oldVal)) {
            mergeTemplates(oldVal, newVal);
        } else if(Array.isArray(newVal) && Array.isArray(oldVal)) {
            // Note: omitted `inclusionAttr` from arrayTemplater here,
            // it's irrelevant as we only want the resulting `_template`.
            var templater = Template.arrayTemplater({_template: oldTemplate}, key);
            for(j = 0; j < newVal.length; j++) {
                var item = newVal[j];
                var oldItem = templater.newItem(item)._template;
                if(oldItem) mergeTemplates(oldItem, item);
            }
            var defaultItems = templater.defaultItems();
            for(j = 0; j < defaultItems.length; j++) newVal.push(defaultItems[j]._template);

            // templateitemname only applies to receiving plots
            for(j = 0; j < newVal.length; j++) delete newVal[j].templateitemname;
        }
    }

    for(i = 0; i < oldKeys.length; i++) {
        var key = oldKeys[i];
        var oldVal = oldTemplate[key];
        if(key in newTemplate) {
            mergeOne(oldVal, newTemplate[key], key);
        } else newTemplate[key] = oldVal;

        // if this is a base key from the old template (eg xaxis), look for
        // extended keys (eg xaxis2) in the new template to merge into
        if(getBaseKey(key) === key) {
            for(var key2 in newTemplate) {
                var baseKey2 = getBaseKey(key2);
                if(key2 !== baseKey2 && baseKey2 === key && !(key2 in oldTemplate)) {
                    mergeOne(oldVal, newTemplate[key2], key);
                }
            }
        }
    }
}

function getBaseKey(key) {
    return key.replace(/[0-9]+$/, '');
}

function walkStyleKeys(parent, templateOut, getAttributeInfo, path, basePath) {
    var pathAttr = basePath && getAttributeInfo(basePath);
    for(var key in parent) {
        var child = parent[key];
        var nextPath = getNextPath(parent, key, path);
        var nextBasePath = getNextPath(parent, key, basePath);
        var attr = getAttributeInfo(nextBasePath);
        if(!attr) {
            var baseKey = getBaseKey(key);
            if(baseKey !== key) {
                nextBasePath = getNextPath(parent, baseKey, basePath);
                attr = getAttributeInfo(nextBasePath);
            }
        }

        // we'll get an attr if path starts with a valid part, then has an
        // invalid ending. Make sure we got all the way to the end.
        if(pathAttr && (pathAttr === attr)) continue;

        if(!attr || attr._noTemplating ||
            attr.valType === 'data_array' ||
            (attr.arrayOk && Array.isArray(child))
        ) {
            continue;
        }

        if(!attr.valType && isPlainObject(child)) {
            walkStyleKeys(child, templateOut, getAttributeInfo, nextPath, nextBasePath);
        } else if(attr._isLinkedToArray && Array.isArray(child)) {
            var dfltDone = false;
            var namedIndex = 0;
            var usedNames = {};
            for(var i = 0; i < child.length; i++) {
                var item = child[i];
                if(isPlainObject(item)) {
                    var name = item.name;
                    if(name) {
                        if(!usedNames[name]) {
                            // named array items: allow all attributes except data arrays
                            walkStyleKeys(item, templateOut, getAttributeInfo,
                                getNextPath(child, namedIndex, nextPath),
                                getNextPath(child, namedIndex, nextBasePath));
                            namedIndex++;
                            usedNames[name] = 1;
                        }
                    } else if(!dfltDone) {
                        var dfltKey = Template.arrayDefaultKey(key);
                        var dfltPath = getNextPath(parent, dfltKey, path);

                        // getAttributeInfo will fail if we try to use dfltKey directly.
                        // Instead put this item into the next array element, then
                        // pull it out and move it to dfltKey.
                        var pathInArray = getNextPath(child, namedIndex, nextPath);
                        walkStyleKeys(item, templateOut, getAttributeInfo, pathInArray,
                            getNextPath(child, namedIndex, nextBasePath));
                        var itemPropInArray = Lib.nestedProperty(templateOut, pathInArray);
                        var dfltProp = Lib.nestedProperty(templateOut, dfltPath);
                        dfltProp.set(itemPropInArray.get());
                        itemPropInArray.set(null);

                        dfltDone = true;
                    }
                }
            }
        } else {
            var templateProp = Lib.nestedProperty(templateOut, nextPath);
            templateProp.set(child);
        }
    }
}

function getLayoutInfo(layout, path) {
    return PlotSchema.getLayoutValObject(
        layout, Lib.nestedProperty({}, path).parts
    );
}

function getTraceInfo(trace, path) {
    return PlotSchema.getTraceValObject(
        trace, Lib.nestedProperty({}, path).parts
    );
}

function getNextPath(parent, key, path) {
    var nextPath;
    if(!path) nextPath = key;
    else if(Array.isArray(parent)) nextPath = path + '[' + key + ']';
    else nextPath = path + '.' + key;

    return nextPath;
}

/**
 * validateTemplate: Test for consistency between the given figure and
 * a template, either already included in the figure or given separately.
 * Note that not every issue we identify here is necessarily a problem,
 * it depends on what you're using the template for.
 *
 * @param {object|DOM element} figure: the plot, with {data, layout} members,
 *     to test the template against
 * @param {Optional(object)} template: the template, with its own {data, layout},
 *     to test. If omitted, we will look for a template already attached as the
 *     plot's `layout.template` attribute.
 *
 * @returns {array} array of error objects each containing:
 *  - {string} code
 *      error code ('missing', 'unused', 'reused', 'noLayout', 'noData')
 *  - {string} msg
 *      a full readable description of the issue.
 */
exports.validateTemplate = function(figureIn, template) {
    var figure = Lib.extendDeep({}, {
        _context: dfltConfig,
        data: figureIn.data,
        layout: figureIn.layout
    });
    var layout = figure.layout || {};
    if(!isPlainObject(template)) template = layout.template || {};
    var layoutTemplate = template.layout;
    var dataTemplate = template.data;
    var errorList = [];

    figure.layout = layout;
    figure.layout.template = template;
    Plots.supplyDefaults(figure);

    var fullLayout = figure._fullLayout;
    var fullData = figure._fullData;

    var layoutPaths = {};
    function crawlLayoutForContainers(obj, paths) {
        for(var key in obj) {
            if(key.charAt(0) !== '_' && isPlainObject(obj[key])) {
                var baseKey = getBaseKey(key);
                var nextPaths = [];
                var i;
                for(i = 0; i < paths.length; i++) {
                    nextPaths.push(getNextPath(obj, key, paths[i]));
                    if(baseKey !== key) nextPaths.push(getNextPath(obj, baseKey, paths[i]));
                }
                for(i = 0; i < nextPaths.length; i++) {
                    layoutPaths[nextPaths[i]] = 1;
                }
                crawlLayoutForContainers(obj[key], nextPaths);
            }
        }
    }

    function crawlLayoutTemplateForContainers(obj, path) {
        for(var key in obj) {
            if(key.indexOf('defaults') === -1 && isPlainObject(obj[key])) {
                var nextPath = getNextPath(obj, key, path);
                if(layoutPaths[nextPath]) {
                    crawlLayoutTemplateForContainers(obj[key], nextPath);
                } else {
                    errorList.push({code: 'unused', path: nextPath});
                }
            }
        }
    }

    if(!isPlainObject(layoutTemplate)) {
        errorList.push({code: 'layout'});
    } else {
        crawlLayoutForContainers(fullLayout, ['layout']);
        crawlLayoutTemplateForContainers(layoutTemplate, 'layout');
    }

    if(!isPlainObject(dataTemplate)) {
        errorList.push({code: 'data'});
    } else {
        var typeCount = {};
        var traceType;
        for(var i = 0; i < fullData.length; i++) {
            var fullTrace = fullData[i];
            traceType = fullTrace.type;
            typeCount[traceType] = (typeCount[traceType] || 0) + 1;
            if(!fullTrace._fullInput._template) {
                // this takes care of the case of traceType in the data but not
                // the template
                errorList.push({
                    code: 'missing',
                    index: fullTrace._fullInput.index,
                    traceType: traceType
                });
            }
        }
        for(traceType in dataTemplate) {
            var templateCount = dataTemplate[traceType].length;
            var dataCount = typeCount[traceType] || 0;
            if(templateCount > dataCount) {
                errorList.push({
                    code: 'unused',
                    traceType: traceType,
                    templateCount: templateCount,
                    dataCount: dataCount
                });
            } else if(dataCount > templateCount) {
                errorList.push({
                    code: 'reused',
                    traceType: traceType,
                    templateCount: templateCount,
                    dataCount: dataCount
                });
            }
        }
    }

    // _template: false is when someone tried to modify an array item
    // but there was no template with matching name
    function crawlForMissingTemplates(obj, path) {
        for(var key in obj) {
            if(key.charAt(0) === '_') continue;
            var val = obj[key];
            var nextPath = getNextPath(obj, key, path);
            if(isPlainObject(val)) {
                if(Array.isArray(obj) && val._template === false && val.templateitemname) {
                    errorList.push({
                        code: 'missing',
                        path: nextPath,
                        templateitemname: val.templateitemname
                    });
                }
                crawlForMissingTemplates(val, nextPath);
            } else if(Array.isArray(val) && hasPlainObject(val)) {
                crawlForMissingTemplates(val, nextPath);
            }
        }
    }
    crawlForMissingTemplates({data: fullData, layout: fullLayout}, '');

    if(errorList.length) return errorList.map(format);
};

function hasPlainObject(arr) {
    for(var i = 0; i < arr.length; i++) {
        if(isPlainObject(arr[i])) return true;
    }
}

function format(opts) {
    var msg;
    switch(opts.code) {
        case 'data':
            msg = 'The template has no key data.';
            break;
        case 'layout':
            msg = 'The template has no key layout.';
            break;
        case 'missing':
            if(opts.path) {
                msg = 'There are no templates for item ' + opts.path +
                    ' with name ' + opts.templateitemname;
            } else {
                msg = 'There are no templates for trace ' + opts.index +
                    ', of type ' + opts.traceType + '.';
            }
            break;
        case 'unused':
            if(opts.path) {
                msg = 'The template item at ' + opts.path +
                    ' was not used in constructing the plot.';
            } else if(opts.dataCount) {
                msg = 'Some of the templates of type ' + opts.traceType +
                    ' were not used. The template has ' + opts.templateCount +
                    ' traces, the data only has ' + opts.dataCount +
                    ' of this type.';
            } else {
                msg = 'The template has ' + opts.templateCount +
                    ' traces of type ' + opts.traceType +
                    ' but there are none in the data.';
            }
            break;
        case 'reused':
            msg = 'Some of the templates of type ' + opts.traceType +
                ' were used more than once. The template has ' +
                opts.templateCount + ' traces, the data has ' +
                opts.dataCount + ' of this type.';
            break;
    }
    opts.msg = msg;

    return opts;
}
