import { observer } from "mobx-react";
import { Doc, StrListCast } from "../../../../../fields/Doc";
import * as React from 'react';
import * as d3 from 'd3';
import { IReactionDisposer, action, computed, observable, reaction } from "mobx";
import { LinkManager } from "../../../../util/LinkManager";
import { Cast, DocCast, StrCast} from "../../../../../fields/Types";
import { PinProps, PresBox } from "../../trails";
import { Docs } from "../../../../documents/Documents";
import { List } from "../../../../../fields/List";
import './Chart.scss';
import { ColorPicker, EditableText, IconButton, Size, Type } from "browndash-components";
import { FaFillDrip } from "react-icons/fa";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { listSpec } from "../../../../../fields/Schema";
import { scaleCreatorNumerical, yAxisCreator } from "../utils/D3Utils";
import { undoBatch, undoable } from "../../../../util/UndoManager";

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

@observer
export class Histogram extends React.Component<HistogramProps> {

    private _disposers: { [key: string]: IReactionDisposer } = {};
    private _histogramRef: React.RefObject<HTMLDivElement> = React.createRef();
    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
    @observable _currSelected: any | undefined = undefined; // Object of selected bar
    private curBarSelected: any = undefined; // histogram bin of selected bar
    private selectedData: any = undefined; // Selection of selected bar
    private hoverOverData: any = undefined; // Selection of bar being hovered over

