import AppKit
import Combine

/// SplitTree represents a tree of views that can be divided.
struct SplitTree<ViewType: NSView & Codable & Identifiable> {
    /// The root of the tree. This can be nil to indicate the tree is empty.
    let root: Node?

    /// The node that is currently zoomed. A zoomed split is expected to take up the full
    /// size of the view area where the splits are shown.
    let zoomed: Node?

    /// A single node in the tree is either a leaf node (a view) or a split (has a
    /// left/right or top/bottom).
    indirect enum Node: Codable {
        case leaf(view: ViewType)
        case split(Split)

        struct Split: Equatable, Codable {
            let direction: Direction
            let ratio: Double
            let left: Node
            let right: Node
        }
    }

    enum Direction: Codable {
        case horizontal // Splits are laid out left and right
        case vertical // Splits are laid out top and bottom
    }

    /// The path to a specific node in the tree.
    struct Path: Codable {
        let path: [Component]

        var isEmpty: Bool { path.isEmpty }

        enum Component: Codable {
            case left
            case right
        }
    }

    /// Spatial representation of the split tree. This can be used to better understand
    /// its physical representation to perform tasks such as navigation.
    struct Spatial {
        let slots: [Slot]

        /// A single slot within the spatial mapping of a tree. Note that the bounds are
        /// _relative_. They can't be mapped to physical pixels because the SplitTree
        /// isn't aware of actual rendering. But relative to each other the bounds are
        /// correct.
        struct Slot {
            let node: Node
            let bounds: CGRect
        }

        /// Direction for spatial navigation within the split tree.
        enum Direction {
            case left
            case right
            case up
            case down
        }
    }

    enum SplitError: Error {
        case viewNotFound
    }

    enum NewDirection {
        case left
        case right
        case down
        case up
    }

    /// The direction that focus can move from a node.
    enum FocusDirection {
        // Follow a consistent tree-like structure.
        case previous
        case next

        // Spatially-aware navigation targets. These take into account the
        // layout to find the spatially correct node to move to. Spatial navigation
        // is always from the top-left corner for now.
        case spatial(Spatial.Direction)
    }
}

// MARK: SplitTree

extension SplitTree {
    var isEmpty: Bool {
        root == nil
    }

    /// Returns true if this tree is split.
    var isSplit: Bool {
        if case .split = root { true } else { false }
    }

    init() {
        self.init(root: nil, zoomed: nil)
    }

    init(view: ViewType) {
        self.init(root: .leaf(view: view), zoomed: nil)
    }

    /// Checks if the tree contains the specified node.
    ///
    /// Note that SplitTree implements Sequence on views so there's already a `contains`
    /// for views too.
    ///
    /// - Parameter node: The node to search for in the tree
    /// - Returns: True if the node exists in the tree, false otherwise
    func contains(_ node: Node) -> Bool {
        guard let root else { return false }
        return root.path(to: node) != nil
    }

