import AppKit
import PoseCore

@MainActor
final class JointCanvasView: NSView {
    weak var model: ReviewerModel?
    var frameID = ""
    var generation = UUID()
    private var dragTarget: ReviewEditTarget?
    var scale = 1.0
    private var dragged: String?
    private var preview: Point2?
    private var leanDragTarget: LeanEditTarget?
    private var leanDragged: LeanHandle?
    private(set) var previewLean: LeanAnnotation?
    override var isFlipped: Bool { true }
    override var acceptsFirstResponder: Bool { true }
    private func screen(_ p: Point2, frame: ReviewFrame) -> NSPoint {
        let v = ReviewCoordinates.display(p, panel: frame.panel, scale: scale)
        return NSPoint(x: v.x, y: v.y)
    }
    private func source(_ event: NSEvent, frame: ReviewFrame) -> Point2 {
        let p = convert(event.locationInWindow, from: nil)
        let v = ReviewCoordinates.source(Point2(x: p.x, y: p.y), panel: frame.panel, scale: scale)
        return Point2(x: min(max(v.x, frame.panel.x), frame.panel.x + frame.panel.width - 1),
                      y: min(max(v.y, frame.panel.y), frame.panel.y + frame.panel.height - 1))
    }
    override func draw(_ dirtyRect: NSRect) {
        guard let model, let frame = model.frame(frameID), let image = model.images[frameID] else { return }
        NSImage(cgImage: image, size: NSSize(width: image.width, height: image.height))
            .draw(in: bounds, from: .zero, operation: .copy, fraction: 1, respectFlipped: true, hints: nil)
        defer { if model.leanMode && model.showLean { drawLean(frame: frame) } }
        guard model.showMarkers else { return }
        func point(_ name: String) -> Point2? { name == dragged ? preview : frame.displayPoint(name, kind: model.settings(frameID).kind) }
        if model.settings(frameID).showRaw {
            NSColor.systemOrange.withAlphaComponent(0.75).setStroke()
            for name in InspectionRenderer.jointNames {
                if let p = frame.rawPoint(name, kind: model.settings(frameID).kind) {
                    let s = screen(p, frame: frame)
                    let circle = NSBezierPath(ovalIn: NSRect(x: s.x-5, y: s.y-5, width: 10, height: 10))
                    circle.lineWidth = 1; circle.stroke()
                }
            }
        }
        for (a,b) in InspectionRenderer.bones {
            guard let p = point(a), let q = point(b) else { continue }
            let reviewed = frame.reviewedPoint(a) != nil && frame.reviewedPoint(b) != nil
            (reviewed ? NSColor.systemGreen : NSColor.systemCyan.withAlphaComponent(0.6)).setStroke()
            let line = NSBezierPath(); line.move(to: screen(p, frame: frame)); line.line(to: screen(q, frame: frame))
            line.lineWidth = 1.5
            if !reviewed { line.setLineDash([4, 4], count: 2, phase: 0) }
            line.stroke()
        }
        for name in InspectionRenderer.jointNames {
            guard let p = point(name), let id = InspectionRenderer.jointID(name) else { continue }
            let center = screen(p, frame: frame)
            let selected = (model.frame?.id == frameID && model.selected == name) || model.highlightedJoints.contains(name)
            let radius: Double = selected ? 10 : 8
            let circle = NSBezierPath(ovalIn: NSRect(x: center.x-radius, y: center.y-radius, width: 2*radius, height: 2*radius))
            NSColor.black.withAlphaComponent(0.85).setFill(); circle.fill()
            let state = frame.edits[name]?.state
            let color: NSColor = state == .uncertain ? .systemOrange : (state == nil ? .systemCyan : .systemGreen)
            (selected ? NSColor.white : color).setStroke(); circle.lineWidth = selected ? 2.5 : 1.5; circle.stroke()
            let label = NSAttributedString(string: String(id), attributes: [.font: NSFont.monospacedDigitSystemFont(ofSize: 10, weight: .bold), .foregroundColor: NSColor.white])
            let size = label.size(); label.draw(at: NSPoint(x: center.x-size.width/2, y: center.y-size.height/2))
        }
    }
    func cancelDrag() {
        if let dragTarget, model?.previewTarget == dragTarget { model?.previewTarget = nil }
        dragged = nil; preview = nil; dragTarget = nil; needsDisplay = true
        if let leanDragTarget, model?.leanPreviewTarget == leanDragTarget { model?.leanPreviewTarget = nil }
        leanDragTarget = nil; leanDragged = nil; previewLean = nil
    }
    override func mouseDown(with event: NSEvent) {
        guard let model, generation == model.canvasGeneration, let frame = model.frame(frameID) else { return }
        let wasPlacing = model.placing && model.frame?.id == frameID
        model.activate(frameID); generation = model.canvasGeneration
        window?.makeFirstResponder(self)
        if model.leanMode {
            guard model.showLean else { return }
            if model.leanPlacement != nil { model.placeLean(source(event, frame: frame), frameID: frameID); return }
            let location = convert(event.locationInWindow, from: nil)
            guard let annotation = model.lean(frameID) else { return }
            let hits = LeanHandle.allCases.compactMap { handle -> (LeanHandle, Double)? in
                guard let p = annotation.point(handle) else { return nil }
                let s = screen(p, frame: frame)
                return (handle, hypot(s.x-location.x, s.y-location.y))
            }
            if let hit = hits.min(by: { $0.1 < $1.1 }), hit.1 <= 18 {
                model.selectedLeanHandle = hit.0; leanDragged = hit.0
                leanDragTarget = model.leanTarget(frameID: frameID); previewLean = annotation
            }
            return
        }
        if wasPlacing {
            model.edit(point: source(event, frame: frame), state: .corrected, target: model.target(frameID: frameID, joint: model.selected))
            return
        }
        guard model.showMarkers else { return }
        let location = convert(event.locationInWindow, from: nil)
        let candidates = InspectionRenderer.jointNames.compactMap { name -> (String, Double)? in
            guard let p = frame.displayPoint(name, kind: model.settings(frameID).kind) else { return nil }
            let s = screen(p, frame: frame); return (name, hypot(s.x-location.x, s.y-location.y))
        }
        if let hit = candidates.min(by: { $0.1 < $1.1 }), hit.1 <= 14 {
            model.selected = hit.0; dragged = hit.0; preview = frame.displayPoint(hit.0, kind: model.settings(frameID).kind)
            dragTarget = model.target(frameID: frameID, joint: hit.0)
        }
    }
    override func mouseDragged(with event: NSEvent) {
        if let model, let leanDragTarget, let handle = leanDragged {
            guard model.validLean(leanDragTarget), let frame = model.frame(frameID) else { cancelDrag(); return }
            model.leanPreviewTarget = leanDragTarget
            previewLean?.move(handle, to: source(event, frame: frame)); needsDisplay = true
            return
        }
        guard let model, let dragTarget, model.valid(dragTarget), let frame = model.frame(frameID) else { cancelDrag(); return }
        model.previewTarget = dragTarget
        preview = source(event, frame: frame); needsDisplay = true
    }
    override func mouseUp(with event: NSEvent) {
        defer { cancelDrag() }
        if let model, let target = leanDragTarget, model.validLean(target), let annotation = previewLean {
            if annotation != model.lean(frameID) { model.commitLean(annotation, target: target) }
            return
        }
        guard let model, let target = dragTarget, model.valid(target), let preview else { return }
        // A click alone never endorses a detection.
        if preview != model.frame(frameID)?.displayPoint(target.joint, kind: model.settings(frameID).kind) {
            model.edit(point: preview, state: .corrected, target: target)
        }
    }
    override func keyDown(with event: NSEvent) {
        guard let model, generation == model.canvasGeneration, model.frame?.id == frameID else { return }
        let step = event.modifierFlags.contains(.shift) ? 10.0 : 1.0
        if model.leanMode {
            switch event.keyCode {
            case 123: cancelDrag(); model.nudgeLean(dx: -step, dy: 0)
            case 124: cancelDrag(); model.nudgeLean(dx: step, dy: 0)
            case 125: cancelDrag(); model.nudgeLean(dx: 0, dy: step)
            case 126: cancelDrag(); model.nudgeLean(dx: 0, dy: -step)
            case 53: cancelDrag(); model.cancelInteraction()
            default: super.keyDown(with: event)
            }
            return
        }
        switch event.keyCode {
        case 123: cancelDrag(); model.nudge(dx: -step, dy: 0, frameID: frameID)
        case 124: cancelDrag(); model.nudge(dx: step, dy: 0, frameID: frameID)
        case 125: cancelDrag(); model.nudge(dx: 0, dy: step, frameID: frameID)
        case 126: cancelDrag(); model.nudge(dx: 0, dy: -step, frameID: frameID)
        case 53: cancelDrag(); model.placing = false
        default: super.keyDown(with: event)
        }
    }

