'use strict';

var Registry = require('../../registry');
var Axes = require('../../plots/cartesian/axes');
var Fx = require('../../components/fx');

var createPlot2D = require('../../../stackgl_modules').gl_plot2d;
var createSpikes = require('../../../stackgl_modules').gl_spikes2d;
var createSelectBox = require('../../../stackgl_modules').gl_select_box;
var getContext = require('webgl-context');

var createOptions = require('./convert');
var createCamera = require('./camera');
var showNoWebGlMsg = require('../../lib/show_no_webgl_msg');
var axisConstraints = require('../cartesian/constraints');
var enforceAxisConstraints = axisConstraints.enforce;
var cleanAxisConstraints = axisConstraints.clean;
var doAutoRange = require('../cartesian/autorange').doAutoRange;

var dragHelpers = require('../../components/dragelement/helpers');
var drawMode = dragHelpers.drawMode;
var selectMode = dragHelpers.selectMode;

var AXES = ['xaxis', 'yaxis'];
var STATIC_CANVAS, STATIC_CONTEXT;

var SUBPLOT_PATTERN = require('../cartesian/constants').SUBPLOT_PATTERN;


function Scene2D(options, fullLayout) {
    this.container = options.container;
    this.graphDiv = options.graphDiv;
    this.pixelRatio = options.plotGlPixelRatio || window.devicePixelRatio;
    this.id = options.id;
    this.staticPlot = !!options.staticPlot;
    this.scrollZoom = this.graphDiv._context._scrollZoom.cartesian;

    this.fullData = null;
    this.updateRefs(fullLayout);

    this.makeFramework();
    if(this.stopped) return;

    // update options
    this.glplotOptions = createOptions(this);
    this.glplotOptions.merge(fullLayout);

    // create the plot
    this.glplot = createPlot2D(this.glplotOptions);

    // create camera
    this.camera = createCamera(this);

    // trace set
    this.traces = {};

    // create axes spikes
    this.spikes = createSpikes(this.glplot);

    this.selectBox = createSelectBox(this.glplot, {
        innerFill: false,
        outerFill: true
    });

    // last button state
    this.lastButtonState = 0;

    // last pick result
    this.pickResult = null;

    // is the mouse over the plot?
    // it's OK if this says true when it's not, so long as
    // when we get a mouseout we set it to false before handling
    this.isMouseOver = true;

    // flag to stop render loop
    this.stopped = false;

    // redraw the plot
    this.redraw = this.draw.bind(this);
    this.redraw();
}

module.exports = Scene2D;

var proto = Scene2D.prototype;

proto.makeFramework = function() {
    // create canvas and gl context
    if(this.staticPlot) {
        if(!STATIC_CONTEXT) {
            STATIC_CANVAS = document.createElement('canvas');

            STATIC_CONTEXT = getContext({
                canvas: STATIC_CANVAS,
                preserveDrawingBuffer: false,
                premultipliedAlpha: true,
                antialias: true
            });

            if(!STATIC_CONTEXT) {
                throw new Error('Error creating static canvas/context for image server');
            }
        }

        this.canvas = STATIC_CANVAS;
        this.gl = STATIC_CONTEXT;
    } else {
        var liveCanvas = this.container.querySelector('.gl-canvas-focus');

        var gl = getContext({
            canvas: liveCanvas,
            preserveDrawingBuffer: true,
            premultipliedAlpha: true
        });

        if(!gl) {
            showNoWebGlMsg(this);
            this.stopped = true;
            return;
        }

        this.canvas = liveCanvas;
        this.gl = gl;
    }

    // position the canvas
    var canvas = this.canvas;

    canvas.style.width = '100%';
    canvas.style.height = '100%';
    canvas.style.position = 'absolute';
    canvas.style.top = '0px';
    canvas.style.left = '0px';
    canvas.style['pointer-events'] = 'none';

    this.updateSize(canvas);

    // create SVG container for hover text
    var svgContainer = this.svgContainer = document.createElementNS(
        'http://www.w3.org/2000/svg',
        'svg');
    svgContainer.style.position = 'absolute';
    svgContainer.style.top = svgContainer.style.left = '0px';
    svgContainer.style.width = svgContainer.style.height = '100%';
    svgContainer.style['z-index'] = 20;
    svgContainer.style['pointer-events'] = 'none';

    // create div to catch the mouse event
    var mouseContainer = this.mouseContainer = document.createElement('div');
    mouseContainer.style.position = 'absolute';
    mouseContainer.style['pointer-events'] = 'auto';

    this.pickCanvas = this.container.querySelector('.gl-canvas-pick');


    // append canvas, hover svg and mouse div to container
    var container = this.container;
    container.appendChild(svgContainer);
    container.appendChild(mouseContainer);

    var self = this;
    mouseContainer.addEventListener('mouseout', function() {
        self.isMouseOver = false;
        self.unhover();
    });
    mouseContainer.addEventListener('mouseover', function() {
        self.isMouseOver = true;
    });
};

