import utils from './utils';

export class GraphInteractions {


    private panEnable!: boolean;

    private mouseTimer!: number;

    private scrollEnable: boolean;

    private scrollTimer!: number;

    private zoomTimer!: number;
    private scaleTimer!: number;

    private preDatewindow!: Array<any>;

    private needRefresh!: boolean;

    private yAxisRangeChanged!: boolean;



    constructor(public callback: any, public dateRange?: Array<number>) {
        this.panEnable = false;
        this.scrollEnable = false;
    }

    private LOG_SCALE = 10;
    private LN_TEN = Math.log(this.LOG_SCALE);


    private log10 = (x: number) => {
        return Math.log(x) / this.LN_TEN;
    };

    private pageX = (e: MouseEvent) => {
        return !e.pageX || e.pageX < 0 ? 0 : e.pageX;
    };

    private pageY = (e: MouseEvent) => {
        return !e.pageY || e.pageY < 0 ? 0 : e.pageY;
    };

    private dragGetX_ = (e: MouseEvent, context: any) => {
        return this.pageX(e) - context.px;
    };

    private dragGetY_ = (e: MouseEvent, context: any) => {
        return this.pageY(e) - context.py;
    };

    private endPan = (event: MouseEvent, g: any, context: any) => {
        context.dragEndX = this.dragGetX_(event, context);
        context.dragEndY = this.dragGetY_(event, context);
        let regionWidth = Math.abs(context.dragEndX - context.dragStartX);
        let regionHeight = Math.abs(context.dragEndY - context.dragStartY);

        if (regionWidth < 2 && regionHeight < 2 &&
            g.lastx_ !== undefined && g.lastx_ != -1) {
            this.treatMouseOpAsClick(g, event, context);
        }

        context.regionWidth = regionWidth;
        context.regionHeight = regionHeight;
    };

    private startPan = (event: MouseEvent, g: any, context: any) => {
        let i, axis;
        context.isPanning = true;
        let xRange = g.xAxisRange();

        if (g.getOptionForAxis("logscale", "x")) {
            context.initialLeftmostDate = this.log10(xRange[0]);
            context.dateRange = this.log10(xRange[1]) - this.log10(xRange[0]);
        } else {
            context.initialLeftmostDate = xRange[0];
            context.dateRange = xRange[1] - xRange[0];
        }
        context.xUnitsPerPixel = context.dateRange / (g.plotter_.area.w - 1);

        if (g.getNumericOption("panEdgeFraction")) {
            let maxXPixelsToDraw = g.width_ * g.getNumericOption("panEdgeFraction");
            let xExtremes = g.xAxisExtremes(); // I REALLY WANT TO CALL THIS xTremes!

            let boundedLeftX = g.toDomXCoord(xExtremes[0]) - maxXPixelsToDraw;
            let boundedRightX = g.toDomXCoord(xExtremes[1]) + maxXPixelsToDraw;

            let boundedLeftDate = g.toDataXCoord(boundedLeftX);
            let boundedRightDate = g.toDataXCoord(boundedRightX);
            context.boundedDates = [boundedLeftDate, boundedRightDate];

            let boundedValues = [];
            let maxYPixelsToDraw = g.height_ * g.getNumericOption("panEdgeFraction");

            for (i = 0; i < g.axes_.length; i++) {
                axis = g.axes_[i];
                let yExtremes = axis.extremeRange;

                let boundedTopY = g.toDomYCoord(yExtremes[0], i) + maxYPixelsToDraw;
                let boundedBottomY = g.toDomYCoord(yExtremes[1], i) - maxYPixelsToDraw;

                let boundedTopValue = g.toDataYCoord(boundedTopY, i);
                let boundedBottomValue = g.toDataYCoord(boundedBottomY, i);

                boundedValues[i] = [boundedTopValue, boundedBottomValue];
            }
            context.boundedValues = boundedValues;
        }

        // Record the range of each y-axis at the start of the drag.
        // If any axis has a valueRange, then we want a 2D pan.
        // We can't store data directly in g.axes_, because it does not belong to us
        // and could change out from under us during a pan (say if there's a data
        // update).
        context.is2DPan = false;
        context.axes = [];
        for (i = 0; i < g.axes_.length; i++) {
            axis = g.axes_[i];
            let axis_data = { initialTopValue: 0, dragValueRange: 0, unitsPerPixel: 0 };
            let yRange = g.yAxisRange(i);
            // TODO(konigsberg): These values should be in |context|.
            // In log scale, initialTopValue, dragValueRange and unitsPerPixel are log scale.
            let logscale = g.attributes_.getForAxis("logscale", i);
            if (logscale) {
                axis_data.initialTopValue = this.log10(yRange[1]);
                axis_data.dragValueRange = this.log10(yRange[1]) - this.log10(yRange[0]);
            } else {
                axis_data.initialTopValue = yRange[1];
                axis_data.dragValueRange = yRange[1] - yRange[0];
            }
            axis_data.unitsPerPixel = axis_data.dragValueRange / (g.plotter_.area.h - 1);
            context.axes.push(axis_data);

            // While calculating axes, set 2dpan.
            if (axis.valueRange) context.is2DPan = true;
        }
    };


