'use strict';

var Lib = require('../../lib');
var axisIds = require('../../plots/cartesian/axis_ids');

var traceIs = require('../../registry').traceIs;
var handleGroupingDefaults = require('../scatter/grouping_defaults');
var validateCornerradius = require('../bar/defaults').validateCornerradius;

var nestedProperty = Lib.nestedProperty;
var getAxisGroup = require('../../plots/cartesian/constraints').getAxisGroup;

var BINATTRS = [
    {aStr: {x: 'xbins.start', y: 'ybins.start'}, name: 'start'},
    {aStr: {x: 'xbins.end', y: 'ybins.end'}, name: 'end'},
    {aStr: {x: 'xbins.size', y: 'ybins.size'}, name: 'size'},
    {aStr: {x: 'nbinsx', y: 'nbinsy'}, name: 'nbins'}
];

var BINDIRECTIONS = ['x', 'y'];

// handle bin attrs and relink auto-determined values so fullData is complete
module.exports = function crossTraceDefaults(fullData, fullLayout) {
    var allBinOpts = fullLayout._histogramBinOpts = {};
    var histTraces = [];
    var mustMatchTracesLookup = {};
    var otherTracesList = [];

    var traceOut, traces, groupName, binDir;
    var i, j, k;

    function coerce(attr, dflt) {
        return Lib.coerce(traceOut._input, traceOut, traceOut._module.attributes, attr, dflt);
    }

    function orientation2binDir(traceOut) {
        return traceOut.orientation === 'v' ? 'x' : 'y';
    }

    function getAxisType(traceOut, binDir) {
        var ax = axisIds.getFromTrace({_fullLayout: fullLayout}, traceOut, binDir);
        return ax.type;
    }

    function fillBinOpts(traceOut, groupName, binDir) {
        // N.B. group traces that don't have a bingroup with themselves
        var fallbackGroupName = traceOut.uid + '__' + binDir;
        if(!groupName) groupName = fallbackGroupName;

        var axType = getAxisType(traceOut, binDir);
        var calendar = traceOut[binDir + 'calendar'] || '';
        var binOpts = allBinOpts[groupName];
        var needsNewItem = true;

        if(binOpts) {
            if(axType === binOpts.axType && calendar === binOpts.calendar) {
                needsNewItem = false;
                binOpts.traces.push(traceOut);
                binOpts.dirs.push(binDir);
            } else {
                groupName = fallbackGroupName;

                if(axType !== binOpts.axType) {
                    Lib.warn([
                        'Attempted to group the bins of trace', traceOut.index,
                        'set on a', 'type:' + axType, 'axis',
                        'with bins on', 'type:' + binOpts.axType, 'axis.'
                    ].join(' '));
                }
                if(calendar !== binOpts.calendar) {
                    // prohibit bingroup for traces using different calendar,
                    // there's probably a way to make this work, but skip for now
                    Lib.warn([
                        'Attempted to group the bins of trace', traceOut.index,
                        'set with a', calendar, 'calendar',
                        'with bins',
                        (binOpts.calendar ? 'on a ' + binOpts.calendar + ' calendar' : 'w/o a set calendar')
                    ].join(' '));
                }
            }
        }

        if(needsNewItem) {
            allBinOpts[groupName] = {
                traces: [traceOut],
                dirs: [binDir],
                axType: axType,
                calendar: traceOut[binDir + 'calendar'] || ''
            };
        }
        traceOut['_' + binDir + 'bingroup'] = groupName;
    }

    for(i = 0; i < fullData.length; i++) {
        traceOut = fullData[i];

        if(traceIs(traceOut, 'histogram')) {
            histTraces.push(traceOut);

            // TODO: this shouldn't be relinked as it's only used within calc
            // https://github.com/plotly/plotly.js/issues/749
            delete traceOut._xautoBinFinished;
            delete traceOut._yautoBinFinished;

            if(traceOut.type === 'histogram') {
                var r = coerce('marker.cornerradius', fullLayout.barcornerradius);
                if(traceOut.marker) {
                    traceOut.marker.cornerradius = validateCornerradius(r);
                }
            }

            // N.B. need to coerce *alignmentgroup* before *bingroup*, as traces
            // in same alignmentgroup "have to match"
            if(!traceIs(traceOut, '2dMap')) {
                handleGroupingDefaults(traceOut._input, traceOut, fullLayout, coerce);
            }
        }
    }

    var alignmentOpts = fullLayout._alignmentOpts || {};

    // Look for traces that "have to match", that is:
    // - 1d histogram traces on the same subplot with same orientation under barmode:stack,
    // - 1d histogram traces on the same subplot with same orientation under barmode:group
    // - 1d histogram traces on the same position axis with the same orientation
    //   and the same *alignmentgroup* (coerced under barmode:group)
    // - Once `stackgroup` gets implemented (see https://github.com/plotly/plotly.js/issues/3614),
    //   traces within the same stackgroup will also "have to match"
    for(i = 0; i < histTraces.length; i++) {
        traceOut = histTraces[i];
        groupName = '';

        if(!traceIs(traceOut, '2dMap')) {
            binDir = orientation2binDir(traceOut);

            if(fullLayout.barmode === 'group' && traceOut.alignmentgroup) {
                var pa = traceOut[binDir + 'axis'];
                var aGroupId = getAxisGroup(fullLayout, pa) + traceOut.orientation;
                if((alignmentOpts[aGroupId] || {})[traceOut.alignmentgroup]) {
                    groupName = aGroupId;
                }
            }

            if(!groupName && fullLayout.barmode !== 'overlay') {
                groupName = (
                    getAxisGroup(fullLayout, traceOut.xaxis) +
                    getAxisGroup(fullLayout, traceOut.yaxis) +
                    orientation2binDir(traceOut)
                );
            }
        }

        if(groupName) {
            if(!mustMatchTracesLookup[groupName]) {
                mustMatchTracesLookup[groupName] = [];
            }
            mustMatchTracesLookup[groupName].push(traceOut);
        } else {
            otherTracesList.push(traceOut);
        }
    }

    // Setup binOpts for traces that have to match,
    // if the traces have a valid bingroup, use that
    // if not use axis+binDir groupName
    for(groupName in mustMatchTracesLookup) {
        traces = mustMatchTracesLookup[groupName];

        // no need to 'force' anything when a single
        // trace is detected as "must match"
        if(traces.length === 1) {
            otherTracesList.push(traces[0]);
            continue;
        }

        var binGroupFound = false;
        if(traces.length) {
            traceOut = traces[0];
            binGroupFound = coerce('bingroup');
        }

        groupName = binGroupFound || groupName;

        for(i = 0; i < traces.length; i++) {
            traceOut = traces[i];
            var bingroupIn = traceOut._input.bingroup;
            if(bingroupIn && bingroupIn !== groupName) {
                Lib.warn([
                    'Trace', traceOut.index, 'must match',
                    'within bingroup', groupName + '.',
                    'Ignoring its bingroup:', bingroupIn, 'setting.'
                ].join(' '));
            }
            traceOut.bingroup = groupName;

            // N.B. no need to worry about 2dMap case
            // (where both bin direction are set in each trace)
            // as 2dMap trace never "have to match"
            fillBinOpts(traceOut, groupName, orientation2binDir(traceOut));
        }
    }

    // setup binOpts for traces that can but don't have to match,
    // notice that these traces can be matched with traces that have to match
    for(i = 0; i < otherTracesList.length; i++) {
        traceOut = otherTracesList[i];

        var binGroup = coerce('bingroup');

        if(traceIs(traceOut, '2dMap')) {
            for(k = 0; k < 2; k++) {
                binDir = BINDIRECTIONS[k];
                var binGroupInDir = coerce(binDir + 'bingroup',
                    binGroup ? binGroup + '__' + binDir : null
                );
                fillBinOpts(traceOut, binGroupInDir, binDir);
            }
        } else {
            fillBinOpts(traceOut, binGroup, orientation2binDir(traceOut));
        }
    }

    // coerce bin attrs!
    for(groupName in allBinOpts) {
        var binOpts = allBinOpts[groupName];
        traces = binOpts.traces;

        for(j = 0; j < BINATTRS.length; j++) {
            var attrSpec = BINATTRS[j];
            var attr = attrSpec.name;
            var aStr;
            var autoVals;

            // nbins(x|y) is moot if we have a size. This depends on
            // nbins coming after size in binAttrs.
            if(attr === 'nbins' && binOpts.sizeFound) continue;

            for(i = 0; i < traces.length; i++) {
                traceOut = traces[i];
                binDir = binOpts.dirs[i];
                aStr = attrSpec.aStr[binDir];

                if(nestedProperty(traceOut._input, aStr).get() !== undefined) {
                    binOpts[attr] = coerce(aStr);
                    binOpts[attr + 'Found'] = true;
                    break;
                }

                autoVals = (traceOut._autoBin || {})[binDir] || {};
                if(autoVals[attr]) {
                    // if this is the *first* autoval
                    nestedProperty(traceOut, aStr).set(autoVals[attr]);
                }
            }

            // start and end we need to coerce anyway, after having collected the
            // first of each into binOpts, in case a trace wants to restrict its
            // data to a certain range
            if(attr === 'start' || attr === 'end') {
                for(; i < traces.length; i++) {
                    traceOut = traces[i];
                    if(traceOut['_' + binDir + 'bingroup']) {
                        autoVals = (traceOut._autoBin || {})[binDir] || {};
                        coerce(aStr, autoVals[attr]);
                    }
                }
            }

            if(attr === 'nbins' && !binOpts.sizeFound && !binOpts.nbinsFound) {
                traceOut = traces[0];
                binOpts[attr] = coerce(aStr);
            }
        }
    }
};
