import Foundation
import MetalKit
import SwiftUI
import GhosttyKit

extension Ghostty {
    /// InspectableSurface is a type of Surface view that allows an inspector to be attached.
    struct InspectableSurface: View {
        @EnvironmentObject var ghostty: Ghostty.App

        /// Same as SurfaceWrapper, see the doc comments there.
        @ObservedObject var surfaceView: SurfaceView
        var isSplit: Bool = false

        // Maintain whether our view has focus or not
        @FocusState private var inspectorFocus: Bool

        // The fractional area of the surface view vs. the inspector (0.5 means a 50/50 split)
        @State private var split: CGFloat = 0.5

        var body: some View {
            let center = NotificationCenter.default
            let pubInspector = center.publisher(for: Notification.didControlInspector, object: surfaceView)

            ZStack {
                if !surfaceView.inspectorVisible {
                    SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit)
                } else {
                    SplitView(.vertical, $split, dividerColor: ghostty.config.splitDividerColor, left: {
                        SurfaceWrapper(surfaceView: surfaceView, isSplit: isSplit)
                    }, right: {
                        InspectorViewRepresentable(surfaceView: surfaceView)
                            .focused($inspectorFocus)
                            .focusedValue(\.ghosttySurfaceView, surfaceView)
                    }, onEqualize: {
                        guard let surface = surfaceView.surface else { return }
                        ghostty.splitEqualize(surface: surface)
                    })
                }
            }
            .onReceive(pubInspector) { onControlInspector($0) }
            .onChange(of: surfaceView.inspectorVisible) { inspectorVisible in
                // When we show the inspector, we want to focus on the inspector.
                // When we hide the inspector, we want to move focus back to the surface.
                if inspectorVisible {
                    // We need to delay this until SwiftUI shows the inspector.
                    DispatchQueue.main.async {
                        _ = surfaceView.resignFirstResponder()
                        inspectorFocus = true
                    }
                } else {
                    Ghostty.moveFocus(to: surfaceView)
                }
            }
        }

