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 lineIndex: DocumentLineIndex
    public var selection: EditorSelection
    public var activeLineIndex: Int

    public var lines: [EditorLine] {
        lineIndex.editorLines(activeLineIndex: activeLineIndex)
    }

    public var lineCount: Int {
        lineIndex.lineCount
    }

    public init(
        document: EditorDocument,
        selection: EditorSelection = EditorSelection(),
        activeLineIndex: Int = 0
    ) {
        self.document = document
        self.selection = selection
        self.activeLineIndex = activeLineIndex
        self.lineIndex = DocumentLineIndex(source: document.source)
    }

    public var hasUnsavedChanges: Bool {
        document.hasUnsavedChanges
    }

    public var activeColumnNumber: Int {
        guard let activeLineRange = lineIndex.lineContentRange(forLine: activeLineIndex) else { return 1 }
        let offset = selection.location - activeLineRange.location
        return max(0, min(offset, activeLineRange.length)) + 1
    }

    public mutating func updateSource(_ source: String) {
        document.source = source
        selection = EditorActiveLineTracker.clampedSelection(selection, in: source)
        lineIndex = DocumentLineIndex(source: source)
        activeLineIndex = lineIndex.lineIndex(containing: selection.location)
    }

    public mutating func updateSource(
        _ source: String,
        edit: DocumentLineIndexEdit,
        selection newSelection: EditorSelection? = nil
    ) {
        lineIndex.replace(edit, updatedSource: source)
        document.source = lineIndex.source
        selection = EditorActiveLineTracker.clampedSelection(newSelection ?? selection, in: document.source)
        activeLineIndex = lineIndex.lineIndex(containing: selection.location)
    }

    public mutating func updateSelection(_ selection: EditorSelection) {
        self.selection = EditorActiveLineTracker.clampedSelection(selection, in: document.source)
        activeLineIndex = lineIndex.lineIndex(containing: self.selection.location)
    }

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

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
}
