/**
* 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 d3 = require('d3');

var Registry = require('../../registry');
var Plots = require('../../plots/plots');

var Lib = require('../../lib');
var Drawing = require('../drawing');
var Color = require('../color');
var Titles = require('../titles');

var Cartesian = require('../../plots/cartesian');
var Axes = require('../../plots/cartesian/axes');

var dragElement = require('../dragelement');
var setCursor = require('../../lib/setcursor');

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

module.exports = function(gd) {
    var fullLayout = gd._fullLayout,
        rangeSliderData = makeRangeSliderData(fullLayout);

    /*
     * <g container />
     *  <rect bg />
     *  < .... range plot />
     *  <rect mask-min />
     *  <rect mask-max />
     *  <rect slidebox />
     *  <g grabber-min />
     *      <rect handle-min />
     *      <rect grabare-min />
     *  <g grabber-max />
     *      <rect handle-max />
     *      <rect grabare-max />
     *
     *  ...
     */

    function keyFunction(axisOpts) {
        return axisOpts._name;
    }

    var rangeSliders = fullLayout._infolayer
        .selectAll('g.' + constants.containerClassName)
        .data(rangeSliderData, keyFunction);

    rangeSliders.enter().append('g')
        .classed(constants.containerClassName, true)
        .attr('pointer-events', 'all');

    // remove exiting sliders and their corresponding clip paths
    rangeSliders.exit().each(function(axisOpts) {
        var rangeSlider = d3.select(this),
            opts = axisOpts[constants.name];

        rangeSlider.remove();
        fullLayout._topdefs.select('#' + opts._clipId).remove();
    });

    // remove push margin object(s)
    if(rangeSliders.exit().size()) clearPushMargins(gd);

    // return early if no range slider is visible
    if(rangeSliderData.length === 0) return;

    // for all present range sliders
    rangeSliders.each(function(axisOpts) {
        var rangeSlider = d3.select(this),
            opts = axisOpts[constants.name],
            oppAxisOpts = fullLayout[Axes.id2name(axisOpts.anchor)],
            oppAxisRangeOpts = opts[Axes.id2name(axisOpts.anchor)];

        // update range
        // Expand slider range to the axis range
        // TODO: what if the ranges are reversed?
        if(opts.range) {
            var outRange = opts.range;
            var axRange = axisOpts.range;

            outRange[0] = axisOpts.l2r(Math.min(axisOpts.r2l(outRange[0]), axisOpts.r2l(axRange[0])));
            outRange[1] = axisOpts.l2r(Math.max(axisOpts.r2l(outRange[1]), axisOpts.r2l(axRange[1])));
            opts._input.range = outRange.slice();
        }

        axisOpts.cleanRange('rangeslider.range');


        // update range slider dimensions

        var margin = fullLayout.margin;
        var graphSize = fullLayout._size;
        var domain = axisOpts.domain;
        var tickHeight = (axisOpts._boundingBox || {}).height || 0;

        var oppBottom = Infinity;
        var subplotData = Axes.getSubplots(gd, axisOpts);
        for(var i = 0; i < subplotData.length; i++) {
            var oppAxis = Axes.getFromId(gd, subplotData[i].substr(subplotData[i].indexOf('y')));
            oppBottom = Math.min(oppBottom, oppAxis.domain[0]);
        }

        opts._id = constants.name + axisOpts._id;
        opts._clipId = opts._id + '-' + fullLayout._uid;

        opts._width = graphSize.w * (domain[1] - domain[0]);
        opts._height = (fullLayout.height - margin.b - margin.t) * opts.thickness;
        opts._offsetShift = Math.floor(opts.borderwidth / 2);

        var x = Math.round(margin.l + (graphSize.w * domain[0]));

        var y = Math.round(
            graphSize.t + graphSize.h * (1 - oppBottom) +
            tickHeight +
            opts._offsetShift + constants.extraPad
        );

        rangeSlider.attr('transform', 'translate(' + x + ',' + y + ')');

        // update data <--> pixel coordinate conversion methods

        var range0 = axisOpts.r2l(opts.range[0]),
            range1 = axisOpts.r2l(opts.range[1]),
            dist = range1 - range0;

        opts.p2d = function(v) {
            return (v / opts._width) * dist + range0;
        };

        opts.d2p = function(v) {
            return (v - range0) / dist * opts._width;
        };

        opts._rl = [range0, range1];

        if(oppAxisRangeOpts.rangemode !== 'match') {
            var range0OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[0]),
                range1OppAxis = oppAxisOpts.r2l(oppAxisRangeOpts.range[1]),
                distOppAxis = range1OppAxis - range0OppAxis;

            opts.d2pOppAxis = function(v) {
                return (v - range0OppAxis) / distOppAxis * opts._height;
            };
        }

        // update inner nodes

        rangeSlider
            .call(drawBg, gd, axisOpts, opts)
            .call(addClipPath, gd, axisOpts, opts)
            .call(drawRangePlot, gd, axisOpts, opts)
            .call(drawMasks, gd, axisOpts, opts, oppAxisRangeOpts)
            .call(drawSlideBox, gd, axisOpts, opts)
            .call(drawGrabbers, gd, axisOpts, opts);

        // setup drag element
        setupDragElement(rangeSlider, gd, axisOpts, opts);

        // update current range
        setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts);

        // title goes next to range slider instead of tick labels, so
        // just take it over and draw it from here
        if(axisOpts.side === 'bottom') {
            Titles.draw(gd, axisOpts._id + 'title', {
                propContainer: axisOpts,
                propName: axisOpts._name + '.title',
                placeholder: fullLayout._dfltTitle.x,
                attributes: {
                    x: axisOpts._offset + axisOpts._length / 2,
                    y: y + opts._height + opts._offsetShift + 10 + 1.5 * axisOpts.titlefont.size,
                    'text-anchor': 'middle'
                }
            });
        }

        // update margins
        Plots.autoMargin(gd, opts._id, {
            x: domain[0],
            y: oppBottom,
            l: 0,
            r: 0,
            t: 0,
            b: opts._height + margin.b + tickHeight,
            pad: constants.extraPad + opts._offsetShift * 2
        });
    });
};