    /// Insert a new view at the given view point by creating a split in the given direction.
    /// This will always reset the zoomed state of the tree.
    func inserting(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self {
        guard let root else { throw SplitError.viewNotFound }
        return .init(
            root: try root.inserting(view: view, at: at, direction: direction),
            zoomed: nil)
    }
    /// Find a node containing a view with the specified ID.
    /// - Parameter id: The ID of the view to find
    /// - Returns: The node containing the view if found, nil otherwise
    func find(id: ViewType.ID) -> Node? {
        guard let root else { return nil }
        return root.find(id: id)
    }

    /// Remove a node from the tree. If the node being removed is part of a split,
    /// the sibling node takes the place of the parent split.
    func removing(_ target: Node) -> Self {
        guard let root else { return self }

        // If we're removing the root itself, return an empty tree
        if root == target {
            return .init(root: nil, zoomed: nil)
        }

        // Otherwise, try to remove from the tree
        let newRoot = root.remove(target)

        // Update zoomed if it was the removed node
        let newZoomed = (zoomed == target) ? nil : zoomed

        return .init(root: newRoot, zoomed: newZoomed)
    }

    /// Replace a node in the tree with a new node.
    func replacing(node: Node, with newNode: Node) throws -> Self {
        guard let root else { throw SplitError.viewNotFound }

        // Get the path to the node we want to replace
        guard let path = root.path(to: node) else {
            throw SplitError.viewNotFound
        }

        // Replace the node
        let newRoot = try root.replacingNode(at: path, with: newNode)

        // Update zoomed if it was the replaced node
        let newZoomed = (zoomed == node) ? newNode : zoomed

        return .init(root: newRoot, zoomed: newZoomed)
    }

    /// Find the next view to focus based on the current focused node and direction
    func focusTarget(for direction: FocusDirection, from currentNode: Node) -> ViewType? {
        guard let root else { return nil }

        switch direction {
        case .previous:
            // For previous, we traverse in order and find the previous leaf from our leftmost
            let allLeaves = root.leaves()
            let currentView = currentNode.leftmostLeaf()
            guard let currentIndex = allLeaves.firstIndex(where: { $0 === currentView }) else {
                // Shouldn't be possible leftmostLeaf can't return something that doesn't exist!
                return nil
            }
            let index = allLeaves.indexWrapping(before: currentIndex)
            return allLeaves[index]

        case .next:
            // For previous, we traverse in order and find the next leaf from our rightmost
            let allLeaves = root.leaves()
            let currentView = currentNode.rightmostLeaf()
            guard let currentIndex = allLeaves.firstIndex(where: { $0 === currentView }) else {
                return nil
            }
            let index = allLeaves.indexWrapping(after: currentIndex)
            return allLeaves[index]

        case .spatial(let spatialDirection):
            // Get spatial representation and find best candidate
            let spatial = root.spatial()
            let nodes = spatial.slots(in: spatialDirection, from: currentNode)

            // If we have no nodes in the direction specified then we don't do
            // anything.
            if nodes.isEmpty {
                return nil
            }

            // Extract the view from the best candidate node. The best candidate
            // node is the closest leaf node. If we have no leaves (impossible?)
            // just use the first node.
            let bestNode = nodes.first(where: {
                if case .leaf = $0.node { return true } else { return false }
            }) ?? nodes[0]
            switch bestNode.node {
            case .leaf(let view):
                return view

            case .split:
                // If the best candidate is a split node, use its the leaf/rightmost
                // depending on our spatial direction.
                return switch spatialDirection {
                case .up, .left: bestNode.node.leftmostLeaf()
                case .down, .right: bestNode.node.rightmostLeaf()
                }
            }
        }
    }

    /// Equalize all splits in the tree so that each split's ratio is based on the
    /// relative weight (number of leaves) of its children.
    func equalized() -> Self {
        guard let root else { return self }
        let newRoot = root.equalize()
        return .init(root: newRoot, zoomed: zoomed)
    }

    /// Resize a node in the tree by the given pixel amount in the specified direction.
    ///
    /// This method adjusts the split ratios of the tree to accommodate the requested resize
    /// operation. For up/down resizing, it finds the nearest parent vertical split and adjusts
    /// its ratio. For left/right resizing, it finds the nearest parent horizontal split.
    /// The bounds parameter is used to construct the spatial tree representation which is
    /// needed to calculate the current pixel dimensions.
    ///
    /// This will always reset the zoomed state.
    ///
    /// - Parameters:
    ///   - node: The node to resize
    ///   - by: The number of pixels to resize by
    ///   - direction: The direction to resize in (up, down, left, right)
    ///   - bounds: The bounds used to construct the spatial tree representation
    /// - Returns: A new SplitTree with the adjusted split ratios
    /// - Throws: SplitError.viewNotFound if the node is not found in the tree or no suitable parent split exists
    func resizing(node: Node, by pixels: UInt16, in direction: Spatial.Direction, with bounds: CGRect) throws -> Self {
        guard let root else { throw SplitError.viewNotFound }

        // Find the path to the target node
        guard let path = root.path(to: node) else {
            throw SplitError.viewNotFound
        }

        // Determine which type of split we need to find based on resize direction
        let targetSplitDirection: Direction = switch direction {
        case .up, .down: .vertical
        case .left, .right: .horizontal
        }

        // Find the nearest parent split of the correct type by walking up the path
        var splitPath: Path?
        var splitNode: Node?

        for i in stride(from: path.path.count - 1, through: 0, by: -1) {
            let parentPath = Path(path: Array(path.path.prefix(i)))
            if let parent = root.node(at: parentPath), case .split(let split) = parent {
                if split.direction == targetSplitDirection {
                    splitPath = parentPath
                    splitNode = parent
                    break
                }
            }
        }

        guard let splitPath = splitPath,
              let splitNode = splitNode,
              case .split(let split) = splitNode else {
            throw SplitError.viewNotFound
        }

        // Get current spatial representation to calculate pixel dimensions
        let spatial = root.spatial(within: bounds.size)
        guard let splitSlot = spatial.slots.first(where: { $0.node == splitNode }) else {
            throw SplitError.viewNotFound
        }

        // Calculate the new ratio based on pixel change
        let pixelOffset = Double(pixels)
        let newRatio: Double

        switch (split.direction, direction) {
        case (.horizontal, .left):
            // Moving left boundary: decrease left side
            newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio - (pixelOffset / splitSlot.bounds.width)))
        case (.horizontal, .right):
            // Moving right boundary: increase left side
            newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio + (pixelOffset / splitSlot.bounds.width)))
        case (.vertical, .up):
            // Moving top boundary: decrease top side
            newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio - (pixelOffset / splitSlot.bounds.height)))
        case (.vertical, .down):
            // Moving bottom boundary: increase top side
            newRatio = Swift.max(0.1, Swift.min(0.9, split.ratio + (pixelOffset / splitSlot.bounds.height)))
        default:
            // Direction doesn't match split type - shouldn't happen due to earlier logic
            throw SplitError.viewNotFound
        }

        // Create new split with adjusted ratio
        let newSplit = Node.Split(
            direction: split.direction,
            ratio: newRatio,
            left: split.left,
            right: split.right
        )

        // Replace the split node with the new one
        let newRoot = try root.replacingNode(at: splitPath, with: .split(newSplit))
        return .init(root: newRoot, zoomed: nil)
    }

    /// Returns the total bounds of the split hierarchy using NSView bounds.
    /// Ignores x/y coordinates and assumes views are laid out in a perfect grid.
    /// Also ignores any possible padding between views.
    /// - Returns: The total width and height needed to contain all views
    func viewBounds() -> CGSize {
        guard let root else { return .zero }
        return root.viewBounds()
    }
}

// MARK: SplitTree Codable

