import { ColorPicker, EditableText, IconButton, Size, Type } from '@dash/components';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
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 { FaFillDrip } from 'react-icons/fa';
import { Doc, NumListCast } 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 { ObservableReactComponent } from '../../../ObservableReactComponent';
import { PinDocView, PinProps } from '../../../PinFuncs';
import { scaleCreatorNumerical, yAxisCreator } from '../utils/D3Utils';
import './Chart.scss';

export interface HistogramData {
    [key: string]: string | number;
}
export interface HistogramProps {
    Doc: Doc;
    layoutDoc: Doc;
    axes: string[];
    records: HistogramData[];
    width: number;
    height: number;
    dataDoc: Doc;
    fieldKey: string;
    margin: {
        top: number;
        right: number;
        bottom: number;
        left: number;
    };
}

@observer
export class Histogram extends ObservableReactComponent<HistogramProps> {
    private _disposers: { [key: string]: IReactionDisposer } = {};
    private _histogramRef: HTMLDivElement | null = null;
    private _histogramSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
    private _numericalXData: boolean = false; // whether the data is organized by numbers rather than categoreis
    private _numericalYData: boolean = false; // whether the y axis is controlled by provided data rather than frequency
    private _maxBins = 15; // maximum number of bins that is readable on a normal sized doc
    private _selectedBars: HistogramData[] = [];
    @observable private _currSelected: { [key: string]: string | number; frequency: number } | undefined = undefined;

    constructor(props: HistogramProps) {
        super(props);
        makeObservable(this);
    }

    @computed get xAxis() {
        return this._props.axes[0];
    }

    @computed get yAxis() {
        return this._props.axes[1];
    }

    @computed get Doc() {
        return this._props.Doc;
    }
    @computed get layoutDoc() {
        return this._props.layoutDoc;
    }

    @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(): Record<string, string | number>[] {
        return !this.parentViz ? this._props.records : this._tableDataIds.map(rowId => this._props.records[rowId]);
    }

    @computed get _histogramData(): HistogramData[] {
        if (this._props.axes.length < 1) return [];
        if (this._props.axes.length < 2) {
            if (!/[A-Za-z-:]/.test(this._props.records[0][this.xAxis] as string)) {
                this._numericalXData = true;
            }
            return this._tableData.map(record => ({ [this.xAxis]: record[this.xAxis] }));
        }
        if (!/[A-Za-z-:]/.test(this._props.records[0][this.xAxis] as string)) {
            this._numericalXData = true;
        }
        if (!/[A-Za-z-:]/.test(this._props.records[0][this.yAxis] as string)) {
            this._numericalYData = true;
        }
        return this._tableData.map(record => ({
            [this.xAxis]: record[this.xAxis],
            [this.yAxis]: record[this.yAxis],
        }));
    }

    @computed get defaultGraphTitle(): string {
        if (!this.yAxis || !/\d/.test(this._props.records[0][this.yAxis] as string) || !this._numericalYData) {
            return this.xAxis + ' Histogram';
        }
        return this.xAxis + ' by ' + this.yAxis + ' Histogram';
    }

    @computed get parentViz() {
        return DocCast(this._props.Doc.dataViz_parentViz);
    }

    @computed get defaultBarColor() {
        return Cast(this.layoutDoc.dataViz_histogram_defaultColor, 'string', '#69b3a2');
    }
    @computed get barColors() {
        return Cast(this.layoutDoc.dataViz_histogram_barColors, listSpec('string'), null);
    }
    @computed get selectedBins() {
        return Cast(this.layoutDoc.dataViz_histogram_selectedBins, listSpec('number'), null);
    }

    @computed get rangeVals(): { xMin?: number; xMax?: number; yMin?: number; yMax?: number } {
        const data = this._numericalXData ? this.data(this._histogramData) : [0];
        return { xMin: Math.min(...data), xMax: Math.max(...data), yMin: 0, yMax: 0 };
    }