        private func onControlInspector(_ notification: SwiftUI.Notification) {
            // Determine our mode
            guard let modeAny = notification.userInfo?["mode"] else { return }
            guard let mode = modeAny as? ghostty_action_inspector_e else { return }

            switch mode {
            case GHOSTTY_INSPECTOR_TOGGLE:
                surfaceView.inspectorVisible = !surfaceView.inspectorVisible

            case GHOSTTY_INSPECTOR_SHOW:
                surfaceView.inspectorVisible = true

            case GHOSTTY_INSPECTOR_HIDE:
                surfaceView.inspectorVisible = false

            default:
                return
            }
        }
    }

    struct InspectorViewRepresentable: NSViewRepresentable {
        /// The surface that this inspector represents.
        let surfaceView: SurfaceView

        func makeNSView(context: Context) -> InspectorView {
            let view = InspectorView()
            view.surfaceView = self.surfaceView
            return view
        }

        func updateNSView(_ view: InspectorView, context: Context) {
            view.surfaceView = self.surfaceView
        }
    }

    /// Inspector view is the view for the surface inspector (similar to a web inspector).
    class InspectorView: MTKView, NSTextInputClient {
        let commandQueue: MTLCommandQueue

        var surfaceView: SurfaceView? {
            didSet { surfaceViewDidChange() }
        }

        private var inspector: Ghostty.Inspector? {
            guard let surfaceView = self.surfaceView else { return nil }
            return surfaceView.inspector
        }

        private var markedText: NSMutableAttributedString = NSMutableAttributedString()

        // We need to support being a first responder so that we can get input events
        override var acceptsFirstResponder: Bool { return true }

        override init(frame: CGRect, device: MTLDevice?) {
            // Initialize our Metal primitives
            guard
              let device = device ?? MTLCreateSystemDefaultDevice(),
              let commandQueue = device.makeCommandQueue() else {
                fatalError("GPU not available")
            }

            // Setup our properties before initializing the parent
            self.commandQueue = commandQueue
            super.init(frame: frame, device: device)

            // Use timed updates mode. This is required for the inspector.
            self.isPaused = false
            self.preferredFramesPerSecond = 30

            // After initializing the parent we can set our own properties
            self.device = MTLCreateSystemDefaultDevice()
            self.clearColor = MTLClearColor(red: 0x28 / 0xFF, green: 0x2C / 0xFF, blue: 0x34 / 0xFF, alpha: 1.0)

            // Setup our tracking areas for mouse events
            updateTrackingAreas()

            // Observe occlusion state to pause rendering when not visible
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(windowDidChangeOcclusionState),
                name: NSWindow.didChangeOcclusionStateNotification,
                object: nil)
        }

        required init(coder: NSCoder) {
            fatalError("init(coder:) is not supported for this view")
        }

        deinit {
            trackingAreas.forEach { removeTrackingArea($0) }
            NotificationCenter.default.removeObserver(self)
        }

        @objc private func windowDidChangeOcclusionState(_ notification: NSNotification) {
            guard let window = notification.object as? NSWindow,
                  window == self.window else { return }
            // Pause rendering when our window isn't visible.
            isPaused = !window.occlusionState.contains(.visible)
        }

        // MARK: Internal Inspector Funcs

        private func surfaceViewDidChange() {
            guard let inspector = self.inspector else { return }
            guard let device = self.device else { return }
            _ = inspector.metalInit(device: device)
        }

        private func updateSize() {
            guard let inspector = self.inspector else { return }

            // Detect our X/Y scale factor so we can update our surface
            let fbFrame = self.convertToBacking(self.frame)
            let xScale = fbFrame.size.width / self.frame.size.width
            let yScale = fbFrame.size.height / self.frame.size.height
            inspector.setContentScale(x: xScale, y: yScale)

            // When our scale factor changes, so does our fb size so we send that too
            inspector.setSize(width: UInt32(fbFrame.size.width), height: UInt32(fbFrame.size.height))
        }

        // MARK: NSView

        override func becomeFirstResponder() -> Bool {
            let result = super.becomeFirstResponder()
            if result {
                if let inspector = self.inspector {
                    inspector.setFocus(true)
                }
            }
            return result
        }

        override func resignFirstResponder() -> Bool {
            let result = super.resignFirstResponder()
            if result {
                if let inspector = self.inspector {
                    inspector.setFocus(false)
                }
            }
            return result
        }

        override func updateTrackingAreas() {
            // To update our tracking area we just recreate it all.
            trackingAreas.forEach { removeTrackingArea($0) }

            // This tracking area is across the entire frame to notify us of mouse movements.
            addTrackingArea(NSTrackingArea(
                rect: frame,
                options: [
                    .mouseMoved,

                    // Only send mouse events that happen in our visible (not obscured) rect
                    .inVisibleRect,

                    // We want active always because we want to still send mouse reports
                    // even if we're not focused or key.
                    .activeAlways,
                ],
                owner: self,
                userInfo: nil))
        }

        override func viewDidChangeBackingProperties() {
            super.viewDidChangeBackingProperties()
            updateSize()
        }

        override func mouseDown(with event: NSEvent) {
            guard let inspector = self.inspector else { return }
            let mods = Ghostty.ghosttyMods(event.modifierFlags)
            inspector.mouseButton(GHOSTTY_MOUSE_PRESS, button: GHOSTTY_MOUSE_LEFT, mods: mods)
        }

        override func mouseUp(with event: NSEvent) {
            guard let inspector = self.inspector else { return }
            let mods = Ghostty.ghosttyMods(event.modifierFlags)
            inspector.mouseButton(GHOSTTY_MOUSE_RELEASE, button: GHOSTTY_MOUSE_LEFT, mods: mods)
        }

        override func rightMouseDown(with event: NSEvent) {
            guard let inspector = self.inspector else { return }
            let mods = Ghostty.ghosttyMods(event.modifierFlags)
            inspector.mouseButton(GHOSTTY_MOUSE_PRESS, button: GHOSTTY_MOUSE_RIGHT, mods: mods)
        }

        override func rightMouseUp(with event: NSEvent) {
            guard let inspector = self.inspector else { return }
            let mods = Ghostty.ghosttyMods(event.modifierFlags)
            inspector.mouseButton(GHOSTTY_MOUSE_RELEASE, button: GHOSTTY_MOUSE_RIGHT, mods: mods)
        }

        override func mouseMoved(with event: NSEvent) {
            guard let inspector = self.inspector else { return }

            // Convert window position to view position. Note (0, 0) is bottom left.
            let pos = self.convert(event.locationInWindow, from: nil)
            inspector.mousePos(x: pos.x, y: frame.height - pos.y)

        }

        override func mouseDragged(with event: NSEvent) {
            self.mouseMoved(with: event)
        }

        override func scrollWheel(with event: NSEvent) {
            guard let inspector = self.inspector else { return }

            // Builds up the "input.ScrollMods" bitmask
            var mods: Int32 = 0

            let x = event.scrollingDeltaX
            let y = event.scrollingDeltaY
            if event.hasPreciseScrollingDeltas {
                mods = 1
            }

            // Determine our momentum value
            var momentum: ghostty_input_mouse_momentum_e = GHOSTTY_MOUSE_MOMENTUM_NONE
            switch event.momentumPhase {
            case .began:
                momentum = GHOSTTY_MOUSE_MOMENTUM_BEGAN
            case .stationary:
                momentum = GHOSTTY_MOUSE_MOMENTUM_STATIONARY
            case .changed:
                momentum = GHOSTTY_MOUSE_MOMENTUM_CHANGED
            case .ended:
                momentum = GHOSTTY_MOUSE_MOMENTUM_ENDED
            case .cancelled:
                momentum = GHOSTTY_MOUSE_MOMENTUM_CANCELLED
            case .mayBegin:
                momentum = GHOSTTY_MOUSE_MOMENTUM_MAY_BEGIN
            default:
                break
            }

            // Pack our momentum value into the mods bitmask
            mods |= Int32(momentum.rawValue) << 1

            inspector.mouseScroll(x: x, y: y, mods: mods)
        }

        override func keyDown(with event: NSEvent) {
            let action = event.isARepeat ? GHOSTTY_ACTION_REPEAT : GHOSTTY_ACTION_PRESS
            keyAction(action, event: event)
            self.interpretKeyEvents([event])
        }

        override func keyUp(with event: NSEvent) {
            keyAction(GHOSTTY_ACTION_RELEASE, event: event)
        }

        override func flagsChanged(with event: NSEvent) {
            let mod: UInt32
            switch event.keyCode {
            case 0x39: mod = GHOSTTY_MODS_CAPS.rawValue
            case 0x38, 0x3C: mod = GHOSTTY_MODS_SHIFT.rawValue
            case 0x3B, 0x3E: mod = GHOSTTY_MODS_CTRL.rawValue
            case 0x3A, 0x3D: mod = GHOSTTY_MODS_ALT.rawValue
            case 0x37, 0x36: mod = GHOSTTY_MODS_SUPER.rawValue
            default: return
            }

            // The keyAction function will do this AGAIN below which sucks to repeat
            // but this is super cheap and flagsChanged isn't that common.
            let mods = Ghostty.ghosttyMods(event.modifierFlags)

            // If the key that pressed this is active, its a press, else release
            var action = GHOSTTY_ACTION_RELEASE
            if mods.rawValue & mod != 0 { action = GHOSTTY_ACTION_PRESS }

            keyAction(action, event: event)
        }

        private func keyAction(_ action: ghostty_input_action_e, event: NSEvent) {
            guard let inspector = self.inspector else { return }
            guard let key = Ghostty.Input.Key(keyCode: event.keyCode) else { return }
            let mods = Ghostty.ghosttyMods(event.modifierFlags)
            inspector.key(action, key: key.cKey, mods: mods)
        }

        // MARK: NSTextInputClient

        func hasMarkedText() -> Bool {
            return markedText.length > 0
        }

        func markedRange() -> NSRange {
            guard markedText.length > 0 else { return NSRange() }
            return NSRange(0...(markedText.length-1))
        }

        func selectedRange() -> NSRange {
            return NSRange()
        }

        func setMarkedText(_ string: Any, selectedRange: NSRange, replacementRange: NSRange) {
            switch string {
            case let v as NSAttributedString:
                self.markedText = NSMutableAttributedString(attributedString: v)

            case let v as String:
                self.markedText = NSMutableAttributedString(string: v)

            default:
                print("unknown marked text: \(string)")
            }
        }

        func unmarkText() {
            self.markedText.mutableString.setString("")
        }

        func validAttributesForMarkedText() -> [NSAttributedString.Key] {
            return []
        }

        func attributedSubstring(forProposedRange range: NSRange, actualRange: NSRangePointer?) -> NSAttributedString? {
            return nil
        }

        func characterIndex(for point: NSPoint) -> Int {
            return 0
        }

        func firstRect(forCharacterRange range: NSRange, actualRange: NSRangePointer?) -> NSRect {
            return NSRect(x: frame.origin.x, y: frame.origin.y, width: 0, height: 0)
        }

        func insertText(_ string: Any, replacementRange: NSRange) {
            // We must have an associated event
            guard NSApp.currentEvent != nil else { return }
            guard let inspector = self.inspector else { return }

            // We want the string view of the any value
            var chars = ""
            switch string {
            case let v as NSAttributedString:
                chars = v.string
            case let v as String:
                chars = v
            default:
                return
            }

            let len = chars.utf8CString.count
            if len == 0 { return }

            inspector.text(chars)
        }

        override func doCommand(by selector: Selector) {
            // This currently just prevents NSBeep from interpretKeyEvents but in the future
            // we may want to make some of this work.
        }

        // MARK: MTKView

        override func draw(_ dirtyRect: NSRect) {
            guard
              let commandBuffer = self.commandQueue.makeCommandBuffer(),
              let descriptor = self.currentRenderPassDescriptor else {
                return
            }

            // If the inspector is nil, then our surface is freed and it is unsafe
            // to use.
            guard let inspector = self.inspector else { return }

            // We always update our size because sometimes draw is called
            // between resize events and if our size is wrong with the underlying
            // drawable we will crash.
            updateSize()

            // Render
            inspector.metalRender(commandBuffer: commandBuffer, descriptor: descriptor)

            guard let drawable = self.currentDrawable else { return }
            commandBuffer.present(drawable)
            commandBuffer.commit()
        }
    }
}
