'use strict';

var isNumeric = require('fast-isnumeric');

var Lib = require('../../lib');
var Registry = require('../../registry');
var Axes = require('../../plots/cartesian/axes');

var arraysToCalcdata = require('../bar/arrays_to_calcdata');
var binFunctions = require('./bin_functions');
var normFunctions = require('./norm_functions');
var doAvg = require('./average');
var getBinSpanLabelRound = require('./bin_label_vals');

function calc(gd, trace) {
    var pos = [];
    var size = [];
    var isHorizontal = trace.orientation === 'h';
    var pa = Axes.getFromId(gd, isHorizontal ? trace.yaxis : trace.xaxis);
    var mainData = isHorizontal ? 'y' : 'x';
    var counterData = {x: 'y', y: 'x'}[mainData];
    var calendar = trace[mainData + 'calendar'];
    var cumulativeSpec = trace.cumulative;
    var i;

    var binsAndPos = calcAllAutoBins(gd, trace, pa, mainData);
    var binSpec = binsAndPos[0];
    var pos0 = binsAndPos[1];

    var nonuniformBins = typeof binSpec.size === 'string';
    var binEdges = [];
    var bins = nonuniformBins ? binEdges : binSpec;
    // make the empty bin array
    var inc = [];
    var counts = [];
    var inputPoints = [];
    var total = 0;
    var norm = trace.histnorm;
    var func = trace.histfunc;
    var densityNorm = norm.indexOf('density') !== -1;
    var i2, binEnd, n;

    if(cumulativeSpec.enabled && densityNorm) {
        // we treat "cumulative" like it means "integral" if you use a density norm,
        // which in the end means it's the same as without "density"
        norm = norm.replace(/ ?density$/, '');
        densityNorm = false;
    }

    var extremeFunc = func === 'max' || func === 'min';
    var sizeInit = extremeFunc ? null : 0;
    var binFunc = binFunctions.count;
    var normFunc = normFunctions[norm];
    var isAvg = false;
    var pr2c = function(v) { return pa.r2c(v, 0, calendar); };
    var rawCounterData;

    if(Lib.isArrayOrTypedArray(trace[counterData]) && func !== 'count') {
        rawCounterData = trace[counterData];
        isAvg = func === 'avg';
        binFunc = binFunctions[func];
    }

    // create the bins (and any extra arrays needed)
    // assume more than 1e6 bins is an error, so we don't crash the browser
    i = pr2c(binSpec.start);

    // decrease end a little in case of rounding errors
    binEnd = pr2c(binSpec.end) + (i - Axes.tickIncrement(i, binSpec.size, false, calendar)) / 1e6;

    while(i < binEnd && pos.length < 1e6) {
        i2 = Axes.tickIncrement(i, binSpec.size, false, calendar);
        pos.push((i + i2) / 2);
        size.push(sizeInit);
        inputPoints.push([]);
        // nonuniform bins (like months) we need to search,
        // rather than straight calculate the bin we're in
        binEdges.push(i);
        // nonuniform bins also need nonuniform normalization factors
        if(densityNorm) inc.push(1 / (i2 - i));
        if(isAvg) counts.push(0);
        // break to avoid infinite loops
        if(i2 <= i) break;
        i = i2;
    }
    binEdges.push(i);

    // for date axes we need bin bounds to be calcdata. For nonuniform bins
    // we already have this, but uniform with start/end/size they're still strings.
    if(!nonuniformBins && pa.type === 'date') {
        bins = {
            start: pr2c(bins.start),
            end: pr2c(bins.end),
            size: bins.size
        };
    }

    // stash left and right gaps by group
    if(!gd._fullLayout._roundFnOpts) gd._fullLayout._roundFnOpts = {};
    var groupName = trace['_' + mainData + 'bingroup'];
    var roundFnOpts = {leftGap: Infinity, rightGap: Infinity};
    if(groupName) {
        if(!gd._fullLayout._roundFnOpts[groupName]) gd._fullLayout._roundFnOpts[groupName] = roundFnOpts;
        roundFnOpts = gd._fullLayout._roundFnOpts[groupName];
    }

    // bin the data
    // and make histogram-specific pt-number-to-cd-index map object
    var nMax = size.length;
    var uniqueValsPerBin = true;
    var leftGap = roundFnOpts.leftGap;
    var rightGap = roundFnOpts.rightGap;
    var ptNumber2cdIndex = {};
    for(i = 0; i < pos0.length; i++) {
        var posi = pos0[i];
        n = Lib.findBin(posi, bins);
        if(n >= 0 && n < nMax) {
            total += binFunc(n, i, size, rawCounterData, counts);
            if(uniqueValsPerBin && inputPoints[n].length && posi !== pos0[inputPoints[n][0]]) {
                uniqueValsPerBin = false;
            }
            inputPoints[n].push(i);
            ptNumber2cdIndex[i] = n;

            leftGap = Math.min(leftGap, posi - binEdges[n]);
            rightGap = Math.min(rightGap, binEdges[n + 1] - posi);
        }
    }
    roundFnOpts.leftGap = leftGap;
    roundFnOpts.rightGap = rightGap;

    var roundFn;
    if(!uniqueValsPerBin) {
        roundFn = function(v, isRightEdge) {
            return function() {
                var roundFnOpts = gd._fullLayout._roundFnOpts[groupName];
                return getBinSpanLabelRound(
                    roundFnOpts.leftGap,
                    roundFnOpts.rightGap,
                    binEdges, pa, calendar
                )(v, isRightEdge);
            };
        };
    }

    // average and/or normalize the data, if needed
    if(isAvg) total = doAvg(size, counts);
    if(normFunc) normFunc(size, total, inc);

    // after all normalization etc, now we can accumulate if desired
    if(cumulativeSpec.enabled) cdf(size, cumulativeSpec.direction, cumulativeSpec.currentbin);

    var seriesLen = Math.min(pos.length, size.length);
    var cd = [];
    var firstNonzero = 0;
    var lastNonzero = seriesLen - 1;

    // look for empty bins at the ends to remove, so autoscale omits them
    for(i = 0; i < seriesLen; i++) {
        if(size[i]) {
            firstNonzero = i;
            break;
        }
    }
    for(i = seriesLen - 1; i >= firstNonzero; i--) {
        if(size[i]) {
            lastNonzero = i;
            break;
        }
    }

    // create the "calculated data" to plot
    for(i = firstNonzero; i <= lastNonzero; i++) {
        if((isNumeric(pos[i]) && isNumeric(size[i]))) {
            var cdi = {
                p: pos[i],
                s: size[i],
                b: 0
            };

            // setup hover and event data fields,
            // N.B. pts and "hover" positions ph0/ph1 don't seem to make much sense
            // for cumulative distributions
            if(!cumulativeSpec.enabled) {
                cdi.pts = inputPoints[i];
                if(uniqueValsPerBin) {
                    cdi.ph0 = cdi.ph1 = (inputPoints[i].length) ? pos0[inputPoints[i][0]] : pos[i];
                } else {
                    // Defer evaluation of ph(0|1) in crossTraceCalc
                    trace._computePh = true;
                    cdi.ph0 = roundFn(binEdges[i]);
                    cdi.ph1 = roundFn(binEdges[i + 1], true);
                }
            }
            cd.push(cdi);
        }
    }

    if(cd.length === 1) {
        // when we collapse to a single bin, calcdata no longer describes bin size
        // so we need to explicitly specify it
        cd[0].width1 = Axes.tickIncrement(cd[0].p, binSpec.size, false, calendar) - cd[0].p;
    }

    arraysToCalcdata(cd, trace);

    if(Lib.isArrayOrTypedArray(trace.selectedpoints)) {
        Lib.tagSelected(cd, trace, ptNumber2cdIndex);
    }

    return cd;
}

