'use strict';

var isNumeric = require('fast-isnumeric');
var Lib = require('../../lib');

var Axes = require('../../plots/cartesian/axes');
var alignPeriod = require('../../plots/cartesian/align_period');
var BADNUM = require('../../constants/numerical').BADNUM;

var subTypes = require('./subtypes');
var calcColorscale = require('./colorscale_calc');
var arraysToCalcdata = require('./arrays_to_calcdata');
var calcSelection = require('./calc_selection');

function calc(gd, trace) {
    var fullLayout = gd._fullLayout;
    var xa = trace._xA = Axes.getFromId(gd, trace.xaxis || 'x', 'x');
    var ya = trace._yA = Axes.getFromId(gd, trace.yaxis || 'y', 'y');
    var origX = xa.makeCalcdata(trace, 'x');
    var origY = ya.makeCalcdata(trace, 'y');
    var xObj = alignPeriod(trace, xa, 'x', origX);
    var yObj = alignPeriod(trace, ya, 'y', origY);
    var x = xObj.vals;
    var y = yObj.vals;

    var serieslen = trace._length;
    var cd = new Array(serieslen);
    var ids = trace.ids;
    var stackGroupOpts = getStackOpts(trace, fullLayout, xa, ya);
    var interpolateGaps = false;
    var isV, i, j, k, interpolate, vali;

    setFirstScatter(fullLayout, trace);

    var xAttr = 'x';
    var yAttr = 'y';
    var posAttr;
    if(stackGroupOpts) {
        Lib.pushUnique(stackGroupOpts.traceIndices, trace._expandedIndex);
        isV = stackGroupOpts.orientation === 'v';

        // size, like we use for bar
        if(isV) {
            yAttr = 's';
            posAttr = 'x';
        } else {
            xAttr = 's';
            posAttr = 'y';
        }
        interpolate = stackGroupOpts.stackgaps === 'interpolate';
    } else {
        var ppad = calcMarkerSize(trace, serieslen);
        calcAxisExpansion(gd, trace, xa, ya, x, y, ppad);
    }

    var hasPeriodX = !!trace.xperiodalignment;
    var hasPeriodY = !!trace.yperiodalignment;

    for(i = 0; i < serieslen; i++) {
        var cdi = cd[i] = {};
        var xValid = isNumeric(x[i]);
        var yValid = isNumeric(y[i]);
        if(xValid && yValid) {
            cdi[xAttr] = x[i];
            cdi[yAttr] = y[i];

            if(hasPeriodX) {
                cdi.orig_x = origX[i]; // used by hover
                cdi.xEnd = xObj.ends[i];
                cdi.xStart = xObj.starts[i];
            }
            if(hasPeriodY) {
                cdi.orig_y = origY[i]; // used by hover
                cdi.yEnd = yObj.ends[i];
                cdi.yStart = yObj.starts[i];
            }
        } else if(stackGroupOpts && (isV ? xValid : yValid)) {
            // if we're stacking we need to hold on to all valid positions
            // even with invalid sizes

            cdi[posAttr] = isV ? x[i] : y[i];
            cdi.gap = true;
            if(interpolate) {
                cdi.s = BADNUM;
                interpolateGaps = true;
            } else {
                cdi.s = 0;
            }
        } else {
            cdi[xAttr] = cdi[yAttr] = BADNUM;
        }

        if(ids) {
            cdi.id = String(ids[i]);
        }
    }

    arraysToCalcdata(cd, trace);
    calcColorscale(gd, trace);
    calcSelection(cd, trace);

    if(stackGroupOpts) {
        // remove bad positions and sort
        // note that original indices get added to cd in arraysToCalcdata
        i = 0;
        while(i < cd.length) {
            if(cd[i][posAttr] === BADNUM) {
                cd.splice(i, 1);
            } else i++;
        }

        Lib.sort(cd, function(a, b) {
            return (a[posAttr] - b[posAttr]) || (a.i - b.i);
        });

        if(interpolateGaps) {
            // first fill the beginning with constant from the first point
            i = 0;
            while(i < cd.length - 1 && cd[i].gap) {
                i++;
            }
            vali = cd[i].s;
            if(!vali) vali = cd[i].s = 0; // in case of no data AT ALL in this trace - use 0
            for(j = 0; j < i; j++) {
                cd[j].s = vali;
            }
            // then fill the end with constant from the last point
            k = cd.length - 1;
            while(k > i && cd[k].gap) {
                k--;
            }
            vali = cd[k].s;
            for(j = cd.length - 1; j > k; j--) {
                cd[j].s = vali;
            }
            // now interpolate internal gaps linearly
            while(i < k) {
                i++;
                if(cd[i].gap) {
                    j = i + 1;
                    while(cd[j].gap) {
                        j++;
                    }
                    var pos0 = cd[i - 1][posAttr];
                    var size0 = cd[i - 1].s;
                    var m = (cd[j].s - size0) / (cd[j][posAttr] - pos0);
                    while(i < j) {
                        cd[i].s = size0 + (cd[i][posAttr] - pos0) * m;
                        i++;
                    }
                }
            }
        }
    }

    return cd;
}

