/**
* 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 createLinePlot = require('gl-line3d');
var createScatterPlot = require('gl-scatter3d');
var createErrorBars = require('gl-error3d');
var createMesh = require('gl-mesh3d');
var triangulate = require('delaunay-triangulate');

var Lib = require('../../lib');
var str2RgbaArray = require('../../lib/str2rgbarray');
var formatColor = require('../../lib/gl_format_color');
var makeBubbleSizeFn = require('../scatter/make_bubble_size_func');
var DASH_PATTERNS = require('../../constants/gl3d_dashes');
var MARKER_SYMBOLS = require('../../constants/gl3d_markers');

var calculateError = require('./calc_errors');

function LineWithMarkers(scene, uid) {
    this.scene = scene;
    this.uid = uid;
    this.linePlot = null;
    this.scatterPlot = null;
    this.errorBars = null;
    this.textMarkers = null;
    this.delaunayMesh = null;
    this.color = null;
    this.mode = '';
    this.dataPoints = [];
    this.axesBounds = [
        [-Infinity, -Infinity, -Infinity],
        [Infinity, Infinity, Infinity]
    ];
    this.textLabels = null;
    this.data = null;
}

var proto = LineWithMarkers.prototype;

proto.handlePick = function(selection) {
    if(selection.object &&
        (selection.object === this.linePlot ||
         selection.object === this.delaunayMesh ||
         selection.object === this.textMarkers ||
         selection.object === this.scatterPlot)) {
        if(selection.object.highlight) {
            selection.object.highlight(null);
        }
        if(this.scatterPlot) {
            selection.object = this.scatterPlot;
            this.scatterPlot.highlight(selection.data);
        }
        if(this.textLabels) {
            if(this.textLabels[selection.data.index] !== undefined) {
                selection.textLabel = this.textLabels[selection.data.index];
            } else {
                selection.textLabel = this.textLabels;
            }
        }
        else selection.textLabel = '';

        var selectIndex = selection.index = selection.data.index;
        selection.traceCoordinate = [
            this.data.x[selectIndex],
            this.data.y[selectIndex],
            this.data.z[selectIndex]
        ];

        return true;
    }
};

function constructDelaunay(points, color, axis) {
    var u = (axis + 1) % 3;
    var v = (axis + 2) % 3;
    var filteredPoints = [];
    var filteredIds = [];
    var i;

    for(i = 0; i < points.length; ++i) {
        var p = points[i];
        if(isNaN(p[u]) || !isFinite(p[u]) ||
           isNaN(p[v]) || !isFinite(p[v])) {
            continue;
        }
        filteredPoints.push([p[u], p[v]]);
        filteredIds.push(i);
    }
    var cells = triangulate(filteredPoints);
    for(i = 0; i < cells.length; ++i) {
        var c = cells[i];
        for(var j = 0; j < c.length; ++j) {
            c[j] = filteredIds[c[j]];
        }
    }
    return {
        positions: points,
        cells: cells,
        meshColor: color
    };
}

function calculateErrorParams(errors) {
    var capSize = [0.0, 0.0, 0.0],
        color = [[0, 0, 0], [0, 0, 0], [0, 0, 0]],
        lineWidth = [0.0, 0.0, 0.0];

    for(var i = 0; i < 3; i++) {
        var e = errors[i];

        if(e && e.copy_zstyle !== false) e = errors[2];
        if(!e) continue;

        capSize[i] = e.width / 2;  // ballpark rescaling
        color[i] = str2RgbaArray(e.color);
        lineWidth = e.thickness;

    }

    return {capSize: capSize, color: color, lineWidth: lineWidth};
}

function calculateTextOffset(tp) {
    // Read out text properties
    var textOffset = [0, 0];
    if(Array.isArray(tp)) return [0, -1];
    if(tp.indexOf('bottom') >= 0) textOffset[1] += 1;
    if(tp.indexOf('top') >= 0) textOffset[1] -= 1;
    if(tp.indexOf('left') >= 0) textOffset[0] -= 1;
    if(tp.indexOf('right') >= 0) textOffset[0] += 1;
    return textOffset;
}


function calculateSize(sizeIn, sizeFn) {
    // rough parity with Plotly 2D markers
    return sizeFn(sizeIn * 4);
}

function calculateSymbol(symbolIn) {
    return MARKER_SYMBOLS[symbolIn];
}

function formatParam(paramIn, len, calculate, dflt, extraFn) {
    var paramOut = null;

    if(Lib.isArrayOrTypedArray(paramIn)) {
        paramOut = [];

        for(var i = 0; i < len; i++) {
            if(paramIn[i] === undefined) paramOut[i] = dflt;
            else paramOut[i] = calculate(paramIn[i], extraFn);
        }

    }
    else paramOut = calculate(paramIn, Lib.identity);

    return paramOut;
}


function convertPlotlyOptions(scene, data) {
    var params, i,
        points = [],
        sceneLayout = scene.fullSceneLayout,
        scaleFactor = scene.dataScale,
        xaxis = sceneLayout.xaxis,
        yaxis = sceneLayout.yaxis,
        zaxis = sceneLayout.zaxis,
        marker = data.marker,
        line = data.line,
        xc, x = data.x || [],
        yc, y = data.y || [],
        zc, z = data.z || [],
        len = x.length,
        xcalendar = data.xcalendar,
        ycalendar = data.ycalendar,
        zcalendar = data.zcalendar,
        text;

    // Convert points
    for(i = 0; i < len; i++) {
        // sanitize numbers and apply transforms based on axes.type
        xc = xaxis.d2l(x[i], 0, xcalendar) * scaleFactor[0];
        yc = yaxis.d2l(y[i], 0, ycalendar) * scaleFactor[1];
        zc = zaxis.d2l(z[i], 0, zcalendar) * scaleFactor[2];

        points[i] = [xc, yc, zc];
    }

    // convert text
    if(Array.isArray(data.text)) text = data.text;
    else if(data.text !== undefined) {
        text = new Array(len);
        for(i = 0; i < len; i++) text[i] = data.text;
    }

    // Build object parameters
    params = {
        position: points,
        mode: data.mode,
        text: text
    };

    if('line' in data) {
        params.lineColor = formatColor(line, 1, len);
        params.lineWidth = line.width;
        params.lineDashes = line.dash;
    }

    if('marker' in data) {
        var sizeFn = makeBubbleSizeFn(data);

        params.scatterColor = formatColor(marker, 1, len);
        params.scatterSize = formatParam(marker.size, len, calculateSize, 20, sizeFn);
        params.scatterMarker = formatParam(marker.symbol, len, calculateSymbol, '●');
        params.scatterLineWidth = marker.line.width;  // arrayOk === false
        params.scatterLineColor = formatColor(marker.line, 1, len);
        params.scatterAngle = 0;
    }

    if('textposition' in data) {
        params.textOffset = calculateTextOffset(data.textposition);  // arrayOk === false
        params.textColor = formatColor(data.textfont, 1, len);
        params.textSize = formatParam(data.textfont.size, len, Lib.identity, 12);
        params.textFont = data.textfont.family;  // arrayOk === false
        params.textAngle = 0;
    }

    var dims = ['x', 'y', 'z'];
    params.project = [false, false, false];
    params.projectScale = [1, 1, 1];
    params.projectOpacity = [1, 1, 1];
    for(i = 0; i < 3; ++i) {
        var projection = data.projection[dims[i]];
        if((params.project[i] = projection.show)) {
            params.projectOpacity[i] = projection.opacity;
            params.projectScale[i] = projection.scale;
        }
    }

    params.errorBounds = calculateError(data, scaleFactor);

    var errorParams = calculateErrorParams([data.error_x, data.error_y, data.error_z]);
    params.errorColor = errorParams.color;
    params.errorLineWidth = errorParams.lineWidth;
    params.errorCapSize = errorParams.capSize;

    params.delaunayAxis = data.surfaceaxis;
    params.delaunayColor = str2RgbaArray(data.surfacecolor);

    return params;
}

function arrayToColor(color) {
    if(Array.isArray(color)) {
        var c = color[0];

        if(Array.isArray(c)) color = c;

        return 'rgb(' + color.slice(0, 3).map(function(x) {
            return Math.round(x * 255);
        }) + ')';
    }

    return null;
}

proto.update = function(data) {
    var gl = this.scene.glplot.gl,
        lineOptions,
        scatterOptions,
        errorOptions,
        textOptions,
        dashPattern = DASH_PATTERNS.solid;

    // Save data
    this.data = data;

    // Run data conversion
    var options = convertPlotlyOptions(this.scene, data);

    if('mode' in options) {
        this.mode = options.mode;
    }
    if('lineDashes' in options) {
        if(options.lineDashes in DASH_PATTERNS) {
            dashPattern = DASH_PATTERNS[options.lineDashes];
        }
    }

    this.color = arrayToColor(options.scatterColor) ||
                 arrayToColor(options.lineColor);

    // Save data points
    this.dataPoints = options.position;

    lineOptions = {
        gl: gl,
        position: options.position,
        color: options.lineColor,
        lineWidth: options.lineWidth || 1,
        dashes: dashPattern[0],
        dashScale: dashPattern[1],
        opacity: data.opacity,
        connectGaps: data.connectgaps
    };

    if(this.mode.indexOf('lines') !== -1) {
        if(this.linePlot) this.linePlot.update(lineOptions);
        else {
            this.linePlot = createLinePlot(lineOptions);
            this.linePlot._trace = this;
            this.scene.glplot.add(this.linePlot);
        }
    } else if(this.linePlot) {
        this.scene.glplot.remove(this.linePlot);
        this.linePlot.dispose();
        this.linePlot = null;
    }

    // N.B. marker.opacity must be a scalar for performance
    var scatterOpacity = data.opacity;
    if(data.marker && data.marker.opacity) scatterOpacity *= data.marker.opacity;

    scatterOptions = {
        gl: gl,
        position: options.position,
        color: options.scatterColor,
        size: options.scatterSize,
        glyph: options.scatterMarker,
        opacity: scatterOpacity,
        orthographic: true,
        lineWidth: options.scatterLineWidth,
        lineColor: options.scatterLineColor,
        project: options.project,
        projectScale: options.projectScale,
        projectOpacity: options.projectOpacity
    };

    if(this.mode.indexOf('markers') !== -1) {
        if(this.scatterPlot) this.scatterPlot.update(scatterOptions);
        else {
            this.scatterPlot = createScatterPlot(scatterOptions);
            this.scatterPlot._trace = this;
            this.scatterPlot.highlightScale = 1;
            this.scene.glplot.add(this.scatterPlot);
        }
    } else if(this.scatterPlot) {
        this.scene.glplot.remove(this.scatterPlot);
        this.scatterPlot.dispose();
        this.scatterPlot = null;
    }

    textOptions = {
        gl: gl,
        position: options.position,
        glyph: options.text,
        color: options.textColor,
        size: options.textSize,
        angle: options.textAngle,
        alignment: options.textOffset,
        font: options.textFont,
        orthographic: true,
        lineWidth: 0,
        project: false,
        opacity: data.opacity
    };

    this.textLabels = data.hovertext || data.text;

    if(this.mode.indexOf('text') !== -1) {
        if(this.textMarkers) this.textMarkers.update(textOptions);
        else {
            this.textMarkers = createScatterPlot(textOptions);
            this.textMarkers._trace = this;
            this.textMarkers.highlightScale = 1;
            this.scene.glplot.add(this.textMarkers);
        }
    } else if(this.textMarkers) {
        this.scene.glplot.remove(this.textMarkers);
        this.textMarkers.dispose();
        this.textMarkers = null;
    }

    errorOptions = {
        gl: gl,
        position: options.position,
        color: options.errorColor,
        error: options.errorBounds,
        lineWidth: options.errorLineWidth,
        capSize: options.errorCapSize,
        opacity: data.opacity
    };
    if(this.errorBars) {
        if(options.errorBounds) {
            this.errorBars.update(errorOptions);
        } else {
            this.scene.glplot.remove(this.errorBars);
            this.errorBars.dispose();
            this.errorBars = null;
        }
    } else if(options.errorBounds) {
        this.errorBars = createErrorBars(errorOptions);
        this.errorBars._trace = this;
        this.scene.glplot.add(this.errorBars);
    }

    if(options.delaunayAxis >= 0) {
        var delaunayOptions = constructDelaunay(
            options.position,
            options.delaunayColor,
            options.delaunayAxis
        );
        delaunayOptions.opacity = data.opacity;

        if(this.delaunayMesh) {
            this.delaunayMesh.update(delaunayOptions);
        } else {
            delaunayOptions.gl = gl;
            this.delaunayMesh = createMesh(delaunayOptions);
            this.delaunayMesh._trace = this;
            this.scene.glplot.add(this.delaunayMesh);
        }
    } else if(this.delaunayMesh) {
        this.scene.glplot.remove(this.delaunayMesh);
        this.delaunayMesh.dispose();
        this.delaunayMesh = null;
    }
};

proto.dispose = function() {
    if(this.linePlot) {
        this.scene.glplot.remove(this.linePlot);
        this.linePlot.dispose();
    }
    if(this.scatterPlot) {
        this.scene.glplot.remove(this.scatterPlot);
        this.scatterPlot.dispose();
    }
    if(this.errorBars) {
        this.scene.glplot.remove(this.errorBars);
        this.errorBars.dispose();
    }
    if(this.textMarkers) {
        this.scene.glplot.remove(this.textMarkers);
        this.textMarkers.dispose();
    }
    if(this.delaunayMesh) {
        this.scene.glplot.remove(this.delaunayMesh);
        this.delaunayMesh.dispose();
    }
};

function createLineWithMarkers(scene, data) {
    var plot = new LineWithMarkers(scene, data.uid);
    plot.update(data);
    return plot;
}

module.exports = createLineWithMarkers;