/*
 * calcAllAutoBins: we want all histograms inside the same bingroup
 * (see logic in Histogram.crossTraceDefaults) to share bin specs
 *
 * If the user has explicitly specified differing
 * bin specs, there's nothing we can do, but if possible we will try to use the
 * smallest bins of any of the auto values for all histograms inside the same
 * bingroup.
 */
function calcAllAutoBins(gd, trace, pa, mainData, _overlayEdgeCase) {
    var binAttr = mainData + 'bins';
    var fullLayout = gd._fullLayout;
    var groupName = trace['_' + mainData + 'bingroup'];
    var binOpts = fullLayout._histogramBinOpts[groupName];
    var isOverlay = fullLayout.barmode === 'overlay';
    var i, traces, tracei, calendar, pos0, autoVals, cumulativeSpec;

    var r2c = function(v) { return pa.r2c(v, 0, calendar); };
    var c2r = function(v) { return pa.c2r(v, 0, calendar); };

    var cleanBound = pa.type === 'date' ?
        function(v) { return (v || v === 0) ? Lib.cleanDate(v, null, calendar) : null; } :
        function(v) { return isNumeric(v) ? Number(v) : null; };

    function setBound(attr, bins, newBins) {
        if(bins[attr + 'Found']) {
            bins[attr] = cleanBound(bins[attr]);
            if(bins[attr] === null) bins[attr] = newBins[attr];
        } else {
            autoVals[attr] = bins[attr] = newBins[attr];
            Lib.nestedProperty(traces[0], binAttr + '.' + attr).set(newBins[attr]);
        }
    }

    // all but the first trace in this group has already been marked finished
    // clear this flag, so next time we run calc we will run autobin again
    if(trace['_' + mainData + 'autoBinFinished']) {
        delete trace['_' + mainData + 'autoBinFinished'];
    } else {
        traces = binOpts.traces;
        var allPos = [];

        // Note: we're including `legendonly` traces here for autobin purposes,
        // so that showing & hiding from the legend won't affect bins.
        // But this complicates things a bit since those traces don't `calc`,
        // hence `isFirstVisible`.
        var isFirstVisible = true;
        var has2dMap = false;
        var hasHist2dContour = false;
        for(i = 0; i < traces.length; i++) {
            tracei = traces[i];

            if(tracei.visible) {
                var mainDatai = binOpts.dirs[i];
                pos0 = tracei['_' + mainDatai + 'pos0'] = pa.makeCalcdata(tracei, mainDatai);

                allPos = Lib.concat(allPos, pos0);
                delete tracei['_' + mainData + 'autoBinFinished'];

                if(trace.visible === true) {
                    if(isFirstVisible) {
                        isFirstVisible = false;
                    } else {
                        delete tracei._autoBin;
                        tracei['_' + mainData + 'autoBinFinished'] = 1;
                    }
                    if(Registry.traceIs(tracei, '2dMap')) {
                        has2dMap = true;
                    }
                    if(tracei.type === 'histogram2dcontour') {
                        hasHist2dContour = true;
                    }
                }
            }
        }

        calendar = traces[0][mainData + 'calendar'];
        var newBinSpec = Axes.autoBin(allPos, pa, binOpts.nbins, has2dMap, calendar, binOpts.sizeFound && binOpts.size);

        var autoBin = traces[0]._autoBin = {};
        autoVals = autoBin[binOpts.dirs[0]] = {};

        if(hasHist2dContour) {
            // the "true" 2nd argument reverses the tick direction (which we can't
            // just do with a minus sign because of month bins)
            if(!binOpts.size) {
                newBinSpec.start = c2r(Axes.tickIncrement(
                    r2c(newBinSpec.start), newBinSpec.size, true, calendar));
            }
            if(binOpts.end === undefined) {
                newBinSpec.end = c2r(Axes.tickIncrement(
                    r2c(newBinSpec.end), newBinSpec.size, false, calendar));
            }
        }

        // Edge case: single-valued histogram overlaying others
        // Use them all together to calculate the bin size for the single-valued one
        // Don't re-calculate bin width if user manually specified it (checing in bingroup=='' or xbins is defined)
        if(isOverlay && !Registry.traceIs(trace, '2dMap') && newBinSpec._dataSpan === 0 &&
        pa.type !== 'category' && pa.type !== 'multicategory' &&
        trace.bingroup === '' && (typeof trace.xbins === 'undefined')) {
            // Several single-valued histograms! Stop infinite recursion,
            // just return an extra flag that tells handleSingleValueOverlays
            // to sort out this trace too
            if(_overlayEdgeCase) return [newBinSpec, pos0, true];

            newBinSpec = handleSingleValueOverlays(gd, trace, pa, mainData, binAttr);
        }

        // adjust for CDF edge cases
        cumulativeSpec = tracei.cumulative || {};
        if(cumulativeSpec.enabled && (cumulativeSpec.currentbin !== 'include')) {
            if(cumulativeSpec.direction === 'decreasing') {
                newBinSpec.start = c2r(Axes.tickIncrement(
                    r2c(newBinSpec.start), newBinSpec.size, true, calendar));
            } else {
                newBinSpec.end = c2r(Axes.tickIncrement(
                    r2c(newBinSpec.end), newBinSpec.size, false, calendar));
            }
        }

        binOpts.size = newBinSpec.size;
        if(!binOpts.sizeFound) {
            autoVals.size = newBinSpec.size;
            Lib.nestedProperty(traces[0], binAttr + '.size').set(newBinSpec.size);
        }

        setBound('start', binOpts, newBinSpec);
        setBound('end', binOpts, newBinSpec);
    }

    pos0 = trace['_' + mainData + 'pos0'];
    delete trace['_' + mainData + 'pos0'];

    // Each trace can specify its own start/end, or if omitted
    // we ensure they're beyond the bounds of this trace's data,
    // and we need to make sure start is aligned with the main start
    var traceInputBins = trace._input[binAttr] || {};
    var traceBinOptsCalc = Lib.extendFlat({}, binOpts);
    var mainStart = binOpts.start;
    var startIn = pa.r2l(traceInputBins.start);
    var hasStart = startIn !== undefined;
    if((binOpts.startFound || hasStart) && startIn !== pa.r2l(mainStart)) {
        // We have an explicit start to reconcile across traces
        // if this trace has an explicit start, shift it down to a bin edge
        // if another trace had an explicit start, shift it down to a
        // bin edge past our data
        var traceStart = hasStart ?
            startIn :
            Lib.aggNums(Math.min, null, pos0);

        var dummyAx = {
            type: (pa.type === 'category' || pa.type === 'multicategory') ? 'linear' : pa.type,
            r2l: pa.r2l,
            dtick: binOpts.size,
            tick0: mainStart,
            calendar: calendar,
            range: ([traceStart, Axes.tickIncrement(traceStart, binOpts.size, false, calendar)]).map(pa.l2r)
        };
        var newStart = Axes.tickFirst(dummyAx);
        if(newStart > pa.r2l(traceStart)) {
            newStart = Axes.tickIncrement(newStart, binOpts.size, true, calendar);
        }
        traceBinOptsCalc.start = pa.l2r(newStart);
        if(!hasStart) Lib.nestedProperty(trace, binAttr + '.start').set(traceBinOptsCalc.start);
    }

    var mainEnd = binOpts.end;
    var endIn = pa.r2l(traceInputBins.end);
    var hasEnd = endIn !== undefined;
    if((binOpts.endFound || hasEnd) && endIn !== pa.r2l(mainEnd)) {
        // Reconciling an explicit end is easier, as it doesn't need to
        // match bin edges
        var traceEnd = hasEnd ?
            endIn :
            Lib.aggNums(Math.max, null, pos0);

        traceBinOptsCalc.end = pa.l2r(traceEnd);
        if(!hasEnd) Lib.nestedProperty(trace, binAttr + '.start').set(traceBinOptsCalc.end);
    }

    // Backward compatibility for one-time autobinning.
    // autobin: true is handled in cleanData, but autobin: false
    // needs to be here where we have determined the values.
    var autoBinAttr = 'autobin' + mainData;
    if(trace._input[autoBinAttr] === false) {
        trace._input[binAttr] = Lib.extendFlat({}, trace[binAttr] || {});
        delete trace._input[autoBinAttr];
        delete trace[autoBinAttr];
    }

    return [traceBinOptsCalc, pos0];
}

