import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react';
import {
    Background,
    BackgroundVariant,
    Controls,
    GeneralHelpers,
    MiniMap, NodeAddChange,
    NodeChange, NodeDimensionChange, NodePositionChange, NodeRemoveChange, NodeReplaceChange, NodeSelectionChange,
    ReactFlow,
    ReactFlowProvider,
    useReactFlow
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import {OfferData, RenderedNode, TestableData, TestablePartial, useNodesStore, WithXSpacing} from './tree/store';
import {ConnectingLine} from "./ConnectingLine";
import {isEqual} from 'lodash';
import {OffersBranch} from "./OffersBranch";
import {getIdWithoutAnySuffix, getIdWithoutLastSuffix, Testables} from "./tree/testables";
import {StepAnchorBranch, StepAnchorData} from "./StepAnchorBranch";
import {BranchAddNodeButtonNodeBranch, BranchAddNodeButtonNodeData} from "./BranchAddNodeButtonNode";
import {SelectActionBranch, SelectActionNodeData} from "./SelectActionBranch";
import {SelectActionSplitBranch} from "./SelectActionSplitBranch";
import {TieredBaseNode} from "./TieredBaseNode";
import {TieredSubchildNode} from "./TieredSubchildNode";
import {BranchData, BranchNode, CenteredId, StateBranchNode} from "./tree/StateBranchNode";
import {Welcome} from "./Welcome";
import {getTree, Tree} from "./tree/Tree";
import {TestableCompositeRenderer} from "./tree/TestableCompositeRenderer";
import {getGroveForReading, NestedTreeNodeData} from "./tree/Grove";
import {TestableCompositeNode} from "./tree/TestableCompositeNode";
import {
    FirstRenderedNodeData,
    LastRenderedNodeData,
    MiddleRenderedNodeData, RendererNode,
    RendererNodeData
} from "./tree/RendererNodeData";
import {wrapTestablesInDefaultGroup} from "./tree/NodeHelpers";
import {TreeNodeFactory} from "./tree/TreeNodeFactory";
import {Offers} from "./tree/Offers";
import {NestedBranchData, NestedTieredData} from "./tree/MemoryBranchNode";
import {QuantityMeta, QuantityOptions} from "./tree/cards/Quantity";
import {equals, maximum, minimum, range, TierType} from "./tree/tiers";
import {NestedTestableNodeData} from "./tree/InMemoryTestableNode";
import {BranchTieredNode} from "./BranchTieredNode";
import {FlowData, RenderedNodeData} from "./tree/FlowDataCreator";
import {TreeNode} from "./tree/TreeNode";
import {AddTierButton} from "./tree/AddTierButton";
import {GroveActions} from "./tree/GroveActions";
import {AmountValidatorOptions} from "./tree/cards/filterinterfaces";
import {UserRoleOptions} from "./tree/cards/UserRole";
import {ProductsOptions} from "./tree/cards/Products";
import {BuyXGetXMeta, BuyXGetXOptions} from "./tree/cards/BuyXGetX";
import {DiscountOptions} from "./tree/cards/Discount";
import {ParentType} from "./tree/components";
import {BundlePriceOptions} from "./tree/cards/BundlePrice";
import {ShippingDiscountOptions} from "./tree/cards/ShippingDiscount";
import {AdvancedData, BuyXGetYOptions, ProductData} from "./tree/cards/BuyXGetY";
import {FeaturedProductsOptions} from "./tree/cards/FeaturedProducts";
import {AnyProductsOptions} from "./tree/cards/AnyProducts";
import {useAtom} from "jotai/index";
import {atomsStore, mainReactFlowInstanceAtom, OverlayOpenCompositeAtom, treeEmitterAtom} from "./tree/atoms";
import mitt from 'mitt'
import {useAtomValue} from "jotai";
import {RootTreeAnchor} from "./RootTreeAnchor";
import {TreeNodeAnchor} from "./tree/TreeNodeAnchor";
import {CouponsPlus} from "./globals";

export const nodeTypes = {
    "tree.root.anchor": RootTreeAnchor,
    "testable.group": TestableCompositeRenderer,
    "branch.select-action": SelectActionBranch,
    "branch.select-action-split": SelectActionSplitBranch,
    "branch.offers": OffersBranch,
    //"branch-first-passing-node": TreeNode,
    "branch.tiered.testable.base": TieredBaseNode,
    "branch.tiered.tier.testable": TieredSubchildNode,
    "branch.tiered": BranchTieredNode,
    "branch.tiered.tier.step-anchor": StepAnchorBranch,
    "branch.tiered.add-tier-button": BranchAddNodeButtonNodeBranch
}

const edgeTypes = {
    'connecting-line': ConnectingLine
}

export type Direction = 'vertical' | 'horizontal' | 'bottom-right';

type CalculatePositionOptionsBase = {
    ySourceNode?: RenderedNode,
}

type CalculatePositionOptions = (CalculatePositionOptionsBase & CalculateVerticalPositionOptions) | undefined

export type CalculateVerticalPositionOptions = CalculatePositionOptionsBase & {
    x: 'start' | 'middle',
    yDistanceFromSource?: number
}

export type CalculateHorizontalPositionOptions = CalculatePositionOptionsBase & {
    y?: 'start' | 'middle',
    nodesToCheckForTheirWidth?: string[]
}

const DomElementsCache: Record<string, Element> = {}
export const tierVerticalSpacing = 0
export const findNodeDomElement = (id: string) => {
    if (DomElementsCache[id]) {
        return DomElementsCache[id];
    }
    const element = document.querySelector(
        id.startsWith('#') ? id : `.react-flow__node[data-id="${id}"]`
    )

    if (element) {
        DomElementsCache[id] = element;
    }

    return element;
}

let changedDimensionsMap = new Map<string, { width: number; height: number }>()

const emptyDimensions = {width: 0, height: 0};
export const getDimensions = (id: string): { width: number, height: number } => {
    if (!id) {
        return emptyDimensions;
    }

    const cached = changedDimensionsMap.get(id);
    if (cached) {
        return cached;
    }

    return findNodeDomElement(id)?.getBoundingClientRect() || emptyDimensions
}

type RenderableNode = FirstRenderedNodeData | MiddleRenderedNodeData | LastRenderedNodeData;

function calculateNodePosition(targetNode: RenderableNode, sourceNode: RenderedNode | undefined, direction: Direction, getNode: GeneralHelpers['getNode'], options: CalculatePositionOptions): RenderedNode['position'] | undefined {
    if (direction === 'vertical') {
        // vertical needs to have a source node
        return calculateNodePositionVertically(targetNode, sourceNode!, getNode, options as CalculateVerticalPositionOptions);
    } else if (direction === 'horizontal') {
        // whereas horizontal doesn't bc it might be a root node with no parent/source
        return calculateNodePositionHorizontally(targetNode, sourceNode, getNode, options as CalculateHorizontalPositionOptions);
    } else if (direction === 'bottom-right') {
        return calculateNodePositionBottomRight(targetNode, sourceNode, getNode, options);
    }
}

const calculateTierHeight = (node: RenderedNode, getNode: GeneralHelpers['getNode'], options: {
    withY?: boolean,
    elementsToCheck?: ('testable' | 'branch')[],
    spacing?: number
} = {withY: true, spacing: 0}) => {
    const nodeTypes = useNodesStore.getState().couponsPlusNodeTypes
    node = getNode(node.id)! as RenderedNode;
    const data = node.data as StepAnchorData;
    const targetNode = getNode(data.targetNodeId)! as RenderedNode;
    const targetBranchNode = getNode(targetNode.data.targetNodeId)! as RenderedNode;

    let testableNodeHeight: number = 0;

    const shouldCheckTestable = options.elementsToCheck === undefined || options.elementsToCheck?.includes('testable');
    const shouldCheckBranch = options.elementsToCheck === undefined || options.elementsToCheck?.includes('branch');

    if (shouldCheckTestable) {
        testableNodeHeight = getDimensions(targetNode.id).height || 0;
    }

    let branchNodeHeight: number = 0;

    if (shouldCheckBranch) {
        const branchTypeOfTheTargetNode = nodeTypes.find(({id}) => `${id}` === targetNode.id)?.branchType;

        if (branchTypeOfTheTargetNode === 'select-action' || branchTypeOfTheTargetNode === 'offers') {

            branchNodeHeight = getDimensions(targetNode.data.targetNodeId).height || 0;
        } else {

            // this is a multi-node branch, so let's get the height of all its children
            const allChildrenOfTheBranch = useNodesStore.getState().branchRelations.find(({branch}) => branch === targetNode.id)?.children || [];

            allChildrenOfTheBranch.forEach((childId, index) => {
                // call recursively this fubcrion again usig their step anchor
                let childAnchorNode = getNode(`${childId}.step-anchor`)! as RenderedNode;
                let baseNodeId
                if (branchTypeOfTheTargetNode === 'tiered' || branchTypeOfTheTargetNode === 'first-passing-node') {
                    baseNodeId = getNode(childAnchorNode.data.targetNodeId) as RenderedNode
                    //childAnchorNode = getNode(baseNodeId.data.targetNodeId)! as RenderedNode;
                }
                let childHeight: number
                try {
                    childHeight = calculateTierHeight(childAnchorNode, getNode, {withY: false});
                } catch (e) {
                    childHeight = 0
                }
                branchNodeHeight += childHeight + (index + 1 !== allChildrenOfTheBranch.length ? tierVerticalSpacing : 0);
            })

        }
    }

    const {withY} = options;

    return Math.max(
        testableNodeHeight + (withY ? targetNode?.position?.y : 0) || 0,
        branchNodeHeight + (withY ? targetBranchNode?.position?.y : 0) || 0
    );
};

const newCalculateTierHeight = (treeNode: TreeNode, getNode: GeneralHelpers['getNode']): number => {
    const testableHeight = getDimensions(treeNode.testableNode.getId()).height || 0

    if (treeNode.branchNode.isSimple()) {
        // which is greatest, the testable or the branch?
        const branchHeight = getDimensions(treeNode.branchNode.getId()).height || 0

        return Math.max(testableHeight, branchHeight)
    } else {
        // for now just get the height of the testable
        return testableHeight
    }
}

// Positions calculated during the current handleNodesChange pass.
// Later nodes in the loop read from this so they see up-to-date positions
// from nodes already processed — single-pass convergence.
let positionOverrides = new Map<string, { x: number; y: number }>()

const calculateYOfRenderedNode = (nodeId: string): number => {
    const node = findNode(nodeId)
    const y = positionOverrides.get(nodeId)?.y ?? node?.position?.y ?? 0;
    return y + (getDimensions(nodeId).height || 0);
}
const calculateYOfPreviousTier = (previousAnchorNode: RendererNode, getNode: GeneralHelpers['getNode']): number => {
    const previousAnchorTargetNodeFlowData = getNode(previousAnchorNode.getTargetRenderNode()?.getId() as string) as RenderedNodeData
    const previousAnchorNodeIsStepAnchor = previousAnchorNode instanceof TreeNode;
    /**
     * The previous target to use is NEVER an anchor, it's always the target node of the previous anchor eg: a testable, what
     * the previous anchor is anchoring to (eg [anchor] -> [testable]))
     */
    const previousTargetToUseFlowData: RenderedNodeData = (previousAnchorNodeIsStepAnchor ? previousAnchorTargetNodeFlowData : (getNode(previousAnchorNode.getId()) as RenderedNodeData))
    let endYOfThePreviousTier: number = 0;

    if (previousAnchorNodeIsStepAnchor) {
        // we need to get the greatest y of the previous tier (inclduing branch)
        // so let's do:
        // if the previous target is an offers or select action branch, then we need to get the y (inclduing height) of the:
        //  -testable
        //      and
        //  -branch
        // then we need to get the greatest y of the two

        const previousTreeNode = previousAnchorNode as TreeNode

        if (previousTreeNode.branchNode.isSimple()) {
            const previousTargetBranchFlowData = getNode(previousTreeNode.branchNode.getId())! as RenderedNodeData;
            const previousTargetBranchHeight = getDimensions(previousTargetBranchFlowData?.id).height || 0;
            const previousTargetTestableHeight = getDimensions(previousTreeNode.testableNode.getId()).height || 0;

            endYOfThePreviousTier = Math.max(
                previousTargetBranchHeight + (previousTargetBranchFlowData?.position?.y || 0),
                previousTargetTestableHeight + (previousTargetToUseFlowData?.position?.y || 0)
            )
        } else {

            const previousLastAddTierButtonPosition = getNode(previousTreeNode.branchNode.getAddTierButton()?.getId() as string)?.position?.y || 0

            let buttonHeight = 16 // eyeballed

            return previousLastAddTierButtonPosition + buttonHeight;
            /*console.log('previousAnchorNode.id', previousAnchorNode.id,)

            const lastChildOfTheBranch = tree.getLastBranchChild(previousAnchorNode.id)

            console.log('lastChildOfTheBranch', lastChildOfTheBranch, previousAnchorNode.id)

            const lastChildOfTheBranchStepAnchorID = `${lastChildOfTheBranch?.id}.step-anchor`
            const lastChildOfTheBranchStepAnchor = getNode(lastChildOfTheBranchStepAnchorID)! as RenderedNode

            if (lastChildOfTheBranchStepAnchor) {
                console.log('calculating nested start')

                const yOfTheLastChildOfTheBranch = calculateYOfPreviousTier(lastChildOfTheBranchStepAnchor, getNode);

                console.log('calculating nested end')
                //console.log('y nested', yOfTheLastChildOfTheBranch, lastChildOfTheBranchStepAnchor)
                endYOfThePreviousTier = yOfTheLastChildOfTheBranch
            }*/
        }

    } else {
        // this is a testable (either a root or the testable of a nested branch)
        // in this case we just need the y and the height of the testable
        endYOfThePreviousTier = previousTargetToUseFlowData.position.y + (getDimensions(previousTargetToUseFlowData.id)?.height || 0)
    }
    return endYOfThePreviousTier;
};

function calculateNodePositionVertically(targetNode: RendererNodeData & RendererNode & MiddleRenderedNodeData, sourceNode: RenderedNode, getNode: GeneralHelpers['getNode'], options?: CalculateVerticalPositionOptions): RenderedNode['position'] {
    /**
     * FOR A PERFOMANCE OPTIMIZATION:
     *  -reduce the calls to getNode() (check if its a rendered node, if its use that one else use getNode())
     */

    const targetDOMElement = findNodeDomElement(targetNode.getId())!;
    if (!targetDOMElement) {
        // DOM not ready yet — keep current position to avoid jumping to (0,0)
        return undefined
    }
    // i figure we should get the parent of the handle element inside the target dom element instead of using the target element itself
    const targetHandleDOMElement = targetDOMElement.querySelector('.react-flow__handle')?.parentElement;
    const targetElementToUse = targetHandleDOMElement || targetDOMElement;
    const sourceDOMElementMeasures = () => getDimensions(targetNode.getSourceRenderNodeId());
    //const YSourceDOMElementMeasures = options?.ySourceNode ? findNodeDomElement(options.ySourceNode.id)?.getBoundingClientRect() : sourceDOMElementMeasures;

    let pickedTheBestHeightOfThePreviousTier: number = 0;
    let currentTierHeight: number = 0;

    let spacing = options?.yDistanceFromSource ?? tierVerticalSpacing;

    //const sourceIsAnchorOrAddButton = sourceNode.type === 'step-anchor' || sourceNode.type === 'branch-add-node-button';
    const sourceIsAnchorOrAddButton = false //we'll check if its instance of anchor or button, but now its not really needed;
    const targetNodeIsAddButton = false//targetNode.type === 'branch-add-node-button';
    const targetIsAnchorOrAddButton = false//targetNode.type === 'step-anchor' || targetNodeIsAddButton;
    const targetIsAnchor = targetNode instanceof TreeNode && (targetNode.getSourceRenderNode() as TreeNode).isTier()
    const targetIsAddTierButton = targetNode instanceof AddTierButton
    const targetIsNestedBranch = targetNode instanceof StateBranchNode && targetNode.isInsideAnotherBranch()

    let YOfThePreviousTier: number = 0
    if (targetIsAnchor || targetIsAddTierButton || targetIsNestedBranch) {
        YOfThePreviousTier = calculateYOfPreviousTier(targetNode.getSourceRenderNode()!, getNode);

        if (targetIsAnchor) {
            currentTierHeight = newCalculateTierHeight(targetNode as TreeNode, getNode)
        }
    } else if (targetNode instanceof StateBranchNode && !targetNode.isInsideAnotherBranch?.()) {
        // THIS WILL ONLY WORK FOR ROOT NODES BECAUSE REMEMBER WE'RE CALCULATING THE POSITION **VERTICLALLY** AND
        // SELECT ACTION BRANCHES INSIDE OTHER BRANCHES ARE HORIZONTAL!
        YOfThePreviousTier = calculateYOfRenderedNode(targetNode.getSourceRenderNodeId())
    }

    //const sourceNodeYPosition = sourceNode?.position.y + (pickedTheBestHeightOfThePreviousTier || YSourceDOMElementMeasures?.height || 0);

    // so for this we need:
    // 1- the height of the testable
    // 2- the height of the branch
    // if the height of the testable is greate then  the height of the branch, then just use that hieght

    //let extraSpacingOnTopBecauseOfCenterAlignment = 0 //currentTierHeight / 2;
    const heightOfTheTargetNode = getDimensions(targetNode.getId()).height || 0;
    //const targetIdIOfThetargetNode = targetNode.data.targetNodeId;
    //const targetHeightOfTheTargetNode = targetIdIOfThetargetNode? findNodeDomElement(targetIdIOfThetargetNode)?.getBoundingClientRect().height : 0;

    if (false && targetHeightOfTheTargetNode) {
        let greatestHeight: number = 0;

        if (Math.floor(targetHeightOfTheTargetNode) >= Math.floor(currentTierHeight)) {
            greatestHeight = targetHeightOfTheTargetNode
        } else {
            greatestHeight = currentTierHeight
        }

        extraSpacingOnTopBecauseOfCenterAlignment = (greatestHeight - heightOfTheTargetNode) / 2;
    }

    // if the previuos tier is a nested branch, we want to subtract the height of the testable node IF THE branch is higer then the etsable node
    if (false && targetIsAnchorOrAddButton && sourceIsAnchorOrAddButton) {
        const sourceNodeBranchType: BranchData['type'] | undefined = useNodesStore.getState().couponsPlusNodeTypes.find(({id}) => `${id}` === sourceNode.data.targetNodeId)?.branchType;

        if (sourceNodeBranchType === 'first-passing-node' || sourceNodeBranchType === 'tiered') {
            const sourceNodeTargetNode = getNode(sourceNode.data.targetNodeId)! as RenderedNode;
            const sourceNodeTargetHeight = findNodeDomElement(sourceNodeTargetNode.id)?.getBoundingClientRect().height || 0;
            const sourceNodeTargetBranchHeight = calculateTierHeight(sourceNode, getNode, {withY: false});

            if (Math.floor(sourceNodeTargetBranchHeight) > Math.floor(sourceNodeTargetHeight)) {
                extraSpacingOnTopBecauseOfCenterAlignment = extraSpacingOnTopBecauseOfCenterAlignment - sourceNodeTargetHeight;
            }
        }
    }

    const topSpacing = targetNode.topSpacing?.() || 0
    const leftSpacing = targetNode.leftSpacing?.() || 0
    const y = YOfThePreviousTier + topSpacing + (targetIsAddTierButton ? currentTierHeight : Math.max(0, (((currentTierHeight - heightOfTheTargetNode) / 2))));
    const sourceNodePosition = getNode(targetNode.getSourceRenderNodeId())?.position || {x: 0, y: 0}

    return {
        // x position is half the width of the source node
        x: sourceNodePosition?.x + (
            options?.x === 'start' ? 0 : (
                (((sourceDOMElementMeasures()?.width || 0) / 2) - ((targetElementToUse?.getBoundingClientRect().width || 0) / 2)) + leftSpacing
            )
        ),
        y
        // y position 100px below the source node, considering its height
        /*y: targetNode?.data?.isFirst? sourceNodeYPosition + extraSpacingOnTopBecauseOfCenterAlignment + spacing : (
            pickedTheBestHeightOfThePreviousTier?
                pickedTheBestHeightOfThePreviousTier + extraSpacingOnTopBecauseOfCenterAlignment + spacing :
                (sourceNodeYPosition + spacing)
        )*/
    };
}

const renderedNodeExists = (nodeIdToCheck: string) => {
    return findNode(nodeIdToCheck);
};
const findNode = (nodeIdToCheck: string) => {
    return useNodesStore.getState().renderedNodes.find((node) => node.id === nodeIdToCheck);
};
const findWidthAndPosition = (nodeIdToCheck: string, getNode: GeneralHelpers['getNode']): number => {
    // get the node's x and width
    const node = renderedNodeExists(nodeIdToCheck);

    if (!node) {
        return 0;
    }

    const x = positionOverrides.get(nodeIdToCheck)?.x ?? node?.position?.x ?? 0;
    return (getDimensions(nodeIdToCheck).width || 0) + x;
};

function calculateNodePositionHorizontally(targetNode: MiddleRenderedNodeData & RendererNode, sourceNode: RenderedNode | undefined, getNode: GeneralHelpers['getNode'], options?: CalculateHorizontalPositionOptions): RenderedNode['position'] {
    let widthOfTheSourceIncludingX: number = 0
    let widthOfTheSourceIncludingPosition: number = 0
    sourceNode = getNode(targetNode.getSourceRenderNodeId()) as RenderedNode
    // so now for the width, we need to find the max width of the source including its branch if the source exists
    const sourceDOMElementMeasures = getDimensions(targetNode.getSourceRenderNodeId());
    const treeNodeForCalculatingFullWidth = targetNode.getSourceRenderNode()?.treeNodeForCalculatingFullWidth?.() as TreeNode | undefined;

    if (treeNodeForCalculatingFullWidth) {
        // if the source is a tree node, then we need to loop through all its descendants and find the max x of the branches
        widthOfTheSourceIncludingX = findLargestEndXOfATreeNode(treeNodeForCalculatingFullWidth, getNode)
    } else if (sourceNode) {
        // here we'll take the dom elements of the source (testable) AND the dom element of the branch
        // and we'll the width of whichever is greater
        widthOfTheSourceIncludingX = sourceDOMElementMeasures?.width!;
        /*const branchDOMElement = findNodeDomElement(getIdSuffixed(sourceNode.id, 'branch'));
        const branddimesions = getDimensions(getIdSuffixed(sourceNode.id, 'branch'))
        console.log('branddimesions', branddimesions, branchDOMElement)
        if (branchDOMElement) {
            widthOfTheSource = Math.max(widthOfTheSource, branchDOMElement.getBoundingClientRect().width);
        }*/
    }
    if (!sourceNode) {
        // @ts-ignore
        sourceNode = {
            position: {
                x: 0,
                y: 0
            }
        }
    }

    let y: number = sourceNode!.position.y ||0 /*40*/

    if (options?.y === 'middle') {
        const sourceHeight = sourceDOMElementMeasures?.height || 0;
        let targetId: string = targetNode.getId();

        if (typeof targetNode.getCenteredId === 'function') {
            const possibleCenteredId = targetNode.getCenteredId() as CenteredId | undefined;
            if (possibleCenteredId) {
                targetId = possibleCenteredId.native ? `#${possibleCenteredId.id}` : possibleCenteredId.id
            }
        }
        const targetHeight = getDimensions(targetId).height || 0;
        y = sourceNode!.position.y - ((targetHeight - sourceHeight) / 2)
    }

    const spacing = targetNode.leftSpacing?.() || 0;
    const positionX = (targetNode as RendererNodeData).horizontalSourceX?.(getNode) ?? sourceNode!.position.x

    return {
        // x position is the x of the source node plus the width of the source node plus 100
        x: (widthOfTheSourceIncludingPosition ? widthOfTheSourceIncludingPosition : positionX + (widthOfTheSourceIncludingX || 0)) + spacing,
        // x position is half the width of the source node
        //   x: sourceNode!.position.x + (sourceDOMElement?.getBoundingClientRect().width || 0) / 2 - (targetDOMElement?.getBoundingClientRect().width || 0) / 2,
        // y position 100px below the source node, considering its height
        y
    };
}

function findLargestEndXOfATreeNode(treeNode: TreeNode | undefined, getNode: GeneralHelpers['getNode']): number {
    if (!treeNode) {
        return 0;
    }

    const widthsWithX: number[] = []

    // first get the width and x of the root testable
    widthsWithX.push(
        getEndX(treeNode.testableNode.getId()!, getNode)
    )

    if (treeNode.branchNode.isSimple()) {
        // if the branch is simple, then we just need to get the end x of the branch
        widthsWithX.push(
            getEndX(treeNode.branchNode.getId(), getNode)
        )
    } else {
        const findEndXOfTiers = (treeNode: TreeNode) => {
            treeNode.branchNode.getChildren<any, TreeNode>(({id}) => treeNode.grove.get<TreeNode>(id)).forEach(tier => {
                const branch = tier.branchNode

                if (branch.isSimple()) {
                    widthsWithX.push(
                        getEndX(branch.getId(), getNode)
                    )
                } else {
                    findEndXOfTiers(tier)
                }
            })
        }
        /**
         * It's a tiered branch, so let's get the end x of all the branches recursively
         */
        findEndXOfTiers(treeNode)
    }

    return Math.max(...widthsWithX);
}

function getEndX(renderedNodeId: string, getNode: GeneralHelpers['getNode']) {
    const node = getNode(renderedNodeId) as RenderedNode
    const nodeWidth = getDimensions(renderedNodeId).width || 0;
    const nodeX = node?.position?.x > 0? node.position.x : 0;

    return nodeX + nodeWidth;
}

function calculateNodePositionBottomRight(targetNode: RenderableNode & MiddleRenderedNodeData & RendererNodeData, sourceNode: RenderedNode | undefined, getNode: GeneralHelpers['getNode'], options?: CalculateVerticalPositionOptions): RenderedNode['position'] {
    const {y} = calculateNodePositionVertically(targetNode, sourceNode!, getNode, options);

    // first the vertical position
    const xCenterFromSource = (getDimensions(targetNode.getSourceRenderNodeId())?.width / 2) || 0;
    let xFromSource = xCenterFromSource + (targetNode.leftSpacing?.() || 0)

    const leftSpacing = targetNode.leftSpacing?.() || 0
    const sourceNodePosition = getNode(targetNode.getSourceRenderNodeId())?.position || {x: 0, y: 0}

    return {
        x: (sourceNodePosition?.x || 0) + xFromSource + leftSpacing,
        y
    };
}

function findRootNode() {
    return node => node.data?.renderedNodeType === 'root';
}

function isFirstNodeOfABranch(changedNode: NodeDimensionChange | NodePositionChange | NodeSelectionChange | NodeRemoveChange | NodeAddChange<NodeBase<Record<string, unknown>, string> & {
    style?: React.CSSProperties;
    className?: React.CSSProperties;
    resizing?: boolean;
    focusable?: boolean
}> | NodeReplaceChange<NodeBase<Record<string, unknown>, string> & {
    style?: React.CSSProperties;
    className?: React.CSSProperties;
    resizing?: boolean;
    focusable?: boolean
}>) {
    return changedNode.id.endsWith('->1');
}

let canUpdate: boolean = true;

let savedGrove = JSON.stringify(CouponsPlus.state)
let source: string | undefined = savedGrove;/* undefined;*/ '{"version":1,"grove":[{"testable":{"type":"AND","mode":"filter","children":[{"type":"context","mode":"filter","children":[{"parentType":"filters","type":"NumberOfItems","options":{"type":"limit","quantity":{"type":"equals","amount":1,"range":{"minimum":1,"maxmimum":2}}},"testableType":"filter"}]}]},"branch":{"type":"offers","children":[{"parentType":"offers","type":"BuyXGetX","options":{"quantity":1,"discount":{"type":"free","amount":0}}}]}}]}' /*'{"version":1,"grove":[{"testable":{"type":"AND","mode":"filter","children":[{"type":"context","mode":"filter","children":[{"parentType":"filters","type":"NumberOfItems","options":{"type":"limit","quantity":{"type":"equals","amount":1,"range":{"minimum":1,"maxmimum":2}}},"testableType":"filter"}]}]},"branch":{"type":"offers","children":[{"parentType":"offers","type":"BuyXGetX","options":{"quantity":1,"discount":{"type":"free","amount":0}}}]}},{"testable":{"type":"OR","mode":"filter","children":[{"type":"context","mode":"filter","children":[{"parentType":"filters","type":"NumberOfItems","options":{"type":"limit","quantity":{"type":"equals","amount":1,"range":{"minimum":1,"maxmimum":2}}},"testableType":"filter"}]},{"type":"context","mode":"filter","children":[{"parentType":"filters","type":"AnyProducts","options":{},"testableType":"filter"}]}]},"branch":{"type":"offers","children":[{"parentType":"offers","type":"BuyXGetX","options":{"quantity":1,"discount":{"type":"free","amount":0}}}]}},{"testable":{"type":"AND","mode":"filter","children":[{"type":"context","mode":"filter","children":[{"parentType":"filters","type":"Products","options":{"mode":"classic","ids":[],"inclusionType":"allowed"},"testableType":"filter"},{"parentType":"filters","type":"NumberOfItems","options":{"type":"limit","quantity":{"type":"equals","amount":1,"range":{"minimum":1,"maxmimum":2}}},"testableType":"filter"}]}]},"branch":{"type":"offers","children":[{"parentType":"offers","type":"BuyXGetY","options":{"mode":"products","data":[],"whenNotInCart":{"actions":[],"notification":{"message":"You\'ve unlocked an exclusive offer!","button":{"text":"Select Reward","url":{"type":"smart","value":""}}}}}}]}}]}'*/

/*JSON.stringify([{
    testable: wrapTestablesInDefaultGroup([
        {
            type: 'AnyProducts',
            testableType: 'filter',
            options: {} as AnyProductsOptions
        },
    ]),
    branch: {
        type: 'select-action',
        // children ignored bc of type = select-action
        children: [
            {
                id: 'iyut',
                parentType: 'offers',
                type: 'BuyXGetX',
                options: {
                    quantity: 1,
                    discount: {
                        amount: 10,
                        type: 'free'
                    }
                } as BuyXGetXOptions
            },
            /!*
                        {
                            id: 'hjgjfyjf',
                            parentType: 'offers',
                            type: 'BuyXGetY',
                            options: {
                                mode: 'advanced',
                                data: {
                                    source: [

                                    ],

                                }
                            },
                        } as(OfferData<BuyXGetYOptions<AdvancedData>> & ParentType)
            *!/
        ]
    } as Omit<NestedBranchData, 'id'>
}])*/;
/*
JSON.stringify([{
    testable: wrapTestablesInDefaultGroup([{
        type: 'Products',
        testableType: 'filter',
        options: {
            inclusionType: 'allowed',
            ids: []
        } as ProductsOptions
    }]),
    branch: {
        type: 'offers',
        children: []
    } as Omit<NestedBranchData, 'id'>
}]);
*/
/*JSON.stringify([
    {
        testable: wrapTestablesInDefaultGroup([{
            type: 'Products',
            testableType: 'filter',
            options: {}
        }]),
        branch: {
            type: 'tiered',
            data: {
                baseTestable: {
                    type: 'UserRole',
                    testableType: 'condition',
                    options: {}
                },
                tierType: TierType.Fixed
            } as NestedTieredData,
            children: [
                {
                    testable: {
                        options: {
                            roles: ['admin']
                        } as UserRoleOptions
                    } as Omit<TestablePartial<UserRoleOptions>, 'id'>,
                    branch: {
                        type: 'select-action',
                        children: []
                    }
                },
            ]
        } as Omit<NestedBranchData, 'id'>
    } as Omit<NestedTreeNodeData, 'id'>,
    {
        testable: wrapTestablesInDefaultGroup([
            {
                type: 'InCategories',
                testableType: 'filter',
                options: {
                    categories: ['jackets']
                }
            } as Omit<TestableData, 'id'>
        ]),
        branch: {
            type: 'tiered',
            data: {
                baseTestable: {
                    type: QuantityMeta.id,
                    testableType: 'filter',
                    options: {}
                },
                tierType: TierType.Numeric
            } as NestedTieredData,
            children: [
                {
                    testable: {
                        options: {
                            quantity: equals(2)
                        }
                    } as Omit<TestablePartial<AmountValidatorOptions>, 'id'>,
                    branch: {
                        type: 'select-action',
                        children: []
                    }
                },
                {
                    testable: {
                        options: {
                            quantity: equals(4)
                        }
                    } as Omit<TestablePartial<AmountValidatorOptions>, 'id'>,
                    branch: {
                        type: 'offers',
                        children: [
                            {
                                type: 'Discount',
                                options: {
                                    type: 'amount',
                                    amount: 20
                                }
                            } as OfferData
                        ]
                    }
                },
                {
                    testable: {
                        options: {
                            quantity: equals(6)
                        }
                    } as Omit<TestablePartial<AmountValidatorOptions>, 'id'>,
                    branch: {
                        type: 'tiered',
                        data: {
                            baseTestable: {
                                type: QuantityMeta.id,
                                testableType: 'filter',
                                options: {}
                            },
                            tierType: TierType.Numeric
                        } as NestedTieredData,
                        children: [
                            {
                                testable: {
                                    options: {
                                        quantity: equals(8)
                                    }
                                } as Omit<TestablePartial<AmountValidatorOptions>, 'id'>,
                                branch: {
                                    type: 'select-action',
                                    children: []
                                }
                            },
                            {
                                testable: {
                                    options: {
                                        quantity: equals(9)
                                    }
                                } as Omit<TestablePartial<AmountValidatorOptions>, 'id'>,
                                branch: {
                                    type: 'offers',
                                    children: [
                                        {
                                            type: 'Discount',
                                            options: {
                                                type: 'amount',
                                                amount: 20
                                            }
                                        } as OfferData,
                                        {
                                            type: 'Discount',
                                            options: {
                                                type: 'amount',
                                                amount: 20
                                            }
                                        } as OfferData,
                                        {
                                            type: 'Discount',
                                            options: {
                                                type: 'amount',
                                                amount: 20
                                            }
                                        } as OfferData,
                                        {
                                            type: 'Discount',
                                            options: {
                                                type: 'amount',
                                                amount: 20
                                            }
                                        } as OfferData,
                                    ]
                                }
                            },
                            {
                                testable: {
                                    options: {
                                        quantity: equals(10)
                                    }
                                } as Omit<TestablePartial<AmountValidatorOptions>, 'id'>,
                                branch: {
                                    type: 'tiered',
                                    data: {
                                        baseTestable: {
                                            type: QuantityMeta.id,
                                            testableType: 'filter',
                                            options: {}
                                        },
                                        tierType: TierType.Numeric
                                    } as NestedTieredData,
                                    children: [
                                        {
                                            testable: {
                                                options: {
                                                    quantity: equals(12)
                                                }
                                            } as Omit<TestablePartial<AmountValidatorOptions>, 'id'>,
                                            branch: {
                                                type: 'select-action',
                                                children: []
                                            }
                                        },
                                        {
                                            testable: {
                                                options: {
                                                    quantity: equals(14)
                                                }
                                            } as Omit<TestablePartial<AmountValidatorOptions>, 'id'>,
                                            branch: {
                                                type: 'offers',
                                                children: [
                                                    {
                                                        type: 'Discount',
                                                        options: {
                                                            type: 'amount',
                                                            amount: 20
                                                        }
                                                    } as OfferData
                                                ]
                                            }
                                        }
                                    ]
                                }
                            },
                            {
                                testable: {
                                    options: {
                                        quantity: range(22, 24)
                                    }
                                } as Omit<TestablePartial<AmountValidatorOptions>, 'id'>,
                                branch: {
                                    type: 'select-action',
                                    children: []
                                }
                            },
                        ]
                    }
                },
            ] as (Omit<NestedTestableNodeData, 'id'>)[]
        } as Omit<NestedBranchData, 'id'>
    } as Omit<NestedTreeNodeData, 'id'>,
    {
        testable: wrapTestablesInDefaultGroup([{
            type: 'Products',
            testableType: 'filter',
            options: {}
        }]),
        branch: {
            type: 'offers',
            children: [
                {
                    type: 'Discount',
                    options: {
                        type: 'amount',
                        amount: 20
                    }
                } as OfferData,
                {
                    type: 'Discount',
                    options: {
                        type: 'amount',
                        amount: 20
                    }
                } as OfferData,
                {
                    type: 'Discount',
                    options: {
                        type: 'amount',
                        amount: 20
                    }
                } as OfferData,
                {
                    type: 'Discount',
                    options: {
                        type: 'amount',
                        amount: 20
                    }
                }
            ]
        } as Omit<NestedBranchData, 'id'>
    } as Omit<NestedTreeNodeData, 'id'>,
    {
        testable: wrapTestablesInDefaultGroup([{
            type: 'InCategories',
            testableType: 'filter',
            options: {}
        }]),
        branch: {
            type: 'select-action',
            children: []
        } as Omit<NestedBranchData, 'id'>
    } as Omit<NestedTreeNodeData, 'id'>,
])
*/
const ProductFiltersFlow = () => {
    const renderedNodes = useNodesStore((state => state.renderedNodes))
    const edges = useNodesStore((state => state.edges))
    const rootTreeCount = useNodesStore((state => state.grove.length))
    const setRenderedNodes = useNodesStore(state => state.setRenderedNodes)
    const {getNode, getNodes, updateNode} = useReactFlow();
    const editorContainerRef = useRef<HTMLDivElement>(null)
    const [reactFlowInstance, setReactFlowInstance] = useAtom(mainReactFlowInstanceAtom)
    const reactFlow = useReactFlow()
    const emitter = useAtomValue(treeEmitterAtom)
    const importFromSource = useNodesStore(store => store.importFromSource)

    useLayoutEffect(() => {
        if (!reactFlowInstance) {
            setReactFlowInstance(reactFlow)
        }
        setTimeout(() => {
            /*this may get called twice so let's make sure we only importin it when its empty*/
            if (source && renderedNodes.length === 0) {
                importFromSource(source)
                source = ''; // make sure we only import once
            }
        }, 150)
        /*
        if (source && renderedNodes.length === 0) {
            const state = useNodesStore.getState();
            const groveActions = new GroveActions(state)
            const treeNodeFactory = new TreeNodeFactory(
                new Testables(state),
                new Offers(state)
            )

            const treeNodes = treeNodeFactory.createFromNestedDataJson(source)

            treeNodes?.forEach?.((treeNode) => {
                groveActions.addTreeNode(treeNode)
            })
        }
*/
    }, [])

    // THIS IS THE KEY PART
    useEffect(() => {
        // This effect runs every time 'nodes' changes.

        // Set a timer. If nodes change again before it fires (e.g., from the layout engine),
        // the cleanup function below will cancel this timer.
        const timer = setTimeout(() => {
            console.log('✅ Nodes have stabilized. Calling fitView.');
            setTimeout(() => {
                // wait for painting to be done...
                emitter.emit('renderedNodes:stabilized', reactFlowInstance!);
            }, 0);
        }, 150); // A 150ms quiet period is usually enough to detect stabilization.

        // This cleanup function is CRITICAL. It runs before the next effect or on unmount.
        return () => {
            clearTimeout(timer);
        };
    }, [renderedNodes]); // The effect is dependent on the nodes state.

    const reactFlowInstanceRef = useRef(reactFlowInstance);
    reactFlowInstanceRef.current = reactFlowInstance;

    const handleInit = useCallback((newReactFlowInstance) => {
        // @ts-ignore
        if (newReactFlowInstance === reactFlowInstanceRef.current) {
            return
        }
        // @ts-ignore
        setReactFlowInstance(newReactFlowInstance)
    }, [setReactFlowInstance]);

    const handleNodeClick = useCallback(() => {
        console.log('clicked node')
    }, []);

    const reactFlowStyle = useMemo(() => ({
        background: 'rgba(255, 255, 255, 0)',
    }), []);

    const handleNodesChange = useCallback((changedNodes: NodeChange<RenderedNode>[]) => {
                    // Skip layout when composite overlay is open — the inline node is frozen
                    if (atomsStore.get(OverlayOpenCompositeAtom) !== null) {
                        return;
                    }

                    const dimensionChanges = changedNodes.filter(c => c.type === 'dimensions');
                    const hasStructuralChanges = changedNodes.some(c => c.type === 'remove' || c.type === 'add' || c.type === 'replace')

                    if (dimensionChanges.length === 0 && !hasStructuralChanges) {
                        // Non-layout changes (selection, etc.) don't affect layout — skip entirely
                        return;
                    }

                    // Store ONLY dimension changes for O(1) getDimensions() lookups
                    changedDimensionsMap = new Map()
                    for (const c of dimensionChanges) {
                        if (c.type === 'dimensions' && c.dimensions) {
                            changedDimensionsMap.set(c.id, c.dimensions)
                        }
                    }
                    const renderedNodes = useNodesStore.getState().renderedNodes
                    const nodes = (hasStructuralChanges || dimensionChanges.length === renderedNodes.length) ? getNodes() : renderedNodes
                    // if this gets updated bellow the REFERENCES should be different, if the references are the same don't update the state
                    let updatedNodes = [...nodes]
                    let updateNodes = false
                    const grove = getGroveForReading()

                    // O(1) index lookups instead of O(n) findIndex in the inner loop
                    const nodeIndexMap = new Map<string, number>()
                    for (let i = 0; i < nodes.length; i++) {
                        nodeIndexMap.set(nodes[i].id, i)
                    }

                    // Feed-forward: as we calculate positions, store them so subsequent
                    // nodes see fresh positions instead of stale React Flow state.
                    // This achieves single-pass convergence (no multi-cycle oscillation).
                    positionOverrides = new Map()
                    const getNodeFeedForward: typeof getNode = (id) => {
                        const node = getNode(id)
                        const pos = positionOverrides.get(id as string)
                        if (node && pos) {
                            return { ...node, position: pos } as typeof node
                        }
                        return node
                    }

                    const nodesChangedByUs: RenderedNode[] = []

                    for (let i = 0; i < nodes.length; i++) {
                        const changedNode: RenderedNodeData = nodes[i];
                        //const changedNodeNode = grove.get(changedNode.id)
                        let direction: Direction = 'vertical';
                        let options: CalculatePositionOptions | {} = {}
                        let targetRenderNode: FirstRenderedNodeData | LastRenderedNodeData | MiddleRenderedNodeData | undefined

                        targetRenderNode = grove.get(changedNode.id)

                        switch (changedNode.type as keyof typeof nodeTypes) {
                            case 'tree.root.anchor':
                                if (grove.isFirst((targetRenderNode as TreeNode))) {
                                    direction = 'vertical'
                                } else {
                                    direction = 'horizontal'
                                }
                                targetRenderNode = (targetRenderNode as TreeNode).getSourceRenderNode() as typeof targetRenderNode
                                break;
                            case 'testable.group':
                                // its always vertical in relation to its anchor
                                direction = 'vertical'
/*
                                if (grove.isFirst((targetRenderNode as TestableCompositeNode).getTreeNode().getId())) {
                                    direction = 'vertical'
                                } else {
                                    direction = 'horizontal'
                                }
*/
                                break;
                            case 'branch.tiered.testable.base':
                            case 'branch.tiered.tier.testable':
                                direction = 'horizontal';
                                options = {
                                    y: 'middle',
                                }
                                break;
                            case 'branch.tiered.tier.step-anchor':
                                const isFirst = (targetRenderNode as TreeNode).isFirstTier();
                                if (isFirst) {
                                    direction = 'horizontal';
                                    options = {
                                        y: 'middle',
                                    }
                                } else {
                                    direction = 'vertical';
                                }
                                break;
                            case 'branch.select-action':
                            case 'branch.select-action-split':
                            case 'branch.offers':
                                if ((targetRenderNode as StateBranchNode).isInsideAnotherBranch()) {
                                    direction = 'horizontal'
                                    options = {
                                        y: 'middle'
                                    }
                                } else {
                                    direction = 'vertical'
                                }
                                break
                            case 'branch.tiered':
                                if ((targetRenderNode as StateBranchNode).getTreeNode().isRoot()) {
                                    direction = 'bottom-right'
                                } else {
                                    // this should actually b e bottom right but we're just refactoring so by the end of the refactoring hopefully this will be fixed
                                    direction = 'vertical'
                                }
                                break
                            case 'branch.tiered.add-tier-button':
                                direction = 'vertical'
                                options = {
                                    x: 'start',
                                }
                                const treeNode = grove.get<TreeNode>(changedNode.data.treeNodeId) as TreeNode
                                targetRenderNode = treeNode.branchNode.getAddTierButton()
                                break
                            default:
                                targetRenderNode = undefined
                        }

                        if (targetRenderNode) {
                            const newPosition = calculateNodePosition(
                                //changedNode as RenderedNode,
                                targetRenderNode,
                                /*sourceNode as RenderedNode | undefined*/undefined,
                                direction,
                                getNodeFeedForward,
                                // @ts-ignore
                                options
                            );

                            if (newPosition && !isEqual(changedNode.position, newPosition)) {
//                                    if ((newPosition.x < 1 || newPosition.y < 1) === false) {
                                //if ((newPosition.y < 1) === false) {
                                if ((newPosition.y >= 0)) {
                                    // by creating a new array reference here we tell our code below that this has changed and needs to be updated
                                    //updatedNodes = [...updatedNodes]
                                    updateNodes = true
                                    // Feed this position forward so later nodes see it
                                    positionOverrides.set(changedNode.id, newPosition)
                                    const index = nodeIndexMap.get(changedNode.id)!;
                                    const updatedNode = {
                                        ...changedNode,
                                        position: newPosition
                                    };

                                    updatedNodes[index] = updatedNode
                                    nodesChangedByUs.push(updatedNode);
                                }
                            }
                        }

                    }

                    if (updateNodes/*nodes !== updatedNodes*/) {
                        setRenderedNodes([...updatedNodes]);
                    }
    }, [getNodes, getNode, setRenderedNodes]);

    const previousRootTreeCountRef = useRef(rootTreeCount)
    useEffect(() => {
        const previousRootTreeCount = previousRootTreeCountRef.current
        previousRootTreeCountRef.current = rootTreeCount

        if (rootTreeCount >= previousRootTreeCount) {
            return
        }

        const frameId = requestAnimationFrame(() => {
            handleNodesChange([{id: '__root-tree-removed__', type: 'remove'} as NodeRemoveChange<RenderedNode>])
        })

        return () => {
            cancelAnimationFrame(frameId)
        }
    }, [rootTreeCount, handleNodesChange])

    return (
        <div ref={editorContainerRef} className="relative w-full h-full">
            {(renderedNodes.length === 0) ?
                <Welcome/> : undefined}
            <ReactFlow
                nodes={renderedNodes}
                edges={edges}
                onInit={handleInit}
                fitView={false}
                className="bg-gray-50"
                style={reactFlowStyle}
                nodeTypes={nodeTypes}
                edgeTypes={edgeTypes}
                onNodeClick={handleNodeClick}
                onNodesChange={handleNodesChange}

                nodesDraggable={false}
                elementsSelectable={false} // Optional: prevents the blue selection outline

                // 2. Fix the Mac Trackpad / Scroll Wheel behavior
                panOnScroll={true}
                panOnScrollSpeed={2.2}     // Fixes the "heavy" feeling

                // 3. Keep the super-smooth Pointer Drag behavior for mouse users
                panOnDrag={true}           // Lets you left-click-drag to move the canvas
                selectionOnDrag={false}
            >
                <Background variant={BackgroundVariant.Cross} gap={28} size={10} style={{opacity: 0.8}}/>
                <Controls/>
            </ReactFlow>
        </div>
    );
};

export default ProductFiltersFlow;

export const Tree = () => {
    return <div className="w-full h-full bg-[#fefefe] rounded-t-1 overflow-hidden">
        <ReactFlowProvider>
            <ProductFiltersFlow/>
        </ReactFlowProvider>
    </div>
}