    private treatMouseOpAsClick = (g: any, event: MouseEvent, context: any) => {
        let clickCallback = g.getFunctionOption('clickCallback');
        let pointClickCallback = g.getFunctionOption('pointClickCallback');

        let selectedPoint = null;

        // Find out if the click occurs on a point.
        let closestIdx = -1;
        let closestDistance = Number.MAX_VALUE;

        // check if the click was on a particular point.
        for (let i = 0; i < g.selPoints_.length; i++) {
            let p = g.selPoints_[i];
            let distance = Math.pow(p.canvasx - context.dragEndX, 2) +
                Math.pow(p.canvasy - context.dragEndY, 2);
            if (!isNaN(distance) &&
                (closestIdx == -1 || distance < closestDistance)) {
                closestDistance = distance;
                closestIdx = i;
            }
        }

        // Allow any click within two pixels of the dot.
        let radius = g.getNumericOption('highlightCircleSize') + 2;
        if (closestDistance <= radius * radius) {
            selectedPoint = g.selPoints_[closestIdx];
        }

        if (selectedPoint) {
            let e: any = {
                cancelable: true,
                point: selectedPoint,
                canvasx: context.dragEndX,
                canvasy: context.dragEndY
            };
            let defaultPrevented = g.cascadeEvents_('pointClick', e);
            if (defaultPrevented) {
                // Note: this also prevents click / clickCallback from firing.
                return;
            }
            if (pointClickCallback) {
                pointClickCallback.call(g, event, selectedPoint);
            }
        }

        let e: any = {
            cancelable: true,
            xval: g.lastx_,  // closest point by x value
            pts: g.selPoints_,
            canvasx: context.dragEndX,
            canvasy: context.dragEndY
        };
        if (!g.cascadeEvents_('click', e)) {
            if (clickCallback) {
                // TODO(danvk): pass along more info about the points, e.g. 'x'
                clickCallback.call(g, event, g.lastx_, g.selPoints_);
            }
        }
    }


    private offsetToPercentage = (g: any, offsetX: number, offsetY: number) => {
        // This is calculating the pixel offset of the leftmost date.
        let xOffset = g.toDomCoords(g.xAxisRange()[0], null)[0];
        let yar0 = g.yAxisRange(0);

        // This is calculating the pixel of the higest value. (Top pixel)
        let yOffset = g.toDomCoords(null, yar0[1])[1];

        // x y w and h are relative to the corner of the drawing area,
        // so that the upper corner of the drawing area is (0, 0).
        let x = offsetX - xOffset;
        let y = offsetY - yOffset;

        // This is computing the rightmost pixel, effectively defining the
        // width.
        let w = g.toDomCoords(g.xAxisRange()[1], null)[0] - xOffset;

        // This is computing the lowest pixel, effectively defining the height.
        let h = g.toDomCoords(null, yar0[0])[1] - yOffset;

        // Percentage from the left.
        let xPct = w == 0 ? 0 : (x / w);
        // Percentage from the top.
        let yPct = h == 0 ? 0 : (y / h);

        // The (1-) part below changes it from "% distance down from the top"
        // to "% distance up from the bottom".
        return [xPct, (1 - yPct)];
    };

