import Foundation
import SwiftUI
import SaplingCore
import SaplingRenderer

#if os(macOS)
import AppKit
#elseif os(iOS)
import UIKit
#endif

@MainActor
public final class HybridMarkdownEditorViewModel: ObservableObject, EditorCoordinator {
    @Published public private(set) var state: EditorState

    public init(document: MarkdownDocument, activeLineIndex: Int = 0) {
        self.state = EditorState(
            document: EditorDocument(markdownDocument: document),
            activeLineIndex: activeLineIndex
        )
    }

    public var document: MarkdownDocument {
        state.document.markdownDocument
    }

    public var hasUnsavedChanges: Bool {
        state.hasUnsavedChanges
    }

    public var activeLineIndex: Int {
        state.activeLineIndex
    }

    public func replaceDocument(_ document: EditorDocument) {
        state = EditorState(document: document)
    }

    public func updateSource(_ source: String) {
        guard state.document.source != source else { return }
        state.updateSource(source)
    }

    public func updateSelection(_ selection: EditorSelection) {
        guard state.selection != selection else { return }
        state.updateSelection(selection)
    }

    public func save() throws {
        try state.document.source.write(to: state.document.url, atomically: true, encoding: .utf8)
        state.markSaved()
    }

    public static func loadDocument(at url: URL) throws -> MarkdownDocument {
        let source = try String(contentsOf: url, encoding: .utf8)
        let title = url.deletingPathExtension().lastPathComponent
        return MarkdownDocument(url: url, title: title, content: source)
    }
}

public struct HybridMarkdownEditor: View, EditorView {
    @ObservedObject private var viewModel: HybridMarkdownEditorViewModel
    private let renderer: any MarkdownRendering

    public init(
        viewModel: HybridMarkdownEditorViewModel,
        renderer: any MarkdownRendering = MarkdownRenderer()
    ) {
        self.viewModel = viewModel
        self.renderer = renderer
    }

    public var state: EditorState {
        viewModel.state
    }

    public var body: some View {
        VStack(spacing: 0) {
            NativeMarkdownTextView(
                text: Binding(
                    get: { viewModel.state.document.source },
                    set: { viewModel.updateSource($0) }
                ),
                selection: Binding(
                    get: { viewModel.state.selection },
                    set: { viewModel.updateSelection($0) }
                ),
                activeLineIndex: viewModel.state.activeLineIndex
            )
            .frame(maxWidth: .infinity, maxHeight: .infinity)

            EditorStatusBar(
                activeLineIndex: viewModel.state.activeLineIndex,
                lineCount: viewModel.state.lines.count,
                hasUnsavedChanges: viewModel.state.hasUnsavedChanges,
                activeLinePreview: activeLinePreview
            )
        }
        .background(platformTextBackground)
    }

    private var activeLinePreview: AttributedString {
        guard viewModel.state.lines.indices.contains(viewModel.state.activeLineIndex) else {
            return AttributedString()
        }
        return renderer.inlineMarkdown(for: viewModel.state.lines[viewModel.state.activeLineIndex].source)
    }
}

private struct EditorStatusBar: View {
    let activeLineIndex: Int
    let lineCount: Int
    let hasUnsavedChanges: Bool
    let activeLinePreview: AttributedString

    var body: some View {
        HStack(spacing: 12) {
            Text("Line \(activeLineIndex + 1) of \(lineCount)")
            Text(hasUnsavedChanges ? "Modified" : "Saved")
                .foregroundStyle(hasUnsavedChanges ? .orange : .secondary)
            Spacer()
            Text(activeLinePreview)
                .lineLimit(1)
                .foregroundStyle(.secondary)
        }
        .font(.caption)
        .padding(.horizontal, 12)
        .padding(.vertical, 6)
        .background(.bar)
    }
}

#if os(macOS)
private struct NativeMarkdownTextView: NSViewRepresentable {
    @Binding var text: String
    @Binding var selection: EditorSelection
    let activeLineIndex: Int

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

