import {TestableNode, TestableNodeWithBranchAndFlow} from "./TestableNode";
import {BranchNode, StateBranchNode} from "./StateBranchNode";
import {
    AfterRemovingFromGroveHook,
    BeforeAddingToGroveHook,
    BeforeRemovingFromGroveHook,
    Grove,
    TreeNodeData,
} from "./Grove";
import {FlowDataCreator, FlowDataWithoutPosition, Identifiable, RenderedNodeData} from "./FlowDataCreator";
import {generateIdForType} from "./NodeHelpers";
import {isUndefined, omitBy} from "lodash";
import mitt from "mitt";
import {ExportableToState} from "./ExportableToState";
import {MiddleRenderedNodeData, RendererNode, RendererNodeType} from "./RendererNodeData";
import {TreeNodeAnchor} from "./TreeNodeAnchor";

export interface BeforeAddingToTreeNodeHook {
    beforeAddingToTreeNode: () => BranchNode | undefined | void
}

export interface RemovedFromTreeNodeHook {
    beforeRemovingFromTreeNode: () => void
}

export interface AddedToTreeNodeHook {
    afterAddingToTreeNode: () => void
}

export enum TreeNodeEvent {
    BranchAdded = 'branch.added',
}

export type TreeNodeEventTypes = {
    [TreeNodeEvent.BranchAdded]: {
        // the id of the branch
        branch: typeof TreeNode.prototype.branchNode
    }
}

