import AppKit
import SwiftUI
import PoseCore

/// Optional display bounds tied to the exact source photo and panel, separate from analysis evidence.
struct PhotoFramingRegion: Codable {
    let frameID: String
    let sourceHash: String
    let panel: PixelRect
    let bounds: PixelRect
    static func load(from url: URL, frames: [ReviewFrame]) throws -> [String: PixelRect] {
        let entries = try JSONDecoder().decode([Self].self, from: Data(contentsOf: url))
        var result: [String: PixelRect] = [:]
        for entry in entries {
            let b = entry.bounds
            guard result[entry.frameID] == nil,
                  let frame = frames.first(where: { $0.id == entry.frameID }),
                  entry.sourceHash == frame.sourceHash, entry.panel == frame.panel,
                  [b.x, b.y, b.width, b.height].allSatisfy({ $0.isFinite }), b.width > 0, b.height > 0,
                  b.x >= frame.panel.x, b.y >= frame.panel.y,
                  b.x + b.width <= frame.panel.x + frame.panel.width,
                  b.y + b.height <= frame.panel.y + frame.panel.height else {
                throw PoseError.invalidInput("Photo framing does not match its source or panel.")
            }
            result[entry.frameID] = b
        }
        return result
    }
}

/// Source-space framing only; no image, joint, or lean evidence is cropped or changed.
struct PhotoViewport {
    let panel: PixelRect
    let viewport: CGSize
    let scale: Double
    let zoom: Double
    let targetCenter: Point2
    var imageSize: CGSize { CGSize(width: panel.width * scale, height: panel.height * scale) }
    // Extra space at an image edge lets an off-center target remain centered without cropping it.
    var documentSize: CGSize {
        let horizontal = max(8, viewport.width / 2 - min(targetCenter.x - panel.x, panel.x + panel.width - targetCenter.x) * scale)
        let vertical = max(8, viewport.height / 2 - min(targetCenter.y - panel.y, panel.y + panel.height - targetCenter.y) * scale)
        return CGSize(width: max(viewport.width, imageSize.width + 2 * horizontal), height: max(viewport.height, imageSize.height + 2 * vertical))
    }
    var imageOrigin: CGPoint { CGPoint(x: (documentSize.width - imageSize.width) / 2, y: (documentSize.height - imageSize.height) / 2) }

    init(panel: PixelRect, region: PixelRect?, viewport: CGSize, zoom requestedZoom: Double?) {
        self.panel = panel
        self.viewport = CGSize(width: max(17, viewport.width), height: max(17, viewport.height))
        let fit = max(0.001, min((self.viewport.width - 16) / panel.width, (self.viewport.height - 16) / panel.height))
        let target = region ?? panel
        let automatic = region == nil ? 1 : min((self.viewport.width - 16) / (target.width * 1.12), (self.viewport.height - 16) / (target.height * 1.12)) / fit
        zoom = min(4, max(1, requestedZoom ?? automatic))
        scale = fit * zoom
        targetCenter = Point2(x: target.x + target.width / 2, y: target.y + target.height / 2)
    }
    func scrollOrigin(center: Point2) -> CGPoint {
        CGPoint(x: min(max(0, (center.x - panel.x) * scale + imageOrigin.x - viewport.width / 2), documentSize.width - viewport.width),
                y: min(max(0, (center.y - panel.y) * scale + imageOrigin.y - viewport.height / 2), documentSize.height - viewport.height))
    }
    func visibleRect(center: Point2) -> CGRect {
        let origin = scrollOrigin(center: center)
        return CGRect(x: panel.x + (origin.x - imageOrigin.x) / scale, y: panel.y + (origin.y - imageOrigin.y) / scale,
                      width: viewport.width / scale, height: viewport.height / scale)
    }
}

