import SwiftUI

/// The kinds of auxiliary panel that can sit around the chat. Adding a new panel
/// type is a matter of adding a case here and a branch in `PanelHost`.
enum PanelKind: String, Codable, CaseIterable, Identifiable, Hashable, Sendable {
    case fileExplorer
    case editor
    case terminal
    case vmMonitor
    case subagents
    case nashViewer

    var id: String { rawValue }

    var title: String {
        switch self {
        case .fileExplorer: "Files"
        case .editor: "Editor"
        case .terminal: "Terminal"
        case .vmMonitor: "VM Monitor"
        case .subagents: "Subagents"
        case .nashViewer: "Nash Viewer"
        }
    }

    var systemImage: String {
        switch self {
        case .fileExplorer: "folder"
        case .editor: "square.and.pencil"
        case .terminal: "terminal"
        case .vmMonitor: "display"
        case .subagents: "person.2"
        case .nashViewer: "waveform"
        }
    }
}

/// Where a panel lives relative to the always-centered chat. Chat is never a panel;
/// it has no slot.
enum PanelSlot: String, Codable, CaseIterable, Hashable, Sendable {
    case left, right, bottom

    var label: String {
        switch self {
        case .left: "Left column"
        case .right: "Right column"
        case .bottom: "Bottom row"
        }
    }

    var systemImage: String {
        switch self {
        case .left: "sidebar.left"
        case .right: "sidebar.right"
        case .bottom: "rectangle.bottomhalf.inset.filled"
        }
    }
}

/// The kinds sharing the region (column or bottom row) a panel is rendered in, itself included.
/// Published by the paneling root so a panel can stand down a feature another pane beside it
/// already covers — the VM Monitor drops its embedded shell log when a full Nash Viewer sits in the
/// same region, since the two would otherwise show the same rows a few inches apart.
///
/// Empty outside the paneling root, which is what the PiP monitor gets: a floating window has no
/// region and therefore no siblings to defer to.
private struct PanelRegionKindsKey: EnvironmentKey {
    static let defaultValue: Set<PanelKind> = []
}

extension EnvironmentValues {
    var panelRegionKinds: Set<PanelKind> {
        get { self[PanelRegionKindsKey.self] }
        set { self[PanelRegionKindsKey.self] = newValue }
    }
}

/// A concrete placed panel. The `id` lets the same `kind` appear in more than one
/// slot and lets a drag identify *which* panel is being moved.
struct PanelInstance: Codable, Identifiable, Equatable, Hashable, Sendable {
    var id: UUID
    var kind: PanelKind
    /// This pane's share of the region it sits in, relative to its siblings — 1 for an
    /// untouched pane, so a fresh column divides evenly. Only ever meaningful against the
    /// other weights in the same slot: what a divider drag stores is the *ratio* between two
    /// neighbours, which is what lets the split survive a window resize.
    ///
    /// Carried on the instance rather than in a side table on `PanelLayout` so it travels
    /// with the pane through every rearrangement for free — a reorder, a saved layout, a
    /// working copy — with no id-keyed dictionary to keep in sync.
    var weight: CGFloat

    init(id: UUID = UUID(), kind: PanelKind, weight: CGFloat = 1) {
        self.id = id
        self.kind = kind
        self.weight = Self.sanitize(weight)
    }

    /// Decoded by hand so layouts persisted before panes had weights still load: a missing
    /// weight is an evenly-shared pane, not a decode failure that would drop the user's whole
    /// arrangement back to chat-only.
    init(from decoder: any Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        id = try container.decode(UUID.self, forKey: .id)
        kind = try container.decode(PanelKind.self, forKey: .kind)
        weight = Self.sanitize(try container.decodeIfPresent(CGFloat.self, forKey: .weight) ?? 1)
    }

    /// A zero, negative, or non-finite weight would collapse the pane to nothing (or take the
    /// whole column's normalization with it), so anything unusable reads as an even share.
    static func sanitize(_ weight: CGFloat) -> CGFloat {
        weight.isFinite && weight > 0 ? weight : 1
    }
}

/// A full arrangement of panels around the chat, plus the sizes of the three
/// regions. Used both as the live working arrangement and as a saved, named layout.
struct PanelLayout: Codable, Identifiable, Equatable, Sendable {
    var id: UUID
    var name: String
    var left: [PanelInstance]
    var right: [PanelInstance]
    var bottom: [PanelInstance]
    var leftWidth: CGFloat
    var rightWidth: CGFloat
    var bottomHeight: CGFloat