    func makeNSView(context: Context) -> NSScrollView {
        let scrollView = NSTextView.scrollableTextView()
        guard let textView = scrollView.documentView as? NSTextView else {
            return scrollView
        }

        textView.delegate = context.coordinator
        textView.string = text
        textView.isRichText = false
        textView.isAutomaticQuoteSubstitutionEnabled = false
        textView.isAutomaticDashSubstitutionEnabled = false
        textView.isAutomaticTextReplacementEnabled = false
        textView.allowsUndo = true
        textView.usesFindPanel = true
        textView.isContinuousSpellCheckingEnabled = true
        textView.textContainerInset = NSSize(width: 20, height: 18)
        textView.backgroundColor = .textBackgroundColor
        textView.insertionPointColor = .controlAccentColor
        textView.font = .monospacedSystemFont(ofSize: 14, weight: .regular)
        textView.textContainer?.widthTracksTextView = true
        textView.textContainer?.containerSize = NSSize(
            width: scrollView.contentSize.width,
            height: CGFloat.greatestFiniteMagnitude
        )

        context.coordinator.applyHybridAttributes(to: textView)
        return scrollView
    }

    func updateNSView(_ scrollView: NSScrollView, context: Context) {
        context.coordinator.parent = self
        guard let textView = scrollView.documentView as? NSTextView else { return }

        if textView.string != text {
            context.coordinator.performProgrammaticUpdate {
                textView.string = text
            }
        }

        let selectedRange = selection.range
        if textView.selectedRange() != selectedRange,
           selectedRange.location <= textView.string.utf16.count {
            context.coordinator.setSelection(selectedRange, in: textView)
        }

        context.coordinator.applyHybridAttributes(to: textView)
    }

    final class Coordinator: NSObject, NSTextViewDelegate {
        var parent: NativeMarkdownTextView
        private var programmaticUpdateDepth = 0
        private var lastStyledText: String?
        private var lastStyledActiveLineIndex: Int?

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

        func textDidChange(_ notification: Notification) {
            guard !isPerformingProgrammaticUpdate else { return }
            guard let textView = notification.object as? NSTextView else { return }

            parent.text = textView.string
            parent.selection = EditorSelection(range: textView.selectedRange())
            lastStyledText = nil
            applyHybridAttributes(to: textView)
        }

        func textViewDidChangeSelection(_ notification: Notification) {
            guard !isPerformingProgrammaticUpdate else { return }
            guard let textView = notification.object as? NSTextView else { return }
            let newSelection = EditorSelection(range: textView.selectedRange())
            guard parent.selection != newSelection else { return }
            applyHybridAttributes(to: textView)
            parent.selection = newSelection
        }

        func applyHybridAttributes(to textView: NSTextView) {
            guard let textStorage = textView.textStorage else { return }
            guard shouldRestyle(textView.string) else { return }

            let selectedRange = textView.selectedRange()
            performProgrammaticUpdate {
                MarkdownTextStyler.apply(
                    to: textStorage,
                    activeLineIndex: parent.activeLineIndex,
                    backgroundColor: .textBackgroundColor,
                    activeLineBackgroundColor: .controlAccentColor.withAlphaComponent(0.10),
                    textColor: .labelColor,
                    secondaryTextColor: .secondaryLabelColor,
                    accentColor: .controlAccentColor
                )
                if textView.selectedRange() != selectedRange,
                   selectedRange.location <= textView.string.utf16.count {
                    textView.setSelectedRange(selectedRange)
                }
            }

            lastStyledText = textView.string
            lastStyledActiveLineIndex = parent.activeLineIndex
        }

        func setSelection(_ range: NSRange, in textView: NSTextView) {
            guard textView.selectedRange() != range else { return }
            performProgrammaticUpdate {
                textView.setSelectedRange(range)
            }
        }

        func performProgrammaticUpdate(_ updates: () -> Void) {
            programmaticUpdateDepth += 1
            defer {
                programmaticUpdateDepth -= 1
            }
            updates()
        }

        private var isPerformingProgrammaticUpdate: Bool {
            programmaticUpdateDepth > 0
        }