    private func drawLean(frame: ReviewFrame) {
        guard let model else { return }
        let annotation = previewLean ?? model.lean(frameID)
        func segment(_ a: NSPoint, _ b: NSPoint, color: NSColor, dashed: Bool = false) {
            color.setStroke()
            let path = NSBezierPath(); path.move(to: a); path.line(to: b); path.lineWidth = 3
            if dashed { path.setLineDash([6, 4], count: 2, phase: 0) }
            path.stroke()
        }
        func label(_ text: String, near point: NSPoint, color: NSColor) {
            let viewport = visibleRect.insetBy(dx: 6, dy: 6)
            guard viewport.width > 30, viewport.height > 30 else { return }
            let string = NSAttributedString(string: text, attributes: [.font: NSFont.monospacedDigitSystemFont(ofSize: 12, weight: .semibold), .foregroundColor: color])
            let size = string.boundingRect(with: NSSize(width: max(20, min(300, viewport.width - 12)), height: 100), options: [.usesLineFragmentOrigin]).size
            let width = min(viewport.width, ceil(size.width)+12), height = min(viewport.height, ceil(size.height)+10)
            let rect = NSRect(x: min(max(point.x, viewport.minX), viewport.maxX-width), y: min(max(point.y, viewport.minY), viewport.maxY-height), width: width, height: height)
            if !rect.contains(point) { segment(point, NSPoint(x: rect.midX, y: rect.midY), color: color.withAlphaComponent(0.7)) }
            NSColor.black.withAlphaComponent(0.88).setFill(); NSBezierPath(roundedRect: rect, xRadius: 5, yRadius: 5).fill()
            string.draw(with: rect.insetBy(dx: 6, dy: 5), options: [.usesLineFragmentOrigin])
        }
        if let start = model.leanPlacementStart, model.frame?.id == frameID {
            let p = screen(start, frame: frame)
            NSColor.systemYellow.setFill(); NSBezierPath(ovalIn: NSRect(x: p.x-6, y: p.y-6, width: 12, height: 12)).fill()
        }
        guard let annotation else {
            label("Place the front-wheel axis using Lean controls.", near: NSPoint(x: visibleRect.minX+12, y: visibleRect.minY+12), color: .systemYellow)
            return
        }
        for (line, color) in [(annotation.wheel, NSColor.systemYellow), (annotation.reference, NSColor.systemCyan)] {
            if let line { segment(screen(line.a, frame: frame), screen(line.b, frame: frame), color: color) }
        }
        for handle in LeanHandle.allCases {
            guard let p = annotation.point(handle) else { continue }
            let s = screen(p, frame: frame), r = 8.0
            let circle = NSBezierPath(ovalIn: NSRect(x: s.x-r, y: s.y-r, width: r*2, height: r*2))
            NSColor.black.setFill(); circle.fill()
            (handle.isReference ? NSColor.systemCyan : .systemYellow).setStroke()
            circle.lineWidth = model.frame?.id == frameID && model.selectedLeanHandle == handle ? 4 : 2; circle.stroke()
        }
        if let reference = annotation.reference {
            let p = screen(reference.a, frame: frame)
            label(annotation.basis == .sharedFeature ? "Shared feature · \(annotation.referenceReviewed ? "reviewed" : "draft")" : "Proxy reference · \(annotation.referenceReviewed ? "reviewed" : "draft")", near: NSPoint(x: p.x+10, y: p.y+12), color: .systemCyan)
        }
        guard let wheel = annotation.wheel, let measurement = LeanAnalysis.measure(annotation) else {
            label(annotation.state == .unavailable ? "Wheel lean unavailable" : "Place valid wheel/reference lines; angle unavailable.", near: NSPoint(x: visibleRect.minX+12, y: visibleRect.minY+12), color: .systemYellow)
            return
        }
        let anchor = screen(wheel.a.y > wheel.b.y ? wheel.a : wheel.b, frame: frame)
        let theta = LeanAnalysis.orientation(wheel)
        let referenceTheta: Double
        if annotation.basis == .imageVertical { referenceTheta = 0 }
        else if let reference = annotation.reference { referenceTheta = LeanAnalysis.orientation(reference) + (annotation.basis == .assumedLevel ? 90 : 0) }
        else { return }
        let delta = LeanAnalysis.wrap(theta - referenceTheta)
        let start = theta - delta
        func ray(_ degrees: Double, radius: Double) -> NSPoint {
            NSPoint(x: anchor.x + sin(degrees * .pi/180) * radius, y: anchor.y - cos(degrees * .pi/180) * radius)
        }
        segment(anchor, ray(start, radius: 70), color: .systemCyan, dashed: true)
        let arc = NSBezierPath(); arc.move(to: ray(start, radius: 38))
        for step in 1...30 { arc.line(to: ray(start + delta * Double(step)/30, radius: 38)) }
        NSColor.systemYellow.setStroke(); arc.lineWidth = 2; arc.stroke()
        let title = annotation.basis == .sharedFeature ? "Wheel-to-feature orientation" : "2D wheel lean"
        let state = model.leanPreviewTarget?.frameID == frameID ? "Preview · release to save" : annotation.state == .reviewed ? "Reviewed" : "Draft · correct the handles"
        label(String(format: "%.0f° %@\n%@", measurement.angle, title, state), near: NSPoint(x: anchor.x+20, y: anchor.y+14), color: .systemYellow)
    }
}