    init(
        id: UUID = UUID(), name: String = "Working",
        left: [PanelInstance] = [], right: [PanelInstance] = [], bottom: [PanelInstance] = [],
        leftWidth: CGFloat = 280, rightWidth: CGFloat = 280, bottomHeight: CGFloat = 240
    ) {
        self.id = id
        self.name = name
        self.left = left
        self.right = right
        self.bottom = bottom
        self.leftWidth = leftWidth
        self.rightWidth = rightWidth
        self.bottomHeight = bottomHeight
    }

    /// An empty arrangement — just the chat, no panels.
    static let empty = PanelLayout(name: "Just chat")

    func instances(in slot: PanelSlot) -> [PanelInstance] {
        switch slot {
        case .left: left
        case .right: right
        case .bottom: bottom
        }
    }

    /// Each pane's share of `slot`, in order, normalized to sum to 1 — an untouched column of
    /// three reads as `[⅓, ⅓, ⅓]`. This, not the raw weights, is what both the layout and the
    /// drop resolver work in, so the two always agree on where a pane's boundaries are.
    func shares(in slot: PanelSlot) -> [CGFloat] {
        let weights = instances(in: slot).map { PanelInstance.sanitize($0.weight) }
        let total = weights.reduce(0, +)
        guard total > 0 else {
            return Array(repeating: weights.isEmpty ? 0 : 1 / CGFloat(weights.count), count: weights.count)
        }
        return weights.map { $0 / total }
    }

    /// Split `available` points among `shares`, giving every pane at least `minimum`.
    ///
    /// A pane squeezed below `minimum` would lose its own header, so short panes are lifted back
    /// to the floor and the difference is taken from whichever siblings still have slack. When
    /// even the floor doesn't fit — a very short column with several panes — the region divides
    /// evenly and lets the panes clip, which at least keeps them all reachable.
    static func paneLengths(
        shares: [CGFloat], available: CGFloat, minimum: CGFloat
    ) -> [CGFloat] {
        let count = shares.count
        guard count > 0 else { return [] }
        guard available > 0 else { return Array(repeating: 0, count: count) }
        guard minimum * CGFloat(count) < available else {
            return Array(repeating: available / CGFloat(count), count: count)
        }

        var lengths = shares.map { $0 * available }
        // Lift the short panes to the floor, paying for it out of the slack the others have
        // above it. Proportional to slack, so no pass can push a sibling below the floor —
        // hence a fixed handful of passes settles it.
        for _ in 0..<count {
            let deficit = lengths.reduce(0) { $0 + max(0, minimum - $1) }
            guard deficit > 0.01 else { break }
            let slack = lengths.reduce(0) { $0 + max(0, $1 - minimum) }
            guard slack > 0 else { break }
            for i in lengths.indices where lengths[i] > minimum {
                lengths[i] -= (lengths[i] - minimum) / slack * deficit
            }
            for i in lengths.indices { lengths[i] = max(lengths[i], minimum) }
        }
        // Hand the rounding residue to the tallest pane so the stack fills the region exactly
        // rather than leaving (or overrunning by) a sub-point sliver at the end.
        let residual = available - lengths.reduce(0, +)
        if abs(residual) > 0.001, let widest = lengths.indices.max(by: { lengths[$0] < lengths[$1] }) {
            lengths[widest] += residual
        }
        return lengths
    }

    /// The identity and order of every placed pane, slot by slot — the value the panel
    /// animations key on. Region sizes are deliberately *not* part of it: a divider drag
    /// fires many changes a second and has to track the cursor exactly, while an
    /// add / remove / move should spring into place.
    var arrangement: [[UUID]] {
        PanelSlot.allCases.map { instances(in: $0).map(\.id) }
    }

    /// The same layout, but anonymized into a fresh working copy (new ids) so saved
    /// layouts and the live working arrangement never alias each other's `id`.
    func asWorkingCopy() -> PanelLayout {
        PanelLayout(
            name: "Working",
            left: left, right: right, bottom: bottom,
            leftWidth: leftWidth, rightWidth: rightWidth, bottomHeight: bottomHeight)
    }
}

/// The payload carried by a panel drag. A drag that creates a new panel leaves
/// `instanceID` nil; a drag from an existing panel's header sets it, so the drop can
/// remove it from its old slot.
///
/// Not `Transferable`: panel drags are gestures inside one window and never touch the
/// pasteboard, which is what makes them work at all — see `PanelDragState`.
struct PanelDrag: Equatable, Sendable {
    var instanceID: UUID?
    var kind: PanelKind
}