private enum CodingKeys: String, CodingKey {
    case version
    case root
    case zoomed

    static let currentVersion: Int = 1
}

extension SplitTree: Codable {
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        // Check version
        let version = try container.decode(Int.self, forKey: .version)
        guard version == CodingKeys.currentVersion else {
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: decoder.codingPath,
                    debugDescription: "Unsupported SplitTree version: \(version)"
                )
            )
        }

        // Decode root
        self.root = try container.decodeIfPresent(Node.self, forKey: .root)

        // Zoomed is encoded as its path. Get the path and then find it.
        if let zoomedPath = try container.decodeIfPresent(Path.self, forKey: .zoomed),
           let root = self.root {
            self.zoomed = root.node(at: zoomedPath)
        } else {
            self.zoomed = nil
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        // Encode version
        try container.encode(CodingKeys.currentVersion, forKey: .version)

        // Encode root
        try container.encodeIfPresent(root, forKey: .root)

        // Zoomed is encoded as its path since its a reference type. This lets us
        // map it on decode back to the correct node in root.
        if let zoomed, let path = root?.path(to: zoomed) {
            try container.encode(path, forKey: .zoomed)
        }
    }
}

// MARK: SplitTree.Node

extension SplitTree.Node {
    typealias Node = SplitTree.Node
    typealias NewDirection = SplitTree.NewDirection
    typealias SplitError = SplitTree.SplitError
    typealias Path = SplitTree.Path

    /// Find a node containing a view with the specified ID.
    /// - Parameter id: The ID of the view to find
    /// - Returns: The node containing the view if found, nil otherwise
    func find(id: ViewType.ID) -> Node? {
        switch self {
        case .leaf(let view):
            return view.id == id ? self : nil

        case .split(let split):
            if let found = split.left.find(id: id) {
                return found
            }

            return split.right.find(id: id)
        }
    }

    /// Returns the node in the tree that contains the given view.
    func node(view: ViewType) -> Node? {
        switch self {
        case .leaf(view):
            return self

        case .split(let split):
            if let result = split.left.node(view: view) {
                return result
            } else if let result = split.right.node(view: view) {
                return result
            }

            return nil

        default:
            return nil
        }
    }

    /// Returns the path to a given node in the tree. If the returned value is nil then the
    /// node doesn't exist.
    func path(to node: Self) -> Path? {
        var components: [Path.Component] = []
        func search(_ current: Self) -> Bool {
            if current == node {
                return true
            }

            switch current {
            case .leaf:
                return false

            case .split(let split):
                // Try left branch
                components.append(.left)
                if search(split.left) {
                    return true
                }
                components.removeLast()

                // Try right branch
                components.append(.right)
                if search(split.right) {
                    return true
                }
                components.removeLast()

                return false
            }
        }

        return search(self) ? Path(path: components) : nil
    }

    /// Returns the node at the given path from this node as root.
    func node(at path: Path) -> Node? {
        if path.isEmpty {
            return self
        }

        guard case .split(let split) = self else {
            return nil
        }

        let component = path.path[0]
        let remainingPath = Path(path: Array(path.path.dropFirst()))

        switch component {
        case .left:
            return split.left.node(at: remainingPath)
        case .right:
            return split.right.node(at: remainingPath)
        }
    }

    /// Inserts a new view into the split tree by creating a split at the location of an existing view.
    ///
    /// This method creates a new split node containing both the existing view and the new view,
    /// The position of the new view relative to the existing view is determined by the direction parameter.
    ///
    /// - Parameters:
    ///   - view: The new view to insert into the tree
    ///   - at: The existing view at whose location the split should be created
    ///   - direction: The direction relative to the existing view where the new view should be placed
    ///
    /// - Note: If the existing view (`at`) is not found in the tree, this method does nothing. We should
    /// maybe throw instead but at the moment we just do nothing.
    func inserting(view: ViewType, at: ViewType, direction: NewDirection) throws -> Self {
        // Get the path to our insertion point. If it doesn't exist we do
        // nothing.
        guard let path = path(to: .leaf(view: at)) else {
            throw SplitError.viewNotFound
        }

        // Determine split direction and which side the new view goes on
        let splitDirection: SplitTree.Direction
        let newViewOnLeft: Bool
        switch direction {
        case .left:
            splitDirection = .horizontal
            newViewOnLeft = true
        case .right:
            splitDirection = .horizontal
            newViewOnLeft = false
        case .up:
            splitDirection = .vertical
            newViewOnLeft = true
        case .down:
            splitDirection = .vertical
            newViewOnLeft = false
        }

        // Create the new split node
        let newNode: Node = .leaf(view: view)
        let existingNode: Node = .leaf(view: at)
        let newSplit: Node = .split(.init(
            direction: splitDirection,
            ratio: 0.5,
            left: newViewOnLeft ? newNode : existingNode,
            right: newViewOnLeft ? existingNode : newNode
        ))

        // Replace the node at the path with the new split
        return try replacingNode(at: path, with: newSplit)
    }