function makeRangeSliderData(fullLayout) {
    var axes = Axes.list({ _fullLayout: fullLayout }, 'x', true),
        name = constants.name,
        out = [];

    if(fullLayout._has('gl2d')) return out;

    for(var i = 0; i < axes.length; i++) {
        var ax = axes[i];

        if(ax[name] && ax[name].visible) out.push(ax);
    }

    return out;
}

function setupDragElement(rangeSlider, gd, axisOpts, opts) {
    var slideBox = rangeSlider.select('rect.' + constants.slideBoxClassName).node(),
        grabAreaMin = rangeSlider.select('rect.' + constants.grabAreaMinClassName).node(),
        grabAreaMax = rangeSlider.select('rect.' + constants.grabAreaMaxClassName).node();

    rangeSlider.on('mousedown', function() {
        var event = d3.event,
            target = event.target,
            startX = event.clientX,
            offsetX = startX - rangeSlider.node().getBoundingClientRect().left,
            minVal = opts.d2p(axisOpts._rl[0]),
            maxVal = opts.d2p(axisOpts._rl[1]);

        var dragCover = dragElement.coverSlip();

        dragCover.addEventListener('mousemove', mouseMove);
        dragCover.addEventListener('mouseup', mouseUp);

        function mouseMove(e) {
            var delta = +e.clientX - startX;
            var pixelMin, pixelMax, cursor;

            switch(target) {
                case slideBox:
                    cursor = 'ew-resize';
                    pixelMin = minVal + delta;
                    pixelMax = maxVal + delta;
                    break;

                case grabAreaMin:
                    cursor = 'col-resize';
                    pixelMin = minVal + delta;
                    pixelMax = maxVal;
                    break;

                case grabAreaMax:
                    cursor = 'col-resize';
                    pixelMin = minVal;
                    pixelMax = maxVal + delta;
                    break;

                default:
                    cursor = 'ew-resize';
                    pixelMin = offsetX;
                    pixelMax = offsetX + delta;
                    break;
            }

            if(pixelMax < pixelMin) {
                var tmp = pixelMax;
                pixelMax = pixelMin;
                pixelMin = tmp;
            }

            opts._pixelMin = pixelMin;
            opts._pixelMax = pixelMax;

            setCursor(d3.select(dragCover), cursor);
            setDataRange(rangeSlider, gd, axisOpts, opts);
        }

        function mouseUp() {
            dragCover.removeEventListener('mousemove', mouseMove);
            dragCover.removeEventListener('mouseup', mouseUp);
            Lib.removeElement(dragCover);
        }
    });
}