    private pan = (event: MouseEvent, g: any, context: any, side: string) => {
        context.dragEndX = this.dragGetX_(event, context);
        context.dragEndY = this.dragGetY_(event, context);

        let minDate = context.initialLeftmostDate - (context.dragEndX - context.dragStartX) * context.xUnitsPerPixel;
        if (context.boundedDates) {
            minDate = Math.max(minDate, context.boundedDates[0]);
        }
        let maxDate = minDate + context.dateRange;
        if (context.boundedDates) {
            if (maxDate > context.boundedDates[1]) {
                // Adjust minDate, and recompute maxDate.
                minDate = minDate - (maxDate - context.boundedDates[1]);
                maxDate = minDate + context.dateRange;
            }
        }

        // y-axis scaling is automatic unless this is a full 2D pan.
        if (context.is2DPan) {
            let pixelsDragged = context.dragEndY - context.dragStartY;
            // Adjust each axis appropriately.
            if (side && ("r" == side || "l" == side)) {
                let index = (side == 'l' ? 0 : 1);
                let axis = g.axes_[index];
                let axis_data = context.axes[index];
                let unitsDragged = pixelsDragged * axis_data.unitsPerPixel;
                let boundedValue = context.boundedValues ? context.boundedValues[index] : null;
                // In log scale, maxValue and minValue are the logs of those values.
                let maxValue = axis_data.initialTopValue + unitsDragged;
                if (boundedValue) {
                    maxValue = Math.min(maxValue, boundedValue[index]);
                }
                let minValue = maxValue - axis_data.dragValueRange;

                if (boundedValue) {
                    if (minValue < boundedValue[0]) {
                        // Adjust maxValue, and recompute minValue.
                        maxValue = maxValue - (minValue - boundedValue[0]);
                        minValue = maxValue - axis_data.dragValueRange;
                    }
                }
                if (g.attributes_.getForAxis("logscale", index)) {
                    axis.valueRange = [Math.pow(10, minValue), Math.pow(10, maxValue)];
                    axis.valueWindow = [Math.pow(10, minValue), Math.pow(10, maxValue)];
                    axis.extremeRange = [Math.pow(10, minValue), Math.pow(10, maxValue)];
                } else {
                    axis.valueRange = [minValue, maxValue];
                    axis.valueWindow = [minValue, maxValue];
                    axis.extremeRange = [minValue, maxValue];
                }
                g.drawGraph_(true);
            } else {
                //
                let zoomRange = this.dateRange;
                if (zoomRange && (minDate < zoomRange[0] || maxDate > zoomRange[1])) {
                    // console.info("return~~~~", new Date(minDate), new Date(zoomRange[0]), new Date(maxDate), new Date(zoomRange[1]));
                    return;
                }
                if (g.getOptionForAxis("logscale", "x")) {
                    g.dateWindow_ = [new Date(Math.pow(10, minDate)), new Date(Math.pow(10, maxDate))];
                } else {
                    g.dateWindow_ = [new Date(minDate), new Date(maxDate)];
                }
                g.drawGraph_(false);
            }
        }
        
    };

    private adjustAxis = (axis: any, zoomInPercentage: number, bias: any) => {
        let delta = axis[1] - axis[0];
        let increment = delta * zoomInPercentage;
        let foo = [increment * bias, increment * (1 - bias)];
        return [axis[0] + foo[0], axis[1] - foo[1]];
    };

    private zoom = (g: any, zoomInPercentage: number, xBias: any, yBias: any, direction: string, side: string, e?: Event) => {

        xBias = xBias || 0.5;
        yBias = yBias || 0.5;
        let yAxes = g.axes_;
        let newYAxes = [];
        for (let i = 0; i < g.numAxes(); i++) {
            if(!yAxes[i].valueRange){
                yAxes[i].valueRange = [yAxes[i].minyval, yAxes[i].maxyval];
            } 
            newYAxes[i] = this.adjustAxis(yAxes[i].valueRange, zoomInPercentage, yBias);
        }

        if ('v' == direction) {
            if (this.zoomTimer) {
                window.clearTimeout(this.zoomTimer);
            }
            if ('l' == side) {
                yAxes[0]['valueRange'] = newYAxes[0];
                yAxes[0]['valueWindow'] = newYAxes[0];
                yAxes[0]['extremeRange'] = newYAxes[0];
            } else if ('r' == side && g.numAxes() == 2) {
                yAxes[1]['valueRange'] = newYAxes[1];
                yAxes[1]['valueWindow'] = newYAxes[1];
                yAxes[1]['extremeRange'] = newYAxes[1];
            }

            this.zoomTimer = window.setTimeout(()=>{
                this.callback(e, g.yAxisRanges(), false);
            }, 500);
            g.drawGraph_(false);
        } else {
            if (this.scrollTimer) {
                window.clearTimeout(this.scrollTimer);
            }
            let ranges = g.dateWindow_;
            if (ranges[0] instanceof Date) {
                ranges[0] = ranges[0].getTime();
                ranges[1] = ranges[1].getTime();
            }

            let newZoomRange = this.adjustAxis(ranges, zoomInPercentage, xBias);
            // do not bigger than range data
            let zoomRange = this.dateRange;
            this.scrollTimer = window.setTimeout(() => {
                this.callback(e, g.yAxisRanges(), true);
            }, 500);
            if (zoomRange && (newZoomRange[0] < zoomRange[0] && newZoomRange[1] > zoomRange[1])) {
                return;
            } else if (newZoomRange[0] >= newZoomRange[1]) {
                return;
            } else if (zoomRange && (newZoomRange[0] <= zoomRange[0] && newZoomRange[1] < zoomRange[1])) {
                g.updateOptions({
                    dateWindow: [zoomRange[0], newZoomRange[1]]
                });
            } else if (zoomRange && (newZoomRange[0] > zoomRange[0] && newZoomRange[1] >= zoomRange[1])) {
                g.updateOptions({
                    dateWindow: [newZoomRange[0], zoomRange[1]]
                });
            } else {
                g.updateOptions({
                    dateWindow: [newZoomRange[0], newZoomRange[1]]
                });
            }
        }
    };





