import { ColorPicker, EditableText, Size, Type } from 'browndash-components';
import * as d3 from 'd3';
import { action, computed, IReactionDisposer, 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 { PinProps, PresBox } from '../../trails';
import './Chart.scss';
import { Checkbox } from '@material-ui/core';

export interface PieChartProps {
    rootDoc: Doc;
    layoutDoc: Doc;
    axes: 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 React.Component<PieChartProps> {
    private _disposers: { [key: string]: IReactionDisposer } = {};
    private _piechartRef: React.RefObject<HTMLDivElement> = React.createRef();
    private _piechartSvg: d3.Selection<SVGGElement, unknown, null, undefined> | undefined;
    private curSliceSelected: any = undefined; // d3 data of selected slice
    private selectedData: any = undefined; // Selection of selected slice
    private hoverOverData: any = undefined; // Selection of slice being hovered over
    @observable _currSelected: any | undefined = undefined; // Object of selected slice

    @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() {
        if (this.props.layoutDoc.dataViz_pie_asHistogram==undefined) this.props.layoutDoc.dataViz_pie_asHistogram = false;
        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() {
        var ax0 = this.props.axes[0];
        var 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.rootDoc.dataViz_parentViz);
        // return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
        //     .filter(link => link.link_anchor_1 == this.props.rootDoc.dataViz_parentViz) // get links where this chart doc is the target of the link
        //     .map(link => DocCast(link.link_anchor_1)); // then return the source of the link
    }

    componentWillUnmount() {
        Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
    }
    componentDidMount = () => {
        this._disposers.chartData = reaction(
            () => ({ dataSet: this._pieChartData, w: this.width, h: this.height }),
            ({ dataSet, w, h }) => {
                if (dataSet!.length > 0) {
                    this.drawChart(dataSet, w, h);
                }
            },
            { fireImmediately: true }
        );
    };

    @action
    restoreView = (data: Doc) => {};
    // 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,
        });
        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.rootDoc);
        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] || Number.isNaN(d[key])));
        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) => {
        var index = -1;
        var sameAsCurrent: boolean;
        const selected = svg.selectAll('.slice').filter((d: any) => {
            index++;
            var p1 = [0, 0]; // center of pie
            var p3 = [arc.centroid(d)[0] * 2, arc.centroid(d)[1] * 2]; // outward peak of arc
            var p2 = [radius * Math.sin(d.startAngle), -radius * Math.cos(d.startAngle)]; // start of arc
            var 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
            var 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) {
                // inside the slice of it crosses an odd number of edges
                var showSelected = this.byCategory ? pieDataSet[index] : this._pieChartData[index];
                if (changeSelectedVariables) {
                    // for when a bar is selected - not just hovered over
                    sameAsCurrent = this._currSelected
                        ? showSelected[Object.keys(showSelected)[0]] == this._currSelected![Object.keys(showSelected)[0]] && showSelected[Object.keys(showSelected)[1]] == this._currSelected![Object.keys(showSelected)[1]]
                        : this._currSelected === showSelected;
                    this._currSelected = sameAsCurrent ? undefined : showSelected;
                    this.selectedData = sameAsCurrent ? undefined : d;
                } else this.hoverOverData = d;
                return true;
            }
            return false;
        });
        if (changeSelectedVariables) {
            if (sameAsCurrent!) this.curSliceSelected = undefined;
            else this.curSliceSelected = selected;
        }
    };

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

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

        // converts data into Objects
        var data = this.data(dataSet);
        var pieDataSet = dataSet.filter((d: { [x: string]: unknown }) => !Object.keys(dataSet[0]).some(key => !d[key] || Number.isNaN(d[key])));
        if (this.byCategory) {
            let uniqueCategories = [...new Set(data)];
            var 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++) {
                let sliceData = pieStringDataSet.filter((each: any) => each[percentField] == data[i]);
                sliceData[0].frequency = sliceData[0].frequency + 1;
            }
            pieDataSet = pieStringDataSet;
            percentField = Object.keys(pieDataSet[0])[0];
            descriptionField = Object.keys(pieDataSet[0])[1]!;
            data = this.data(pieStringDataSet);
        }
        var trackDuplicates: { [key: string]: any } = {};
        data.forEach((eachData: any) => (!trackDuplicates[eachData] ? (trackDuplicates[eachData] = 0) : null));

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

        // 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((e: any) => {
            this.hoverOverData = undefined;
            updateHighlights();
        });
        const updateHighlights = () => {
            const hoverOverSlice = this.hoverOverData;
            const selectedData = this.selectedData;
            svg.selectAll('path').attr('class', function (d: any) {
                return (selectedData && d.startAngle == selectedData.startAngle && d.endAngle == selectedData.endAngle) || (hoverOverSlice && d.startAngle == hoverOverSlice.startAngle && d.endAngle == hoverOverSlice.endAngle)
                    ? 'slice hover'
                    : 'slice';
            });
        };

        // drawing the slices
        var selected = this.selectedData;
        var arcs = g.selectAll('arc').data(pie(data)).enter().append('g');
        const possibleDataPointVals: { [x: string]: any }[] = [];
        pieDataSet.forEach((each: { [x: string]: any | { valueOf(): number } }) => {
            var 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) {}
            possibleDataPointVals.push(dataPointVal);
        });
        const sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::'));
        arcs.append('path')
            .attr('fill', (d, i) => {
                var dataPoint;
                const possibleDataPoints = possibleDataPointVals.filter((pval: any) => pval[percentField] === Number(d.data));
                if (possibleDataPoints.length == 1) dataPoint = possibleDataPoints[0];
                else {
                    dataPoint = possibleDataPoints[trackDuplicates[d.data.toString()]];
                    trackDuplicates[d.data.toString()] = trackDuplicates[d.data.toString()] + 1;
                }
                var sliceColor;
                if (dataPoint) {
                    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 => each[0] == accessByName && (sliceColor = each[1]));
                }
                return sliceColor ? StrCast(sliceColor) : d3.schemeSet3[i] ? d3.schemeSet3[i] : d3.schemeSet3[i % d3.schemeSet3.length];
            })
            .attr(
                'class',
                selected
                    ? function (d) {
                          return selected && d.startAngle == selected.startAngle && d.endAngle == selected.endAngle ? 'slice hover' : 'slice';
                      }
                    : function (d) {
                          return 'slice';
                      }
            )
            .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', function (d) {
                    var centroid = arc.centroid(d as unknown as d3.DefaultArcObject);
                    var heightOffset = (centroid[1] / radius) * Math.abs(centroid[1]);
                    return 'translate(' + (centroid[0] + centroid[0] / (radius * 0.02)) + ',' + (centroid[1] + heightOffset) + ')';
                })
                .attr('text-anchor', 'middle')
                .text(function (d) {
                    var 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.map(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() {
        var titleAccessor: any = '';
        if (this.props.axes.length == 2) titleAccessor = 'dataViz_pie_title' + this.props.axes[0] + '-' + this.props.axes[1];
        else if (this.props.axes.length > 0) titleAccessor = 'dataViz_pie_title' + 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>();
        var selected: string;
        var curSelectedSliceName = '';
        if (this._currSelected) {
            const sliceTitle = this._currSelected[this.props.axes[0]];
            curSelectedSliceName = StrCast(sliceTitle) ? StrCast(sliceTitle).replace(/\$/g, '').replace(/\%/g, '').replace(/\#/g, '').replace(/\</g, '') : sliceTitle;
            selected = '{ ';
            Object.keys(this._currSelected).map(key => {
                key != '' ? (selected += key + ': ' + this._currSelected[key] + ', ') : '';
            });
            selected = selected.substring(0, selected.length - 2);
            selected += ' }';
        } else selected = 'none';
        var selectedSliceColor;
        var sliceColors = StrListCast(this.props.layoutDoc.dataViz_pie_sliceColors).map(each => each.split('::'));
        sliceColors.forEach(each => {
            if (each[0] == curSelectedSliceName!) selectedSliceColor = each[1];
        });

        if (this._pieChartData.length > 0 || !this.parentViz) {
            return this.props.axes.length >= 1 ? (
                <div className="chart-container">
                    <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={this._piechartRef} />
                    {selected != 'none' ? (
                        <div className={'selected-data'}>
                            Selected: {selected}
                            &nbsp; &nbsp;
                            <ColorPicker
                                tooltip={'Change Slice Color'}
                                type={Type.SEC}
                                icon={<FaFillDrip />}
                                selectedColor={selectedSliceColor ? 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>
            );
        } else
            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>
            );
    }
}