/*
 * Adjust single-value histograms in overlay mode to make as good a
 * guess as we can at autobin values the user would like.
 *
 * Returns the binSpec for the trace that sparked all this
 */
function handleSingleValueOverlays(gd, trace, pa, mainData, binAttr) {
    var fullLayout = gd._fullLayout;
    var overlaidTraceGroup = getConnectedHistograms(gd, trace);
    var pastThisTrace = false;
    var minSize = Infinity;
    var singleValuedTraces = [trace];
    var i, tracei, binOpts;

    // first collect all the:
    // - min bin size from all multi-valued traces
    // - single-valued traces
    for(i = 0; i < overlaidTraceGroup.length; i++) {
        tracei = overlaidTraceGroup[i];

        if(tracei === trace) {
            pastThisTrace = true;
        } else if(!pastThisTrace) {
            // This trace has already had its autobins calculated, so either:
            // - it is part of a bingroup
            // - it is NOT a single-valued trace
            binOpts = fullLayout._histogramBinOpts[tracei['_' + mainData + 'bingroup']];
            minSize = Math.min(minSize, binOpts.size || tracei[binAttr].size);
        } else {
            var resulti = calcAllAutoBins(gd, tracei, pa, mainData, true);
            var binSpeci = resulti[0];
            var isSingleValued = resulti[2];

            // so we can use this result when we get to tracei in the normal
            // course of events, mark it as done and put _pos0 back
            tracei['_' + mainData + 'autoBinFinished'] = 1;
            tracei['_' + mainData + 'pos0'] = resulti[1];

            if(isSingleValued) {
                singleValuedTraces.push(tracei);
            } else {
                minSize = Math.min(minSize, binSpeci.size);
            }
        }
    }

    // find the real data values for each single-valued trace
    // hunt through pos0 for the first valid value
    var dataVals = new Array(singleValuedTraces.length);
    for(i = 0; i < singleValuedTraces.length; i++) {
        var pos0 = singleValuedTraces[i]['_' + mainData + 'pos0'];
        for(var j = 0; j < pos0.length; j++) {
            if(pos0[j] !== undefined) {
                dataVals[i] = pos0[j];
                break;
            }
        }
    }

    // are ALL traces are single-valued? use the min difference between
    // all of their values (which defaults to 1 if there's still only one)
    if(!isFinite(minSize)) {
        minSize = Lib.distinctVals(dataVals).minDiff;
    }

    // now apply the min size we found to all single-valued traces
    for(i = 0; i < singleValuedTraces.length; i++) {
        tracei = singleValuedTraces[i];
        var calendar = tracei[mainData + 'calendar'];

        var newBins = {
            start: pa.c2r(dataVals[i] - minSize / 2, 0, calendar),
            end: pa.c2r(dataVals[i] + minSize / 2, 0, calendar),
            size: minSize
        };

        tracei._input[binAttr] = tracei[binAttr] = newBins;

        binOpts = fullLayout._histogramBinOpts[tracei['_' + mainData + 'bingroup']];
        if(binOpts) Lib.extendFlat(binOpts, newBins);
    }

    return trace[binAttr];
}