function calcAxisExpansion(gd, trace, xa, ya, x, y, ppad) {
    var serieslen = trace._length;
    var fullLayout = gd._fullLayout;
    var xId = xa._id;
    var yId = ya._id;
    var firstScatter = fullLayout._firstScatter[firstScatterGroup(trace)] === trace.uid;
    var stackOrientation = (getStackOpts(trace, fullLayout, xa, ya) || {}).orientation;
    var fill = trace.fill;

    // cancel minimum tick spacings (only applies to bars and boxes)
    xa._minDtick = 0;
    ya._minDtick = 0;

    // check whether bounds should be tight, padded, extended to zero...
    // most cases both should be padded on both ends, so start with that.
    var xOptions = {padded: true};
    var yOptions = {padded: true};

    if(ppad) {
        xOptions.ppad = yOptions.ppad = ppad;
    }

    // TODO: text size

    var openEnded = serieslen < 2 || (x[0] !== x[serieslen - 1]) || (y[0] !== y[serieslen - 1]);

    if(openEnded && (
        (fill === 'tozerox') ||
        ((fill === 'tonextx') && (firstScatter || stackOrientation === 'h'))
    )) {
        // include zero (tight) and extremes (padded) if fill to zero
        // (unless the shape is closed, then it's just filling the shape regardless)

        xOptions.tozero = true;
    } else if(!(trace.error_y || {}).visible && (
        // if no error bars, markers or text, or fill to y=0 remove x padding

            (fill === 'tonexty' || fill === 'tozeroy') ||
            (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace))
        )) {
        xOptions.padded = false;
        xOptions.ppad = 0;
    }

    if(openEnded && (
        (fill === 'tozeroy') ||
        ((fill === 'tonexty') && (firstScatter || stackOrientation === 'v'))
    )) {
        // now check for y - rather different logic, though still mostly padded both ends
        // include zero (tight) and extremes (padded) if fill to zero
        // (unless the shape is closed, then it's just filling the shape regardless)

        yOptions.tozero = true;
    } else if(fill === 'tonextx' || fill === 'tozerox') {
        // tight y: any x fill

        yOptions.padded = false;
    }

    // N.B. asymmetric splom traces call this with blank {} xa or ya
    if(xId) trace._extremes[xId] = Axes.findExtremes(xa, x, xOptions);
    if(yId) trace._extremes[yId] = Axes.findExtremes(ya, y, yOptions);
}

function calcMarkerSize(trace, serieslen) {
    if(!subTypes.hasMarkers(trace)) return;

    // Treat size like x or y arrays --- Run d2c
    // this needs to go before ppad computation
    var marker = trace.marker;
    var sizeref = 1.6 * (trace.marker.sizeref || 1);
    var markerTrans;

    if(trace.marker.sizemode === 'area') {
        markerTrans = function(v) {
            return Math.max(Math.sqrt((v || 0) / sizeref), 3);
        };
    } else {
        markerTrans = function(v) {
            return Math.max((v || 0) / sizeref, 3);
        };
    }

    if(Lib.isArrayOrTypedArray(marker.size)) {
        // I tried auto-type but category and dates dont make much sense.
        var ax = {type: 'linear'};
        Axes.setConvert(ax);

        var s = ax.makeCalcdata(trace.marker, 'size');

        var sizeOut = new Array(serieslen);
        for(var i = 0; i < serieslen; i++) {
            sizeOut[i] = markerTrans(s[i]);
        }
        return sizeOut;
    } else {
        return markerTrans(marker.size);
    }
}

/**
 * mark the first scatter trace for each subplot
 * note that scatter and scattergl each get their own first trace
 * note also that I'm doing this during calc rather than supplyDefaults
 * so I don't need to worry about transforms, but if we ever do
 * per-trace calc this will get confused.
 */
function setFirstScatter(fullLayout, trace) {
    var group = firstScatterGroup(trace);
    var firstScatter = fullLayout._firstScatter;
    if(!firstScatter[group]) firstScatter[group] = trace.uid;
}

function firstScatterGroup(trace) {
    var stackGroup = trace.stackgroup;
    return trace.xaxis + trace.yaxis + trace.type +
        (stackGroup ? '-' + stackGroup : '');
}

function getStackOpts(trace, fullLayout, xa, ya) {
    var stackGroup = trace.stackgroup;
    if(!stackGroup) return;
    var stackOpts = fullLayout._scatterStackOpts[xa._id + ya._id][stackGroup];
    var stackAx = stackOpts.orientation === 'v' ? ya : xa;
    // Allow stacking only on numeric axes
    // calc is a little late to be figuring this out, but during supplyDefaults
    // we don't know the axis type yet
    if(stackAx.type === 'linear' || stackAx.type === 'log') return stackOpts;
}

module.exports = {
    calc: calc,
    calcMarkerSize: calcMarkerSize,
    calcAxisExpansion: calcAxisExpansion,
    setFirstScatter: setFirstScatter,
    getStackOpts: getStackOpts
};
