import {TreeNode} from "./TreeNode";
import {FullState, Grove, NestedTreeNodeData, NestedTreeNodeDataWithoutIds, StateObjects} from "./Grove";
import {TreeNodeFactory} from "./TreeNodeFactory";
import {Testables} from "./testables";
import {Offers} from "./Offers";
import {StateBranchNode, TieredExtraData} from "./StateBranchNode";
import {Flow} from "./Flow";
import {generateDefaultNestedSelectActionsBranchData, generateIdForType, removeIds} from "./NodeHelpers";
import {NestedTestableNodeData} from "./InMemoryTestableNode";
import {NestedBranchData} from "./MemoryBranchNode";
import {OfferData, TestableData, TestablePartial} from "./store";
import {cloneStrategies} from "./grove-actions";
import {cloneDeep} from "lodash";
import {TierType} from "./tiers";
import {pickByPaths} from "./actions/SelectTierNew";
import {getComponentMetaFromType} from "./ComponentsMeta";

/**
 * See this class could have a better name, but it's been a long time and I need to get an MVP soon so...
 */
export class GroveActions {
    private testables: Testables;
    private offers: Offers;
    private treeNodeFactory: TreeNodeFactory;
    private grove: Grove;
    private flow: Flow;
    private stateObjects: StateObjects;

    constructor(
        private state: FullState,
    ) {
        this.stateObjects = Grove.fromState(state, true);

        const {grove, testables, offers, treeNodeFactory, flow} = this.stateObjects

        this.testables = testables
        this.offers = offers
        this.treeNodeFactory = treeNodeFactory
        this.grove = grove
        this.flow = flow
    }

    addTreeNode(treeNode: NestedTreeNodeData | NestedTreeNodeDataWithoutIds | TreeNode, options?: {
        parentBranchId?: string, position?: number, newTieredBranchData?: {
            baseTestable: Omit<TestableData, 'id'>,
            tierType: TierType
        }, extraData?: {suggestedScope?: string}
    }) {
        // where to add the tree node?
        if (options?.parentBranchId) {
            // check if the parent branch is a tiered branch
            const parentBranch = this.grove.get<StateBranchNode>(options.parentBranchId)

            if (!parentBranch) {
                console.error('Cannot add tree node, parent branch not found')
                return
            }

            if (parentBranch.isSelectAction()) {
                // convert branch to tiered
                const newBranch = this.grove.convertBranchToTiered(parentBranch, {
                    baseTestable: options.newTieredBranchData!.baseTestable,
                    tierType: options.newTieredBranchData!.tierType,
                    treeNodeData: treeNode as NestedTreeNodeData | NestedTreeNodeDataWithoutIds,
                })

                this.reRenderFlowData(this.grove.get<StateBranchNode>(newBranch?.getId?.())!)
            } else if (parentBranch.isTiered()) {
                this.addTier(
                    treeNode as NestedTreeNodeData | NestedTreeNodeDataWithoutIds,
                    options.parentBranchId,
                    options.position
                )
            }

            // convert it to a tiered branch if target is select actions
        } else {
            // add it to the root
            const treeNodeToAdd = treeNode instanceof TreeNode ? treeNode : this.treeNodeFactory.createFromNestedData(treeNode)!

            this.grove.addTreeNode(treeNodeToAdd)
            /**
             * IMPORTANT!
             * Get it from the state since the given tree node object is temporary and
             * may not have all the runtime dependencies needed
             */
            const newTreeNode = this.grove.get<TreeNode>(treeNodeToAdd.getId())!

            this.reRenderFlowData(newTreeNode)
            this.flow.centerRootNode(newTreeNode)
        }
    }