proto.toImage = function(format) {
    if(!format) format = 'png';

    this.stopped = true;

    if(this.staticPlot) this.container.appendChild(STATIC_CANVAS);

    // update canvas size
    this.updateSize(this.canvas);


    // grab context and yank out pixels
    var gl = this.glplot.gl;
    var w = gl.drawingBufferWidth;
    var h = gl.drawingBufferHeight;

    // force redraw
    gl.clearColor(1, 1, 1, 0);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    this.glplot.setDirty();
    this.glplot.draw();

    gl.bindFramebuffer(gl.FRAMEBUFFER, null);

    var pixels = new Uint8Array(w * h * 4);
    gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

    // flip pixels
    for(var j = 0, k = h - 1; j < k; ++j, --k) {
        for(var i = 0; i < w; ++i) {
            for(var l = 0; l < 4; ++l) {
                var tmp = pixels[4 * (w * j + i) + l];
                pixels[4 * (w * j + i) + l] = pixels[4 * (w * k + i) + l];
                pixels[4 * (w * k + i) + l] = tmp;
            }
        }
    }

    var canvas = document.createElement('canvas');
    canvas.width = w;
    canvas.height = h;

    var context = canvas.getContext('2d', {willReadFrequently: true});
    var imageData = context.createImageData(w, h);
    imageData.data.set(pixels);
    context.putImageData(imageData, 0, 0);

    var dataURL;

    switch(format) {
        case 'jpeg':
            dataURL = canvas.toDataURL('image/jpeg');
            break;
        case 'webp':
            dataURL = canvas.toDataURL('image/webp');
            break;
        default:
            dataURL = canvas.toDataURL('image/png');
    }

    if(this.staticPlot) this.container.removeChild(STATIC_CANVAS);

    return dataURL;
};

proto.updateSize = function(canvas) {
    if(!canvas) canvas = this.canvas;

    var pixelRatio = this.pixelRatio;
    var fullLayout = this.fullLayout;

    var width = fullLayout.width;
    var height = fullLayout.height;
    var pixelWidth = Math.ceil(pixelRatio * width) |0;
    var pixelHeight = Math.ceil(pixelRatio * height) |0;

    // check for resize
    if(canvas.width !== pixelWidth || canvas.height !== pixelHeight) {
        canvas.width = pixelWidth;
        canvas.height = pixelHeight;
    }

    return canvas;
};

proto.computeTickMarks = function() {
    this.xaxis.setScale();
    this.yaxis.setScale();

    var nextTicks = [
        Axes.calcTicks(this.xaxis),
        Axes.calcTicks(this.yaxis)
    ];

    for(var j = 0; j < 2; ++j) {
        for(var i = 0; i < nextTicks[j].length; ++i) {
            // coercing tick value (may not be a string) to a string
            nextTicks[j][i].text = nextTicks[j][i].text + '';
        }
    }

    return nextTicks;
};

function compareTicks(a, b) {
    for(var i = 0; i < 2; ++i) {
        var aticks = a[i];
        var bticks = b[i];

        if(aticks.length !== bticks.length) return true;

        for(var j = 0; j < aticks.length; ++j) {
            if(aticks[j].x !== bticks[j].x) return true;
        }
    }

    return false;
}

proto.updateRefs = function(newFullLayout) {
    this.fullLayout = newFullLayout;

    var spmatch = this.id.match(SUBPLOT_PATTERN);
    var xaxisName = 'xaxis' + spmatch[1];
    var yaxisName = 'yaxis' + spmatch[2];

    this.xaxis = this.fullLayout[xaxisName];
    this.yaxis = this.fullLayout[yaxisName];
};

