import { Checkbox } from '@mui/material';
import { ColorPicker, EditableText, Size, Type } 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 { FaFillDrip } from 'react-icons/fa';
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 { ObservableReactComponent } from '../../../ObservableReactComponent';
import { PinProps, PinDocView } from '../../../PinFuncs';
import './Chart.scss';

export interface PieChartProps {
    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 PieChart extends ObservableReactComponent<PieChartProps> {
    private _disposers: { [key: string]: IReactionDisposer } = {};
    private _piechartRef: HTMLDivElement | null = null;
    private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
    private curSliceSelected: any = undefined; // d3 data of selected slice for when just one slice is selected
    private selectedData: any[] = []; // array of selected slices
    private hoverOverData: any = undefined; // Selection of slice being hovered over
    @observable _currSelected: any | undefined = undefined; // Object of selected slice

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

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

    // organized by specified number percentages/ratios if one column is selected and it contains numbers
    // otherwise, assume data is organized by categories
    @computed get byCategory() {
        return !/\d/.test(this._props.records[0][this._props.axes[0]]) || this._props.layoutDoc.dataViz_pie_asHistogram;
    }
    // filters all data to just display selected data if brushed (created from an incoming link)
    @computed get _pieChartData() {
        if (this._props.axes.length < 1) return [];

        const ax0 = this._props.axes[0];
        if (this._props.axes.length < 2) {
            return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]] }));
        }
        const ax1 = this._props.axes[1];
        return this._tableData.map(record => ({ [ax0]: record[this._props.axes[0]], [ax1]: record[this._props.axes[1]] }));
    }

    @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 + ' Pie Chart';
        }
        return ax1 + ' by ' + ax0 + ' Pie Chart';
    }

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

    componentWillUnmount() {
        Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
    }
    componentDidMount() {
        // restore selected slices
        const svg = this._piechartSvg;
        if (svg && this._pieChartData[0]) {
            const key = Object.keys(this._pieChartData[0])[0];
            const selectedDataBars = StrListCast(this._props.layoutDoc.dataViz_pie_selectedData);
            svg.selectAll('path').attr('class', (d: any) => {
                let selected = false;
                selectedDataBars.forEach(eachSelectedData => {
                    if (d[key] === eachSelectedData) selected = true;
                });
                if (selected) {
                    this.selectedData.push(d);
                    return 'slice hover';
                }
                return 'slice';
            });
        }
    }

    // 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: 'piechart doc selection' + this._currSelected,
        });
        PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this._props.Document);
        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: any) => {
        const validData = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] /* || isNaN(d[key] as any) */));
        const field = dataSet[0] ? Object.keys(dataSet[0])[0] : undefined;
        return !field
            ? undefined
            : validData.map((d: { [x: string]: any }) =>
                  this.byCategory
                      ? d[field] //
                      : +d[field].replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '')
              );
    };

    // outlines the slice selected / hovered over
    highlightSelectedSlice = (changeSelectedVariables: boolean, svg: any, arc: any, radius: any, pointer: any, pieDataSet: any) => {
        let index = -1;
        const selected = svg.selectAll('.slice').filter((d: any) => {
            index++;
            const p1 = [0, 0]; // center of pie
            const p3 = [arc.centroid(d)[0] * 2, arc.centroid(d)[1] * 2]; // outward peak of arc
            const p2 = [radius * Math.sin(d.startAngle), -radius * Math.cos(d.startAngle)]; // start of arc
            const p4 = [radius * Math.sin(d.endAngle), -radius * Math.cos(d.endAngle)]; // end of arc

            // draw an imaginary horizontal line from the pointer to see how many times it crosses a slice edge
            let lineCrossCount = 0;
            // if for all 4 lines
            if (Math.min(p1[1], p2[1]) <= pointer[1] && pointer[1] <= Math.max(p1[1], p2[1])) {
                // within y bounds
                if (pointer[0] <= ((pointer[1] - p1[1]) * (p2[0] - p1[0])) / (p2[1] - p1[1]) + p1[0]) lineCrossCount++;
            } // intercepts x
            if (Math.min(p2[1], p3[1]) <= pointer[1] && pointer[1] <= Math.max(p2[1], p3[1])) {
                if (pointer[0] <= ((pointer[1] - p2[1]) * (p3[0] - p2[0])) / (p3[1] - p2[1]) + p2[0]) lineCrossCount++;
            }
            if (Math.min(p3[1], p4[1]) <= pointer[1] && pointer[1] <= Math.max(p3[1], p4[1])) {
                if (pointer[0] <= ((pointer[1] - p3[1]) * (p4[0] - p3[0])) / (p4[1] - p3[1]) + p3[0]) lineCrossCount++;
            }
            if (Math.min(p4[1], p1[1]) <= pointer[1] && pointer[1] <= Math.max(p4[1], p1[1])) {
                if (pointer[0] <= ((pointer[1] - p4[1]) * (p1[0] - p4[0])) / (p1[1] - p4[1]) + p4[0]) lineCrossCount++;
            }
            if (lineCrossCount % 2 !== 0 || d.startAngle % (2 * Math.PI) === d.endAngle % (2 * Math.PI)) {
                // inside the slice of it crosses an odd number of edges
                const showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index];
                let key = 'data'; // key that represents slice
                // eslint-disable-next-line prefer-destructuring
                if (Object.keys(showSelected)[0] === 'frequency') key = Object.keys(showSelected)[1];
                if (changeSelectedVariables) {
                    let sameAsAny = false;
                    const selectedDataSlices = Cast(this._props.layoutDoc.dataViz_pie_selectedData, listSpec('number'), null);
                    this.selectedData.forEach(eachData => {
                        if (!sameAsAny) {
                            let match = true;
                            Object.keys(d).forEach(objKey => {
                                if (d[objKey] !== eachData[objKey]) match = false;
                            });
                            if (match) {
                                sameAsAny = true;
                                const selIndex = this.selectedData.indexOf(eachData);
                                this.selectedData.splice(selIndex, 1);
                                selectedDataSlices.splice(selIndex, 1);
                                this._currSelected = undefined;
                            }
                        }
                    });
                    if (!sameAsAny) {
                        this.selectedData.push(d);
                        selectedDataSlices.push(d[key]);
                        this._currSelected = this.selectedData.length > 1 ? undefined : showSelected;
                    }

                    // 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 => {
                            let match = false;
                            if (this._props.records[rowID][this._props.axes[0]] == d[key]) match = true;
                            if (match && !selectedRows?.includes(rowID))
                                selectedRows?.push(rowID); // adding to filtered rows
                            else if (match && sameAsAny) selectedRows.splice(selectedRows.indexOf(rowID), 1); // removing from filtered rows
                        });
                    }
                } else this.hoverOverData = d;
                return true;
            }
            return false;
        });
        if (changeSelectedVariables) {
            if (this._currSelected) this.curSliceSelected = selected;
            else this.curSliceSelected = undefined;
        }
    };

    // draws the pie chart
    drawChart = (dataSet: any, width: number, height: number) => {
        if (!dataSet?.length) return;
        d3.select(this._piechartRef).select('svg').remove();
        d3.select(this._piechartRef).select('.tooltip').remove();

        let percentField = Object.keys(dataSet[0])[0];
        let descriptionField = Object.keys(dataSet[0])[1]!;
        const radius = Math.min(width, height - this._props.margin.top - this._props.margin.bottom) / 2;

        // converts data into Objects
        let data = this.data(dataSet);
        let pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key]));
        if (!pieDataSet.length) return;
        if (this.byCategory) {
            const uniqueCategories = [...new Set(data)];
            const pieStringDataSet: { frequency: number }[] = [];
            for (let i = 0; i < uniqueCategories.length; i++) {
                pieStringDataSet.push({ frequency: 0, [percentField]: uniqueCategories[i] });
            }
            for (let i = 0; i < data.length; i++) {
                // eslint-disable-next-line no-loop-func
                const sliceData = pieStringDataSet.filter((each: any) => each[percentField] == data[i]);
                sliceData[0].frequency += 1;
            }
            pieDataSet = pieStringDataSet;
            if (!pieDataSet.length) return;
            [percentField, descriptionField] = Object.keys(pieDataSet[0]);
            data = this.data(pieStringDataSet);
        }
        let trackDuplicates: { [key: string]: any } = {};
        data.forEach((eachData: any) => {
            !trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null;
        });

        // initial chart
        const svg = (this._piechartSvg = d3
            .select(this._piechartRef)
            .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'));
        const g = svg.append('g').attr('transform', 'translate(' + (width / 2 + this._props.margin.left) + ',' + height / 2 + ')');
        const pie = d3.pie();
        const arc = d3.arc().innerRadius(0).outerRadius(radius);

        const updateHighlights = () => {
            const hoverOverSlice = this.hoverOverData;
            const { selectedData } = this;
            svg.selectAll('path').attr('class', (d: any) => {
                let selected = false;
                selectedData.forEach((eachSelectedData: any) => {
                    if (d.startAngle === eachSelectedData.startAngle) selected = true;
                });
                return selected || (hoverOverSlice && d.startAngle === hoverOverSlice.startAngle && d.endAngle === hoverOverSlice.endAngle) ? 'slice hover' : 'slice';
            });
        };

        // click/hover
        const onPointClick = action((e: any) => this.highlightSelectedSlice(true, svg, arc, radius, d3.pointer(e), pieDataSet));
        const onHover = action((e: any) => {
            this.highlightSelectedSlice(false, svg, arc, radius, d3.pointer(e), pieDataSet);
            updateHighlights();
        });
        const mouseOut = action(() => {
            this.hoverOverData = undefined;
            updateHighlights();
        });
        // drawing the slices
        const selected = this.selectedData;
        const arcs = g.selectAll('arc').data(pie(data)).enter().append('g');
        const possibleDataPointVals: { [x: string]: any }[] = [];
        pieDataSet.forEach((each: { [x: string]: any | { valueOf(): number } }) => {
            const dataPointVal: { [x: string]: any } = {};
            dataPointVal[percentField] = each[percentField];
            if (descriptionField) dataPointVal[descriptionField] = each[descriptionField];
            try {
                dataPointVal[percentField] = Number(dataPointVal[percentField].replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, ''));
            } catch (error) {
                /* empty */
            }
            possibleDataPointVals.push(dataPointVal);
        });
        const sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::'));

        // to make sure all important slice information is on 'd' object
        let addKey: any = false;
        if (pieDataSet.length && Object.keys(pieDataSet[0])[0] === 'frequency') {
            // eslint-disable-next-line prefer-destructuring
            addKey = Object.keys(pieDataSet[0])[1];
        }
        arcs.append('path')
            .attr('fill', (d: any, i) => {
                let dataPoint;
                const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data));
                if (possibleDataPoints.length === 1) [dataPoint] = possibleDataPoints;
                else {
                    dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]];
                    trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1;
                }
                let sliceColor;
                if (dataPoint) {
                    if (addKey) d[addKey] = dataPoint[addKey]; // adding all slice information to d
                    const sliceTitle = dataPoint[this._props.axes[0]];
                    const accessByName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle;
                    sliceColors.forEach(each => {
                        // eslint-disable-next-line prefer-destructuring
                        each[0] === accessByName && (sliceColor = each[1]);
                    });
                }
                return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length];
            })
            .attr('class', d => {
                let selectThisData = false;
                selected.forEach((eachSelectedData: any) => {
                    if (d.startAngle === eachSelectedData.startAngle) selectThisData = true;
                });
                return selectThisData ? 'slice hover' : 'slice';
            })
            // @ts-ignore
            .attr('d', arc)
            .on('click', onPointClick)
            .on('mouseover', onHover)
            .on('mouseout', mouseOut);

        // adding labels
        trackDuplicates = {};
        data.forEach((eachData: any) => {
            !trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null;
        });
        arcs.size() < 100 &&
            arcs
                .append('text')
                .attr('transform', d => {
                    const centroid = arc.centroid(d as unknown as d3.DefaultArcObject);
                    const heightOffset = (centroid[1] / radius) * Math.abs(centroid[1]);
                    return 'translate(' + (centroid[0] + centroid[0] / (radius * 0.02)) + ',' + (centroid[1] + heightOffset) + ')';
                })
                .attr('pointer-events', 'none')
                .attr('text-anchor', 'middle')
                .text(d => {
                    let dataPoint;
                    const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data));
                    if (possibleDataPoints.length === 1) dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[0])];
                    else {
                        dataPoint = pieDataSet[possibleDataPointVals.indexOf(possibleDataPoints[trackDuplicates[d.data.toString()]])];
                        trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1;
                    }
                    return dataPoint ? dataPoint[percentField]! + (!descriptionField ? '' : ' - ' + dataPoint[descriptionField])! : '';
                });
    };

    @action changeSelectedColor = (color: string) => {
        this.curSliceSelected.attr('fill', color);
        const sliceTitle = this._currSelected[this._props.axes[0]];
        const sliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle;

        const sliceColors = Cast(this._props.layoutDoc.dataViz_pie_sliceColors, listSpec('string'), null);
        sliceColors.forEach(each => {
            if (each.split('::')[0] === sliceName) sliceColors.splice(sliceColors.indexOf(each), 1);
        });
        sliceColors.push(StrCast(sliceName + '::' + color));
    };

    @action changeHistogramCheckBox = () => {
        this._props.layoutDoc.dataViz_pie_asHistogram = !this._props.layoutDoc.dataViz_pie_asHistogram;
        this.drawChart(this._pieChartData, this.width, this.height);
    };

    render() {
        let titleAccessor: any = 'dataViz_pie_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];
        if (!this._props.layoutDoc[titleAccessor]) this._props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
        if (!this._props.layoutDoc.dataViz_pie_sliceColors) this._props.layoutDoc.dataViz_pie_sliceColors = new List<string>();
        if (!this._props.layoutDoc.dataViz_pie_selectedData) this._props.layoutDoc.dataViz_pie_selectedData = new List<string>();
        let selected: string;
        let curSelectedSliceName = '';
        if (this._currSelected) {
            selected = '{ ';
            const sliceTitle = this._currSelected[this._props.axes[0]];
            curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/%/g, '').replace(/#/g, '').replace(/</g, '') : sliceTitle;
            Object.keys(this._currSelected).forEach(key => {
                key !== '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : '';
            });
            selected = selected.substring(0, selected.length - 2);
            selected += ' }';
            if (this._props.titleCol !== '' && (!this._currSelected.frequency || this._currSelected.frequency < 10)) {
                selected += '\n' + this._props.titleCol + ': ';
                this._tableData.forEach(each => {
                    if (this._currSelected[this._props.axes[0]] === each[this._props.axes[0]]) {
                        if (this._props.axes[1]) {
                            if (this._currSelected[this._props.axes[1]] === each[this._props.axes[1]]) selected += each[this._props.titleCol] + ', ';
                        } else selected += each[this._props.titleCol] + ', ';
                    }
                });
                selected = selected.slice(0, -1).slice(0, -1);
            }
        } else selected = 'none';
        let selectedSliceColor;
        const sliceColors = StrListCast(this._props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::'));
        sliceColors.forEach(each => {
            // eslint-disable-next-line prefer-destructuring
            if (each[0] === curSelectedSliceName!) selectedSliceColor = each[1];
        });

        if (this._pieChartData.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._props.layoutDoc[titleAccessor])}
                            setVal={undoable(
                                action(val => {
                                    this._props.layoutDoc[titleAccessor] = val as string;
                                }),
                                'Change Graph Title'
                            )}
                            color="black"
                            size={Size.LARGE}
                            fillWidth
                        />
                    </div>
                    {this._props.axes.length === 1 && /\d/.test(this._props.records[0][this._props.axes[0]]) ? (
                        <div className="asHistogram-checkBox" style={{ width: this._props.width }}>
                            <Checkbox color="primary" onChange={this.changeHistogramCheckBox} checked={this._props.layoutDoc.dataViz_pie_asHistogram as boolean} />
                            Organize data as histogram
                        </div>
                    ) : null}
                    <div
                        ref={r => {
                            this._piechartRef = r;
                            this.drawChart(this._pieChartData, this.width, this.height);
                        }}
                    />
                    {selected !== 'none' ? (
                        <div className="selected-data">
                            Selected: {selected}
                            &nbsp; &nbsp;
                            <ColorPicker
                                tooltip="Change Slice Color"
                                type={Type.SEC}
                                icon={<FaFillDrip />}
                                selectedColor={selectedSliceColor || this.curSliceSelected.attr('fill')}
                                setFinalColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Slice Color')}
                                setSelectedColor={undoable(color => this.changeSelectedColor(color), 'Change Selected Slice Color')}
                                size={Size.XSMALL}
                            />
                        </div>
                    ) : null}
                </div>
            ) : (
                <span className="chart-container"> first use table view to select a column to graph</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>
        );
    }
}
