import {
    Autowired,
    Bean,
    ChangedPath,
    ColDef,
    ColGroupDef,
    Column,
    ColumnController,
    EventService,
    IRowModel,
    IRowNodeStage,
    RowNode,
    StageExecuteParams,
    Utils,
    ValueService
} from "ag-grid-community";
import {PivotColDefService} from "./pivotColDefService";

@Bean('pivotStage')
export class PivotStage implements IRowNodeStage {

    // these should go into the pivot column creator
    @Autowired('rowModel') private rowModel: IRowModel;
    @Autowired('valueService') private valueService: ValueService;
    @Autowired('columnController') private columnController: ColumnController;
    @Autowired('eventService') private eventService: EventService;
    @Autowired('pivotColDefService') private pivotColDefService: PivotColDefService;

    private uniqueValues: any = {};

    private pivotColumnGroupDefs: (ColDef | ColGroupDef)[];
    private pivotColumnDefs: ColDef[];

    private aggregationColumnsHashLastTime: string | null;
    private aggregationFuncsHashLastTime: string;

    public execute(params: StageExecuteParams): void {
        let rootNode = params.rowNode;
        let changedPath = params.changedPath;
        if (this.columnController.isPivotActive()) {
            this.executePivotOn(rootNode, changedPath);
        } else {
            this.executePivotOff(changedPath);
        }
    }

    private executePivotOff(changedPath: ChangedPath | undefined): void {
        this.aggregationColumnsHashLastTime = null;
        this.uniqueValues = {};
        if (this.columnController.isSecondaryColumnsPresent()) {
            this.columnController.setSecondaryColumns(null, "rowModelUpdated");
            if (changedPath) {
                changedPath.setInactive();
            }
        }
    }

    private executePivotOn(rootNode: RowNode, changedPath: ChangedPath | undefined): void {
        let uniqueValues = this.bucketUpRowNodes(rootNode);

        let uniqueValuesChanged = this.setUniqueValues(uniqueValues);

        let aggregationColumns = this.columnController.getValueColumns();
        let aggregationColumnsHash = aggregationColumns.map((column) => column.getId()).join('#');
        let aggregationFuncsHash = aggregationColumns.map((column) => column.getAggFunc().toString()).join('#');

        let aggregationColumnsChanged = this.aggregationColumnsHashLastTime !== aggregationColumnsHash;
        let aggregationFuncsChanged = this.aggregationFuncsHashLastTime !== aggregationFuncsHash;
        this.aggregationColumnsHashLastTime = aggregationColumnsHash;
        this.aggregationFuncsHashLastTime = aggregationFuncsHash;

        if (uniqueValuesChanged || aggregationColumnsChanged || aggregationFuncsChanged) {
            let result = this.pivotColDefService.createPivotColumnDefs(this.uniqueValues);
            this.pivotColumnGroupDefs = result.pivotColumnGroupDefs;
            this.pivotColumnDefs = result.pivotColumnDefs;
            this.columnController.setSecondaryColumns(this.pivotColumnGroupDefs, "rowModelUpdated");
            // because the secondary columns have changed, then the aggregation needs to visit the whole
            // tree again, so we make the changedPath not active, to force aggregation to visit all paths.
            if (changedPath) {
                changedPath.setInactive();
            }
        }
    }

    private setUniqueValues(newValues: any): boolean {
        let json1 = JSON.stringify(newValues);
        let json2 = JSON.stringify(this.uniqueValues);

        let uniqueValuesChanged = json1 !== json2;

        // we only continue the below if the unique values are different, as otherwise
        // the result will be the same as the last time we did it
        if (uniqueValuesChanged) {
            this.uniqueValues = newValues;
            return true;
        } else {
            return false;
        }
    }

    // returns true if values were different
    private bucketUpRowNodes(rootNode: RowNode): any {

        // accessed from inside inner function
        let uniqueValues: any = {};

        // finds all leaf groups and calls mapRowNode with it
        let recursivelySearchForLeafNodes = (rowNode: RowNode) => {
            if (rowNode.leafGroup) {
                this.bucketRowNode(rowNode, uniqueValues);
            } else {
                rowNode.childrenAfterFilter.forEach(child => {
                    recursivelySearchForLeafNodes(child);
                });
            }
        };

        recursivelySearchForLeafNodes(rootNode);

        return uniqueValues;
    }

    private bucketRowNode(rowNode: RowNode, uniqueValues: any): void {

        let pivotColumns = this.columnController.getPivotColumns();

        if (pivotColumns.length === 0) {
            rowNode.childrenMapped = null;
            return;
        }

        rowNode.childrenMapped = this.bucketChildren(rowNode.childrenAfterFilter, pivotColumns, 0, uniqueValues);
    }

    private bucketChildren(children: RowNode[], pivotColumns: Column[], pivotIndex: number, uniqueValues: any): any {

        let mappedChildren: any = {};
        let pivotColumn = pivotColumns[pivotIndex];

        // map the children out based on the pivot column
        children.forEach((child: RowNode) => {

            let key: string = this.valueService.getKeyForNode(pivotColumn, child);

            if (Utils.missing(key)) {
                key = '';
            }

            if (!uniqueValues[key]) {
                uniqueValues[key] = {};
            }

            if (!mappedChildren[key]) {
                mappedChildren[key] = [];
            }
            mappedChildren[key].push(child);
        });

        // if it's the last pivot column, return as is, otherwise go one level further in the map
        if (pivotIndex === pivotColumns.length - 1) {
            return mappedChildren;
        } else {
            let result: any = {};

            Utils.iterateObject(mappedChildren, (key: string, value: RowNode[]) => {
                result[key] = this.bucketChildren(value, pivotColumns, pivotIndex + 1, uniqueValues[key]);
            });

            return result;
        }
    }

    public getPivotColumnDefs(): ColDef[] {
        return this.pivotColumnDefs;
    }

}