proto.relayoutCallback = function() {
    var graphDiv = this.graphDiv;
    var xaxis = this.xaxis;
    var yaxis = this.yaxis;
    var layout = graphDiv.layout;

    // make a meaningful value to be passed on to possible 'plotly_relayout' subscriber(s)
    var update = {};
    var xrange = update[xaxis._name + '.range'] = xaxis.range.slice();
    var yrange = update[yaxis._name + '.range'] = yaxis.range.slice();
    update[xaxis._name + '.autorange'] = xaxis.autorange;
    update[yaxis._name + '.autorange'] = yaxis.autorange;

    Registry.call('_storeDirectGUIEdit', graphDiv.layout, graphDiv._fullLayout._preGUI, update);

    // update the input layout
    var xaIn = layout[xaxis._name];
    xaIn.range = xrange;
    xaIn.autorange = xaxis.autorange;

    var yaIn = layout[yaxis._name];
    yaIn.range = yrange;
    yaIn.autorange = yaxis.autorange;

    // lastInputTime helps determine which one is the latest input (if async)
    update.lastInputTime = this.camera.lastInputTime;
    graphDiv.emit('plotly_relayout', update);
};

proto.cameraChanged = function() {
    var camera = this.camera;

    this.glplot.setDataBox(this.calcDataBox());

    var nextTicks = this.computeTickMarks();
    var curTicks = this.glplotOptions.ticks;

    if(compareTicks(nextTicks, curTicks)) {
        this.glplotOptions.ticks = nextTicks;
        this.glplotOptions.dataBox = camera.dataBox;
        this.glplot.update(this.glplotOptions);
        this.handleAnnotations();
    }
};

proto.handleAnnotations = function() {
    var gd = this.graphDiv;
    var annotations = this.fullLayout.annotations;

    for(var i = 0; i < annotations.length; i++) {
        var ann = annotations[i];

        if(ann.xref === this.xaxis._id && ann.yref === this.yaxis._id) {
            Registry.getComponentMethod('annotations', 'drawOne')(gd, i);
        }
    }
};

proto.destroy = function() {
    if(!this.glplot) return;

    var traces = this.traces;

    if(traces) {
        Object.keys(traces).map(function(key) {
            traces[key].dispose();
            delete traces[key];
        });
    }

    this.glplot.dispose();

    this.container.removeChild(this.svgContainer);
    this.container.removeChild(this.mouseContainer);

    this.fullData = null;
    this.glplot = null;
    this.stopped = true;
    this.camera.mouseListener.enabled = false;
    this.mouseContainer.removeEventListener('wheel', this.camera.wheelListener);
    this.camera = null;
};

proto.plot = function(fullData, calcData, fullLayout) {
    var glplot = this.glplot;

    this.updateRefs(fullLayout);
    this.xaxis.clearCalc();
    this.yaxis.clearCalc();
    this.updateTraces(fullData, calcData);
    this.updateFx(fullLayout.dragmode);

    var width = fullLayout.width;
    var height = fullLayout.height;

    this.updateSize(this.canvas);

    var options = this.glplotOptions;
    options.merge(fullLayout);
    options.screenBox = [0, 0, width, height];

    var mockGraphDiv = {_fullLayout: {
        _axisConstraintGroups: fullLayout._axisConstraintGroups,
        xaxis: this.xaxis,
        yaxis: this.yaxis,
        _size: fullLayout._size
    }};

    cleanAxisConstraints(mockGraphDiv, this.xaxis);
    cleanAxisConstraints(mockGraphDiv, this.yaxis);

    var size = fullLayout._size;
    var domainX = this.xaxis.domain;
    var domainY = this.yaxis.domain;

    options.viewBox = [
        size.l + domainX[0] * size.w,
        size.b + domainY[0] * size.h,
        (width - size.r) - (1 - domainX[1]) * size.w,
        (height - size.t) - (1 - domainY[1]) * size.h
    ];

    this.mouseContainer.style.width = size.w * (domainX[1] - domainX[0]) + 'px';
    this.mouseContainer.style.height = size.h * (domainY[1] - domainY[0]) + 'px';
    this.mouseContainer.height = size.h * (domainY[1] - domainY[0]);
    this.mouseContainer.style.left = size.l + domainX[0] * size.w + 'px';
    this.mouseContainer.style.top = size.t + (1 - domainY[1]) * size.h + 'px';

    var ax, i;

    for(i = 0; i < 2; ++i) {
        ax = this[AXES[i]];
        ax._length = options.viewBox[i + 2] - options.viewBox[i];

        doAutoRange(this.graphDiv, ax);
        ax.setScale();
    }

    enforceAxisConstraints(mockGraphDiv);

    options.ticks = this.computeTickMarks();

    options.dataBox = this.calcDataBox();

    options.merge(fullLayout);
    glplot.update(options);

    // force redraw so that promise is returned when rendering is completed
    this.glplot.draw();
};

proto.calcDataBox = function() {
    var xaxis = this.xaxis;
    var yaxis = this.yaxis;
    var xrange = xaxis.range;
    var yrange = yaxis.range;
    var xr2l = xaxis.r2l;
    var yr2l = yaxis.r2l;

    return [xr2l(xrange[0]), yr2l(yrange[0]), xr2l(xrange[1]), yr2l(yrange[1])];
};