    public mouseUp = (e: MouseEvent, g: any, context: any) => {
        // call callback on windows mouseup
        // console.debug("mouse up");
        let currentDatewindow = g.dateWindow_;
        if (currentDatewindow[0] instanceof Date) {
            currentDatewindow[0] = currentDatewindow[0].getTime();
            currentDatewindow[1] = currentDatewindow[1].getTime();
        }
        context.isPanning = false;
        // Dygraph.endPan(event, g, context);
        this.endPan(e, g, context);
        // call upadte this.panEnable = false;
        if (this.panEnable && this.needRefresh && (this.preDatewindow[0] != currentDatewindow[0] || this.preDatewindow[1] != currentDatewindow[1])) {
            this.callback(e, g.yAxisRanges(), true);
            this.panEnable = false;
        } else if (this.yAxisRangeChanged) {
            this.callback(e, g.yAxisRanges(), false);
            this.panEnable = false;
        }
    };

    public mouseDown = (e: MouseEvent, g: any, context: any) => {
        this.preDatewindow = g.dateWindow_;
        if (this.preDatewindow[0] instanceof Date) {
            this.preDatewindow[0] = this.preDatewindow[0].getTime();
            this.preDatewindow[1] = this.preDatewindow[1].getTime();
        }

        this.panEnable = true;
        context.initializeMouseDown(event, g, context);
        this.startPan(e, g, context);
        // console.debug("mouse down", context);
    };

    public mouseMove = (e: MouseEvent, g: any, context: any) => {
        if(this.scaleTimer){
            window.clearTimeout(this.scaleTimer);
        }
        if (this.panEnable && context.isPanning) {
            if (e.offsetX <= (g.plotter_.area.x)) {
                this.needRefresh = false;
                this.yAxisRangeChanged = true;
                this.pan(e, g, context, 'l');
                this.callback(e, g.yAxisRanges(), false);
            } else if (e.offsetX >= (g.plotter_.area.x + g.plotter_.area.w)) {
                this.needRefresh = false;
                this.yAxisRangeChanged = true;
                this.pan(e, g, context, 'r');
                this.callback(e, g.yAxisRanges(), false);
            } else {
                this.needRefresh = true;
                this.pan(e, g, context, 'h');
            }
        }
    };

    public mouseOut = (e: MouseEvent, g: any, context: any) => {
        // console.debug("mouse out");
        if (this.mouseTimer) {
            window.clearTimeout(this.mouseTimer);
        }
        this.scrollEnable = false;

    };

    public mouseScroll = (e: any, g: any, context: any) => {
        if (this.scrollEnable) {
            //
            let normal;

            if (e instanceof WheelEvent) {
                normal = e.detail ? e.detail * -1 : e.deltaY / 40;
            } else {
                normal = e.detail ? e.detail * -1 : e.wheelDelta / 40;
            }

            // For me the normalized value shows 0.075 for one click. If I took
            // that verbatim, it would be a 7.5%.
            let percentage = normal / 50;

            if (!(e.offsetX && e.offsetY)) {
                e.offsetX = e.layerX - e.target.offsetLeft;
                e.offsetY = e.layerY - e.target.offsetTop;
            }
            let percentages = this.offsetToPercentage(g, e.offsetX, e.offsetY);
            let xPct = percentages[0];
            let yPct = percentages[1];
            //
            if (e.offsetX <= (g.plotter_.area.x)) {
                // left zoom
                this.zoom(g, percentage, xPct, yPct, 'v', 'l');
            } else if (e.offsetX >= (g.plotter_.area.x + g.plotter_.area.w)) {
                // right zoom
                this.zoom(g, percentage, xPct, yPct, 'v', 'r');
            } else {
                // middle zoom
                this.zoom(g, percentage, xPct, yPct, 'h', 'm');
            }
            utils.cancelEvent(e);
        }
    };

    public mouseEnter = (e: MouseEvent, g: any, context: any) => {

        if (this.mouseTimer) {
            window.clearTimeout(this.mouseTimer);
        }
        this.mouseTimer = window.setTimeout(() => {
            this.scrollEnable = true;
            // console.debug("enable scroll zooming~");
        }, 1000);
    }
}