/*
 * Return an array of histograms that share axes and orientation.
 *
 * Only considers histograms. In principle we could include bars in a
 * similar way to how we do manually binned histograms, though this
 * would have tons of edge cases and value judgments to make.
 */
function getConnectedHistograms(gd, trace) {
    var xid = trace.xaxis;
    var yid = trace.yaxis;
    var orientation = trace.orientation;

    var out = [];
    var fullData = gd._fullData;
    for(var i = 0; i < fullData.length; i++) {
        var tracei = fullData[i];
        if(tracei.type === 'histogram' &&
            tracei.visible === true &&
            tracei.orientation === orientation &&
            tracei.xaxis === xid && tracei.yaxis === yid
        ) {
            out.push(tracei);
        }
    }

    return out;
}

function cdf(size, direction, currentBin) {
    var i, vi, prevSum;

    function firstHalfPoint(i) {
        prevSum = size[i];
        size[i] /= 2;
    }

    function nextHalfPoint(i) {
        vi = size[i];
        size[i] = prevSum + vi / 2;
        prevSum += vi;
    }

    if(currentBin === 'half') {
        if(direction === 'increasing') {
            firstHalfPoint(0);
            for(i = 1; i < size.length; i++) {
                nextHalfPoint(i);
            }
        } else {
            firstHalfPoint(size.length - 1);
            for(i = size.length - 2; i >= 0; i--) {
                nextHalfPoint(i);
            }
        }
    } else if(direction === 'increasing') {
        for(i = 1; i < size.length; i++) {
            size[i] += size[i - 1];
        }

        // 'exclude' is identical to 'include' just shifted one bin over
        if(currentBin === 'exclude') {
            size.unshift(0);
            size.pop();
        }
    } else {
        for(i = size.length - 2; i >= 0; i--) {
            size[i] += size[i + 1];
        }

        if(currentBin === 'exclude') {
            size.push(0);
            size.shift();
        }
    }
}

module.exports = {
    calc: calc,
    calcAllAutoBins: calcAllAutoBins
};
