import AppKit
import SwiftUI

struct QuickChatTextView: NSViewRepresentable {
    @Binding var text: String
    let selectionRange: NSRange?
    let onSubmit: (Bool) -> Void
    let onEscape: () -> Void
    let onUserEdit: () -> Void
    let onHeightChange: (CGFloat) -> Void
    let onTextViewReady: (NSTextView) -> Void

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    func makeNSView(context: Context) -> NSScrollView {
        let textView = QuickChatNSTextView()
        textView.delegate = context.coordinator
        textView.drawsBackground = false
        textView.isRichText = false
        textView.isAutomaticQuoteSubstitutionEnabled = false
        textView.isAutomaticTextReplacementEnabled = false
        textView.font = .systemFont(ofSize: 13.5)
        textView.textColor = .labelColor
        textView.insertionPointColor = .controlAccentColor
        textView.textContainer?.lineBreakMode = .byWordWrapping
        textView.textContainer?.lineFragmentPadding = 0
        textView.textContainerInset = NSSize(width: 2, height: 6)
        textView.minSize = .zero
        textView.maxSize = NSSize(
            width: CGFloat.greatestFiniteMagnitude,
            height: CGFloat.greatestFiniteMagnitude)
        textView.isHorizontallyResizable = false
        textView.isVerticallyResizable = true
        textView.autoresizingMask = [.width]
        textView.textContainer?.containerSize = NSSize(width: 0, height: CGFloat.greatestFiniteMagnitude)
        textView.textContainer?.widthTracksTextView = true
        textView.focusRingType = .none
        textView.string = self.text
        textView.onSubmit = self.onSubmit
        textView.onEscape = self.onEscape

        let scrollView = NSScrollView()
        scrollView.drawsBackground = false
        scrollView.borderType = .noBorder
        scrollView.hasVerticalScroller = false
        scrollView.autohidesScrollers = true
        scrollView.scrollerStyle = .overlay
        scrollView.hasHorizontalScroller = false
        scrollView.documentView = textView
        context.coordinator.scrollView = scrollView

        DispatchQueue.main.async {
            self.onTextViewReady(textView)
            context.coordinator.updateHeight(for: textView)
        }
        return scrollView
    }

    func updateNSView(_ scrollView: NSScrollView, context: Context) {
        context.coordinator.parent = self
        guard let textView = scrollView.documentView as? QuickChatNSTextView else { return }
        textView.onSubmit = self.onSubmit
        textView.onEscape = self.onEscape
        let textChanged = textView.string != self.text && !textView.hasMarkedText()
        if textChanged {
            context.coordinator.isProgrammaticUpdate = true
            textView.string = self.text
        }
        if !textView.hasMarkedText(), let selectionRange = self.selectionRange {
            let selection = NSRange(
                location: min(selectionRange.location, textView.string.utf16.count),
                length: 0)
            if context.coordinator.lastAppliedSelectionRange != selection {
                textView.setSelectedRange(selection)
                context.coordinator.lastAppliedSelectionRange = selection
            }
        } else if textChanged {
            let selection = NSRange(location: textView.string.utf16.count, length: 0)
            textView.setSelectedRange(selection)
            context.coordinator.lastAppliedSelectionRange = nil
        } else if self.selectionRange == nil {
            context.coordinator.lastAppliedSelectionRange = nil
        }
        if textChanged {
            context.coordinator.isProgrammaticUpdate = false
        }
        DispatchQueue.main.async {
            context.coordinator.updateHeight(for: textView)
        }
    }

    @MainActor
    final class Coordinator: NSObject, NSTextViewDelegate {
        var parent: QuickChatTextView
        weak var scrollView: NSScrollView?
        var isProgrammaticUpdate = false
        var lastAppliedSelectionRange: NSRange?
        private var lastHeight: CGFloat = 0

        init(_ parent: QuickChatTextView) {
            self.parent = parent
        }

        func textDidChange(_ notification: Notification) {
            guard let textView = notification.object as? QuickChatNSTextView else { return }
            if !self.isProgrammaticUpdate {
                self.parent.onUserEdit()
                self.parent.text = textView.string
            }
            self.updateHeight(for: textView)
        }

        func updateHeight(for textView: NSTextView) {
            guard let layoutManager = textView.layoutManager,
                  let textContainer = textView.textContainer else { return }
            layoutManager.ensureLayout(for: textContainer)
            let font = textView.font ?? .systemFont(ofSize: 13.5)
            let lineHeight = ceil(layoutManager.defaultLineHeight(for: font))
            let naturalHeight = ceil(layoutManager.usedRect(for: textContainer).height + 12)
            let minHeight = lineHeight + 12
            let maxHeight = (lineHeight * 5) + 12
            let height = min(max(naturalHeight, minHeight), maxHeight)
            self.scrollView?.hasVerticalScroller = naturalHeight > maxHeight
            guard abs(height - self.lastHeight) > 0.5 else { return }
            self.lastHeight = height
            self.parent.onHeightChange(height)
        }
    }
}

private final class QuickChatNSTextView: NSTextView {
    var onSubmit: ((Bool) -> Void)?
    var onEscape: (() -> Void)?

    override func keyDown(with event: NSEvent) {
        if event.keyCode == 53 {
            guard !self.hasMarkedText() else {
                super.keyDown(with: event)
                return
            }
            self.onEscape?()
            return
        }

        guard event.keyCode == 36 || event.keyCode == 76 else {
            super.keyDown(with: event)
            return
        }
        guard !self.hasMarkedText() else {
            super.keyDown(with: event)
            return
        }

        let modifiers = event.modifierFlags.intersection([.command, .shift])
        if modifiers.contains(.shift), !modifiers.contains(.command) {
            self.insertNewline(nil)
        } else {
            self.onSubmit?(modifiers.contains(.command))
        }
    }
}