function setDataRange(rangeSlider, gd, axisOpts, opts) {

    function clamp(v) {
        return axisOpts.l2r(Lib.constrain(v, opts._rl[0], opts._rl[1]));
    }

    var dataMin = clamp(opts.p2d(opts._pixelMin)),
        dataMax = clamp(opts.p2d(opts._pixelMax));

    window.requestAnimationFrame(function() {
        Registry.call('relayout', gd, axisOpts._name + '.range', [dataMin, dataMax]);
    });
}

function setPixelRange(rangeSlider, gd, axisOpts, opts, oppAxisOpts, oppAxisRangeOpts) {
    var hw2 = constants.handleWidth / 2;

    function clamp(v) {
        return Lib.constrain(v, 0, opts._width);
    }

    function clampOppAxis(v) {
        return Lib.constrain(v, 0, opts._height);
    }

    function clampHandle(v) {
        return Lib.constrain(v, -hw2, opts._width + hw2);
    }

    var pixelMin = clamp(opts.d2p(axisOpts._rl[0])),
        pixelMax = clamp(opts.d2p(axisOpts._rl[1]));

    rangeSlider.select('rect.' + constants.slideBoxClassName)
        .attr('x', pixelMin)
        .attr('width', pixelMax - pixelMin);

    rangeSlider.select('rect.' + constants.maskMinClassName)
        .attr('width', pixelMin);

    rangeSlider.select('rect.' + constants.maskMaxClassName)
        .attr('x', pixelMax)
        .attr('width', opts._width - pixelMax);

    if(oppAxisRangeOpts.rangemode !== 'match') {
        var pixelMinOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[1])),
            pixelMaxOppAxis = opts._height - clampOppAxis(opts.d2pOppAxis(oppAxisOpts._rl[0]));

        rangeSlider.select('rect.' + constants.maskMinOppAxisClassName)
            .attr('x', pixelMin)
            .attr('height', pixelMinOppAxis)
            .attr('width', pixelMax - pixelMin);

        rangeSlider.select('rect.' + constants.maskMaxOppAxisClassName)
            .attr('x', pixelMin)
            .attr('y', pixelMaxOppAxis)
            .attr('height', opts._height - pixelMaxOppAxis)
            .attr('width', pixelMax - pixelMin);

        rangeSlider.select('rect.' + constants.slideBoxClassName)
            .attr('y', pixelMinOppAxis)
            .attr('height', pixelMaxOppAxis - pixelMinOppAxis);
    }

    // add offset for crispier corners
    // https://github.com/plotly/plotly.js/pull/1409
    var offset = 0.5;

    var xMin = Math.round(clampHandle(pixelMin - hw2)) - offset,
        xMax = Math.round(clampHandle(pixelMax - hw2)) + offset;

    rangeSlider.select('g.' + constants.grabberMinClassName)
        .attr('transform', 'translate(' + xMin + ',' + offset + ')');

    rangeSlider.select('g.' + constants.grabberMaxClassName)
        .attr('transform', 'translate(' + xMax + ',' + offset + ')');
}