    /// Helper function to replace a node at the given path from the root
    func replacingNode(at path: Path, with newNode: Self) throws -> Self {
        // If path is empty, replace the root
        if path.isEmpty {
            return newNode
        }

        // Otherwise, we need to replace the proper left/right all along
        // the way since Node is a value type (enum). To do that, we need
        // recursion. We can't use a simple iterative approach because we
        // can't update in-place.
        func replaceInner(current: Node, pathOffset: Int) throws -> Node {
            // Base case: if we've consumed the entire path, replace this node
            if pathOffset >= path.path.count {
                return newNode
            }

            // We need to go deeper, so current must be a split for the path
            // to be valid. Otherwise, the path is invalid.
            guard case .split(let split) = current else {
                throw SplitError.viewNotFound
            }

            let component = path.path[pathOffset]
            switch component {
            case .left:
                return .split(.init(
                    direction: split.direction,
                    ratio: split.ratio,
                    left: try replaceInner(current: split.left, pathOffset: pathOffset + 1),
                    right: split.right
                ))
            case .right:
                return .split(.init(
                    direction: split.direction,
                    ratio: split.ratio,
                    left: split.left,
                    right: try replaceInner(current: split.right, pathOffset: pathOffset + 1)
                ))
            }
        }

        return try replaceInner(current: self, pathOffset: 0)
    }

    /// Remove a node from the tree. Returns the modified tree, or nil if removing
    /// the node results in an empty tree.
    func remove(_ target: Node) -> Node? {
        // If we're removing ourselves, return nil
        if self == target {
            return nil
        }

        switch self {
        case .leaf:
            // A leaf that isn't the target stays as is
            return self

        case .split(let split):
            // Neither child is directly the target, so we need to recursively
            // try to remove from both children
            let newLeft = split.left.remove(target)
            let newRight = split.right.remove(target)

            // If both are nil then we remove everything. This shouldn't ever
            // happen because duplicate nodes shouldn't exist, but we want to
            // be robust against it.
            if newLeft == nil && newRight == nil {
                return nil
            } else if newLeft == nil {
                return newRight
            } else if newRight == nil {
                return newLeft
            }

            // Both children still exist after removal
            return .split(.init(
                direction: split.direction,
                ratio: split.ratio,
                left: newLeft!,
                right: newRight!
            ))
        }
    }

    /// Resize a split node to the specified ratio.
    /// For leaf nodes, this returns the node unchanged.
    /// For split nodes, this creates a new split with the updated ratio.
    func resizing(to ratio: Double) -> Self {
        switch self {
        case .leaf:
            // Leaf nodes don't have a ratio to resize
            return self

        case .split(let split):
            // Create a new split with the updated ratio
            return .split(.init(
                direction: split.direction,
                ratio: ratio,
                left: split.left,
                right: split.right
            ))
        }
    }

    /// Get the leftmost leaf in this subtree
    func leftmostLeaf() -> ViewType {
        switch self {
        case .leaf(let view):
            return view
        case .split(let split):
            return split.left.leftmostLeaf()
        }
    }

    /// Get the rightmost leaf in this subtree
    func rightmostLeaf() -> ViewType {
        switch self {
        case .leaf(let view):
            return view
        case .split(let split):
            return split.right.rightmostLeaf()
        }
    }

    /// Equalize this node and all its children, returning a new node with splits
    /// adjusted so that each split's ratio is based on the relative weight
    /// (number of leaves) of its children.
    func equalize() -> Node {
        let (equalizedNode, _) = equalizeWithWeight()
        return equalizedNode
    }

    /// Internal helper that equalizes and returns both the node and its weight.
    private func equalizeWithWeight() -> (node: Node, weight: Int) {
        switch self {
        case .leaf:
            // A leaf has weight 1 and doesn't change
            return (self, 1)

        case .split(let split):
            // Calculate weights based on split direction
            let leftWeight = split.left.weightForDirection(split.direction)
            let rightWeight = split.right.weightForDirection(split.direction)

            // Calculate new ratio based on relative weights
            let totalWeight = leftWeight + rightWeight
            let newRatio = Double(leftWeight) / Double(totalWeight)

            // Recursively equalize children
            let (leftNode, _) = split.left.equalizeWithWeight()
            let (rightNode, _) = split.right.equalizeWithWeight()

            // Create new split with equalized ratio
            let newSplit = Split(
                direction: split.direction,
                ratio: newRatio,
                left: leftNode,
                right: rightNode
            )

            return (.split(newSplit), totalWeight)
        }
    }

    /// Calculate weight for equalization based on split direction.
    /// Children with the same direction contribute their full weight,
    /// children with different directions count as 1.
    private func weightForDirection(_ direction: SplitTree.Direction) -> Int {
        switch self {
        case .leaf:
            return 1
        case .split(let split):
            if split.direction == direction {
                return split.left.weightForDirection(direction) + split.right.weightForDirection(direction)
            } else {
                return 1
            }
        }
    }

    /// Calculate the bounds of all views in this subtree based on split ratios
    func calculateViewBounds(in bounds: CGRect) -> [(view: ViewType, bounds: CGRect)] {
        switch self {
        case .leaf(let view):
            return [(view, bounds)]

        case .split(let split):
            // Calculate bounds for left and right based on split direction and ratio
            let leftBounds: CGRect
            let rightBounds: CGRect

            switch split.direction {
            case .horizontal:
                // Split horizontally: left | right
                let splitX = bounds.minX + bounds.width * split.ratio
                leftBounds = CGRect(
                    x: bounds.minX,
                    y: bounds.minY,
                    width: bounds.width * split.ratio,
                    height: bounds.height
                )
                rightBounds = CGRect(
                    x: splitX,
                    y: bounds.minY,
                    width: bounds.width * (1 - split.ratio),
                    height: bounds.height
                )

            case .vertical:
                // Split vertically: top / bottom
                // Note: In our normalized coordinate system, Y increases upward
                let splitY = bounds.minY + bounds.height * split.ratio
                leftBounds = CGRect(
                    x: bounds.minX,
                    y: splitY,
                    width: bounds.width,
                    height: bounds.height * (1 - split.ratio)
                )
                rightBounds = CGRect(
                    x: bounds.minX,
                    y: bounds.minY,
                    width: bounds.width,
                    height: bounds.height * split.ratio
                )
            }

            // Recursively calculate bounds for children
            return split.left.calculateViewBounds(in: leftBounds) +
                   split.right.calculateViewBounds(in: rightBounds)
        }
    }

