import {TestableData, useNodesStore} from "./store";
import {Testables, TestablesState} from "./testables";
import {BranchData, BranchNodeEvent, StateBranchNode, TieredData, TieredExtraData} from "./StateBranchNode";
import {TestableCompositeNode} from "./TestableCompositeNode";
import {Offers, OffersState} from "./Offers";
import {TreeNode, TreeNodeEvent} from "./TreeNode";
import {TestableNode} from "./TestableNode";
import {Flow, FlowState} from "./Flow";
import {NestedTestableNodeData} from "./InMemoryTestableNode";
import {MemoryBranchNode, NestedBranchData} from "./MemoryBranchNode";
import {TreeNodeFactory} from "./TreeNodeFactory";
import {generateIdForType} from "./NodeHelpers";
import {TierType} from "./tiers";

export type TreeNodeData = {
    id: string,
    testableId: string,
    branch: BranchData,
    extraData?: any,
}

export type NestedTreeNodeData<ChildrenType extends object = any> = {
    id: string,
    testable: NestedTestableNodeData,
    branch: NestedBranchData<ChildrenType>,
    parentBranchTestableId?: string,
    extraData?: any
}

export type NestedTreeNodeDataWithoutIds<ChildrenType extends object = any> = {
    testable: Omit<NestedTestableNodeData, 'id'>,
    branch: Omit<NestedBranchData<ChildrenType>, 'id'>,
    parentBranchTestableId?: string,
    extraData?: any
}

export type GroveData = TreeNodeData[]
export type GroveState = {
    grove: GroveData
}
export type PreconditionsState = {
    preconditionsID: string | undefined, // the id of the root testable for preconditions, which is always a testable composite group
}
export type PredatesState = {
    predatesID: string | undefined, // the id of the root testable for preconditions, which is always a testable composite group
}
export type FullState = Partial<PreconditionsState> & Partial<PredatesState> & GroveState & OffersState & TestablesState & FlowState

/**
 * This is only meant for reading the grove data from the store.
 *
 * Do not use this to modify the grove data.
 */
export const getGroveForReading = (state?: FullState) => {
    return Grove.fromState(state || useNodesStore.getState())
};

export type PossibleNodes = TreeNode | TestableCompositeNode | TestableNode | StateBranchNode

export interface BeforeAddingToGroveHook {
    beforeAddingToGrove: () => void
}

// no the hook for before and after removing
export interface BeforeRemovingFromGroveHook {
    beforeRemovingFromGrove: () => void
}

export interface AfterRemovingFromGroveHook {
    afterRemovingFromGrove: () => void
}

export type StateObjects = {
    state: FullState,
    grove: Grove,
    testables: Testables,
    offers: Offers,
    flow: Flow,
    treeNodeFactory: TreeNodeFactory
};

export class Grove {
    private treeNodesCreated: Record<string, TreeNode> = {}

    constructor(
        public readonly grove: GroveData,
        private readonly offers: Offers,
        private readonly testables: Testables,
        private readonly flow: Flow,
        private readonly treeNodeFactory: TreeNodeFactory
    ) {
        treeNodeFactory.setGrove(this)
    }

