import { action, computed, IReactionDisposer, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { DataPoint } from '../ChartBox';
// import d3
import * as d3 from 'd3';
import { minMaxRange, createLineGenerator, xGrid, yGrid, drawLine, xAxisCreator, yAxisCreator, scaleCreatorNumerical, scaleCreatorCategorical } from '../utils/D3Utils';
import { Docs } from '../../../../documents/Documents';
import { Doc, DocListCast } from '../../../../../fields/Doc';
import { Chart, ChartProps } from '../ChartInterface';
import './Chart.scss';
import { List } from '../../../../../fields/List';
import { PinProps, PresBox } from '../../trails';
import { listSpec } from '../../../../../fields/Schema';
import { Cast } from '../../../../../fields/Types';

type minMaxRange = {
    xMin: number | undefined;
    xMax: number | undefined;
    yMin: number | undefined;
    yMax: number | undefined;
};

interface SelectedDataPoint {
    x: number;
    y: number;
    elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>;
}

@observer
export class LineChart extends React.Component<ChartProps> implements Chart {
    private _dataReactionDisposer: IReactionDisposer | undefined = undefined;
    private _heightReactionDisposer: IReactionDisposer | undefined = undefined;
    private _widthReactionDisposer: IReactionDisposer | undefined;
    @observable private _x: number = 0;
    @observable private _y: number = 0;
    @observable private _currSelected: SelectedDataPoint | undefined = undefined;
    // create ref for the div
    private _chartRef: React.RefObject<HTMLDivElement> = React.createRef();
    private _rangeVals: minMaxRange = {
        xMin: undefined,
        xMax: undefined,
        yMin: undefined,
        yMax: undefined,
    };
    // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates

    private _chartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;

    // anything that doesn't need to be recalculated should just be stored as drawCharts (i.e. computed values) and drawChart is gonna iterate over these observables and generate svgs based on that

    // write the getanchor function that gets whatever I want as the link anchor

    componentDidMount = () => {
        this._dataReactionDisposer = reaction(
            () => this.props.chartData,
            chartData => {
                this._rangeVals = minMaxRange(chartData.data);
                this.drawChart();
            },
            { fireImmediately: true }
        );
        // DocumentDecorations.Instance.Interacting
        this._heightReactionDisposer = reaction(() => this.props.height, this.drawChart.bind(this));
        this._widthReactionDisposer = reaction(() => this.props.width, this.drawChart.bind(this));
        reaction(
            () => DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']),
            annotations => {
                // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way
                // could be blue colored to make it look like anchor
                console.log(annotations);
                // this.drawAnnotations()
                // loop through annotations and draw them
                annotations.forEach(a => {
                    this.drawAnnotations(Number(a.x), Number(a.y));
                });
                // this.drawAnnotations(annotations.x, annotations.y);
            },
            { fireImmediately: true }
        );

        this.props.setCurrChart(this);
    };

    // gets called whenever the "data-annotations" fields gets updated
    drawAnnotations(dataX: number, dataY: number) {
        // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements
        // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY
        // if it exists, then highlight it
        // if it doesn't exist, then remove the highlight
        const elements = document.querySelectorAll('.datapoint');
        for (let i = 0; i < elements.length; i++) {
            const element = elements[i];
            const x = element.getAttribute('data-x');
            const y = element.getAttribute('data-y');
            if (x === dataX.toString() && y === dataY.toString()) {
                element.classList.add('highlight');
            }
            // TODO: nda - this remove highlight code should go where we remove the links
            // } else {
            //     element.classList.remove('highlight');
            // }
        }
    }

    removeAnnotations(dataX: number, dataY: number) {
        // loop through and remove any annotations that no longer exist
    }

    restoreView = (data: Doc) => {
        const coords = Cast(data.presDataViz, listSpec('number'), null);
        if ((coords && this._currSelected?.x !== coords[0]) || this._currSelected?.y !== coords[1]) {
            this.setCurrSelected(coords[0], coords[1]);
            return true;
        }
        return false;
    };
    _getAnchor = (pinProps?: PinProps) => {
        // store whatever information would allow me to reselect the same thing (store parameters I would pass to get the exact same element)

        const anchor = Docs.Create.TextanchorDocument({
            /*put in some options*/
            title: 'line doc selection' + this._currSelected?.x,
        });
        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), dataviz: this._currSelected ? [this._currSelected.x, this._currSelected.y] : undefined } }, this.props.dataDoc);
        return anchor;
        // have some other function in code that
    };

    componentWillUnmount() {
        if (this._dataReactionDisposer) {
            this._dataReactionDisposer();
        }
        if (this._heightReactionDisposer) {
            this._heightReactionDisposer();
        }
        if (this._widthReactionDisposer) {
            this._widthReactionDisposer();
        }
    }

    tooltipContent(data: DataPoint) {
        return `<b>(${data.x},${data.y})</b>`;
    }

    @computed get height(): number {
        return this.props.height - this.props.margin.top - this.props.margin.bottom;
    }

    @computed get width(): number {
        return this.props.width - this.props.margin.left - this.props.margin.right;
    }

    setupTooltip() {
        const tooltip = d3
            .select(this._chartRef.current)
            .append('div')
            .attr('class', 'tooltip')
            .style('opacity', 0)
            .style('background', '#fff')
            .style('border', '1px solid #ccc')
            .style('padding', '5px')
            .style('position', 'absolute')
            .style('font-size', '12px');
        return tooltip;
    }

    // TODO: nda - use this everyewhere we update currSelected?
    @action
    setCurrSelected(x: number, y: number) {
        // TODO: nda - get rid of svg element in the list?
        this._currSelected = { x: x, y: y };
    }

    drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>) {
        if (this._chartSvg) {
            const circleClass = '.circle-' + idx;
            this._chartSvg
                .selectAll(circleClass)
                .data(data)
                .join('circle') // enter append
                .attr('class', `${circleClass} datapoint`)
                .attr('r', '3') // radius
                .attr('cx', d => xScale(d.x))
                .attr('cy', d => yScale(d.y))
                .attr('data-x', d => d.x)
                .attr('data-y', d => d.y);
        }
    }

    // TODO: nda - can use d3.create() to create html element instead of appending
    drawChart() {
        const { chartData, margin } = this.props;
        const data = chartData.data[0];
        // clearing tooltip and the current chart
        d3.select(this._chartRef.current).select('svg').remove();
        d3.select(this._chartRef.current).select('.tooltip').remove();

        // TODO: nda - refactor code so that it only recalculates min max and things related to data on data update

        const { xMin, xMax, yMin, yMax } = this._rangeVals;
        // const svg = d3.select(this._chartRef.current).append(this.svgContainer.html());
        // adding svg
        this._chartSvg = d3
            .select(this._chartRef.current)
            .append('svg')
            .attr('width', `${this.width + margin.right + margin.left}`)
            .attr('height', `${this.height + margin.top + margin.bottom}`)
            .append('g')
            .attr('transform', `translate(${margin.left}, ${margin.top})`);

        const svg = this._chartSvg;

        if (xMin == undefined || xMax == undefined || yMin == undefined || yMax == undefined) {
            // TODO: nda - error handle
            return;
        }

        // creating the x and y scales
        const xScale = scaleCreatorNumerical(xMin, xMax, 0, this.width);
        const yScale = scaleCreatorNumerical(0, yMax, this.height, 0);

        // create a line function that takes in the data.data.x and data.data.y
        // TODO: nda - fix the types for the d here
        const lineGen = createLineGenerator(xScale, yScale);

        // create x and y grids
        xGrid(svg.append('g'), this.height, xScale);
        yGrid(svg.append('g'), this.width, yScale);
        xAxisCreator(svg.append('g'), this.height, xScale);
        yAxisCreator(svg.append('g'), this.width, yScale);

        // draw the line
        drawLine(svg.append('path'), data, lineGen);

        // draw the datapoint circle
        this.drawDataPoints(data, 0, xScale, yScale);

        const focus = svg.append('g').attr('class', 'focus').style('display', 'none');
        focus.append('circle').attr('r', 5).attr('class', 'circle');
        const tooltip = this.setupTooltip();
        // add all the tooltipContent to the tooltip
        const mousemove = action((e: any) => {
            const bisect = d3.bisector((d: DataPoint) => d.x).left;
            const xPos = d3.pointer(e)[0];
            const x0 = bisect(data, xScale.invert(xPos - 25));
            const d0 = data[x0];
            this._x = d0.x;
            this._y = d0.y;
            focus.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`);
            // TODO: nda - implement tooltips
            tooltip.transition().duration(300).style('opacity', 0.9);
            // TODO: nda - updating the inner html could be deadly cause injection attacks!
            tooltip
                .html(() => this.tooltipContent(d0))
                .style('pointer-events', 'none')
                .style('transform', `translate(${xScale(d0.x) - this.width / 2 + 12}px,${yScale(d0.y) + 30}px)`);
        });

        const onPointClick = action((e: any) => {
            const bisect = d3.bisector((d: DataPoint) => d.x).left;
            const xPos = d3.pointer(e)[0];
            const x0 = bisect(data, xScale.invert(xPos - 25));
            const d0 = data[x0];
            this._x = d0.x;
            this._y = d0.y;
            // find .circle-d1 with data-x = d0.x and data-y = d0.y
            const selected = svg.selectAll('.datapoint').filter((d: any) => d['data-x'] === d0.x && d['data-y'] === d0.y);
            this._currSelected = { x: d0.x, y: d0.y, elem: selected };
        });

        this._chartSvg
            .append('rect')
            .attr('class', 'overlay')
            .attr('width', this.width)
            .attr('height', this.height + margin.top + margin.bottom)
            .attr('fill', 'none')
            .attr('translate', `translate(${margin.left}, ${-(margin.top + margin.bottom)})`)
            .style('opacity', 0)
            .on('mouseover', () => {
                focus.style('display', null);
            })
            .on('mouseout', () => {
                tooltip.transition().duration(300).style('opacity', 0);
            })
            .on('mousemove', mousemove)
            .on('click', onPointClick);
    }

    render() {
        return (
            <div ref={this._chartRef} className="chart-container">
                <span>Curr Selected: {this._currSelected ? `x: ${this._currSelected.x} y: ${this._currSelected.y}` : 'none'}</span>
            </div>
        );
    }
}