    /// Returns the total bounds of this subtree using NSView bounds.
    /// Ignores x/y coordinates and assumes views are laid out in a perfect grid.
    /// - Returns: The total width and height needed to contain all views in this subtree
    func viewBounds() -> CGSize {
        switch self {
        case .leaf(let view):
            return view.bounds.size

        case .split(let split):
            let leftBounds = split.left.viewBounds()
            let rightBounds = split.right.viewBounds()

            switch split.direction {
            case .horizontal:
                // Horizontal split: width is sum, height is max
                return CGSize(
                    width: leftBounds.width + rightBounds.width,
                    height: Swift.max(leftBounds.height, rightBounds.height)
                )

            case .vertical:
                // Vertical split: height is sum, width is max
                return CGSize(
                    width: Swift.max(leftBounds.width, rightBounds.width),
                    height: leftBounds.height + rightBounds.height
                )
            }
        }
    }
}

// MARK: SplitTree.Node Spatial

extension SplitTree.Node {
    /// Returns the spatial representation of this node and its subtree.
    ///
    /// This method creates a `Spatial` representation that maps the logical split tree structure
    /// to 2D coordinate space. The coordinate system uses (0,0) as the top-left corner with
    /// positive X extending right and positive Y extending down.
    ///
    /// The spatial representation provides:
    /// - Relative bounds for each node based on split ratios
    /// - Grid-like dimensions where each split adds 1 to the column/row count
    /// - Accurate positioning that reflects the actual layout structure
    ///
    /// The bounds are pixel perfect based on assuming that each row and column are 1 pixel
    /// tall or wide, respectively. This needs to be scaled up to the proper bounds for a real
    /// layout.
    ///
    /// Example:
    /// ```
    /// // For a layout like:
    /// // +--------+----+
    /// // |   A    | B  |
    /// // +--------+----+
    /// // |   C    | D  |
    /// // +--------+----+
    /// //
    /// // The spatial representation would have:
    /// // - Total dimensions: (width: 2, height: 2)
    /// // - Node bounds based on actual split ratios
    /// ```
    ///
    /// - Parameter bounds: Optional size constraints for the spatial representation. If nil, uses artificial dimensions based
    ///   on grid layout
    /// - Returns: A `Spatial` struct containing all slots with their calculated bounds
    func spatial(within bounds: CGSize? = nil) -> SplitTree.Spatial {
        // If we're not given bounds, we use artificial dimensions based on
        // the total width/height in columns/rows.
        let width: Double
        let height: Double
        if let bounds {
            width = bounds.width
            height = bounds.height
        } else {
            let (w, h) = self.dimensions()
            width = Double(w)
            height = Double(h)
        }

        // Calculate slots with relative bounds
        let slots = spatialSlots(in: CGRect(x: 0, y: 0, width: width, height: height))
        return SplitTree.Spatial(slots: slots)
    }

    /// Calculates the grid dimensions (columns and rows) needed to represent this subtree.
    ///
    /// This method recursively analyzes the split tree structure to determine how many
    /// columns and rows are needed to represent the layout in a 2D grid. Each leaf node
    /// occupies one grid cell (1×1), and each split extends the grid in one direction:
    ///
    /// - **Horizontal splits**: Add columns (increase width)
    /// - **Vertical splits**: Add rows (increase height)
    ///
    /// The calculation rules are:
    /// - **Leaf nodes**: Always (1, 1) - one column, one row
    /// - **Horizontal splits**: Width = sum of children widths, Height = max of children heights
    /// - **Vertical splits**: Width = max of children widths, Height = sum of children heights
    ///
    /// Example:
    /// ```
    /// // Single leaf: (1, 1)
    /// // Horizontal split with 2 leaves: (2, 1)
    /// // Vertical split with 2 leaves: (1, 2)
    /// // Complex layout with both: (2, 2) or larger
    /// ```
    ///
    /// - Returns: A tuple containing (width: columns, height: rows) as unsigned integers
    private func dimensions() -> (width: UInt, height: UInt) {
        switch self {
        case .leaf:
            return (1, 1)

        case .split(let split):
            let leftDimensions = split.left.dimensions()
            let rightDimensions = split.right.dimensions()

            switch split.direction {
            case .horizontal:
                // Horizontal split: width is sum, height is max
                return (
                    width: leftDimensions.width + rightDimensions.width,
                    height: Swift.max(leftDimensions.height, rightDimensions.height)
                )

            case .vertical:
                // Vertical split: height is sum, width is max
                return (
                    width: Swift.max(leftDimensions.width, rightDimensions.width),
                    height: leftDimensions.height + rightDimensions.height
                )
            }
        }
    }