    cloneTier(targetTreeNodeId: string) {
        // remember tiers are represented as the tree nodes whose parent branch is tiered
        const targetTreeNodeToClone = this.grove.get<TreeNode>(targetTreeNodeId)
        const targetBranch = targetTreeNodeToClone?.getParentBranch()!

        if (!targetTreeNodeToClone) {
            console.error('Cannot clone tier: tree node not found %s', targetTreeNodeId)
            return
        }

        if (!targetBranch) {
            console.error('Cannot clone tier: branch not found %s', targetTreeNodeId)
            return
        }

        const treeNodeOfTargetBranch = targetBranch.getTreeNode()!

        let testablePartialToClone = targetTreeNodeToClone.testableNode.getTestable();

        const testableDataToAdd = removeIds<NestedTestableNodeData>(cloneDeep(testablePartialToClone.getNestedData())) as TestablePartial
        const branchDataToAdd = removeIds(cloneDeep(targetTreeNodeToClone.branchNode.exportData('clone-tier'))) as NestedBranchData

        const cloneStrategyGroups = cloneStrategies(this.stateObjects)

        for (const clonerGroupType in cloneStrategyGroups) {
            const clonerGroup = cloneStrategyGroups[clonerGroupType]

            clonerGroup.forEach(cloner => {
                if (cloner.canCloneTier(targetTreeNodeToClone)) {
                    cloner.cloneTier(
                        testableDataToAdd,
                        // @ts-ignore
                        branchDataToAdd as NestedBranchData<NestedBranchData, TieredExtraData<NestedTestableNodeData>>,
                        targetTreeNodeToClone,
                    )
                }
            })

        }

        const meta = getComponentMetaFromType(testablePartialToClone.getType())!

        testableDataToAdd.options = pickByPaths(testableDataToAdd.options, meta.tierFields || [])

        const newNodeData: Omit<NestedTreeNodeData, 'id'> = ({
            // don't pass an id so that it generates a new one
            //id: ?,
            testable: testableDataToAdd,
            branch: branchDataToAdd,
        })!

        const position = targetBranch.getChildIndex(targetTreeNodeId)! + 1

        this.addTier(newNodeData, targetBranch.getId(), position + 1)
    }

    /**
     * To manually add a tier to a tiered branch, use addTreeNode()
     */
    private addTier(treeNodeData: NestedTreeNodeData | NestedTreeNodeDataWithoutIds, tieredBranchId: string, position?: number) {
        // get the branch
        const targetTieredBranch = this.grove.get<StateBranchNode>(tieredBranchId)

        if (!targetTieredBranch) {
            console.error('Cannot add tier to branch, branch not found')
            return
        }
        // let's set the parent testable id to the tree node data so that the testable partial cab be added to the state
        treeNodeData.parentBranchTestableId = targetTieredBranch.getExtraData().baseTestableId
        // first create the node if we were given nested data
        const treeNodeToAdd = this.treeNodeFactory.createFromNestedData(treeNodeData)!
        // run hooks
        treeNodeToAdd.beforeAddingToGrove()
        // then add the treenode to it
        targetTieredBranch.addChildren([treeNodeToAdd], position)
        // then save the grove state
        this.grove.setTreeNodeDataInGrove(targetTieredBranch.getTreeNode())

        this.reRenderFlowData(targetTieredBranch)
    }

    removeTier(treeNodeId: string) {
        const treeNodeToRemove = this.grove.get<TreeNode>(treeNodeId)

        if (!treeNodeToRemove) {
            console.error('Cannot remove tier, tree node not found')
            return
        }

        let parentBranch = treeNodeToRemove.getParentBranch()

        if (!parentBranch) {
            console.error('Cannot remove tier, parent branch not found')
            return
        }

        const parentTreeNode = parentBranch.getTreeNode()

        // remove it from the branch
        parentBranch = this.removeBranchChild(parentBranch, treeNodeToRemove);

        // update the grove state
        this.grove.setTreeNodeDataInGrove(parentBranch.getTreeNode())

        // get the root tree node to update its state
        const rootTreeNode: TreeNode = parentBranch.getTreeNode().getRoot()!;
        // then regenerate the flow data, its has to be THE ROOT NODE!
        // if this was a root tree node, we need to remove it from the flow instead of updating it
        if (treeNodeToRemove.isRoot()) {
            this.flow.removeTreeNode(treeNodeToRemove.id)
        } else {
            this.flow.renderTreeNode(rootTreeNode)
        }
    }