proto.setRanges = function(dataBox) {
    var xaxis = this.xaxis;
    var yaxis = this.yaxis;
    var xl2r = xaxis.l2r;
    var yl2r = yaxis.l2r;

    xaxis.range = [xl2r(dataBox[0]), xl2r(dataBox[2])];
    yaxis.range = [yl2r(dataBox[1]), yl2r(dataBox[3])];
};

proto.updateTraces = function(fullData, calcData) {
    var traceIds = Object.keys(this.traces);
    var i, j, fullTrace;

    this.fullData = fullData;

    // remove empty traces
    traceIdLoop:
    for(i = 0; i < traceIds.length; i++) {
        var oldUid = traceIds[i];
        var oldTrace = this.traces[oldUid];

        for(j = 0; j < fullData.length; j++) {
            fullTrace = fullData[j];

            if(fullTrace.uid === oldUid && fullTrace.type === oldTrace.type) {
                continue traceIdLoop;
            }
        }

        oldTrace.dispose();
        delete this.traces[oldUid];
    }

    // update / create trace objects
    for(i = 0; i < fullData.length; i++) {
        fullTrace = fullData[i];
        var calcTrace = calcData[i];
        var traceObj = this.traces[fullTrace.uid];

        if(traceObj) traceObj.update(fullTrace, calcTrace);
        else {
            traceObj = fullTrace._module.plot(this, fullTrace, calcTrace);
            this.traces[fullTrace.uid] = traceObj;
        }
    }

    // order object per traces
    this.glplot.objects.sort(function(a, b) {
        return a._trace.index - b._trace.index;
    });
};

proto.updateFx = function(dragmode) {
    // switch to svg interactions in lasso/select mode & shape drawing
    if(selectMode(dragmode) || drawMode(dragmode)) {
        this.pickCanvas.style['pointer-events'] = 'none';
        this.mouseContainer.style['pointer-events'] = 'none';
    } else {
        this.pickCanvas.style['pointer-events'] = 'auto';
        this.mouseContainer.style['pointer-events'] = 'auto';
    }

    // set proper cursor
    if(dragmode === 'pan') {
        this.mouseContainer.style.cursor = 'move';
    } else if(dragmode === 'zoom') {
        this.mouseContainer.style.cursor = 'crosshair';
    } else {
        this.mouseContainer.style.cursor = null;
    }
};

proto.emitPointAction = function(nextSelection, eventType) {
    var uid = nextSelection.trace.uid;
    var ptNumber = nextSelection.pointIndex;
    var trace;

    for(var i = 0; i < this.fullData.length; i++) {
        if(this.fullData[i].uid === uid) {
            trace = this.fullData[i];
        }
    }

    var pointData = {
        x: nextSelection.traceCoord[0],
        y: nextSelection.traceCoord[1],
        curveNumber: trace.index,
        pointNumber: ptNumber,
        data: trace._input,
        fullData: this.fullData,
        xaxis: this.xaxis,
        yaxis: this.yaxis
    };

    Fx.appendArrayPointValue(pointData, trace, ptNumber);

    this.graphDiv.emit(eventType, {points: [pointData]});
};