    /// Calculates the spatial slots (nodes with bounds) for this subtree within the given bounds.
    ///
    /// This method recursively traverses the split tree and calculates the precise bounds
    /// for each node based on the split ratios and directions. The bounds are calculated
    /// relative to the provided bounds rectangle.
    ///
    /// The calculation process:
    /// 1. **Leaf nodes**: Create a single slot with the provided bounds
    /// 2. **Split nodes**:
    ///    - Divide the bounds according to the split ratio and direction
    ///    - Create a slot for the split node itself
    ///    - Recursively calculate slots for both children
    ///    - Return all slots combined
    ///
    /// Split ratio interpretation:
    /// - **Horizontal splits**: Ratio determines left/right width distribution
    ///   - Left child gets `ratio * width`
    ///   - Right child gets `(1 - ratio) * width`
    /// - **Vertical splits**: Ratio determines top/bottom height distribution
    ///   - Top (left) child gets `ratio * height`
    ///   - Bottom (right) child gets `(1 - ratio) * height`
    ///
    /// Coordinate system: (0,0) is top-left, positive X goes right, positive Y goes down.
    ///
    /// - Parameter bounds: The bounding rectangle to subdivide for this subtree
    /// - Returns: An array of `Spatial.Slot` objects, each containing a node and its bounds
    private func spatialSlots(in bounds: CGRect) -> [SplitTree.Spatial.Slot] {
        switch self {
        case .leaf:
            // A leaf takes up our full bounds.
            return [.init(node: self, bounds: bounds)]

        case .split(let split):
            let leftBounds: CGRect
            let rightBounds: CGRect

            switch split.direction {
            case .horizontal:
                // Split horizontally: left | right using the ratio
                let splitX = bounds.minX + bounds.width * split.ratio
                leftBounds = CGRect(
                    x: bounds.minX,
                    y: bounds.minY,
                    width: bounds.width * split.ratio,
                    height: bounds.height
                )
                rightBounds = CGRect(
                    x: splitX,
                    y: bounds.minY,
                    width: bounds.width * (1 - split.ratio),
                    height: bounds.height
                )

            case .vertical:
                // Split vertically: top / bottom using the ratio
                // Top-left is (0,0), so top (left) gets the upper portion
                let splitY = bounds.minY + bounds.height * split.ratio
                leftBounds = CGRect(
                    x: bounds.minX,
                    y: bounds.minY,
                    width: bounds.width,
                    height: bounds.height * split.ratio
                )
                rightBounds = CGRect(
                    x: bounds.minX,
                    y: splitY,
                    width: bounds.width,
                    height: bounds.height * (1 - split.ratio)
                )
            }

            // Recursively calculate slots for children and include a slot for this split
            var slots: [SplitTree.Spatial.Slot] = [.init(node: self, bounds: bounds)]
            slots += split.left.spatialSlots(in: leftBounds)
            slots += split.right.spatialSlots(in: rightBounds)

            return slots
        }
    }
}

// MARK: SplitTree.Spatial

extension SplitTree.Spatial {
    /// Returns all slots in the specified direction relative to the reference node.
    ///
    /// This method finds all slots positioned in the given direction from the reference node:
    /// - **Left**: Slots with bounds to the left of the reference node
    /// - **Right**: Slots with bounds to the right of the reference node
    /// - **Up**: Slots with bounds above the reference node (Y=0 is top)
    /// - **Down**: Slots with bounds below the reference node
    ///
    /// Results are sorted by 2D euclidean distance from the reference node, with closest slots first.
    /// Distance is calculated from the top-left corners of the bounds, prioritizing nodes that are
    /// closer in both dimensions.
    ///
    /// **Important**: The returned array contains both split nodes and leaf nodes. When using this
    /// for navigation or focus management, you typically want to filter for leaf nodes first, as they
    /// represent the actual views that can receive focus. Split nodes are included in the results
    /// because they have bounds and occupy space in the layout, but they are structural elements
    /// that cannot themselves be focused. If no leaf nodes are found in the results, you may need
    /// to traverse into a split node to find its appropriate leaf child.
    ///
    /// - Parameters:
    ///   - direction: The direction to search for slots
    ///   - referenceNode: The node to use as the reference point
    /// - Returns: An array of slots in the specified direction, sorted by 2D distance (closest first)
    func slots(in direction: Direction, from referenceNode: SplitTree.Node) -> [Slot] {
        guard let refSlot = slots.first(where: { $0.node == referenceNode }) else { return [] }

        // Helper function to calculate 2D euclidean distance between top-left corners of two rectangles
        func distance(from rect1: CGRect, to rect2: CGRect) -> Double {
            // Calculate distance between top-left corners
            let dx = rect2.minX - rect1.minX
            let dy = rect2.minY - rect1.minY
            return sqrt(dx * dx + dy * dy)
        }

        let result = switch direction {
        case .left:
            // Slots to the left: their right edge is at or left of reference's left edge
            slots.filter {
                $0.node != referenceNode && $0.bounds.maxX <= refSlot.bounds.minX
            }.sorted {
                distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds)
            }

        case .right:
            // Slots to the right: their left edge is at or right of reference's right edge
            slots.filter {
                $0.node != referenceNode && $0.bounds.minX >= refSlot.bounds.maxX
            }.sorted {
                distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds)
            }

