import _ from 'lodash';
import {Store, BackendResponse} from './Store';
import {HashMap} from '../typings/common';

export abstract class TreeStore<V> extends Store<V> {
    treeHashMap: HashMap<V> = {};

    constructor(params) {
        super(params);
        params.pageSize = 10000;
    }

    useBackend(): boolean {
        return false;
    }

    getChildrenKey(): string {
        return 'children';
    }

    getChildren(node): V[] | void {
        return node[this.getChildrenKey()];
    }

    addChild(parent: V, child: V): void {
        const childrenKey = this.getChildrenKey();
        if (!parent[childrenKey]) {
            parent[childrenKey] = [];
        }

        parent[childrenKey].push(child);
    }

    getParentKey(): string {
        return 'parentId';
    }

    getParent(node: V, hashMap?: HashMap<V>): V | undefined {
        hashMap = hashMap || this.treeHashMap;
        const parentId = node[this.getParentKey()];

        if (parentId === undefined && parentId === null) {
            // eslint-disable-next-line no-console
            console.warn(
                `Tree node with id ${
                    node[this.primaryKey]
                } is not connected with tree. Please, check your data.`
            );
            return undefined;
        }

        return hashMap[parentId];
    }

    getHashMap(): HashMap<V> {
        return this.treeHashMap;
    }

    getNodeById(id): V | void {
        return this.getHashMap()[id];
    }

    countItems(list): number {
        return list.length;
    }

    addParentsToResultArray(node, array, hashMap): void {
        while (this.getParent(node)) {
            const parent = this.getParent(node, hashMap);
            array.push(parent);

            node = parent;
        }
    }

    addChildrenToResultArray(node, array, hashMap): void {
        const foundChildren = Object.values(hashMap).reduce((children: any, childToCheck: any) => {
            const parent = this.getParent(childToCheck);
            if (!parent || parent[this.primaryKey] !== node[this.primaryKey]) {
                return children;
            }

            array.push(childToCheck);
            children.push(childToCheck);

            return children;
        }, []) as any;

        foundChildren.forEach(child => this.addChildrenToResultArray(child, array, hashMap));
    }

    getFilteredTree(list, hashMap): HashMap<V> {
        const filtered = this.getFiltered(list);

        if (list.length === filtered.length) {
            return filtered as any;
        }

        const balanced = filtered.reduce(
            (result, item) => {
                this.addChildrenToResultArray(item, result, hashMap);
                this.addParentsToResultArray(item, result, hashMap);

                return result;
            },
            [...filtered]
        );

        return _.uniq(balanced) as any;
    }

    getItemsHashMap(data): HashMap<V> {
        const items = {};
        data.forEach(item => {
            items[item[this.primaryKey]] = item;
        });
        return items;
    }

    assembleTree(nodes, hashMap): V[] {
        let keysQueue = nodes.map(node => node[this.primaryKey]);
        const tree = [];

        keysQueue.forEach(key => {
            const node = hashMap[key];
            const parent = this.getParent(node, hashMap);

            if (parent) {
                this.addChild(parent, node);
            } else {
                tree.push(node as never);
            }
        });

        return tree;
    }

    convert(data): V[] {
        this.rawData = super.convert(data);

        this.totalCount = this.countItems(this.rawData);
        this.treeHashMap = this.getItemsHashMap(this.rawData);

        this.assembleTree(this.rawData, this.treeHashMap); // it mutates this.rawData

        return this.rawData;
    }

    calculate(data): void {
        const hasRawDataChanges = !_.isEqual(this.rawData, data);

        const copy = _.cloneDeep(data);

        copy.forEach(item => {
            delete item[this.getChildrenKey()];
        });

        const hashMap = this.getItemsHashMap(copy);

        let list: any = this.getFilteredTree(copy, hashMap);
        this.filteredCount = this.countItems(list);

        list = this.getSorted(list);

        this.data = this.assembleTree(list, hashMap);

        this.notifyChanges(hasRawDataChanges);
    }

    applyCallbackToSubtree(node, index, fn, level?: number): void {
        fn(node, index, level);

        const children = this.getChildren(node);

        if (children && children.length > 0) {
            children.forEach((child, childIndex) => {
                this.applyCallbackToSubtree(child, childIndex, fn, (level || 0) + 1);
            });
        }
    }

    applyCallbackToParents(node, fn): void {
        while (this.getParent(node)) {
            const parent = this.getParent(node);

            fn(parent);

            node = parent;
        }
    }

    loadWithParams(): Promise<BackendResponse<V>> {
        throw new Error('Not implemented');
    }
}