function drawBg(rangeSlider, gd, axisOpts, opts) {
    var bg = Lib.ensureSingle(rangeSlider, 'rect', constants.bgClassName, function(s) {
        s.attr({
            x: 0,
            y: 0,
            'shape-rendering': 'crispEdges'
        });
    });

    var borderCorrect = (opts.borderwidth % 2) === 0 ?
            opts.borderwidth :
            opts.borderwidth - 1;

    var offsetShift = -opts._offsetShift;
    var lw = Drawing.crispRound(gd, opts.borderwidth);

    bg.attr({
        width: opts._width + borderCorrect,
        height: opts._height + borderCorrect,
        transform: 'translate(' + offsetShift + ',' + offsetShift + ')',
        fill: opts.bgcolor,
        stroke: opts.bordercolor,
        'stroke-width': lw
    });
}

function addClipPath(rangeSlider, gd, axisOpts, opts) {
    var fullLayout = gd._fullLayout;

    var clipPath = Lib.ensureSingleById(fullLayout._topdefs, 'clipPath', opts._clipId, function(s) {
        s.append('rect').attr({ x: 0, y: 0 });
    });

    clipPath.select('rect').attr({
        width: opts._width,
        height: opts._height
    });
}

function drawRangePlot(rangeSlider, gd, axisOpts, opts) {
    var subplotData = Axes.getSubplots(gd, axisOpts),
        calcData = gd.calcdata;

    var rangePlots = rangeSlider.selectAll('g.' + constants.rangePlotClassName)
        .data(subplotData, Lib.identity);

    rangePlots.enter().append('g')
        .attr('class', function(id) { return constants.rangePlotClassName + ' ' + id; })
        .call(Drawing.setClipUrl, opts._clipId);

    rangePlots.order();

    rangePlots.exit().remove();

    var mainplotinfo;

    rangePlots.each(function(id, i) {
        var plotgroup = d3.select(this),
            isMainPlot = (i === 0);

        var oppAxisOpts = Axes.getFromId(gd, id, 'y'),
            oppAxisName = oppAxisOpts._name,
            oppAxisRangeOpts = opts[oppAxisName];

        var mockFigure = {
            data: [],
            layout: {
                xaxis: {
                    type: axisOpts.type,
                    domain: [0, 1],
                    range: opts.range.slice(),
                    calendar: axisOpts.calendar
                },
                width: opts._width,
                height: opts._height,
                margin: { t: 0, b: 0, l: 0, r: 0 }
            },
            _context: gd._context
        };

        mockFigure.layout[oppAxisName] = {
            type: oppAxisOpts.type,
            domain: [0, 1],
            range: oppAxisRangeOpts.rangemode !== 'match' ? oppAxisRangeOpts.range.slice() : oppAxisOpts.range.slice(),
            calendar: oppAxisOpts.calendar
        };

        Plots.supplyDefaults(mockFigure);

        var xa = mockFigure._fullLayout.xaxis;
        var ya = mockFigure._fullLayout[oppAxisName];

        var plotinfo = {
            id: id,
            plotgroup: plotgroup,
            xaxis: xa,
            yaxis: ya,
            isRangePlot: true
        };

        if(isMainPlot) mainplotinfo = plotinfo;
        else {
            plotinfo.mainplot = 'xy';
            plotinfo.mainplotinfo = mainplotinfo;
        }

        Cartesian.rangePlot(gd, plotinfo, filterRangePlotCalcData(calcData, id));
    });
}

function filterRangePlotCalcData(calcData, subplotId) {
    var out = [];

    for(var i = 0; i < calcData.length; i++) {
        var calcTrace = calcData[i],
            trace = calcTrace[0].trace;

        if(trace.xaxis + trace.yaxis === subplotId) {
            out.push(calcTrace);
        }
    }

    return out;
}