        case .up:
            // Slots above: their bottom edge is at or above reference's top edge
            slots.filter {
                $0.node != referenceNode && $0.bounds.maxY <= refSlot.bounds.minY
            }.sorted {
                distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds)
            }

        case .down:
            // Slots below: their top edge is at or below reference's bottom edge
            slots.filter {
                $0.node != referenceNode && $0.bounds.minY >= refSlot.bounds.maxY
            }.sorted {
                distance(from: refSlot.bounds, to: $0.bounds) < distance(from: refSlot.bounds, to: $1.bounds)
            }
        }

        return result
    }

    /// Returns whether the given node borders the specified side of the spatial bounds.
    ///
    /// This method checks if a node's bounds touch the edge of the overall spatial area:
    /// - **Up**: Node's top edge touches the top of the spatial area (Y=0)
    /// - **Down**: Node's bottom edge touches the bottom of the spatial area (Y=maxY)
    /// - **Left**: Node's left edge touches the left of the spatial area (X=0)
    /// - **Right**: Node's right edge touches the right of the spatial area (X=maxX)
    ///
    /// - Parameters:
    ///   - side: The side of the spatial bounds to check
    ///   - node: The node to check if it borders the specified side
    /// - Returns: True if the node borders the specified side, false otherwise
    func doesBorder(side: Direction, from node: SplitTree.Node) -> Bool {
        // Find the slot for this node
        guard let slot = slots.first(where: { $0.node == node }) else { return false }

        // Calculate the overall bounds of all slots
        let overallBounds = slots.reduce(CGRect.null) { result, slot in
            result.union(slot.bounds)
        }

        return switch side {
        case .up:
            slot.bounds.minY == overallBounds.minY
        case .down:
            slot.bounds.maxY == overallBounds.maxY
        case .left:
            slot.bounds.minX == overallBounds.minX
        case .right:
            slot.bounds.maxX == overallBounds.maxX
        }
    }
}

// MARK: SplitTree.Node Protocols

extension SplitTree.Node: Equatable {
    static func == (lhs: Self, rhs: Self) -> Bool {
        switch (lhs, rhs) {
        case let (.leaf(leftView), .leaf(rightView)):
            // Compare NSView instances by object identity
            return leftView === rightView

        case let (.split(split1), .split(split2)):
            return split1 == split2

        default:
            return false
        }
    }
}

// MARK: SplitTree Codable

extension SplitTree.Node {
    enum CodingKeys: String, CodingKey {
        case view
        case split
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)

        if container.contains(.view) {
            let view = try container.decode(ViewType.self, forKey: .view)
            self = .leaf(view: view)
        } else if container.contains(.split) {
            let split = try container.decode(Split.self, forKey: .split)
            self = .split(split)
        } else {
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: decoder.codingPath,
                    debugDescription: "No valid node type found"
                )
            )
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)

        switch self {
        case .leaf(let view):
            try container.encode(view, forKey: .view)

        case .split(let split):
            try container.encode(split, forKey: .split)
        }
    }
}

// MARK: SplitTree Sequences

extension SplitTree.Node {
    /// Returns all leaf views in this subtree
    func leaves() -> [ViewType] {
        switch self {
        case .leaf(let view):
            return [view]

        case .split(let split):
            return split.left.leaves() + split.right.leaves()
        }
    }
}

extension SplitTree: Sequence {
    func makeIterator() -> [ViewType].Iterator {
        return root?.leaves().makeIterator() ?? [].makeIterator()
    }
}

extension SplitTree.Node: Sequence {
    func makeIterator() -> [ViewType].Iterator {
        return leaves().makeIterator()
    }
}

// MARK: SplitTree Collection

extension SplitTree: Collection {
    typealias Index = Int
    typealias Element = ViewType

    var startIndex: Int {
        return 0
    }

    var endIndex: Int {
        return root?.leaves().count ?? 0
    }

    subscript(position: Int) -> ViewType {
        precondition(position >= 0 && position < endIndex, "Index out of bounds")
        let leaves = root?.leaves() ?? []
        return leaves[position]
    }

    func index(after i: Int) -> Int {
        precondition(i < endIndex, "Cannot increment index beyond endIndex")
        return i + 1
    }
}

// MARK: SplitTree Combine

extension SplitTree {
    /// Builds a publisher that emits current values for all leaf views keyed by view ID.
    ///
    /// The returned publisher emits a full `[ViewType.ID: Value]` snapshot whenever any leaf view
    /// publishes through the provided publisher key path.
    func valuesPublisher<Value>(
        valueKeyPath: KeyPath<ViewType, Value>,
        publisherKeyPath: KeyPath<ViewType, Published<Value>.Publisher>
    ) -> AnyPublisher<[ViewType.ID: Value], Never> {
        // Flatten the split tree into a list of current leaf views.
        let views = map { $0 }
        guard !views.isEmpty else {
            // If there are no leaves, immediately publish an empty snapshot.
            // `Just([:])` keeps the return type simple and makes downstream usage easy.
            return Just([:]).eraseToAnyPublisher()
        }

        // Capture each view's current value up front.
        // We key by `ViewType.ID` so updates can replace the correct entry later.
        // This avoids waiting for all views to emit before consumers see data.
        let initial = Dictionary(uniqueKeysWithValues: views.map { view in
            (view.id, view[keyPath: valueKeyPath])
        })

        // Build one publisher per view from the requested key path.
        // Each emission is mapped into `(id, value)` so we know which entry changed.
        // `MergeMany` combines all per-view streams into a single update stream.
        let updates = Publishers.MergeMany(views.map { view in
            view[keyPath: publisherKeyPath]
                .map { (view.id, $0) }
                .eraseToAnyPublisher()
        })

        return updates
            // Accumulate updates into a full "latest value per ID" dictionary.
            // This turns incremental events into complete state snapshots.
            .scan(initial) { state, update in
                var state = state
                state[update.0] = update.1
                return state
            }
            // Emit the initial snapshot first so subscribers always get a
            // complete value dictionary immediately upon subscription.
            .prepend(initial)
            // Hide implementation details and expose a stable API type.
            .eraseToAnyPublisher()
    }
}