export class TreeNode<Branch extends BranchNode = StateBranchNode> implements BeforeAddingToGroveHook,
    BeforeRemovingFromGroveHook,
    AfterRemovingFromGroveHook,
    BeforeAddingToTreeNodeHook,
    FlowDataCreator,
    Identifiable,
    ExportableToState<TreeNodeData>,
    RendererNode,
    MiddleRenderedNodeData,
    RendererNode {
    private emitter = mitt<TreeNodeEventTypes>()
    // @ts-ignore
    branchNode: Branch & Partial<BeforeAddingToGroveHook> & Partial<AfterRemovingFromGroveHook> & Partial<RemovedFromTreeNodeHook> & Partial<AddedToTreeNodeHook> & FlowDataCreator;
    public parentBranchNode?: StateBranchNode;
    public suggestedScope?: string

    private constructor(
        private _testableNode: TestableNodeWithBranchAndFlow & Partial<BeforeAddingToGroveHook> & Partial<AfterRemovingFromGroveHook>,
        public readonly grove: Grove,
        // regular connects a normal testable composite node to a branch
        public readonly id: string = generateIdForType('tree.node')!,
    ) {}

    get testableNode() {
        return this._testableNode;
    }

    get sourceAnchor(): RendererNode | undefined {
        if (!this.isRoot()) {
            return
        }

        // @ts-ignore
        return new TreeNodeAnchor(this)
    }

    /**
     * Designed for creating a tree node instance for adding a tree node to branches or to the grove.
     * **DO NOT** use this for reading because it is not context-aware. it is not aware of the apps's state.
     * For runtime tree nodes, use Grove.get<TreeNode>(id)
     */
    static createTemporaryFromComponentsWithoutAncestors(testableNode: TestableNodeWithBranchAndFlow, branchNode: BranchNode, grove: Grove, id?: string, extraData?: any): TreeNode {
        // ok so the
        const treeNode = new TreeNode(
            testableNode,
            grove,
            id,
        );

        testableNode.setBranch(branchNode);
        treeNode.setBranchNode(branchNode)

        if (extraData?.suggestedScope) {
            treeNode.suggestedScope = extraData.suggestedScope;
        }

        return treeNode;

        // first, build the testable node that holds the temp testable data in memory
        // this object is a nested representation of the testable data
        // ideally, this should be a 1:1 map of the server schema for the 'testableGroup' => [php]

        // this class (TreeNode) also implements the hooks (beforeMovingToGrove and afterMovingToGrove) so that we can call them on the testableNode and branchNode

        // ok this was extremely easy and ok
        // now, the testablenode and branchnode may not exist yet, they may only exits in memory but ot in thestate
        // so we going to have a hook api, its going to be optional, sometink like:
        // testableNode.beforeMovingToGrove()
        // for the branch, maybe:
        // branchNode.afterMovingToGrove()

        // so were goung to create a temprraryTestableNode that implements the TestableNode interface and that only has the testable data in memory
        // then it will implement beforeMovingToGrove()
        // when that is called, we'll add the testable data from memory t the state
        // then on the get testableNode() getter, we'll return the real testablenode from he state using the Testables clsss (Testables.getNode())
        // essentially removing the temporary testableNode from memory
        // it's brilliant ig
        // also afterMovingToGrove() we'll add the rendered node for the testable to the state
        // and the edge that connects to its branch


        // now for the branch node, its going to be a similar approach.
        // for example, if an offers branch, we'll add the offers to the state when beforeMovingToGrove() is called
        // and when afterMovingToGrove is called, we'll add the rendered node to the state
    }


    setBranchNode(branchNode: BranchNode & Partial<BeforeAddingToTreeNodeHook>) {
        if (this.branchNode) {
            this.branchNode.beforeRemovingFromTreeNode?.()
        }
        // @ts-ignore
        this.branchNode = branchNode.beforeAddingToTreeNode?.() || branchNode;
        this.branchNode.setTreeNode(this);
        this.testableNode.setTreeNode(this);
        this.testableNode.setBranch(this.branchNode);

        this.branchNode.afterAddingToTreeNode?.()

        this.emitter.emit(TreeNodeEvent.BranchAdded, {
            branch: this.branchNode
        })
    }

    setTestableNode(testableNode: TestableNodeWithBranchAndFlow) {
        this._testableNode = testableNode;
        this.testableNode.setTreeNode(this);
        this.testableNode.setBranch(this.branchNode);
        this.branchNode.setTestable(this.testableNode);
    }

    /**
     * Optional only for the nested branches (eg we need to set the parent branch if this is a tier)
     */
    setParentBranchNode(parentBranchNode: BranchNode) {
        if (!this.parentBranchNode) {
            this.parentBranchNode = parentBranchNode;
        }
    }

    get type(): 'tiered' | 'regular' {
        return this.branchNode.isTiered() ? 'tiered' : 'regular';
    }

    getId(): string {
        return this.id;
    }

    // will return the parent branch (a BranchNode not a TreeNode!) if nested,
    // undefined if it is a root node
    getParentBranch() {
        return this.parentBranchNode;
    }

    isRoot(): boolean {
        return !this.parentBranchNode;
    }

    hasParent() {
        return !!this.parentBranchNode;
    }

    beforeAddingToTreeNode() {
        // this is where we add the testable data to the state
        // and possibly the children if any
        // @ts-ignore
        this.testableNode.beforeAddingToTreeNode?.()
        // @ts-ignore
        this.branchNode.beforeAddingToTreeNode?.()
    }

    beforeAddingToGrove() {
        // this where the in-memory data may be added to the state
        this.testableNode.beforeAddingToGrove?.()
        this.branchNode.beforeAddingToGrove?.()
    }

    beforeRemovingFromGrove() {
        this.afterRemovingFromGrove()
    }

    afterRemovingFromGrove() {
        // remove the testable data from the state
        // and possibly the children if any
        this.testableNode.afterRemovingFromGrove?.()
        this.branchNode.afterRemovingFromGrove?.()
    }

    addSubscriber(event: TreeNodeEvent, callback: (data: TreeNodeEventTypes[typeof event]) => void) {
        this.emitter.on(event, callback);
    }

    exportToState(): TreeNodeData {
        return {
            id: this.id,
            testableId: this.testableNode.getId(),
            branch: (this.branchNode as StateBranchNode).exportToState(),
            extraData: {
                suggestedScope: this.suggestedScope,
            }
        };
    }

    createFlowData(): FlowDataWithoutPosition {
        const testableFlowData = this.testableNode.createFlowData();
        const branchFlowData = this.branchNode.createFlowData();

        const tierFlowData: FlowDataWithoutPosition = {
            renderedNodes: this.isTier() ? [
                /**
                 * The step anchor if this tree node is a tier
                 */
                {
                    id: this.id, // this is the id of the tree node that represents the step anchor
                    type: 'branch.tiered.tier.step-anchor',
                    data: {treeNodeId: this.id},
                } as RenderedNodeData
            ] : [],
            edges: this.getTierEdges()
        }

        let rootStepAnchorFlowData = this.getRootStepAnchorFlowData();
        return {
            renderedNodes: [
                ...rootStepAnchorFlowData.renderedNodes,
                ...tierFlowData.renderedNodes,
                ...testableFlowData.renderedNodes,
                ...branchFlowData.renderedNodes,
            ].filter(v => v !== undefined),
            edges: [
                ...rootStepAnchorFlowData.edges,
                this.getTestableToBranchEdge(),
                ...branchFlowData.edges,
                ...tierFlowData.edges,
                ...testableFlowData.edges,
                ...this.getEdgesForChildrenTiers()
            ].filter(v => v !== undefined),
        };
    }

    createRenderedNodeWithData(renderedNodeData: Omit<RenderedNodeData, 'data' | 'position'>): Omit<RenderedNodeData, 'position'> {
        return {
            ...renderedNodeData,
            data: {treeNodeId: this.id},
        }
    }

    leftSpacing(): number {
        if (this.isTier() && this.isFirstTier()) {
            return 16
        }

        return 0
    }

    topSpacing(): number {
        if (this.isTier()) {
            return 12
        }

        return 0
    }

    /**
     * True if this is the child of a tiered branch.
     * NOT to be confused with isTiered(), 'tiered' refers to a branch,
     * 'tier' refers to the child of a tiered branch,
     */
    isTier(): boolean {
        return !!this.parentBranchNode?.isTiered?.()
    }

    isFirstTier(): boolean {
        return this.parentBranchNode?.getChildIndex(this.getId()) === 0
    }

    /**
     * undefined if this is no a tier
     */
    getTierPosition(): number | undefined {
        const tierIndex = this.getTierIndex()

        if (typeof tierIndex === 'undefined') {
            return undefined
        }

        return tierIndex + 1
    }

    private getTierIndex() {
        if (!this.isTier()) {
            return undefined
        }

        return this.parentBranchNode!.getChildIndex(this.id)
    }

    private getPreviousTreeNode(): TreeNode | undefined {
        if (!this.isTier()) {
            return undefined
        }

        return this.parentBranchNode?.getChildAtIndex<TreeNode | undefined>(
            this.getTierIndex()! - 1,
            (treeNodeData) => this.grove.get<TreeNode>((treeNodeData as TreeNodeData).id)
        )
    }

    private getTestableToBranchEdge() {
        let handles = {}

        if (!this.isRoot() && this.branchNode.isTiered()) {
            handles = {
                // hide the from root testable to first tiered branch
                sourceHandle: 'bottom',
                targetHandle: 'left',
            }
        } else if (this.isTier()) {
            handles = {
                sourceHandle: 'right',
                targetHandle: 'left',
            }
        } else {
            handles = {
                /*sourceHandle: 'bottom',
                targetHandle: 'top',*/
            }
        }
        return omitBy({
            id: this.testableNode.getId(),
            ...handles,
            source: this.testableNode.getId(),
            target: this.testableNode.getTargetRenderNodeId(),
            type: 'connecting-line',
        }, isUndefined);
    }

    private getTierEdges(): FlowDataWithoutPosition['edges'] {
        if (!this.isTier()) {
            return []
        }

        let previousNode: TreeNode | undefined

        return [
            // connect the previous step anchor to the this step anchor if this is not the first tier
            this.getTierPosition()! > 1 ? {
                id: `${(previousNode = this.getPreviousTreeNode()!).id}->${this.id}`,
                targetHandle: 'top',
                sourceHandle: 'bottom',
                source: previousNode.id,
                target: this.id,
                type: 'connecting-line',
            } : undefined,
            // the step anchor to the tier testable
            {
                id: this.id,
                targetHandle: 'left',
                sourceHandle: 'right',
                source: this.id,
                target: this.testableNode.getId(),
                type: 'connecting-line',
            },
            // the tier testable to branch is in method getTestableToBranchEdge()
            // why? because that method works for non-tiers too
        ].filter(v => v !== undefined)
    }

    private getEdgesForChildrenTiers(): FlowDataWithoutPosition['edges'] {
        if (this.branchNode.isTiered()) {
            return this.branchNode.getChildren(({id}) => this.grove.get<TreeNode>(id)).map((treeNodeTier: TreeNode) => treeNodeTier.createFlowData().edges).flat()
        }

        return []
    }

    getRoot(): TreeNode {
        if (this.parentBranchNode) {
            return this.parentBranchNode.getTreeNode().getRoot()
        }

        return this;
    }

    getSourceRenderNode(): RendererNodeType | undefined {
        /**
         * This is for when this is a TIER inside a TIERED BRANCH
         */
        if (this.isTier()) {
            // so this is a step anchor, so its source is the base of the parent branch
            if (this.isFirstTier()) {
                return this.parentBranchNode!.testableBaseNode
            } else {
                // this is not the first tier, so its source is the previous step anchor
                return this.getPreviousTreeNode()
            }
        } else if (this.isRoot()) {
            // @ts-ignore
            return this.sourceAnchor
        }
    }

    getSourceRenderNodeId(): string {
        return this.getSourceRenderNode()?.getId() || ''
    }

    getTargetRenderNode(): RendererNodeType | undefined {
        /**
         * Again, this is for when this is a TIER inside a TIERED BRANCH
         */
        // so this is a step anchor, so its target is the testable of this tree node which is a testable partial
        if (this.isTier()) {
            return this.testableNode
        }
    }

    getTargetRenderNodeId(): string {
        return this.getTargetRenderNode()?.getId() || ''
    }

    isFirstRoot() {
        return this.grove.isFirst(this.getId())
    }

    private getRootStepAnchorFlowData(): FlowDataWithoutPosition {
        let renderedNodes: FlowDataWithoutPosition['renderedNodes'] = []
        let edges: FlowDataWithoutPosition['edges'] = []

        if (this.isRoot()) {
            renderedNodes =  [
                {
                    id: this.id,
                    type: 'tree.root.anchor',
                    data: {treeNodeId: this.id},
                }
            ]

            edges = [
                {
                    id: `root->${this.testableNode.getId()}`,
                    source: this.id,
                    target: this.testableNode.getId(),
                },
                // if this anit the first one, we also want to connect it to the previous root
                !this.isFirstRoot() ? {
                    id: `previous.root->${this.id}`,
                    source: this.grove.getPreviousTreeNode(this.id)!.id,
                    target: this.id,
                } : undefined
            ].filter(v => v !== undefined);
        }

        return {
            renderedNodes,
            edges
        }
    }
}

