import { Button, EditableText, Size } from 'browndash-components';
import * as d3 from 'd3';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { Doc, NumListCast, StrListCast } from '../../../../../fields/Doc';
import { List } from '../../../../../fields/List';
import { listSpec } from '../../../../../fields/Schema';
import { Cast, DocCast, StrCast } from '../../../../../fields/Types';
import { Docs } from '../../../../documents/Documents';
import { undoable } from '../../../../util/UndoManager';
import {} from '../../../DocComponent';
import { ObservableReactComponent } from '../../../ObservableReactComponent';
import { PinDocView, PinProps } from '../../../PinFuncs';
import { DocumentView } from '../../DocumentView';
import { DataVizBox } from '../DataVizBox';
import { DataPoint, createLineGenerator, drawLine, minMaxRange, scaleCreatorNumerical, xAxisCreator, xGrid, yAxisCreator, yGrid } from '../utils/D3Utils';
import './Chart.scss';

export interface SelectedDataPoint extends DataPoint {
    elem?: d3.Selection<d3.BaseType, unknown, SVGGElement, unknown>;
}
export interface LineChartProps {
    vizBox: DataVizBox;
    Document: Doc;
    layoutDoc: Doc;
    axes: string[];
    titleCol: string;
    records: { [key: string]: any }[];
    width: number;
    height: number;
    dataDoc: Doc;
    fieldKey: string;
    margin: {
        top: number;
        right: number;
        bottom: number;
        left: number;
    };
}

@observer
export class LineChart extends ObservableReactComponent<LineChartProps> {
    private _disposers: { [key: string]: IReactionDisposer } = {};
    private _lineChartRef: HTMLDivElement | null = null;
    private _lineChartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
    @observable _currSelected: DataPoint | undefined = undefined;

    // TODO: nda - some sort of mapping that keeps track of the annotated points so we can easily remove when annotations list updates
    constructor(props: any) {
        super(props);
        makeObservable(this);
    }

    @computed get titleAccessor() {
        let titleAccessor: any = 'dataViz_lineChart_title';
        if (this._props.axes.length === 2) titleAccessor = titleAccessor + this._props.axes[0] + '-' + this._props.axes[1];
        else if (this._props.axes.length > 0) titleAccessor += this._props.axes[0];
        return titleAccessor;
    }