// MARK: Structural Identity

extension SplitTree.Node {
    /// Returns a hashable representation that captures this node's structural identity.
    var structuralIdentity: StructuralIdentity {
        StructuralIdentity(self)
    }

    /// A hashable representation of a node that captures its structural identity.
    ///
    /// This type provides a way to track changes to a node's structure in SwiftUI
    /// by implementing `Hashable` based on:
    /// - The node's hierarchical structure (splits and their directions)
    /// - The identity of view instances in leaf nodes (using object identity)
    /// - The split directions (but not ratios, as those may change slightly)
    ///
    /// This is useful for SwiftUI's `id()` modifier to detect when a node's structure
    /// has changed, triggering appropriate view updates while preserving view identity
    /// for unchanged portions of the tree.
    struct StructuralIdentity: Hashable {
        private let node: SplitTree.Node

        init(_ node: SplitTree.Node) {
            self.node = node
        }

        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.node.isStructurallyEqual(to: rhs.node)
        }

        func hash(into hasher: inout Hasher) {
            node.hashStructure(into: &hasher)
        }
    }

    /// Checks if this node is structurally equal to another node.
    /// Two nodes are structurally equal if they have the same tree structure
    /// and the same views (by identity) in the same positions.
    fileprivate func isStructurallyEqual(to other: Node) -> Bool {
        switch (self, other) {
        case let (.leaf(view1), .leaf(view2)):
            // Views must be the same instance
            return view1 === view2

        case let (.split(split1), .split(split2)):
            // Splits must have same direction and structurally equal children
            // Note: We intentionally don't compare ratios as they may change slightly
            return split1.direction == split2.direction &&
                   split1.left.isStructurallyEqual(to: split2.left) &&
                   split1.right.isStructurallyEqual(to: split2.right)

        default:
            // Different node types
            return false
        }
    }

    /// Hash keys for structural identity
    private enum HashKey: UInt8 {
        case leaf = 0
        case split = 1
    }

    /// Hashes the structural identity of this node.
    /// Includes the tree structure and view identities in the hash.
    fileprivate func hashStructure(into hasher: inout Hasher) {
        switch self {
        case .leaf(let view):
            hasher.combine(HashKey.leaf)
            hasher.combine(ObjectIdentifier(view))

        case .split(let split):
            hasher.combine(HashKey.split)
            hasher.combine(split.direction)
            // Note: We intentionally don't hash the ratio
            split.left.hashStructure(into: &hasher)
            split.right.hashStructure(into: &hasher)
        }
    }
}

extension SplitTree {
    /// Returns a hashable representation that captures this tree's structural identity.
    var structuralIdentity: StructuralIdentity {
        StructuralIdentity(self)
    }

    /// A hashable representation of a SplitTree that captures its structural identity.
    ///
    /// This type provides a way to track changes to a SplitTree's structure in SwiftUI
    /// by implementing `Hashable` based on:
    /// - The tree's hierarchical structure (splits and their directions)
    /// - The identity of view instances in leaf nodes (using object identity)
    /// - The zoomed node state (if any)
    ///
    /// This is useful for SwiftUI's `id()` modifier to detect when a tree's structure
    /// has changed, triggering appropriate view updates while preserving view identity
    /// for unchanged portions of the tree.
    ///
    /// Example usage:
    /// ```swift
    /// var body: some View {
    ///     SplitTreeView(tree: splitTree)
    ///         .id(splitTree.structuralIdentity)
    /// }
    /// ```
    struct StructuralIdentity: Hashable {
        private let root: Node?
        private let zoomed: Node?

        init(_ tree: SplitTree) {
            self.root = tree.root
            self.zoomed = tree.zoomed
        }

        static func == (lhs: Self, rhs: Self) -> Bool {
            areNodesStructurallyEqual(lhs.root, rhs.root) &&
            areNodesStructurallyEqual(lhs.zoomed, rhs.zoomed)
        }

        func hash(into hasher: inout Hasher) {
            hasher.combine(0) // Tree marker
            if let root = root {
                root.hashStructure(into: &hasher)
            }
            hasher.combine(1) // Zoomed marker
            if let zoomed = zoomed {
                zoomed.hashStructure(into: &hasher)
            }
        }

        /// Helper to compare optional nodes for structural equality
        private static func areNodesStructurallyEqual(_ lhs: Node?, _ rhs: Node?) -> Bool {
            switch (lhs, rhs) {
            case (nil, nil):
                return true
            case let (node1?, node2?):
                return node1.isStructurallyEqual(to: node2)
            default:
                return false
            }
        }
    }
}