    componentWillUnmount() {
        Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
    }
    componentDidMount() {
        // restore selected bars
        this._histogramSvg?.selectAll('rect').attr('class', dIn => {
            const d = dIn as HistogramData;
            if (this.selectedBins.some(selBin => d[0] === selBin)) {
                this._selectedBars.push(d);
                return 'histogram-bar hover';
            }
            return 'histogram-bar';
        });
        // setup filters to watch selections and filter toggle
        this._disposers.selection = reaction(
            () => ({ filter: this.layoutDoc.dataViz_filterSelection, hists: this._selectedBars.slice(), cur: this._currSelected }),
            ({ filter, hists }) => {
                this.layoutDoc.dataViz_selectedRows = !filter
                    ? undefined
                    : new List<number>(
                          this._tableDataIds.filter(rowID =>
                              hists.some(h => {
                                  const val = this._props.records[rowID][this.xAxis];
                                  return val == h.x0 || (+val >= +h.x0 && +val <= +h.x1);
                              })
                          )
                      );
            },
            { fireImmediately: true }
        );
    }

    // 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: 'histogram doc selection' + this._currSelected,
        });
        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Doc);
        return anchor;
    };

    @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;
    }

    // cleans data by converting numerical data to numbers and taking out empty cells
    data = (dataSet: HistogramData[]): number[] => {
        const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as number)));
        const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined;
        return !field
            ? []
            : validData.map(d =>
                  !this._numericalXData //
                      ? (d[field] as number)
                      : +d[field].toString().replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')
              );
    };

    barLabel = (d: d3.Bin<number, number> | HistogramData) => '' + (Array.isArray(d) ? d[0] : d[0]);

    // outlines the bar selected / hovered over
    highlightSelectedBar = (changeSelectedVariables: boolean, svg: d3.Selection<SVGGElement, unknown, null, undefined>, eachRectWidth: number, pointerX: number, xAxisTitle: string, yAxisTitle: string, histDataSet: HistogramData[]) => {
        let barCounter = -1;
        let hoverOverBar: HistogramData | undefined;
        svg.selectAll('.histogram-bar').filter(dIn => {
            const d = dIn as HistogramData;
            barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over
            if (d.length && (barCounter * eachRectWidth <= pointerX + 1 || (!barCounter && pointerX <= 0)) && pointerX - 1 <= (barCounter + 1) * eachRectWidth) {
                if (changeSelectedVariables) {
                    // for when a bar is selected - not just hovered over
                    const alreadySelected = this._selectedBars.findIndex(eachData => !Object.keys(d).some(key => d[key] !== eachData[key]));
                    if (alreadySelected !== -1) {
                        this._selectedBars.splice(alreadySelected, 1);
                        this.selectedBins.splice(alreadySelected, 1);
                    } else {
                        this._selectedBars.push(d);
                        this.selectedBins.push(d[0] as number);
                    }
                    const showSelectedLabel = (dataset: HistogramData[]) => {
                        const datum = dataset.lastElement();
                        const datumNum = datum as unknown as number[];
                        const showSelectedStart = this._numericalYData
                            ? this._histogramData.filter(data => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0]
                            : histDataSet.filter(data => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') == d[0])[0];

                        const selectionLabel =  dataset.length > 1
                            ? dataset.map(dd => this.barLabel(dd)).join('::')
                            : !this._numericalXData
                                ? this.barLabel(d)
                                : datum[0] !== undefined && datum[1] && datum[0] !== datum[1]
                                    ? d3.min(datumNum) + ' to ' + d3.max(datumNum)
                                    : !this._numericalYData
                                        ? showSelectedStart?.[xAxisTitle]
                                        : this.barLabel(d); // prettier-ignore
                        return { [xAxisTitle]: selectionLabel, frequency: dataset.length > 1 ? Number.NaN : datum.length } as { [key: string]: string | number; frequency: number };
                    };
                    this._currSelected = this._selectedBars.length ? showSelectedLabel(this._selectedBars) : undefined;
                } else hoverOverBar = d;
                return true;
            }
            return false;
        });
        return hoverOverBar;
    };

    // draws the histogram
    drawChart = (dataSet: HistogramData[], width: number, height: number) => {
        if (dataSet?.length <= 0) return;
        d3.select(this._histogramRef).select('svg').remove();
        d3.select(this._histogramRef).select('.tooltip').remove();

        const data = this.data(dataSet);
        const xAxisTitle = Object.keys(dataSet[0])[0];
        const yAxisTitle = this._numericalYData ? Object.keys(dataSet[0])[1] : 'frequency';
        const uniqueArr: unknown[] = [...new Set(data)];
        let numBins = this._numericalXData && Number.isInteger(data[0]) ? this.rangeVals.xMax! - this.rangeVals.xMin! : uniqueArr.length;
        let translateXAxis = !this._numericalXData || numBins < this._maxBins ? width / (numBins + 1) / 2 : 0;
        if (numBins > this._maxBins) numBins = this._maxBins;
        const startingPoint = this._numericalXData ? this.rangeVals.xMin! : 0;
        const endingPoint = this._numericalXData ? this.rangeVals.xMax! : numBins;

        // converts data into Objects
        let histDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || isNaN(d[key] as number)));
        if (!this._numericalXData) {
            const histStringDataSet: { [x: string]: number }[] = [];
            if (this._numericalYData) {
                for (let i = 0; i < dataSet.length; i++) {
                    histStringDataSet.push({ [yAxisTitle]: dataSet[i][yAxisTitle] as number, [xAxisTitle]: dataSet[i][xAxisTitle] as number });
                }
            } else {
                for (let i = 0; i < uniqueArr.length; i++) {
                    histStringDataSet.push({ [yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i] as number });
                }
                for (let i = 0; i < data.length; i++) {
                    const barData = histStringDataSet.filter(each => each[xAxisTitle] == data[i]);
                    histStringDataSet.filter(each => each[xAxisTitle] == data[i])[0][yAxisTitle] = Number(barData[0][yAxisTitle]) + 1;
                }
            }
            histDataSet = histStringDataSet;
        }

        // initial graph and binning data for histogram
        const svg = (this._histogramSvg = d3
            .select(this._histogramRef)
            .append('svg')
            .attr('class', 'graph')
            .attr('width', width + this._props.margin.right + this._props.margin.left)
            .attr('height', height + this._props.margin.top + this._props.margin.bottom)
            .append('g')
            .attr('transform', 'translate(' + this._props.margin.left + ',' + this._props.margin.top + ')'));
        let x = d3
            .scaleLinear()
            .domain(this._numericalXData ? [startingPoint!, endingPoint!] : [0, numBins])
            .range([0, width]);
        const histogram = d3
            .bin()
            .value(d => d)
            .domain([startingPoint, endingPoint])
            .thresholds(x.ticks(numBins));
        const bins = histogram(data);
        let eachRectWidth = width / bins.length;
        const graphStartingPoint = bins[0].x1 && bins[1] ? bins[0].x1! - (bins[1].x1! - bins[1].x0!) : 0;
        bins[0].x0 = graphStartingPoint;
        x = x.domain([graphStartingPoint, endingPoint]).range([0, Number.isInteger(this.rangeVals.xMin!) ? width - eachRectWidth : width]);
        let xAxis;

        // more calculations based on bins
        // x-axis
        if (!this._numericalXData) {
            // reorganize to match data if the data is strings rather than numbers
            // uniqueArr.sort()
            histDataSet.sort();
            for (let i = 0; i < data.length; i++) {
                let index = 0;
                for (let j = 0; j < uniqueArr.length; j++) {
                    if (uniqueArr[j] == data[i]) {
                        index = j;
                    }
                }
                if (bins[index]) bins[index].push(data[i]);
            }
            bins.pop();
            eachRectWidth = width / bins.length;
            xAxis = d3
                .axisBottom(x)
                .ticks(bins.length > 1 ? bins.length - 1 : 1)
                .tickFormat(i => uniqueArr[i.valueOf()] as string)
                .tickPadding(10);
            x.range([0, width - eachRectWidth]);
            x.domain([0, bins.length - 1]);
            translateXAxis = eachRectWidth / 2;
        } else {
            let allSame = true;
            for (let i = 0; i < bins.length; i++) {
                if (bins[i] && bins[i][0]) {
                    const compare = bins[i][0];
                    for (let j = 1; j < bins[i].length; j++) {
                        if (bins[i][j] !== compare) allSame = false;
                    }
                }
            }
            if (allSame) {
                translateXAxis = eachRectWidth / 2;
                eachRectWidth = width / bins.length;
            } else {
                eachRectWidth = width / (bins.length + 1);
                const tickDiff = bins.length >= 2 ? bins[bins.length - 2].x1! - bins[bins.length - 2].x0! : 0;
                const curDomain = x.domain();
                x.domain([curDomain[0], curDomain[0] + tickDiff * bins.length]);
            }

            xAxis = d3.axisBottom(x).ticks(bins.length - 1);
            x.range([0, width - eachRectWidth]);
        }
        // y-axis
        const maxFrequency = this._numericalYData ? 
              d3.max(histDataSet, d => (d[yAxisTitle] ? 
                Number(StrCast(d[yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '')) : 0)) : 
              d3.max(bins, d => d.length); // prettier-ignore
        const y = d3.scaleLinear().range([height, 0]);
        y.domain([0, maxFrequency ?? 0]);
        const yAxis = d3.axisLeft(y).ticks(maxFrequency!);
        if (this._numericalYData) {
            const yScale = scaleCreatorNumerical(0, maxFrequency ?? 0, height, 0);
            yAxisCreator(svg.append('g'), width, yScale);
        } else {
            svg.append('g').call(yAxis);
        }
        svg.append('g')
            .attr('transform', 'translate(' + translateXAxis + ', ' + height + ')')
            .call(xAxis);

        // click/hover

        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const updateHighlights = (hoverOverBar?: HistogramData) => svg.selectAll('rect').attr('class', (d: any) => 'histogram-bar' + (hoverOverBar?.[0] == d[0] || this._selectedBars.some(hist => d[0] === hist[0]) ? ' hover' : ''));
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet));
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const mouseEnter = (e: any) => updateHighlights(this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet));
        svg.on('click', onPointClick).on('pointerenter', mouseEnter).on('pointerleave', updateHighlights);

        // axis titles
        svg.append('text')
            .attr('transform', 'translate(' + width / 2 + ' ,' + (height + 40) + ')')
            .style('text-anchor', 'middle')
            .text(xAxisTitle);
        svg.append('text')
            .attr('transform', 'rotate(-90) translate( 0, ' + -10 + ')')
            .attr('x', -(height / 2))
            .attr('y', -20)
            .style('text-anchor', 'middle')
            .text(yAxisTitle);
        d3.format('.0f');

        // draw bars
        const selected = this._selectedBars;
        svg.selectAll('rect')
            .data(bins)
            .enter()
            .append('rect')
            .attr('transform', this._numericalYData
                    ? d => {
                          const eachData = histDataSet.filter((hData: HistogramData) => hData[xAxisTitle] == d[0]);
                          const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0;
                          return 'translate(' + x(d.x0!) + ',' + y(Number(length)) + ')';
                      }
                    : d => 'translate(' + x(d.x0!) + ',' + y(d.length) + ')')
            .attr('height', this._numericalYData
                    ? d => {
                          const eachData = histDataSet.filter(hData => hData[xAxisTitle] == d[0]);
                          const length = eachData.length ? StrCast(eachData[0][yAxisTitle]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '') : 0;
                          return height - y(+length);
                      }
                    : d => height - y(d.length))
            .attr('width', eachRectWidth)
            .attr('class', selected ? d => (selected?.[0]?.x0 == d.x0 ? 'histogram-bar hover' : 'histogram-bar') : () => 'histogram-bar')
            .attr('fill', d => this.barColors?.map(bar => bar.split('::')).find(([barLabel]) => barLabel === this.barLabel(d))?.[1] ?? this.defaultBarColor); // prettier-ignore
    };

    @action changeSelectedColor = (color: string, erase?: boolean) => {
        if (!this.barColors) this.layoutDoc.dataViz_histogram_barColors = new List<string>();
        this._selectedBars.map(this.barLabel).forEach(barName => {
            this.barColors.forEach(bar => bar.split('::')[0] === barName && this.barColors.splice(this.barColors.indexOf(bar), 1));
            !erase && this.barColors.push(barName + '::' + color);
        });
    };

    render() {
        if (!this.selectedBins) this.layoutDoc.dataViz_histogram_selectedBins = new List<string>();

        const titleAccessor = 'dataViz_histogram_title ' + this.xAxis + (this.yAxis ? '-' + this._props.axes[1] : '');
        const selected = !this._currSelected ? 'none' : '{ ' + Object.keys(this._currSelected).map(key => key ? key + ': ' + this._currSelected?.[key]:'').join(", ") + ' }'; // prettier-ignore
        const curSelectedBarName = this._selectedBars.length && this.barLabel(this._selectedBars.lastElement()); //.[this.xAxis]).replace(/\$/g, '').replace(/%/g, '').replace(/</g, '');
        const selectedBarColor = this.barColors?.map(bar => bar.split('::'))?.find(([barLabel]) => barLabel === curSelectedBarName)?.[1];

        if (this._histogramData.length > 0 || !this.parentViz) {
            return this._props.axes.length >= 1 ? (
                <div className="chart-container" style={{ width: this._props.width + this._props.margin.right }}>
                    <div className="graph-title">
                        <EditableText
                            val={StrCast(this.layoutDoc[titleAccessor], this.defaultGraphTitle)}
                            setVal={undoable(
                                action(val => (this.layoutDoc[titleAccessor] = val)),
                                'Change Graph Title'
                            )}
                            color="black"
                            size={Size.LARGE}
                            fillWidth
                        />
                        &nbsp; &nbsp;
                        <ColorPicker
                            tooltip="Change Default Bar Color"
                            type={Type.SEC}
                            icon={<FaFillDrip />}
                            selectedColor={this.defaultBarColor}
                            setFinalColor={undoable(color => (this.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')}
                            setSelectedColor={undoable(color => (this.layoutDoc.dataViz_histogram_defaultColor = color), 'Change Default Bar Color')}
                            size={Size.XSMALL}
                        />
                    </div>
                    <div
                        ref={r => {
                            this._histogramRef = r;
                            r && this.drawChart(this._histogramData, this.width, this.height);
                        }}
                    />
                    {selected !== 'none' ? (
                        <div className="selected-data">
                            Selected: {selected}
                            &nbsp; &nbsp;
                            <ColorPicker
                                tooltip="Change Bar Color"
                                type={Type.SEC}
                                icon={<FaFillDrip />}
                                selectedColor={selectedBarColor}
                                setFinalColor={undoable(this.changeSelectedColor, 'Change Selected Bar Color')}
                                setSelectedColor={undoable(this.changeSelectedColor, 'Change Selected Bar Color')}
                                size={Size.XSMALL}
                            />
                            &nbsp;
                            <IconButton
                                icon={<FontAwesomeIcon icon="eraser" />}
                                size={Size.XSMALL}
                                color="black"
                                type={Type.SEC}
                                tooltip="Revert to the default bar color" //
                                onClick={undoable(() => this.changeSelectedColor(this.defaultBarColor, true), 'Change Selected Bar Color')}
                            />
                        </div>
                    ) : null}
                </div>
            ) : (
                <span className="chart-container"> first use table view to select a column to graph</span>
            );
        }
        // when it is a brushed table and the incoming table doesn't have any rows selected
        return <div className="chart-container">Selected rows of data from the incoming DataVizBox to display.</div>;
    }
}
