import Foundation

/// Pure gapped-fractional-ordering math: append, insert, midpoint, and the
/// renumber target, plus the display-order tie-break rule shared by the
/// loader and the writer. No filesystem or model dependency — see
/// DESIGN/01-storage-format.md § Ordering, Deletion.
enum Ranks: Sendable {

    /// Gap between successive ranks on append/head-insert, and the multiple
    /// used by `renumbered(count:)`.
    private static let gap: Double = 1024

    // MARK: - Append / insert

    /// Rank for a new item appended after all visible siblings.
    /// An empty lane's first item lands at `1024` (the board convention).
    static func append(toVisible orders: some Sequence<Double>) -> Double {
        (orders.max() ?? 0) + gap
    }

    /// Rank for a new item inserted before all visible siblings.
    /// An empty lane's first item lands at `1024` (the board convention).
    static func insertAtHead(ofVisible orders: some Sequence<Double>) -> Double {
        guard let minOrder = orders.min() else { return gap }
        return minOrder - gap
    }

    /// Rank strictly between `a` and `b`, or `nil` if no `Double` is
    /// representable between them — including when `a == b` (a duplicate
    /// order, the tie case). Never returns a value ≤ min(a, b) or
    /// ≥ max(a, b).
    static func midpoint(between a: Double, and b: Double) -> Double? {
        let lower = min(a, b)
        let upper = max(a, b)
        guard lower < upper else { return nil }
        let mid = lower + (upper - lower) / 2
        guard mid > lower, mid < upper else { return nil }
        return mid
    }

    /// The rank an item takes when it lands at display position `index` among `orders` — the
    /// visible siblings it is joining, **in display order and with the item itself already
    /// excluded**.
    ///
    /// One function for the three cases every insertion gesture has (a lane drag's release, a
    /// drop between two cards, ⌘N's after-the-anchor position), so no call site re-derives which
    /// of `insertAtHead`/`midpoint`/`append` its position calls for:
    ///
    /// - at or before the head → `insertAtHead(ofVisible:)`;
    /// - at or past the end (an empty `orders` included) → `append(toVisible:)`;
    /// - between two siblings → their `midpoint`.
    ///
    /// **`nil` means the gap is exhausted, not that the insertion is illegal**: `midpoint` returns
    /// no value when two neighbours are adjacent `Double`s or share an order (the duplicate-order
    /// tie). That is the renumber trigger (01-storage-format.md § Ordering) and the caller's cue to
    /// compact and ask again — never something to paper over with an arbitrary rank, which would
    /// silently reorder the board.
    static func insertionRank(amongVisible orders: [Double], at index: Int) -> Double? {
        if orders.isEmpty || index >= orders.count { return append(toVisible: orders) }
        if index <= 0 { return insertAtHead(ofVisible: orders) }
        return midpoint(between: orders[index - 1], and: orders[index])
    }

    /// The `count` ranks a **contiguous run** takes when it lands at display position `index`
    /// among `orders` — `insertionRank(amongVisible:at:)` for a multi-drag, whose whole set inserts
    /// at one spot in preserved order (04-interactions.md ▸ Drag and drop, DRAG-REORDER.md §
    /// Multi-drag).
    ///
    /// `orders` is the visible siblings **in display order with the run itself already excluded** —
    /// the resting layout's convention, the same one the geometry's index is counted in.
    ///
    /// The three cases mirror the single-rank twin, spread over `count` values:
    ///
    /// - at or before the head → `count` whole gaps *below* the first sibling, ascending;
    /// - at or past the end (an empty `orders` included) → `count` whole gaps above the last;
    /// - between two siblings → `count` evenly spaced points strictly inside their interval.
    ///
    /// **`nil` means the gap is exhausted, not that the insertion is illegal** — the interior case
    /// fails when the two neighbours are close enough that `count` distinct, strictly increasing
    /// `Double`s do not fit between them (adjacent doubles, or the duplicate-order tie). That is
    /// the renumber trigger (01-storage-format.md § Ordering) and the caller's cue to compact and
    /// ask again, exactly as an exhausted midpoint is everywhere else.
    static func insertionRanks(amongVisible orders: [Double], at index: Int, count: Int) -> [Double]? {
        guard count > 0 else { return [] }
        if orders.isEmpty || index >= orders.count {
            let base = orders.max() ?? 0
            return (1...count).map { base + gap * Double($0) }
        }
        if index <= 0 {
            let base = orders.min() ?? 0
            // Ascending, and every value below `base`: the deepest is `count` gaps down.
            return (1...count).map { base - gap * Double(count - $0 + 1) }
        }

        let lower = orders[index - 1]
        let upper = orders[index]
        guard lower < upper else { return nil }
        let step = (upper - lower) / Double(count + 1)
        var ranks: [Double] = []
        var previous = lower
        for position in 1...count {
            let rank = lower + step * Double(position)
            // Every rank must sit strictly inside the interval *and* strictly above the last one:
            // at the precision floor the arithmetic silently collapses onto a neighbour, and a
            // duplicate rank would hand display order to the folder-name tie-break.
            guard rank > previous, rank < upper else { return nil }
            ranks.append(rank)
            previous = rank
        }
        return ranks
    }

    /// `count` fresh ranks, whole multiples of 1024 in ascending order
    /// (1024, 2048, …) — the renumber target when midpoint precision is
    /// exhausted. Deterministic by construction; the writer applies these,
    /// in order, to the current visible siblings in display order.
    static func renumbered(count: Int) -> [Double] {
        guard count > 0 else { return [] }
        return (1...count).map { Double($0) * gap }
    }

    // MARK: - Display order

    /// Ascending display order: primary key `order`, ties broken by folder
    /// name (lexicographic) for deterministic rendering. Shared by the
    /// loader and the writer so both apply the same tie-break rule.
    static func isOrderedForDisplay<T>(
        _ lhs: T, before rhs: T,
        order: (T) -> Double, name: (T) -> String
    ) -> Bool {
        let lhsOrder = order(lhs)
        let rhsOrder = order(rhs)
        return lhsOrder != rhsOrder ? lhsOrder < rhsOrder : name(lhs) < name(rhs)
    }

    /// Sorts siblings into display order — ascending `order`, ties broken by
    /// folder name.
    static func sortedForDisplay<T>(
        _ items: [T],
        order: (T) -> Double,
        name: (T) -> String
    ) -> [T] {
        items.sorted { isOrderedForDisplay($0, before: $1, order: order, name: name) }
    }

    // MARK: - Tombstone exclusion

    /// `append(toVisible:)`, ignoring tombstoned siblings. Tombstones are
    /// inert to ordering — appends operate on visible siblings only.
    static func append(toVisible items: some Sequence<(order: Double, isDeleted: Bool)>) -> Double {
        append(toVisible: items.filter { !$0.isDeleted }.map { $0.order })
    }

    /// `insertAtHead(ofVisible:)`, ignoring tombstoned siblings. Tombstones
    /// are inert to ordering — inserts operate on visible siblings only.
    static func insertAtHead(ofVisible items: some Sequence<(order: Double, isDeleted: Bool)>) -> Double {
        insertAtHead(ofVisible: items.filter { !$0.isDeleted }.map { $0.order })
    }
}