proto.draw = function() {
    if(this.stopped) return;

    requestAnimationFrame(this.redraw);

    var glplot = this.glplot;
    var camera = this.camera;
    var mouseListener = camera.mouseListener;
    var mouseUp = this.lastButtonState === 1 && mouseListener.buttons === 0;
    var fullLayout = this.fullLayout;

    this.lastButtonState = mouseListener.buttons;

    this.cameraChanged();

    var x = mouseListener.x * glplot.pixelRatio;
    var y = this.canvas.height - glplot.pixelRatio * mouseListener.y;

    var result;

    if(camera.boxEnabled && fullLayout.dragmode === 'zoom') {
        this.selectBox.enabled = true;

        var selectBox = this.selectBox.selectBox = [
            Math.min(camera.boxStart[0], camera.boxEnd[0]),
            Math.min(camera.boxStart[1], camera.boxEnd[1]),
            Math.max(camera.boxStart[0], camera.boxEnd[0]),
            Math.max(camera.boxStart[1], camera.boxEnd[1])
        ];

        // 1D zoom
        for(var i = 0; i < 2; i++) {
            if(camera.boxStart[i] === camera.boxEnd[i]) {
                selectBox[i] = glplot.dataBox[i];
                selectBox[i + 2] = glplot.dataBox[i + 2];
            }
        }

        glplot.setDirty();
    } else if(!camera.panning && this.isMouseOver) {
        this.selectBox.enabled = false;

        var size = fullLayout._size;
        var domainX = this.xaxis.domain;
        var domainY = this.yaxis.domain;

        result = glplot.pick(
            (x / glplot.pixelRatio) + size.l + domainX[0] * size.w,
            (y / glplot.pixelRatio) - (size.t + (1 - domainY[1]) * size.h)
        );

        var nextSelection = result && result.object._trace.handlePick(result);

        if(nextSelection && mouseUp) {
            this.emitPointAction(nextSelection, 'plotly_click');
        }

        if(result && result.object._trace.hoverinfo !== 'skip' && fullLayout.hovermode) {
            if(nextSelection && (
                !this.lastPickResult ||
                this.lastPickResult.traceUid !== nextSelection.trace.uid ||
                this.lastPickResult.dataCoord[0] !== nextSelection.dataCoord[0] ||
                this.lastPickResult.dataCoord[1] !== nextSelection.dataCoord[1])
            ) {
                var selection = nextSelection;

                this.lastPickResult = {
                    traceUid: nextSelection.trace ? nextSelection.trace.uid : null,
                    dataCoord: nextSelection.dataCoord.slice()
                };
                this.spikes.update({ center: result.dataCoord });

                selection.screenCoord = [
                    ((glplot.viewBox[2] - glplot.viewBox[0]) *
                    (result.dataCoord[0] - glplot.dataBox[0]) /
                        (glplot.dataBox[2] - glplot.dataBox[0]) + glplot.viewBox[0]) /
                            glplot.pixelRatio,
                    (this.canvas.height - (glplot.viewBox[3] - glplot.viewBox[1]) *
                    (result.dataCoord[1] - glplot.dataBox[1]) /
                        (glplot.dataBox[3] - glplot.dataBox[1]) - glplot.viewBox[1]) /
                            glplot.pixelRatio
                ];

                // this needs to happen before the next block that deletes traceCoord data
                // also it's important to copy, otherwise data is lost by the time event data is read
                this.emitPointAction(nextSelection, 'plotly_hover');

                var trace = this.fullData[selection.trace.index] || {};
                var ptNumber = selection.pointIndex;
                var hoverinfo = Fx.castHoverinfo(trace, fullLayout, ptNumber);

                if(hoverinfo && hoverinfo !== 'all') {
                    var parts = hoverinfo.split('+');
                    if(parts.indexOf('x') === -1) selection.traceCoord[0] = undefined;
                    if(parts.indexOf('y') === -1) selection.traceCoord[1] = undefined;
                    if(parts.indexOf('z') === -1) selection.traceCoord[2] = undefined;
                    if(parts.indexOf('text') === -1) selection.textLabel = undefined;
                    if(parts.indexOf('name') === -1) selection.name = undefined;
                }

                Fx.loneHover({
                    x: selection.screenCoord[0],
                    y: selection.screenCoord[1],
                    xLabel: this.hoverFormatter('xaxis', selection.traceCoord[0]),
                    yLabel: this.hoverFormatter('yaxis', selection.traceCoord[1]),
                    zLabel: selection.traceCoord[2],
                    text: selection.textLabel,
                    name: selection.name,
                    color: Fx.castHoverOption(trace, ptNumber, 'bgcolor') || selection.color,
                    borderColor: Fx.castHoverOption(trace, ptNumber, 'bordercolor'),
                    fontFamily: Fx.castHoverOption(trace, ptNumber, 'font.family'),
                    fontSize: Fx.castHoverOption(trace, ptNumber, 'font.size'),
                    fontColor: Fx.castHoverOption(trace, ptNumber, 'font.color'),
                    nameLength: Fx.castHoverOption(trace, ptNumber, 'namelength'),
                    textAlign: Fx.castHoverOption(trace, ptNumber, 'align')
                }, {
                    container: this.svgContainer,
                    gd: this.graphDiv
                });
            }
        }
    }

    // Remove hover effects if we're not over a point OR
    // if we're zooming or panning (in which case result is not set)
    if(!result) {
        this.unhover();
    }

    glplot.draw();
};

proto.unhover = function() {
    if(this.lastPickResult) {
        this.spikes.update({});
        this.lastPickResult = null;
        this.graphDiv.emit('plotly_unhover');
        Fx.loneUnhover(this.svgContainer);
    }
};

proto.hoverFormatter = function(axisName, val) {
    if(val === undefined) return undefined;

    var axis = this[axisName];
    return Axes.tickText(axis, axis.c2l(val), 'hover').text;
};