    /**
     * Will remove all tiers and thus the branch itself will be removed and replaced with a default select action branch
     */
    removeTieredBranch(branchId: string) {
        const targetBranch = this.grove.get<StateBranchNode>(branchId)
        if (!targetBranch) {
            console.error('Cannot remove tiered branch, branch not found')
            return
        } else if (!targetBranch.isTiered()) {
            console.error('Cannot remove tiered branch, branch is not tiered')
            return
        }

        // remove all children from the branch, simple as that.-
        targetBranch.getChildrenIds().forEach(this.removeTier.bind(this))
    }

    private removeBranchChild(branch: StateBranchNode, itemToRemove: string | TreeNode) {
        // @ts-ignore
        branch.removeChildren([itemToRemove])

        // now if this is the last item, we need to replace the branch with a default select action branch
        if (branch.getChildrenIds().length === 0) {
            branch = this.replaceBranchWithDefault(branch.getId())!
        }

        return branch;
    }

    replaceBranchWithDefault(branchId: string): StateBranchNode | undefined {
        const branch = this.grove.get<StateBranchNode>(branchId)

        if (!branch) {
            console.error('Cannot replace branch with default, branch not found')
            return
        }

        const newBranch = new StateBranchNode(
            this.offers,
            generateDefaultNestedSelectActionsBranchData(),
            this.testables
        );
        branch.getTreeNode().setBranchNode(
            newBranch
        )

        // update the grove state
        this.grove.setTreeNodeDataInGrove(branch.getTreeNode())

        this.reRenderFlowData(branch)

        return newBranch
    }

    removeBranchOffer(offerId: string, branchId: string) {
        const branch = this.grove.get<StateBranchNode>(branchId)

        if (!branch) {
            console.error('Cannot remove offer from branch, branch not found')
            return
        }

        this.removeBranchChild(branch, offerId)
        // state should have been updated by the removeBranchChild method via replaceBranchWithDefault()
    }

    addBranchOffer(offerData: Omit<OfferData, 'id'> | OfferData, branchId: string) {
        let branch = this.grove.get<StateBranchNode>(branchId)

        if (!branch) {
            console.error('Cannot add offer to branch, branch not found')
            return
        }

        if (branch.isSelectAction()) {
            // if the branch is a select action, we need to replace it with an offers branch
            const treeNode = branch.getTreeNode()

            const newOffersBranch = new StateBranchNode(
                this.stateObjects.offers,
                {
                    id: generateIdForType('branch'),
                    type: 'offers',
                    children: [
                        // no children here because they have not been added to the state yet
                    ]
                },
                this.stateObjects.testables,
            );

            treeNode.setBranchNode(
                newOffersBranch
            )

            // let's update the grove with the new branch so that we can use it later to add the offer
            this.grove.setTreeNodeDataInGrove(treeNode)
            // now let's update the reference to the branch so that we can add the offer to it
            branch = this.grove.get<StateBranchNode>(newOffersBranch.getId())!
        }
        // add the offer to the branch
        branch.addChildren([
            // to prevent any issues with other libraries (jotai was causing issues)
            cloneDeep(offerData)
        ])
        // update the grove state
        this.grove.setTreeNodeDataInGrove(branch.getTreeNode())

        this.reRenderFlowData(branch)
    }

    private reRenderFlowData(node: TreeNode)
    private reRenderFlowData(node: StateBranchNode)
    private reRenderFlowData(node: undefined)
    private reRenderFlowData(node: TreeNode | StateBranchNode | undefined) {
        if (!node) {
            // this for when you want to pass a node from a function that may return undefined like grove.get()
            console.error('Cannot re-render flow data, node is undefined')
            return
        }

        const treeNode: TreeNode = node instanceof TreeNode ? node : node.getTreeNode()
        // get the root tree node to update its state
        const rootTreeNode: TreeNode = treeNode.getRoot()!
        // then regenerate the flow data, its has to be THE ROOT NODE!
        this.flow.renderTreeNode(rootTreeNode)
    }
}