    /**
     * IMPORTANT: This should be the only way to get a TreeNode instance,
     * since the grove registers events needed to re-render the flow on changes
     * and because ancestors relationships are set here
     */
    get<Type = PossibleNodes>(id: string | undefined): Type | undefined {
        if (!id) {
            // for methods or properties that may return undefined
            return undefined;
        }
        const find = (trees: TreeNodeData[], id: string): Type | undefined => {
            for (const tree of trees) {
                if (tree.id === id) {
                    return this.createTestableNodeFromTreeNodeData(tree).getTreeNode() as unknown as Type | undefined;
                } else if (tree.testableId === id) {
                    //const testable = new Testable(this.manager, id, true) as unknown as Type;
                    //return this.createTestableNodeFromTreeNodeData(tree) as unknown as Type | undefined;

                    // we need the parent to be set, so unfortunately we have to re-search this node using the tree node id
                    // it sucks for performance but let's make it work and then we can optimize later
                    const treeNode = this.get<TreeNode>(tree.id)!

                    return treeNode.testableNode as unknown as Type | undefined;
                } else if (tree.branch.id === id) {
                    //const testable = this.createTestableNodeFromTreeNodeData(tree)

                    // we need the parent to be set, so unfortunately we have to re-search this node using the tree node id
                    // it sucks for performance but let's make it work and then we can optimize later
                    const treeNode = this.get<TreeNode>(tree.id)!

                    return treeNode.branchNode as unknown as Type | undefined;
                } else if (id === 'tobedone') {

                } else if (tree.branch.type === 'tiered') {
                    if ((tree.branch.data as TieredData).baseTestableId === id) {
                        const treeNode = this.get<TreeNode>(tree.id)!

                        return treeNode.branchNode.testableBaseNode as unknown as Type | undefined;
                    }
                    // check the children and they descendants
                    const result = find(tree.branch.children as TreeNodeData[], id);

                    if (result) {
                        // make sure we set the parent tree node ONLY IF IT HAS NOT BEEN SET!
                        if (result instanceof TreeNode) {
                            let parentTreeNode: TreeNode

                            /**
                             * IMPORTANT!
                             * Make sure we set the tree node if its already been created SO THAT ITS PARENT IS SAVED
                             * OTHERWISE THE FARTHEST ANCESTORS WON'T BE SET
                             */
                            if (this.treeNodesCreated[tree.id]) {
                                parentTreeNode = this.treeNodesCreated[tree.id]
                            } else {
                                /**
                                 * so unfortunately we have to re-search this node using the tree node id so that ALL the ancestors are set correctly
                                 * bad for performance but let's make it work now
                                 *
                                 * For some reason, this.createTestableNodeFromTreeNodeData(tree).getTreeNode() work when testing in grove.test.ts,
                                 * but it fails in groveActions.test.ts. I dont know why and honestly I don't care right now im extremely exhausted
                                 * this turned out to be absolutely more complex than I expected
                                 */
                                //parentTreeNode = this.createTestableNodeFromTreeNodeData(tree).getTreeNode()
                                parentTreeNode = this.get<TreeNode>(tree.id)!
                            }

                            (result as unknown as TreeNode).setParentBranchNode(parentTreeNode.branchNode)

                            this.treeNodesCreated[(result as unknown as TreeNode).id] = result as unknown as TreeNode
                            this.treeNodesCreated[parentTreeNode.id] = parentTreeNode
                        }

                        return result;
                    }
                }
            }

        }

        return find(this.grove, id)
    }

    isFirst(treeId): boolean {
        return this.getIndexOfTree(treeId) === 0;
    }

    /**
     * Returns the index of the tree FIRST LEVEL ONLY
     *
     * It can be either the testableId or the branchId
     */
    getIndexOfTree(treeId): number | undefined {
        return this.grove.findIndex((tree) => tree.id === treeId || tree.testableId === treeId || tree.branch.id === treeId);
    }

    getPreviousTreeNode(treeId): TreeNode | undefined {
        let lastIndex: number = -1

        for (const [index, tree] of this.grove.entries()) {
            if (tree.id === treeId) {
                break
            }

            lastIndex = index
        }

        if (lastIndex > -1) {
            return this.get<TreeNode>(this.grove[lastIndex].id)
        }
    }

    getTreeByIndex(index: number) {
        return this.grove[index];
    }

    /**
     * This is a pure function and DOES NOT modify the flow state.
     *
     * If you are calling this from the UI, please use GroveActions.addTreeNode() instead so that the flow data is updated.
     */
    addTreeNode(treeNode: TreeNode & Partial<BeforeAddingToGroveHook>) {
        // first call the 'before adding' hook so that the testable and branch nodes can prepare the data if need be
        treeNode.beforeAddingToGrove?.();

        // then add it to the state
        this.grove.push(treeNode.exportToState())
    }

    removeTreeNode(treeId: string) {
        const index = this.getIndexOfTree(treeId);

        if (typeof index === 'number' && index !== -1) {
            const treeNode = this.get<TreeNode & Partial<AfterRemovingFromGroveHook>>(treeId)!

            this.grove.splice(index, 1);

            treeNode?.afterRemovingFromGrove?.();

            this.flow.removeTreeNode(treeNode.id)
        }
    }

    private createTestableNodeFromTreeNodeData(tree: TreeNodeData) {
        const {testableNode, branch} = this.treeNodeFactory.createComponentsFromStateTreeNodeData(tree)

        const treeNode = TreeNode.createTemporaryFromComponentsWithoutAncestors(
            testableNode, branch, this, tree.id, tree.extraData
        );

        if (treeNode.branchNode.isTiered()) {
            // set the base testable node
            treeNode.branchNode.setTestableBase(
                new TestableCompositeNode(
                    this.testables,
                    (treeNode.branchNode as StateBranchNode).getExtraData().baseTestableId
                )
            )
        }

        // listen for tree node changes
        treeNode.addSubscriber(TreeNodeEvent.BranchAdded, this.onBranchAdded.bind(this));

        (treeNode.branchNode as StateBranchNode).addSubscriber(BranchNodeEvent.ChildrenAdded, this.updateGroveFromBranch.bind(this));
        (treeNode.branchNode as StateBranchNode).addSubscriber(BranchNodeEvent.ChildrenRemoved, this.updateGroveFromBranch.bind(this));

        return testableNode;
    }