    @computed get _tableDataIds() {
        return !this.parentViz ? this._props.records.map((rec, i) => i) : NumListCast(this.parentViz.dataViz_selectedRows);
    }
    // returns all the data records that will be rendered by only returning those records that have been selected by the parent visualization (or all records if there is no parent)
    @computed get _tableData() {
        return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]);
    }
    @computed get _lineChartData() {
        if (this._props.axes.length <= 1) return [];
        return this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[1]]) })).sort((a, b) => (a.x < b.x ? -1 : 1));
    }
    @computed get graphTitle() {
        return this._props.axes[1] + ' vs. ' + this._props.axes[0] + ' Line Chart';
    }
    @computed get parentViz() {
        return DocCast(this._props.Document.dataViz_parentViz);
    }
    @computed get incomingHighlited() {
        // return selected x and y axes
        // otherwise, use the selection of whatever is linked to us
        const incomingVizBox = DocumentView.getFirstDocumentView(this.parentViz)?.ComponentView as DataVizBox;
        const highlitedRowIds = NumListCast(incomingVizBox?.layoutDoc?.dataViz_highlitedRows);
        return this._tableData.filter((record, i) => highlitedRowIds.includes(this._tableDataIds[i])); // get all the datapoints they have selected field set by incoming anchor
    }
    @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
        return minMaxRange([this._lineChartData]);
    }
    componentWillUnmount() {
        Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
    }
    componentDidMount() {
        // coloring the selected point
        if (!this._props.layoutDoc[this.titleAccessor]) this._props.layoutDoc[this.titleAccessor] = this.defaultGraphTitle;
        if (!this._props.layoutDoc.dataViz_lineChart_selectedData) this._props.layoutDoc.dataViz_lineChart_selectedData = new List<string>();
        this._disposers.selector = reaction(() => StrListCast(this._props.layoutDoc.dataViz_lineChart_selectedData).slice(), this.colorSelectedPts, { fireImmediately: true });
    }

    // 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

    clearAnnotations = () => {
        const elements = document.querySelectorAll('.datapoint');
        for (let i = 0; i < elements.length; i++) {
            const element = elements[i];
            element.classList.remove('brushed');
            element.classList.remove('selected');
        }
    };

    // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc)
    getAnchor = (pinProps?: PinProps) => {
        const anchor = Docs.Create.ConfigDocument({
            //
            title: 'line doc selection' + (this._currSelected?.x ?? ''),
        });
        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document);
        anchor.config_dataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined;
        return anchor;
    };

    private colorSelectedPts = () => {
        const elements = document.querySelectorAll('.datapoint');
        for (let i = 0; i < elements.length; i++) {
            const dx = Number(elements[i].getAttribute('data-x'));
            const dy = Number(elements[i].getAttribute('data-y'));
            const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_lineChart_selectedData);
            const selected = selectedDataBars.some(eachSelectedData => {
                const [sx, sy] = eachSelectedData.split(','); // parse each selected point into x,y
                return Number(sx) === dx && Number(sy) === dy;
            });
            if (selected) elements[i].classList.add('brushed');
            else elements[i].classList.remove('brushed');
        }
    };

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

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

    @computed get defaultGraphTitle() {
        const ax0 = this._props.axes[0];
        const ax1 = this._props.axes.length > 1 ? this._props.axes[1] : undefined;
        if (this._props.axes.length < 2 || !/\d/.test(this._props.records[0][ax0]) || !ax1) {
            return ax0 + ' Line Chart';
        }
        return ax1 + ' by ' + ax0 + ' Line Chart';
    }

    setupTooltip() {
        return d3.select(this._lineChartRef).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');
    }

    @action
    setCurrSelected(d: DataPoint) {
        let ptWasSelected = false;
        const selectedDatapoints = Cast(this._props.layoutDoc.dataViz_lineChart_selectedData, listSpec('string'), null);
        selectedDatapoints?.forEach(eachData => {
            if (!ptWasSelected) {
                const [dx, dy] = eachData.split(',');
                if (Number(dx) === d.x && Number(dy) === d.y) {
                    ptWasSelected = true;
                    const index = selectedDatapoints.indexOf(eachData);
                    selectedDatapoints.splice(index, 1);
                    this._currSelected = undefined;
                }
            }
        });
        if (!ptWasSelected) {
            selectedDatapoints.push(d.x + ',' + d.y);
            this._currSelected = selectedDatapoints.length > 1 ? undefined : d;
        }

        // for filtering child dataviz docs
        if (this._props.layoutDoc.dataViz_filterSelection) {
            const selectedRows = Cast(this._props.layoutDoc.dataViz_selectedRows, listSpec('number'), null);
            this._tableDataIds.forEach(rowID => {
                if (
                    Number(this._props.records[rowID][this._props.axes[0]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')) === d.x && //
                    Number(this._props.records[rowID][this._props.axes[1]].replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')) === d.y
                ) {
                    if (!selectedRows?.includes(rowID))
                        selectedRows?.push(rowID); // adding to filtered rows
                    else if (ptWasSelected) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows
                }
            });
        }
    }

    drawDataPoints(data: DataPoint[], idx: number, xScale: d3.ScaleLinear<number, number, never>, yScale: d3.ScaleLinear<number, number, never>, higlightFocusPt: any, tooltip: any) {
        if (this._lineChartSvg) {
            const circleClass = '.circle-' + idx;
            this._lineChartSvg
                .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)
                .on('mouseenter', e => {
                    const d0 = { x: Number(e.target.getAttribute('data-x')), y: Number(e.target.getAttribute('data-y')) };
                    this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
                    higlightFocusPt.style('display', null);
                })
                .on('mouseleave', () => {
                    tooltip?.transition().duration(300).style('opacity', 0);
                })
                .on('click', (e: any) => {
                    const d0 = { x: Number(e.target.getAttribute('data-x')), y: Number(e.target.getAttribute('data-y')) };
                    // find .circle-d1 with data-x = d0.x and data-y = d0.y
                    this.setCurrSelected(d0);
                    this.updateTooltip(higlightFocusPt, xScale, d0, yScale, tooltip);
                });
        }
    }

    drawChart = (dataSet: any[][], rangeVals: { xMin?: number; xMax?: number; yMin?: number; yMax?: number }, width: number, height: number) => {
        // clearing tooltip and the current chart
        d3.select(this._lineChartRef).select('svg').remove();
        d3.select(this._lineChartRef).select('.tooltip').remove();

        let { xMin, xMax, yMin, yMax } = rangeVals;
        if (xMin === undefined || xMax === undefined || yMin === undefined || yMax === undefined) {
            return;
        }

        // adding svg
        const { margin } = this._props;
        const svg = (this._lineChartSvg = d3
            .select(this._lineChartRef)
            .append('svg')
            .attr('class', 'graph')
            .attr('width', `${width + margin.left + margin.right}`)
            .attr('height', `${height + margin.top + margin.bottom}`)
            .append('g')
            .attr('transform', `translate(${margin.left}, ${margin.top})`));

        let validSecondData;
        if (this._props.axes.length > 2) {
            // for when there are 2 lines on the chart
            const next = this._tableData.map(record => ({ x: Number(record[this._props.axes[0]]), y: Number(record[this._props.axes[2]]) })).sort((a, b) => (a.x < b.x ? -1 : 1));
            validSecondData = next.filter(d => {
                if (!d.x || isNaN(d.x) || !d.y || isNaN(d.y)) return false;
                return true;
            });
            const secondDataRange = minMaxRange([validSecondData]);
            if (secondDataRange.xMax! > xMax) xMax = secondDataRange.xMax;
            if (secondDataRange.yMax! > yMax) yMax = secondDataRange.yMax;
            if (secondDataRange.xMin! < xMin) xMin = secondDataRange.xMin;
            if (secondDataRange.yMin! < yMin) yMin = secondDataRange.yMin;
        }

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

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

        const higlightFocusPt = svg.append('g').style('display', 'none');
        higlightFocusPt.append('circle').attr('r', 5).attr('class', 'circle');
        const tooltip = this.setupTooltip();

        if (validSecondData) {
            drawLine(svg.append('path'), validSecondData, lineGen, true);
            this.drawDataPoints(validSecondData, 0, xScale, yScale, higlightFocusPt, tooltip);
            svg.append('path').attr('stroke', 'red');

            // legend
            const color: any = d3.scaleOrdinal().range(['black', 'blue']).domain([this._props.axes[1], this._props.axes[2]]);
            svg.selectAll('mydots')
                .data([this._props.axes[1], this._props.axes[2]])
                .enter()
                .append('circle')
                .attr('cx', 5)
                .attr('cy', (d, i) => -30 + i * 15)
                .attr('r', 7)
                .style('fill', d => color(d));
            svg.selectAll('mylabels')
                .data([this._props.axes[1], this._props.axes[2]])
                .enter()
                .append('text')
                .attr('x', 25)
                .attr('y', (d, i) => -30 + i * 15)
                .style('fill', d => color(d))
                .text(d => d)
                .attr('text-anchor', 'left')
                .style('alignment-baseline', 'middle');
        }

        // get valid data points
        const data = dataSet[0];
        const keys = Object.keys(data[0]);
        const validData = data.filter(d => !keys.some(key => isNaN(d[key])));

        // draw the plot line
        drawLine(svg.append('path'), validData, lineGen, false);

        // draw the datapoint circle
        this.drawDataPoints(validData, 0, xScale, yScale, higlightFocusPt, tooltip);

        // axis titles
        svg.append('text')
            .attr('transform', 'translate(' + width / 2 + ' ,' + (height + 40) + ')')
            .style('text-anchor', 'middle')
            .text(this._props.axes[0]);
        svg.append('text')
            .attr('transform', 'rotate(-90) translate(0, -10)')
            .attr('x', -(height / 2))
            .attr('y', -30)
            .attr('height', 20)
            .attr('width', 20)
            .style('text-anchor', 'middle')
            .text(this._props.axes[1]);
        this.colorSelectedPts();
    };

    private updateTooltip(
        higlightFocusPt: d3.Selection<SVGGElement, unknown, null, undefined>,
        xScale: d3.ScaleLinear<number, number, never>,
        d0: DataPoint,
        yScale: d3.ScaleLinear<number, number, never>,
        tooltip: d3.Selection<HTMLDivElement, unknown, null, undefined>
    ) {
        higlightFocusPt.attr('transform', `translate(${xScale(d0.x)},${yScale(d0.y)})`).attr('class', this._currSelected?.x === d0.x && this._currSelected?.y === d0.y ? 'hoverHighlight-selected' : 'hoverHighlight');
        tooltip.transition().duration(300).style('opacity', 0.9);
        // TODO: nda - updating the inner html could be deadly cause injection attacks!
        tooltip
            .html(() => `<b>(${d0.x},${d0.y})</b>`) // text content for tooltip
            .style('pointer-events', 'none')
            .style('transform', `translate(${xScale(d0.x) - this.width}px,${yScale(d0.y)}px)`);
    }

    render() {
        const selectedPt = this._currSelected ? `{ ${this._props.axes[0]}: ${this._currSelected.x} ${this._props.axes[1]}: ${this._currSelected.y} }` : 'none';
        let selectedTitle = '';
        if (this._currSelected && this._props.titleCol) {
            selectedTitle += '\n' + this._props.titleCol + ': ';
            this._tableData.forEach(each => {
                let mapThisEntry = false;
                if (this._currSelected?.x === each[this._props.axes[0]] && this._currSelected?.y === each[this._props.axes[1]]) mapThisEntry = true;
                else if (this._currSelected?.y === each[this._props.axes[0]] && this._currSelected?.x === each[this._props.axes[1]]) mapThisEntry = true;
                if (mapThisEntry) selectedTitle += each[this._props.titleCol] + ', ';
            });
            selectedTitle = selectedTitle.slice(0, -1).slice(0, -1);
        }
        if (this._lineChartData.length > 0 || !this.parentViz || this.parentViz.length === 0) {
            return this._props.axes.length >= 2 && /\d/.test(this._props.records[0][this._props.axes[0]]) && /\d/.test(this._props.records[0][this._props.axes[1]]) ? (
                <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}>
                    <div className="graph-title">
                        <EditableText
                            val={StrCast(this._props.layoutDoc[this.titleAccessor])}
                            setVal={undoable(
                                action(val => {
                                    this._props.layoutDoc[this.titleAccessor] = val as string;
                                }),
                                'Change Graph Title'
                            )}
                            color="black"
                            size={Size.LARGE}
                            fillWidth
                        />
                    </div>
                    <div
                        ref={r => {
                            this._lineChartRef = r;
                            this.drawChart([this._lineChartData], this.rangeVals, this.width, this.height);
                        }}
                    />
                    {selectedPt !== 'none' ? (
                        <div className="selected-data">
                            {`Selected: ${selectedPt}`}
                            {`${selectedTitle}`}
                            <Button
                                onClick={() => {
                                    this._props.vizBox.sidebarBtnDown;
                                    this._props.vizBox.sidebarAddDocument;
                                }}
                            />
                        </div>
                    ) : null}
                </div>
            ) : (
                <span className="chart-container"> first use table view to select two numerical axes to plot</span>
            );
        }
        return (
            // when it is a brushed table and the incoming table doesn't have any rows selected
            <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>
        );
    }
}
