import _ from 'lodash';
import Store from './Store';

export class TreeStore extends Store {
    constructor(params) {
        params.pageSize = 10000;

        super(params);
    }

    useBackend() {
        return false;
    }

    getChildrenKey() {
        return 'children';
    }

    getChildren(node) {
        return node[this.getChildrenKey()];
    }

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

        parent[childrenKey].push(child);
    }

    getParentKey() {
        return 'parentId';
    }

    getParent(node, hashMap) {
        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;
        }

        return hashMap[parentId];
    }

    getHashMap() {
        return this.treeHashMap;
    }

    getNodeById(id) {
        return this.getHashMap()[id];
    }

    countItems(list) {
        return list.length;
    }

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

            node = parent;
        }
    }

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

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

            return children;
        }, []);

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

    getFilteredTree(list, hashMap) {
        const filtered = this.getFiltered(list);

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

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

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

        return _.uniq(balanced);
    }

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

    assembleTree(nodes, hashMap) {
        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);
            }
        });

        return tree;
    }

    convert(data) {
        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) {
        const hasRawDataChanges = !_.isEqual(this.rawData, data);

        const copy = _.cloneDeep(data);

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

        const hashMap = this.getItemsHashMap(copy);

        let list = 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 = 1) {
        fn(node, index, level);

        const children = this.getChildren(node);

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

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

            fn(parent);

            node = parent;
        }
    }

    loadWithParams() {
        throw new Error('Not implemented');
    }
}
