/**
* Copyright 2012-2018, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

var createMatrix = require('regl-splom');
var arrayRange = require('array-range');

var Registry = require('../../registry');
var Grid = require('../../components/grid');
var Lib = require('../../lib');
var AxisIDs = require('../../plots/cartesian/axis_ids');

var subTypes = require('../scatter/subtypes');
var calcMarkerSize = require('../scatter/calc').calcMarkerSize;
var calcAxisExpansion = require('../scatter/calc').calcAxisExpansion;
var calcColorscales = require('../scatter/colorscale_calc');
var convertMarkerSelection = require('../scattergl/convert').convertMarkerSelection;
var convertMarkerStyle = require('../scattergl/convert').convertMarkerStyle;
var calcHover = require('../scattergl').calcHover;

var BADNUM = require('../../constants/numerical').BADNUM;
var TOO_MANY_POINTS = require('../scattergl/constants').TOO_MANY_POINTS;

function calc(gd, trace) {
    var dimensions = trace.dimensions;
    var commonLength = trace._length;
    var stash = {};
    var opts = {};
    // 'c' for calculated, 'l' for linear,
    // only differ here for log axes, pass ldata to createMatrix as 'data'
    var cdata = opts.cdata = [];
    var ldata = opts.data = [];
    var i, k, dim;

    for(i = 0; i < dimensions.length; i++) {
        dim = dimensions[i];

        if(dim.visible) {
            var axId = trace._diag[i][0] || trace._diag[i][1];
            var ax = AxisIDs.getFromId(gd, axId);
            if(ax) {
                var ccol = makeCalcdata(ax, trace, dim);
                var lcol = ax.type === 'log' ? Lib.simpleMap(ccol, ax.c2l) : ccol;
                cdata.push(ccol);
                ldata.push(lcol);
            }
        }
    }

    calcColorscales(trace);
    Lib.extendFlat(opts, convertMarkerStyle(trace));

    var visibleLength = cdata.length;
    var hasTooManyPoints = (visibleLength * commonLength) > TOO_MANY_POINTS;

    for(i = 0, k = 0; i < dimensions.length; i++) {
        dim = dimensions[i];

        if(dim.visible) {
            var xa = AxisIDs.getFromId(gd, trace._diag[i][0]) || {};
            var ya = AxisIDs.getFromId(gd, trace._diag[i][1]) || {};

            // Re-use SVG scatter axis expansion routine except
            // for graph with very large number of points where it
            // performs poorly.
            // In big data case, fake Axes.expand outputs with data bounds,
            // and an average size for array marker.size inputs.
            var ppad;
            if(hasTooManyPoints) {
                ppad = 2 * (opts.sizeAvg || Math.max(opts.size, 3));
            } else {
                ppad = calcMarkerSize(trace, commonLength);
            }

            calcAxisExpansion(gd, trace, xa, ya, cdata[k], cdata[k], ppad);
            k++;
        }
    }

    var scene = stash._scene = sceneUpdate(gd, stash);
    if(!scene.matrix) scene.matrix = true;
    scene.matrixOptions = opts;

    scene.selectedOptions = convertMarkerSelection(trace, trace.selected);
    scene.unselectedOptions = convertMarkerSelection(trace, trace.unselected);

    return [{x: false, y: false, t: stash, trace: trace}];
}

function makeCalcdata(ax, trace, dim) {
    // call makeCalcdata with fake input
    var ccol = ax.makeCalcdata({
        v: dim.values,
        vcalendar: trace.calendar
    }, 'v');

    for(var i = 0; i < ccol.length; i++) {
        ccol[i] = ccol[i] === BADNUM ? NaN : ccol[i];
    }

    return ccol;
}

function sceneUpdate(gd, stash) {
    var scene = stash._scene;

    var reset = {
        dirty: true
    };

    var first = {
        selectBatch: null,
        unselectBatch: null,
        matrix: false,
        select: null
    };

    if(!scene) {
        scene = stash._scene = Lib.extendFlat({}, reset, first);

        scene.draw = function draw() {
            // draw traces in selection mode
            if(scene.matrix && scene.selectBatch) {
                scene.matrix.draw(scene.unselectBatch, scene.selectBatch);
            }

            else if(scene.matrix) {
                scene.matrix.draw();
            }

            scene.dirty = false;
        };

        // remove scene resources
        scene.destroy = function destroy() {
            if(scene.matrix) scene.matrix.destroy();

            scene.matrixOptions = null;
            scene.selectBatch = null;
            scene.unselectBatch = null;

            stash._scene = null;
        };
    }

    // In case if we have scene from the last calc - reset data
    if(!scene.dirty) {
        Lib.extendFlat(scene, reset);
    }

    return scene;
}

function plot(gd, _, splomCalcData) {
    if(!splomCalcData.length) return;

    for(var i = 0; i < splomCalcData.length; i++) {
        plotOne(gd, splomCalcData[i][0]);
    }
}

function plotOne(gd, cd0) {
    var fullLayout = gd._fullLayout;
    var gs = fullLayout._size;
    var trace = cd0.trace;
    var stash = cd0.t;
    var scene = stash._scene;
    var matrixOpts = scene.matrixOptions;
    var cdata = matrixOpts.cdata;
    var regl = fullLayout._glcanvas.data()[0].regl;
    var dragmode = fullLayout.dragmode;
    var xa, ya;
    var i, j, k;

    if(cdata.length === 0) return;

    // augment options with proper upper/lower halves
    // regl-splom's default grid starts from bottom-left
    matrixOpts.lower = trace.showupperhalf;
    matrixOpts.upper = trace.showlowerhalf;
    matrixOpts.diagonal = trace.diagonal.visible;

    var dimensions = trace.dimensions;
    var visibleLength = cdata.length;
    var viewOpts = {};
    viewOpts.ranges = new Array(visibleLength);
    viewOpts.domains = new Array(visibleLength);

    for(i = 0, k = 0; i < dimensions.length; i++) {
        if(trace.dimensions[i].visible) {
            var rng = viewOpts.ranges[k] = new Array(4);
            var dmn = viewOpts.domains[k] = new Array(4);

            xa = AxisIDs.getFromId(gd, trace._diag[i][0]);
            if(xa) {
                rng[0] = xa._rl[0];
                rng[2] = xa._rl[1];
                dmn[0] = xa.domain[0];
                dmn[2] = xa.domain[1];
            }

            ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
            if(ya) {
                rng[1] = ya._rl[0];
                rng[3] = ya._rl[1];
                dmn[1] = ya.domain[0];
                dmn[3] = ya.domain[1];
            }

            k++;
        }
    }

    viewOpts.viewport = [gs.l, gs.b, gs.w + gs.l, gs.h + gs.b];

    if(scene.matrix === true) {
        scene.matrix = createMatrix(regl);
    }

    var selectMode = dragmode === 'lasso' || dragmode === 'select' || !!trace.selectedpoints;
    scene.selectBatch = null;
    scene.unselectBatch = null;

    if(selectMode) {
        var commonLength = trace._length;

        if(!scene.selectBatch) {
            scene.selectBatch = [];
            scene.unselectBatch = [];
        }

        // regenerate scene batch, if traces number changed during selection
        if(trace.selectedpoints) {
            scene.selectBatch = trace.selectedpoints;

            var selPts = trace.selectedpoints;
            var selDict = {};
            for(i = 0; i < selPts.length; i++) {
                selDict[selPts[i]] = true;
            }
            var unselPts = [];
            for(i = 0; i < commonLength; i++) {
                if(!selDict[i]) unselPts.push(i);
            }
            scene.unselectBatch = unselPts;
        }

        // precalculate px coords since we are not going to pan during select
        var xpx = stash.xpx = new Array(visibleLength);
        var ypx = stash.ypx = new Array(visibleLength);

        for(i = 0, k = 0; i < dimensions.length; i++) {
            if(trace.dimensions[i].visible) {
                xa = AxisIDs.getFromId(gd, trace._diag[i][0]);
                if(xa) {
                    xpx[k] = new Array(commonLength);
                    for(j = 0; j < commonLength; j++) {
                        xpx[k][j] = xa.c2p(cdata[k][j]);
                    }
                }

                ya = AxisIDs.getFromId(gd, trace._diag[i][1]);
                if(ya) {
                    ypx[k] = new Array(commonLength);
                    for(j = 0; j < commonLength; j++) {
                        ypx[k][j] = ya.c2p(cdata[k][j]);
                    }
                }

                k++;
            }
        }

        if(scene.selectBatch) {
            scene.matrix.update(matrixOpts, matrixOpts);
            scene.matrix.update(scene.unselectedOptions, scene.selectedOptions);
            scene.matrix.update(viewOpts, viewOpts);
        }
        else {
            // delete selection pass
            scene.matrix.update(viewOpts, null);
        }
    }
    else {
        scene.matrix.update(matrixOpts);
        scene.matrix.update(viewOpts);
        stash.xpx = stash.ypx = null;
    }

    scene.draw();
}

function hoverPoints(pointData, xval, yval) {
    var cd = pointData.cd;
    var trace = cd[0].trace;
    var stash = cd[0].t;
    var scene = stash._scene;
    var cdata = scene.matrixOptions.cdata;
    var xa = pointData.xa;
    var ya = pointData.ya;
    var xpx = xa.c2p(xval);
    var ypx = ya.c2p(yval);
    var maxDistance = pointData.distance;

    var xi = getDimIndex(trace, xa);
    var yi = getDimIndex(trace, ya);
    if(xi === false || yi === false) return [pointData];

    var x = cdata[xi];
    var y = cdata[yi];

    var id, dxy;
    var minDist = maxDistance;

    for(var i = 0; i < x.length; i++) {
        var ptx = x[i];
        var pty = y[i];
        var dx = xa.c2p(ptx) - xpx;
        var dy = ya.c2p(pty) - ypx;
        var dist = Math.sqrt(dx * dx + dy * dy);

        if(dist < minDist) {
            minDist = dxy = dist;
            id = i;
        }
    }

    pointData.index = id;
    pointData.distance = minDist;
    pointData.dxy = dxy;

    if(id === undefined) return [pointData];

    calcHover(pointData, x, y, trace);

    return [pointData];
}

function selectPoints(searchInfo, polygon) {
    var cd = searchInfo.cd;
    var trace = cd[0].trace;
    var stash = cd[0].t;
    var scene = stash._scene;
    var cdata = scene.matrixOptions.cdata;
    var xa = searchInfo.xaxis;
    var ya = searchInfo.yaxis;
    var selection = [];
    var i;

    if(!scene) return selection;

    var hasOnlyLines = (!subTypes.hasMarkers(trace) && !subTypes.hasText(trace));
    if(trace.visible !== true || hasOnlyLines) return selection;

    var xi = getDimIndex(trace, xa);
    var yi = getDimIndex(trace, ya);
    if(xi === false || yi === false) return selection;

    var xpx = stash.xpx[xi];
    var ypx = stash.ypx[yi];
    var x = cdata[xi];
    var y = cdata[yi];

    // degenerate polygon does not enable selection
    // filter out points by visible scatter ones
    var els = null;
    var unels = null;
    if(polygon !== false && !polygon.degenerate) {
        els = [], unels = [];
        for(i = 0; i < x.length; i++) {
            if(polygon.contains([xpx[i], ypx[i]])) {
                els.push(i);
                selection.push({
                    pointNumber: i,
                    x: x[i],
                    y: y[i]
                });
            }
            else {
                unels.push(i);
            }
        }
    } else {
        unels = arrayRange(stash.count);
    }

    // make sure selectBatch is created
    if(!scene.selectBatch) {
        scene.selectBatch = [];
        scene.unselectBatch = [];
    }

    if(!scene.selectBatch) {
        // enter every trace select mode
        for(i = 0; i < scene.count; i++) {
            scene.selectBatch = [];
            scene.unselectBatch = [];
        }
        // we should turn scatter2d into unselected once we have any points selected
        scene.matrix.update(scene.unselectedOptions, scene.selectedOptions);
    }

    scene.selectBatch = els;
    scene.unselectBatch = unels;


    return selection;
}

function style(gd, cds) {
    if(!cds) return;

    var fullLayout = gd._fullLayout;
    var cd0 = cds[0];
    var scene0 = cd0[0].t._scene;
    scene0.matrix.regl.clear({color: true, depth: true});

    if(fullLayout._splomGrid) {
        fullLayout._splomGrid.draw();
    }

    for(var i = 0; i < cds.length; i++) {
        var scene = cds[i][0].t._scene;
        scene.draw();
    }

    // redraw all subplot with scattergl traces,
    // as we cleared the whole canvas above
    if(fullLayout._has('cartesian')) {
        for(var k in fullLayout._plots) {
            var sp = fullLayout._plots[k];
            if(sp._scene) sp._scene.draw();
        }
    }
}

function getDimIndex(trace, ax) {
    var axId = ax._id;
    var axLetter = axId.charAt(0);
    var ind = {x: 0, y: 1}[axLetter];
    var dimensions = trace.dimensions;

    for(var i = 0, k = 0; i < dimensions.length; i++) {
        if(dimensions[i].visible) {
            if(trace._diag[i][ind] === axId) return k;
            k++;
        }
    }
    return false;
}

module.exports = {
    moduleType: 'trace',
    name: 'splom',

    basePlotModule: require('./base_plot'),
    categories: ['gl', 'regl', 'cartesian', 'symbols', 'markerColorscale', 'showLegend', 'scatter-like'],

    attributes: require('./attributes'),
    supplyDefaults: require('./defaults'),

    calc: calc,
    plot: plot,
    hoverPoints: hoverPoints,
    selectPoints: selectPoints,
    style: style,

    meta: {
        description: [
            'Splom traces generate scatter plot matrix visualizations.',
            'Each splom `dimensions` items correspond to a generated axis.',
            'Values for each of those dimensions are set in `dimensions[i].values`.',
            'Splom traces support all `scattergl` marker style attributes.',
            'Specify `layout.grid` attributes and/or layout x-axis and y-axis attributes',
            'for more control over the axis positioning and style. '
        ].join(' ')
    }
};

// splom traces use the 'grid' component to generate their axes,
// register it here
Registry.register(Grid);
