import SwiftUI
import AppKit

/// Above this size we skip the (whole-buffer) re-scan on every keystroke and just
/// show plain monospaced text, so typing in a large file stays responsive. The editor
/// already caps loads at 2 MB; this is the point where live coloring would start to lag.
private let liveHighlightCap = 200_000

/// A plain-text code editor with syntax highlighting. SwiftUI's `TextEditor` can't
/// color individual ranges, so this drops to an `NSTextView` and recolors its text
/// storage as the buffer changes. The text itself stays a plain `String` bound back
/// to the model — highlighting is purely presentational attributes layered on top.
struct CodeEditorView: NSViewRepresentable {
    @Binding var text: String
    var language: LanguageSyntax
    var colorVision: ColorVisionMode = .standard
    var fontSize: CGFloat = 12

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

    func makeNSView(context: Context) -> NSScrollView {
        let textView = NSTextView()
        textView.delegate = context.coordinator
        textView.isRichText = false
        textView.allowsUndo = true
        textView.isAutomaticQuoteSubstitutionEnabled = false
        textView.isAutomaticDashSubstitutionEnabled = false
        textView.isAutomaticTextReplacementEnabled = false
        textView.isAutomaticSpellingCorrectionEnabled = false
        textView.isContinuousSpellCheckingEnabled = false
        textView.isGrammarCheckingEnabled = false
        textView.smartInsertDeleteEnabled = false
        textView.drawsBackground = false
        textView.textContainerInset = NSSize(width: 6, height: 8)
        textView.font = context.coordinator.font
        textView.string = text
        textView.insertionPointColor = SyntaxTheme.palette(for: colorVision).plain
        textView.isVerticallyResizable = true
        textView.isHorizontallyResizable = false
        textView.autoresizingMask = [.width]
        textView.textContainer?.widthTracksTextView = true
        context.coordinator.textView = textView
        context.coordinator.highlight()

        let scroll = NSScrollView()
        scroll.documentView = textView
        scroll.hasVerticalScroller = true
        scroll.drawsBackground = false
        scroll.borderType = .noBorder
        return scroll
    }

    func updateNSView(_ scrollView: NSScrollView, context: Context) {
        context.coordinator.parent = self
        guard let textView = context.coordinator.textView else { return }
        // The binding changed from outside (file load, revert, switching files).
        // Replace the buffer, keep the caret in range, and recolor.
        if textView.string != text {
            let caret = textView.selectedRange()
            textView.string = text
            let length = (text as NSString).length
            textView.setSelectedRange(NSRange(location: min(caret.location, length), length: 0))
            context.coordinator.highlight()
        } else {
            // Same text but font/language may have changed — recolor in place.
            context.coordinator.highlight()
        }
    }

    final class Coordinator: NSObject, NSTextViewDelegate {
        var parent: CodeEditorView
        weak var textView: NSTextView?

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

        var font: NSFont {
            .monospacedSystemFont(ofSize: parent.fontSize, weight: .regular)
        }

        func textDidChange(_ notification: Notification) {
            guard let textView else { return }
            parent.text = textView.string
            highlight()
        }

        /// Repaint token colors over the current buffer. Only attributes change — the
        /// characters and the caret are untouched, so this is safe to run on every
        /// edit. Wrapped in `begin/endEditing` to coalesce the layout pass.
        @MainActor
        func highlight() {
            guard let textView, let storage = textView.textStorage else { return }
            let text = textView.string
            let fullRange = NSRange(location: 0, length: (text as NSString).length)
            let palette = SyntaxTheme.palette(for: parent.colorVision)

            storage.beginEditing()
            storage.setAttributes(
                [.font: font, .foregroundColor: palette.plain], range: fullRange)

            if text.utf16.count <= liveHighlightCap {
                for (range, token) in SyntaxScanner.tokens(in: text, language: parent.language) {
                    storage.addAttribute(
                        .foregroundColor, value: palette.color(token),
                        range: NSRange(range, in: text))
                }
            }
            storage.endEditing()
            textView.insertionPointColor = palette.plain
        }
    }
}