        private func shouldRestyle(_ text: String) -> Bool {
            lastStyledText != text || lastStyledActiveLineIndex != parent.activeLineIndex
        }
    }
}
#elseif os(iOS)
private struct NativeMarkdownTextView: UIViewRepresentable {
    @Binding var text: String
    @Binding var selection: EditorSelection
    let activeLineIndex: Int

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

    func makeUIView(context: Context) -> UITextView {
        let textView = UITextView()
        textView.delegate = context.coordinator
        textView.text = text
        textView.allowsEditingTextAttributes = false
        textView.autocorrectionType = .yes
        textView.smartDashesType = .no
        textView.smartQuotesType = .no
        textView.font = .monospacedSystemFont(ofSize: 15, weight: .regular)
        textView.textContainerInset = UIEdgeInsets(top: 18, left: 16, bottom: 18, right: 16)
        textView.backgroundColor = .systemBackground
        context.coordinator.applyHybridAttributes(to: textView)
        return textView
    }

    func updateUIView(_ textView: UITextView, context: Context) {
        context.coordinator.parent = self
        if textView.text != text {
            context.coordinator.performProgrammaticUpdate {
                textView.text = text
            }
        }
        context.coordinator.applyHybridAttributes(to: textView)
    }

    final class Coordinator: NSObject, UITextViewDelegate {
        var parent: NativeMarkdownTextView
        private var programmaticUpdateDepth = 0
        private var lastStyledText: String?
        private var lastStyledActiveLineIndex: Int?

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

        func textViewDidChange(_ textView: UITextView) {
            guard !isPerformingProgrammaticUpdate else { return }
            parent.text = textView.text
            parent.selection = EditorSelection(range: textView.selectedRange)
            lastStyledText = nil
            applyHybridAttributes(to: textView)
        }

        func textViewDidChangeSelection(_ textView: UITextView) {
            guard !isPerformingProgrammaticUpdate else { return }
            let newSelection = EditorSelection(range: textView.selectedRange)
            guard parent.selection != newSelection else { return }
            applyHybridAttributes(to: textView)
            parent.selection = newSelection
        }

        func applyHybridAttributes(to textView: UITextView) {
            guard shouldRestyle(textView.text) else { return }

            let selectedRange = textView.selectedRange
            performProgrammaticUpdate {
                MarkdownTextStyler.apply(
                    to: textView.textStorage,
                    activeLineIndex: parent.activeLineIndex,
                    backgroundColor: .systemBackground,
                    activeLineBackgroundColor: .systemBlue.withAlphaComponent(0.10),
                    textColor: .label,
                    secondaryTextColor: .secondaryLabel,
                    accentColor: .systemBlue
                )
                if textView.selectedRange != selectedRange,
                   selectedRange.location <= textView.text.utf16.count {
                    textView.selectedRange = selectedRange
                }
            }

            lastStyledText = textView.text
            lastStyledActiveLineIndex = parent.activeLineIndex
        }

        func performProgrammaticUpdate(_ updates: () -> Void) {
            programmaticUpdateDepth += 1
            defer {
                programmaticUpdateDepth -= 1
            }
            updates()
        }

        private var isPerformingProgrammaticUpdate: Bool {
            programmaticUpdateDepth > 0
        }

        private func shouldRestyle(_ text: String) -> Bool {
            lastStyledText != text || lastStyledActiveLineIndex != parent.activeLineIndex
        }
    }
}
#endif

private enum MarkdownTextStyler {
    #if os(macOS)
    typealias PlatformColor = NSColor
    #elseif os(iOS)
    typealias PlatformColor = UIColor
    #endif