struct FramedPhotoCanvas: NSViewRepresentable {
    @ObservedObject var model: ReviewerModel
    let frameID: String
    @Binding var displayedZoom: Double
    func makeNSView(context: Context) -> PhotoScrollView { PhotoScrollView() }
    func updateNSView(_ view: PhotoScrollView, context: Context) {
        guard let frame = model.frame(frameID) else { return }
        view.onZoom = { value in
            DispatchQueue.main.async { if abs(displayedZoom - value) > 0.001 { displayedZoom = value } }
        }
        view.configure(model: model, frameID: frameID, panel: frame.panel, region: model.framingRegions[frameID])
    }
}

private final class PhotoDocumentView: NSView { override var isFlipped: Bool { true } }

@MainActor
final class PhotoScrollView: NSScrollView {
    let canvas = JointCanvasView()
    private let photoDocument = PhotoDocumentView()
    private weak var model: ReviewerModel?
    private var frameID = ""
    private var panel = PixelRect(x: 0, y: 0, width: 1, height: 1)
    private var region: PixelRect?
    private var applied: Configuration?
    private var applying = false
    private(set) var currentZoom = 1.0
    var onZoom: ((Double) -> Void)?
    private struct Configuration: Equatable {
        let frameID: String
        let sessionID: UUID
        let viewport: CGSize
        let autoFrame: Bool
        let zoom: Double
        let request: Int
        let region: PixelRect?
    }
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        hasHorizontalScroller = true; hasVerticalScroller = true; autohidesScrollers = true
        scrollerStyle = .overlay; drawsBackground = true; backgroundColor = .black
        photoDocument.addSubview(canvas); documentView = photoDocument
        contentView.postsBoundsChangedNotifications = true
        NotificationCenter.default.addObserver(self, selector: #selector(didScroll), name: NSView.boundsDidChangeNotification, object: contentView)
    }
    required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
    func configure(model: ReviewerModel, frameID: String, panel: PixelRect, region: PixelRect?) {
        if canvas.generation != model.canvasGeneration || canvas.frameID != frameID { canvas.cancelDrag() }
        self.model = model; self.frameID = frameID; self.panel = panel; self.region = region
        canvas.model = model; canvas.frameID = frameID; canvas.generation = model.canvasGeneration
        canvas.needsDisplay = true; needsLayout = true
    }
    override func layout() {
        super.layout()
        guard let model, contentSize.width > 16, contentSize.height > 16 else { return }
        let settings = model.settings(frameID)
        let next = Configuration(frameID: frameID, sessionID: model.sessionID, viewport: contentSize,
                                 autoFrame: settings.autoFrame, zoom: settings.zoom, request: settings.framingRequest, region: region)
        guard next != applied else { return }
        let reset = applied?.frameID == frameID && applied?.sessionID == model.sessionID && applied?.request != settings.framingRequest
        let layout = PhotoViewport(panel: panel, region: settings.autoFrame || settings.zoom > 1 ? region : nil, viewport: contentSize,
                                   zoom: settings.autoFrame ? nil : settings.zoom)
        let center = reset ? layout.targetCenter : model.viewportCenters[frameID] ?? layout.targetCenter
        applying = true
        photoDocument.setFrameSize(layout.documentSize)
        canvas.frame = CGRect(origin: layout.imageOrigin, size: layout.imageSize)
        canvas.scale = layout.scale; canvas.needsDisplay = true
        contentView.scroll(to: layout.scrollOrigin(center: center)); reflectScrolledClipView(contentView)
        applied = next; currentZoom = layout.zoom
        applying = false
        rememberCenter()
        onZoom?(layout.zoom)
    }
    @objc private func didScroll(_ notification: Notification) {
        guard !applying, applied?.frameID == frameID, applied?.viewport == contentSize else { return }
        rememberCenter()
    }
    private func rememberCenter() {
        guard canvas.scale > 0 else { return }
        let center = CGPoint(x: contentView.bounds.midX, y: contentView.bounds.midY)
        let point = canvas.convert(center, from: contentView)
        model?.viewportCenters[frameID] = ReviewCoordinates.source(Point2(x: point.x, y: point.y), panel: panel, scale: canvas.scale)
    }
}