    private updateGroveFromBranch({branch}) {
        this.setTreeNodeDataInGrove(branch.getTreeNode())
    }

    onBranchAdded({branch}) {
        this.flow.renderTreeNode(branch.getTreeNode())
        this.setTreeNodeDataInGrove(branch.getTreeNode())
    }

    public setTreeNodeDataInGrove(treeNode: TreeNode) {
        // find the index of the tree node in the grove
        // then replace the tree node with the new one with generatetreendoedata()
        function exportTreeNodeState(grove: GroveData) {
            for (const i in grove) {
                if (grove[i].id === treeNode.id) {
                    grove[i] = treeNode.exportToState()
                    return true
                }


                // check the children
                const branch = grove[i].branch;
                const children = branch.children

                if (branch.type === 'tiered' && Array.isArray(children)) {
                    const found = exportTreeNodeState(children as TreeNodeData[])

                    if (found) {
                        return true
                    }
                }
            }
        }

        exportTreeNodeState(this.grove);
    }

    afterRemovedRootTestableGroup({id}) {
        // remove the tree node
        this.removeTreeNode(
            this.get<TestableCompositeNode>(id)?.getTreeNode().id!
        )
    }

    /**
     * fromState expects a state whose objects have IDS!
     */
    private static _cachedState: FullState | null = null;
    private static _cachedGrove: Grove | null = null;
    private static _cachedStateObjects: StateObjects | null = null;

    static fromState(state: FullState, withComponents: true): StateObjects;
    static fromState(state: FullState, withComponents?: false): Grove;
    static fromState(state: FullState, withComponents: boolean = false) {
        // Reuse cached instance when selectors receive the same state snapshot
        if (Grove._cachedState === state) {
            if (withComponents && Grove._cachedStateObjects) return Grove._cachedStateObjects;
            if (!withComponents && Grove._cachedGrove) return Grove._cachedGrove;
        }

        const testables = new Testables(state)
        const offers = new Offers(state)

        const treeNodeFactory = new TreeNodeFactory(testables, offers)

        const flow = new Flow(state);
        const grove = new Grove(
            state.grove,
            offers,
            testables,
            flow,
            treeNodeFactory
        )

        treeNodeFactory.setGrove(grove)

        Grove._cachedState = state;
        Grove._cachedGrove = grove;

        if (withComponents) {
            const stateObjects = {
                state,
                grove,
                testables,
                offers,
                flow,
                treeNodeFactory
            };
            Grove._cachedStateObjects = stateObjects;
            return stateObjects;
        }

        Grove._cachedStateObjects = null;
        return grove
    }

    static getBranchDataFromState(grove: GroveData, id: string) {
        for (const treeNodeData of grove) {
            if (treeNodeData.branch.id === id) {
                return treeNodeData.branch
            }
        }
    }

    resetCache() {
        //this.treeNodesCreated = {}
    }

    convertBranchToTiered(branchToConvert: StateBranchNode, data: {
        baseTestable: Omit<TestableData, 'id'>,
        tierType: TierType,
        treeNodeData: NestedTreeNodeData | NestedTreeNodeDataWithoutIds
    }): StateBranchNode | undefined {
        // the given tree node IS the tiered branch so we need to replace
        const treeNode = branchToConvert.getTreeNode()
        const {baseTestable, tierType, treeNodeData} = data
        const newMemoryBranch = new MemoryBranchNode(
            {
                id: generateIdForType('branch'),
                type: 'tiered',
                data: {
                    baseTestable,
                    tierType,
                } as TieredExtraData,
                children: [
                    treeNodeData
                ]
            },
            this.offers,
            this.testables
        )
        // first create the node if we were given nested data

        treeNode.setBranchNode(newMemoryBranch)

        // let's update the grove with the new branch so that we can use it later to add the offer
        this.setTreeNodeDataInGrove(treeNode)

        return this.get<StateBranchNode>(newMemoryBranch.getId())!

    }

    /**
     * todo: needs to account for nested trees (it's currently shallow (1st level only))
     */
    getTrees(): TreeNode[] {
        return this.grove.map(treeData => this.get<TreeNode>(treeData.id)!).filter(t => t !== undefined)
    }
}