    static func apply(
        to textStorage: NSTextStorage,
        activeLineIndex: Int,
        backgroundColor: PlatformColor,
        activeLineBackgroundColor: PlatformColor,
        textColor: PlatformColor,
        secondaryTextColor: PlatformColor,
        accentColor: PlatformColor
    ) {
        let source = textStorage.string as NSString
        let fullRange = NSRange(location: 0, length: source.length)
        guard fullRange.length > 0 else { return }

        textStorage.beginEditing()
        textStorage.setAttributes(baseAttributes(textColor: textColor), range: fullRange)

        for line in lines(in: source as String) {
            if line.index == activeLineIndex {
                textStorage.addAttributes([
                    .backgroundColor: activeLineBackgroundColor,
                    .font: monospacedFont(size: 14, weight: .regular)
                ], range: line.range)
                styleSourceLineMarkers(in: textStorage, line: line, secondaryTextColor: secondaryTextColor)
            } else {
                styleRenderedLine(
                    in: textStorage,
                    line: line,
                    source: source,
                    textColor: textColor,
                    secondaryTextColor: secondaryTextColor,
                    accentColor: accentColor
                )
            }
        }

        textStorage.endEditing()
    }

    private static func styleRenderedLine(
        in textStorage: NSTextStorage,
        line: EditorLine,
        source: NSString,
        textColor: PlatformColor,
        secondaryTextColor: PlatformColor,
        accentColor: PlatformColor
    ) {
        guard line.range.length > 0 else { return }

        let rawLine = source.substring(with: line.range)
        let trimmed = rawLine.trimmingCharacters(in: .whitespaces)

        if let heading = headingPrefixRange(in: rawLine, lineRange: line.range) {
            textStorage.addAttributes([
                .foregroundColor: secondaryTextColor,
                .font: monospacedFont(size: 12, weight: .regular)
            ], range: heading.markerRange)
            textStorage.addAttributes([
                .font: systemFont(size: headingFontSize(level: heading.level), weight: .semibold)
            ], range: heading.textRange)
            return
        }

        if trimmed.hasPrefix("- [x] ") || trimmed.hasPrefix("- [X] ") || trimmed.hasPrefix("- [ ] ") {
            textStorage.addAttributes([
                .foregroundColor: secondaryTextColor,
                .font: monospacedFont(size: 13, weight: .regular)
            ], range: NSRange(location: line.range.location, length: min(6, line.range.length)))
        }

        styleInlineMarkdown(
            in: textStorage,
            line: line,
            textColor: textColor,
            secondaryTextColor: secondaryTextColor,
            accentColor: accentColor
        )
    }

    private static func styleInlineMarkdown(
        in textStorage: NSTextStorage,
        line: EditorLine,
        textColor: PlatformColor,
        secondaryTextColor: PlatformColor,
        accentColor: PlatformColor
    ) {
        applyRegex("\\*\\*([^*]+)\\*\\*", in: textStorage, line: line) { match in
            guard match.numberOfRanges > 1 else { return }
            textStorage.addAttributes([.font: systemFont(size: 14, weight: .semibold)], range: match.range(at: 1))
            markdownDelimiterRanges(match.range, leading: 2, trailing: 2).forEach {
                textStorage.addAttributes([.foregroundColor: secondaryTextColor], range: $0)
            }
        }

        applyRegex("(?<!\\*)\\*([^*]+)\\*(?!\\*)", in: textStorage, line: line) { match in
            guard match.numberOfRanges > 1 else { return }
            textStorage.addAttributes([.font: italicSystemFont(size: 14)], range: match.range(at: 1))
            markdownDelimiterRanges(match.range, leading: 1, trailing: 1).forEach {
                textStorage.addAttributes([.foregroundColor: secondaryTextColor], range: $0)
            }
        }

        applyRegex("\\[([^\\]]+)\\]\\(([^\\)]+)\\)", in: textStorage, line: line) { match in
            guard match.numberOfRanges > 2 else { return }
            textStorage.addAttributes([
                .foregroundColor: accentColor,
                .underlineStyle: NSUnderlineStyle.single.rawValue
            ], range: match.range(at: 1))
            let markerRanges = [
                NSRange(location: match.range.location, length: 1),
                NSRange(location: match.range(at: 1).upperBound, length: 2),
                NSRange(location: match.range(at: 2).upperBound, length: 1)
            ]
            markerRanges.forEach {
                textStorage.addAttributes([.foregroundColor: secondaryTextColor], range: $0)
            }
            textStorage.addAttributes([.foregroundColor: secondaryTextColor], range: match.range(at: 2))
        }
    }

