import AppKit
import CodeEditLanguages
import CodeEditSourceEditor
import CodeEditTextView
import SwiftUI

@MainActor
public final class CodeEditEditorController: ObservableObject {
    weak var controller: TextViewController?

    @Published public var canUndo = false
    @Published public var canRedo = false

    public init() {}

    public func undo() {
        controller?.textView.undoManager?.undo()
        updateUndoState()
    }

    public func redo() {
        controller?.textView.undoManager?.redo()
        updateUndoState()
    }

    public func insertSymbol(_ symbol: String) {
        guard let textView = controller?.textView else { return }
        textView.insertText(symbol, replacementRange: textView.selectedRange())
    }

    func updateUndoState() {
        canUndo = controller?.textView.undoManager?.canUndo ?? false
        canRedo = controller?.textView.undoManager?.canRedo ?? false
    }
}

private extension NSColor {
    var forEditor: NSColor {
        usingColorSpace(.sRGB) ?? self
    }
}

private func makeTheme(for colorScheme: ColorScheme) -> EditorTheme {
    var theme: EditorTheme!
    let build = {
        theme = EditorTheme(
            text: .init(color: NSColor.labelColor.forEditor),
            insertionPoint: NSColor.labelColor.forEditor,
            invisibles: .init(color: NSColor.tertiaryLabelColor.forEditor),
            background: NSColor.textBackgroundColor.forEditor,
            lineHighlight: NSColor.quaternaryLabelColor.forEditor,
            selection: NSColor.selectedTextBackgroundColor.forEditor,
            keywords: .init(color: NSColor.systemPurple.forEditor),
            commands: .init(color: NSColor.systemCyan.forEditor),
            types: .init(color: NSColor.systemCyan.forEditor),
            attributes: .init(color: NSColor.systemCyan.forEditor),
            variables: .init(color: NSColor.labelColor.forEditor),
            values: .init(color: NSColor.systemOrange.forEditor),
            numbers: .init(color: NSColor.systemOrange.forEditor),
            strings: .init(color: NSColor.systemGreen.forEditor),
            characters: .init(color: NSColor.systemGreen.forEditor),
            comments: .init(color: NSColor.secondaryLabelColor.forEditor)
        )
    }
    if let appearance = NSAppearance(named: colorScheme == .dark ? .darkAqua : .aqua) {
        appearance.performAsCurrentDrawingAppearance(build)
    } else {
        build()
    }
    return theme
}

private func makeConfiguration(isEditable: Bool, colorScheme: ColorScheme) -> SourceEditorConfiguration {
    SourceEditorConfiguration(
        appearance: .init(
            theme: makeTheme(for: colorScheme),
            font: .monospacedSystemFont(ofSize: 14, weight: .regular),
            lineHeightMultiple: 1.3,
            wrapLines: false
        ),
        behavior: .init(
            isEditable: isEditable,
            isSelectable: true
        ),
        peripherals: .init(
            showMinimap: false,
            showFoldingRibbon: false
        )
    )
}

struct CodeEditTextView: NSViewRepresentable {
    @Binding var text: String
    let isEditable: Bool
    let editorController: CodeEditEditorController?

    @Environment(\.colorScheme) private var colorScheme

    init(text: Binding<String>, isEditable: Bool, editorController: CodeEditEditorController? = nil) {
        _text = text
        self.isEditable = isEditable
        self.editorController = editorController
    }

    func makeNSView(context: Context) -> NSView {
        let controller = TextViewController(
            string: text,
            language: .json,
            configuration: makeConfiguration(isEditable: isEditable, colorScheme: colorScheme),
            cursorPositions: []
        )
        controller.loadView()

        let containerView = NSView()
        containerView.translatesAutoresizingMaskIntoConstraints = false

        let controllerView = controller.view
        controllerView.translatesAutoresizingMaskIntoConstraints = false
        containerView.addSubview(controllerView)

        NSLayoutConstraint.activate([
            controllerView.topAnchor.constraint(equalTo: containerView.topAnchor),
            controllerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
            controllerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
            controllerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
        ])

        context.coordinator.controller = controller
        context.coordinator.lastColorScheme = colorScheme
        context.coordinator.setupObservation()
        editorController?.controller = controller
        Task { @MainActor in
            editorController?.updateUndoState()
        }

        return containerView
    }

    func updateNSView(_: NSView, context: Context) {
        guard let controller = context.coordinator.controller else { return }
        if controller.text != text {
            controller.text = text
            // setText() creates a new Highlighter but doesn't trigger initial highlighting.
            // Re-setting the language forces the highlighter to invalidate and re-highlight.
            controller.language = .json
        }
        if controller.configuration.behavior.isEditable != isEditable {
            controller.configuration.behavior.isEditable = isEditable
        }
        if context.coordinator.lastColorScheme != colorScheme {
            context.coordinator.lastColorScheme = colorScheme
            controller.configuration.appearance.theme = makeTheme(for: colorScheme)
        }
        editorController?.controller = controller
    }

    func makeCoordinator() -> Coordinator {
        Coordinator(text: $text, editorController: editorController)
    }

    class Coordinator: NSObject {
        var controller: TextViewController?
        var lastColorScheme: ColorScheme?
        @Binding var text: String
        private var observation: NSObjectProtocol?
        private weak var editorController: CodeEditEditorController?

        init(text: Binding<String>, editorController: CodeEditEditorController?) {
            _text = text
            self.editorController = editorController
            super.init()
        }

        func setupObservation() {
            guard let controller else { return }
            observation = NotificationCenter.default.addObserver(
                forName: TextView.textDidChangeNotification,
                object: controller.textView,
                queue: .main
            ) { [weak self] _ in
                guard let self, let controller = self.controller else { return }
                self.text = controller.text
                Task { @MainActor in
                    self.editorController?.updateUndoState()
                }
            }
        }

        deinit {
            if let observation {
                NotificationCenter.default.removeObserver(observation)
            }
        }
    }
}