    // filters all data to just display selected data if brushed (created from an incoming link)
    @computed get _histogramData() {
        var guids = StrListCast(this.props.layoutDoc.rowGuids);
        if (this.props.axes.length < 1) return [];
        if (this.props.axes.length < 2) {
            var ax0 = this.props.axes[0];
            if (/\d/.test(this.props.pairs[0][ax0])){ this.numericalXData = true }
            return this.props.pairs
                ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.selected && StrListCast(this.incomingLinks[0].selected).includes(guids[this.props.pairs.indexOf(pair)])))
                .map(pair => ({ [ax0]: (pair[this.props.axes[0]])}))
        };
        var ax0 = this.props.axes[0];
        var ax1 = this.props.axes[1];
        if (/\d/.test(this.props.pairs[0][ax0])) { this.numericalXData = true;}
        if (/\d/.test(this.props.pairs[0][ax1]) ) { this.numericalYData = true;}
        return this.props.pairs
            ?.filter(pair => (!this.incomingLinks.length ? true : this.incomingLinks[0]!.selected && StrListCast(this.incomingLinks[0].selected).includes(guids[this.props.pairs.indexOf(pair)])))
            .map(pair => ({ [ax0]: (pair[this.props.axes[0]]), [ax1]: (pair[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 || !ax1 || !/\d/.test(this.props.pairs[0][ax1]) || !this.numericalYData){
            return ax0 + " Histogram";
        }
        else return ax0 + " by " + ax1 + " Histogram";
    }    

    @computed get incomingLinks() {
        return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links
            .filter(link => link.link_anchor_1 == this.props.rootDoc.draggedFrom) // 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
    }

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

    componentWillUnmount() {
        Array.from(Object.keys(this._disposers)).forEach(key => this._disposers[key]());
    }
    componentDidMount = () => {
        this._disposers.chartData = reaction(
            () => ({ dataSet: this._histogramData, 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: 'histogram doc selection' + this._currSelected,
        });
        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc);
        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) => {
        var validData = dataSet.filter((d: { [x: string]: unknown; }) => {
            var valid = true;
            Object.keys(dataSet[0]).map(key => {
                if (!d[key] || Number.isNaN(d[key])) valid = false;
            })
            return valid;
        })
        var field = dataSet[0]? Object.keys(dataSet[0])[0] : undefined;
        const data = validData.map((d: { [x: string]: any; }) => {
            if (this.numericalXData) { return +d[field!].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') }
            return d[field!]
        })
        return data;
    }

    // outlines the bar selected / hovered over
    highlightSelectedBar = (changeSelectedVariables: boolean, svg: any, eachRectWidth: any, pointerX: any, xAxisTitle: any, yAxisTitle: any, histDataSet: any) => {
        var sameAsCurrent: boolean;
        var barCounter = -1;
        const selected = svg.selectAll('.histogram-bar').filter((d: any) => {
            barCounter++; // uses the order of bars and width of each bar to find which one the pointer is over
            if ((barCounter*eachRectWidth ) <= pointerX && pointerX <= ((barCounter+1)*eachRectWidth)){
                var showSelected = this.numericalYData? this._histogramData.filter((data: { [x: string]: any; }) => StrCast(data[xAxisTitle]).replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')==d[0])[0] 
                : histDataSet.filter((data: { [x: string]: any; }) => data[xAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')==d[0])[0];
                if (this.numericalXData){
                    // calculating frequency
                    if (d[0] && d[1] && d[0]!=d[1]){
                        showSelected = {[xAxisTitle]: (d3.min(d) + " to " + d3.max(d)), frequency: d.length}
                    }
                    else if (!this.numericalYData) showSelected = {[xAxisTitle]: showSelected[xAxisTitle], frequency: d.length}
                } 
                if (changeSelectedVariables){
                    // for when a bar is selected - not just hovered over
                    sameAsCurrent = this._currSelected? 
                        (showSelected[xAxisTitle]==this._currSelected![xAxisTitle] 
                    && showSelected[yAxisTitle]==this._currSelected![yAxisTitle])
                        : false;
                    this._currSelected = sameAsCurrent? undefined: showSelected;
                    this.selectedData = sameAsCurrent? undefined: d;
                }
                else this.hoverOverData = d;
                return true
            }
            return false;
        });
        if (changeSelectedVariables){
            if (sameAsCurrent!) this.curBarSelected = undefined;
            else this.curBarSelected = selected;
        }
    }

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

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

        // converts data into Objects
        var histDataSet = dataSet.filter((d: { [x: string]: unknown; }) => {
            var valid = true;
            Object.keys(dataSet[0]).map(key => {
                if (!d[key] || Number.isNaN(d[key])) valid = false;
            })
            return valid;
        });
        if (!this.numericalXData) {
            var histStringDataSet: { [x: string]: unknown; }[] = [];
            if (this.numericalYData){
                for (let i=0; i<dataSet.length; i++){
                    histStringDataSet.push({[yAxisTitle]: dataSet[i][yAxisTitle], [xAxisTitle]: dataSet[i][xAxisTitle]})
                }
            }
            else{
                for (let i=0; i<uniqueArr.length; i++){
                    histStringDataSet.push({[yAxisTitle]: 0, [xAxisTitle]: uniqueArr[i]})
                }
                for (let i=0; i<data.length; i++){
                    let 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
        var svg = (this._histogramSvg = d3
            .select(this._histogramRef.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")
                .attr("transform",
                      "translate(" + this.props.margin.left + "," + this.props.margin.top + ")"));
        var x = d3.scaleLinear()
                .domain(this.numericalXData? [startingPoint!, endingPoint!] : [0, numBins])
                .range([0, width ]);
        var histogram = d3.histogram()
            .value(function(d) {return d})
            .domain([startingPoint!, endingPoint!])
            .thresholds(x.ticks(numBins))
        var bins = histogram(data)
        var eachRectWidth = width/(bins.length)
        var 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 ])
        var 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++){
                var 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)
            bins.forEach(d => d.x0 = d.x0!)
            xAxis = d3.axisBottom(x)
                .ticks(bins.length-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 {
            var allSame = true;
            for (var i=0; i<bins.length; i++){
                if (bins[i] && bins[i][0]){
                    var 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)
                var tickDiff = (bins.length>=2? (bins[bins.length-2].x1!-bins[bins.length-2].x0!): 0)
                var 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, function(d: any) { 
            return d[yAxisTitle]? Number(d[yAxisTitle]!.replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '')) : 0})
                    : d3.max(bins, function(d) { return d.length; })
        var y = d3.scaleLinear()
            .range([height, 0]);
            y.domain([0, +maxFrequency!]);
        var yAxis = d3.axisLeft(y)
            .ticks(maxFrequency!)
        if (this.numericalYData){
            const yScale = scaleCreatorNumerical(0, Number(maxFrequency), 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
        const onPointClick = action((e: any) => this.highlightSelectedBar(true, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet));
        const onHover = action((e: any) => {
            const selected = this.highlightSelectedBar(false, svg, eachRectWidth, d3.pointer(e)[0], xAxisTitle, yAxisTitle, histDataSet)
            updateHighlights();
        })
        const mouseOut = action((e: any) => {
            this.hoverOverData = undefined;
            updateHighlights();
        })
        const updateHighlights = () => {
            const hoverOverBar = this.hoverOverData;
            const selectedData = this.selectedData;
            svg.selectAll('rect').attr("class", function(d: any) { return ((hoverOverBar && hoverOverBar[0]==d[0]) || selectedData && selectedData[0]==d[0])? 'histogram-bar hover' : 'histogram-bar'; })
        }
        svg.on('click', onPointClick)
            .on('mouseover', onHover)
            .on('mouseout', mouseOut)

        // 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
        var selected = this.selectedData;
        svg.selectAll("rect")
            .data(bins)
            .enter()
            .append("rect")
              .attr("transform", this.numericalYData?
                    function (d) {
                        var eachData = histDataSet.filter((data: { [x: string]: number; }) => {return data[xAxisTitle]==d[0]}) 
                        var length = eachData.length? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0;
                        return "translate(" + x(d.x0!) + "," + y(length) + ")";
                    }
                    : function(d) { return "translate(" + x(d.x0!) + "," + y(d.length) + ")"; })
              .attr("height", this.numericalYData? 
                    function(d) {
                        var eachData = histDataSet.filter((data: { [x: string]: number; }) => {return data[xAxisTitle]==d[0]}) 
                        var length = eachData.length? eachData[0][yAxisTitle].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, '') : 0;
                        return height-y(length)}
                    : function(d) { 
                        return height - y(d.length)})
              .attr("width", eachRectWidth)
              .attr("class", selected? 
                    function(d) {
                        return (selected && selected[0]==d[0])? 'histogram-bar hover' : 'histogram-bar';
                    }: function(d) {return 'histogram-bar'})
              .attr("fill", (d)=>{ 
                var barColor;
                var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::'));
                barColors.map(each => { 
                    if (d[0] && d[0].toString() && each[0]==d[0].toString()) barColor = each[1]; 
                    else {
                        var range = StrCast(each[0]).split(" to ");
                        if (Number(range[0])<=d[0] && d[0]<=Number(range[1])) barColor = each[1];
                    }
                });
                return barColor? StrCast(barColor) : StrCast(this.props.layoutDoc.defaultHistogramColor)})
    };

    @action changeSelectedColor = (color: string) => {
        this.curBarSelected.attr("fill", color);
        var barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''))

        const barColors = Cast(this.props.layoutDoc.histogramBarColors, listSpec("string"), null);
        barColors.map(each => { if (each.split('::')[0] == barName) barColors.splice(barColors.indexOf(each), 1) });
        barColors.push(StrCast(barName + '::' + color));
    };
    
    @action eraseSelectedColor= () => {
        this.curBarSelected.attr("fill", this.props.layoutDoc.defaultHistogramColor);
        var barName = StrCast(this._currSelected[this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''))

        const barColors = Cast(this.props.layoutDoc.histogramBarColors, listSpec("string"), null);
        barColors.map(each => { if (each.split('::')[0] == barName) barColors.splice(barColors.indexOf(each), 1) });
    };

    render() {
        this._histogramData
        var curSelectedBarName;
        var titleAccessor: any='';
        if (this.props.axes.length==2) titleAccessor = 'histogram-title-'+this.props.axes[0]+'-'+this.props.axes[1];
        else if (this.props.axes.length>0) titleAccessor = 'histogram-title-'+this.props.axes[0];
        if (!this.props.layoutDoc[titleAccessor]) this.props.layoutDoc[titleAccessor] = this.defaultGraphTitle;
        if (!this.props.layoutDoc.defaultHistogramColor) this.props.layoutDoc.defaultHistogramColor = '#69b3a2';
        if (!this.props.layoutDoc.histogramBarColors) this.props.layoutDoc.histogramBarColors = new List<string>();
        var selected: string;
        if (this._currSelected){
            curSelectedBarName = StrCast(this._currSelected![this.props.axes[0]].replace(/\$/g, '').replace(/\%/g, '').replace(/\</g, ''))
            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 selectedBarColor;
        var barColors = StrListCast(this.props.layoutDoc.histogramBarColors).map(each => each.split('::'));
        barColors.map(each => {if (each[0]==curSelectedBarName!) selectedBarColor = each[1]});

        this.componentDidMount();
        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
                    />
                    &nbsp; &nbsp; 
                    <ColorPicker
                        tooltip={'Change Default Bar Color'} 
                        type={Type.SEC} 
                        icon={<FaFillDrip/>}
                        selectedColor={StrCast(this.props.layoutDoc.defaultHistogramColor)}
                        setSelectedColor={undoable (color => this.props.layoutDoc.defaultHistogramColor = color, "Change Default Bar Color")}
                        size={Size.XSMALL}
                    />
                </div>
                <div ref={this._histogramRef} />
                {selected != 'none' ? 
                    <div className={'selected-data'}>
                        Selected: {selected}
                        &nbsp; &nbsp;
                        <ColorPicker
                            tooltip={'Change Bar Color'} 
                            type={Type.SEC} 
                            icon={<FaFillDrip/>}
                            selectedColor={selectedBarColor? selectedBarColor : this.curBarSelected.attr("fill")}
                            setSelectedColor={undoable (color => this.changeSelectedColor(color), "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 (action(() => this.eraseSelectedColor()), "Change Selected Bar Color")}
                        />
                    </div>
                : null}
            </div>
            ) : <span className="chart-container"> {'first use table view to select a column to graph'}</span>
        );
    }

}