    private static func styleSourceLineMarkers(
        in textStorage: NSTextStorage,
        line: EditorLine,
        secondaryTextColor: PlatformColor
    ) {
        applyRegex("(```|#{1,6}|\\*\\*|\\*|\\[[ xX]\\]|\\[|\\]|\\(|\\))", in: textStorage, line: line) { match in
            textStorage.addAttributes([.foregroundColor: secondaryTextColor], range: match.range)
        }
    }

    private static func baseAttributes(textColor: PlatformColor) -> [NSAttributedString.Key: Any] {
        let paragraph = NSMutableParagraphStyle()
        paragraph.lineSpacing = 4
        paragraph.paragraphSpacing = 3
        return [
            .font: systemFont(size: 14, weight: .regular),
            .foregroundColor: textColor,
            .paragraphStyle: paragraph
        ]
    }

    private static func lines(in source: String) -> [EditorLine] {
        EditorState(document: EditorDocument(url: URL(fileURLWithPath: "/dev/null"), title: "", source: source)).lines
    }

    private static func headingPrefixRange(
        in rawLine: String,
        lineRange: NSRange
    ) -> (level: Int, markerRange: NSRange, textRange: NSRange)? {
        let markerCount = rawLine.prefix { $0 == "#" }.count
        guard (1...6).contains(markerCount),
              rawLine.dropFirst(markerCount).first == " "
        else { return nil }

        let textOffset = markerCount + 1
        return (
            markerCount,
            NSRange(location: lineRange.location, length: markerCount),
            NSRange(location: lineRange.location + textOffset, length: max(0, lineRange.length - textOffset))
        )
    }

    private static func applyRegex(
        _ pattern: String,
        in textStorage: NSTextStorage,
        line: EditorLine,
        handler: (NSTextCheckingResult) -> Void
    ) {
        guard line.range.length > 0,
              let regex = try? NSRegularExpression(pattern: pattern)
        else { return }

        regex.matches(in: textStorage.string, range: line.range).forEach(handler)
    }

    private static func markdownDelimiterRanges(
        _ fullRange: NSRange,
        leading: Int,
        trailing: Int
    ) -> [NSRange] {
        [
            NSRange(location: fullRange.location, length: leading),
            NSRange(location: fullRange.upperBound - trailing, length: trailing)
        ]
    }

    private static func headingFontSize(level: Int) -> CGFloat {
        switch level {
        case 1: 24
        case 2: 20
        case 3: 18
        default: 15
        }
    }

    #if os(macOS)
    private static func systemFont(size: CGFloat, weight: NSFont.Weight) -> NSFont {
        NSFont.systemFont(ofSize: size, weight: weight)
    }

    private static func italicSystemFont(size: CGFloat) -> NSFont {
        NSFontManager.shared.convert(NSFont.systemFont(ofSize: size), toHaveTrait: .italicFontMask)
    }

    private static func monospacedFont(size: CGFloat, weight: NSFont.Weight) -> NSFont {
        NSFont.monospacedSystemFont(ofSize: size, weight: weight)
    }
    #elseif os(iOS)
    private static func systemFont(size: CGFloat, weight: UIFont.Weight) -> UIFont {
        UIFont.systemFont(ofSize: size, weight: weight)
    }

    private static func italicSystemFont(size: CGFloat) -> UIFont {
        UIFont.italicSystemFont(ofSize: size)
    }

    private static func monospacedFont(size: CGFloat, weight: UIFont.Weight) -> UIFont {
        UIFont.monospacedSystemFont(ofSize: size, weight: weight)
    }
    #endif
}

private extension NSRange {
    var upperBound: Int {
        location + length
    }
}

private var platformTextBackground: Color {
    #if os(macOS)
    Color(nsColor: .textBackgroundColor)
    #else
    Color(uiColor: .systemBackground)
    #endif
}
