import Foundation
import SaplingCore

public enum EditorLineMode: String, Hashable, Codable, Sendable {
    case source
    case rendered
}

public protocol EditorView {
    var state: EditorState { get }
}

public struct EditorDocument: Identifiable, Hashable, Codable, Sendable {
    public var id: UUID
    public var url: URL
    public var title: String
    public var source: String
    public var lastSavedSource: String

    public init(
        id: UUID = UUID(),
        url: URL,
        title: String,
        source: String,
        lastSavedSource: String? = nil
    ) {
        self.id = id
        self.url = url
        self.title = title
        self.source = source
        self.lastSavedSource = lastSavedSource ?? source
    }

    public init(markdownDocument: MarkdownDocument) {
        self.init(
            id: markdownDocument.id,
            url: markdownDocument.url,
            title: markdownDocument.title,
            source: markdownDocument.content
        )
    }

    public var hasUnsavedChanges: Bool {
        source != lastSavedSource
    }

    public var markdownDocument: MarkdownDocument {
        MarkdownDocument(id: id, url: url, title: title, content: source)
    }
}

public struct EditorLine: Identifiable, Hashable, Sendable {
    public var id: Int { index }
    public var index: Int
    public var source: String
    public var range: NSRange
    public var mode: EditorLineMode

    public init(index: Int, source: String, range: NSRange, mode: EditorLineMode) {
        self.index = index
        self.source = source
        self.range = range
        self.mode = mode
    }
}

public struct EditorSelection: Hashable, Codable, Sendable {
    public var location: Int
    public var length: Int

    public init(location: Int = 0, length: Int = 0) {
        self.location = max(0, location)
        self.length = max(0, length)
    }

    public init(range: NSRange) {
        self.init(location: range.location, length: range.length)
    }

    public var range: NSRange {
        NSRange(location: location, length: length)
    }
}

public struct EditorState: Hashable, Sendable {
    public var document: EditorDocument
    public var lines: [EditorLine]
    public var selection: EditorSelection
    public var activeLineIndex: Int

    public init(
        document: EditorDocument,
        selection: EditorSelection = EditorSelection(),
        activeLineIndex: Int = 0
    ) {
        self.document = document
        self.selection = selection
        self.activeLineIndex = activeLineIndex
        self.lines = Self.makeLines(
            from: document.source,
            activeLineIndex: activeLineIndex
        )
    }

    public var hasUnsavedChanges: Bool {
        document.hasUnsavedChanges
    }

    public mutating func updateSource(_ source: String) {
        document.source = source
        activeLineIndex = Self.lineIndex(containing: selection.location, in: source)
        lines = Self.makeLines(from: source, activeLineIndex: activeLineIndex)
    }

    public mutating func updateSelection(_ selection: EditorSelection) {
        self.selection = selection
        activeLineIndex = Self.lineIndex(containing: selection.location, in: document.source)
        lines = Self.makeLines(from: document.source, activeLineIndex: activeLineIndex)
    }

    public mutating func markSaved() {
        document.lastSavedSource = document.source
    }

    private static func makeLines(from source: String, activeLineIndex: Int) -> [EditorLine] {
        var lines: [EditorLine] = []
        var lineStart = source.startIndex
        var utf16Location = 0
        var index = 0

        while lineStart < source.endIndex {
            let lineEnd = source[lineStart...].firstIndex(of: "\n") ?? source.endIndex
            let line = String(source[lineStart..<lineEnd])
            let length = line.utf16.count
            lines.append(EditorLine(
                index: index,
                source: line,
                range: NSRange(location: utf16Location, length: length),
                mode: index == activeLineIndex ? .source : .rendered
            ))

            if lineEnd == source.endIndex {
                lineStart = lineEnd
                utf16Location += length
            } else {
                lineStart = source.index(after: lineEnd)
                utf16Location += length + 1
            }
            index += 1
        }

        if source.isEmpty || source.hasSuffix("\n") {
            lines.append(EditorLine(
                index: index,
                source: "",
                range: NSRange(location: utf16Location, length: 0),
                mode: index == activeLineIndex ? .source : .rendered
            ))
        }

        return lines
    }

    private static func lineIndex(containing location: Int, in source: String) -> Int {
        let clampedLocation = max(0, min(location, source.utf16.count))
        var currentLocation = 0

        for (index, line) in source.split(separator: "\n", omittingEmptySubsequences: false).enumerated() {
            let length = line.utf16.count
            if clampedLocation <= currentLocation + length {
                return index
            }
            currentLocation += length + 1
        }

        return 0
    }
}

public protocol EditorRenderer: Sendable {
    func renderedLine(for line: EditorLine) -> AttributedString
}

@MainActor
public protocol EditorCoordinator: AnyObject {
    var state: EditorState { get }

    func replaceDocument(_ document: EditorDocument)
    func updateSource(_ source: String)
    func updateSelection(_ selection: EditorSelection)
    func save() throws
}