function drawMasks(rangeSlider, gd, axisOpts, opts, oppAxisRangeOpts) {
    var maskMin = Lib.ensureSingle(rangeSlider, 'rect', constants.maskMinClassName, function(s) {
        s.attr({
            x: 0,
            y: 0,
            'shape-rendering': 'crispEdges'
        });
    });

    maskMin
        .attr('height', opts._height)
        .call(Color.fill, constants.maskColor);

    var maskMax = Lib.ensureSingle(rangeSlider, 'rect', constants.maskMaxClassName, function(s) {
        s.attr({
            y: 0,
            'shape-rendering': 'crispEdges'
        });
    });

    maskMax
        .attr('height', opts._height)
        .call(Color.fill, constants.maskColor);

    // masks used for oppAxis zoom
    if(oppAxisRangeOpts.rangemode !== 'match') {
        var maskMinOppAxis = Lib.ensureSingle(rangeSlider, 'rect', constants.maskMinOppAxisClassName, function(s) {
            s.attr({
                y: 0,
                'shape-rendering': 'crispEdges'
            });
        });

        maskMinOppAxis
            .attr('width', opts._width)
            .call(Color.fill, constants.maskOppAxisColor);

        var maskMaxOppAxis = Lib.ensureSingle(rangeSlider, 'rect', constants.maskMaxOppAxisClassName, function(s) {
            s.attr({
                y: 0,
                'shape-rendering': 'crispEdges'
            });
        });

        maskMaxOppAxis
            .attr('width', opts._width)
            .style('border-top', constants.maskOppBorder)
            .call(Color.fill, constants.maskOppAxisColor);
    }
}

function drawSlideBox(rangeSlider, gd, axisOpts, opts) {
    if(gd._context.staticPlot) return;

    var slideBox = Lib.ensureSingle(rangeSlider, 'rect', constants.slideBoxClassName, function(s) {
        s.attr({
            y: 0,
            cursor: constants.slideBoxCursor,
            'shape-rendering': 'crispEdges'
        });
    });

    slideBox.attr({
        height: opts._height,
        fill: constants.slideBoxFill
    });
}

function drawGrabbers(rangeSlider, gd, axisOpts, opts) {
    // <g grabber />
    var grabberMin = Lib.ensureSingle(rangeSlider, 'g', constants.grabberMinClassName);
    var grabberMax = Lib.ensureSingle(rangeSlider, 'g', constants.grabberMaxClassName);

    // <g handle />
    var handleFixAttrs = {
        x: 0,
        width: constants.handleWidth,
        rx: constants.handleRadius,
        fill: Color.background,
        stroke: Color.defaultLine,
        'stroke-width': constants.handleStrokeWidth,
        'shape-rendering': 'crispEdges'
    };
    var handleDynamicAttrs = {
        y: Math.round(opts._height / 4),
        height: Math.round(opts._height / 2),
    };
    var handleMin = Lib.ensureSingle(grabberMin, 'rect', constants.handleMinClassName, function(s) {
        s.attr(handleFixAttrs);
    });
    handleMin.attr(handleDynamicAttrs);

    var handleMax = Lib.ensureSingle(grabberMax, 'rect', constants.handleMaxClassName, function(s) {
        s.attr(handleFixAttrs);
    });
    handleMax.attr(handleDynamicAttrs);

    // <g grabarea />
    if(gd._context.staticPlot) return;

    var grabAreaFixAttrs = {
        width: constants.grabAreaWidth,
        x: 0,
        y: 0,
        fill: constants.grabAreaFill,
        cursor: constants.grabAreaCursor
    };

    var grabAreaMin = Lib.ensureSingle(grabberMin, 'rect', constants.grabAreaMinClassName, function(s) {
        s.attr(grabAreaFixAttrs);
    });
    grabAreaMin.attr('height', opts._height);

    var grabAreaMax = Lib.ensureSingle(grabberMax, 'rect', constants.grabAreaMaxClassName, function(s) {
        s.attr(grabAreaFixAttrs);
    });
    grabAreaMax.attr('height', opts._height);
}

function clearPushMargins(gd) {
    var pushMargins = gd._fullLayout._pushmargin || {},
        keys = Object.keys(pushMargins);

    for(var i = 0; i < keys.length; i++) {
        var k = keys[i];

        if(k.indexOf(constants.name) !== -1) {
            Plots.autoMargin(gd, k);
        }
    }
}
