import Foundation

public struct EditableRegion: Hashable, Sendable {
    public var lineIndexes: [Int]

    public init(lineIndexes: some Sequence<Int>) {
        self.lineIndexes = Array(Set(lineIndexes)).sorted()
    }

    public var primaryLineIndex: Int {
        lineIndexes.first ?? -1
    }

    public var isEmpty: Bool {
        lineIndexes.isEmpty
    }

    public func contains(_ lineIndex: Int) -> Bool {
        lineIndexes.binarySearch(lineIndex)
    }

    public static func none() -> EditableRegion {
        EditableRegion(lineIndexes: [])
    }

    public static func selection(_ selection: NSRange, in lineIndex: DocumentLineIndex) -> EditableRegion {
        guard lineIndex.lineCount > 0 else {
            return .none()
        }

        let sourceLength = lineIndex.source.utf16.count
        let startLocation = max(0, min(selection.location, sourceLength))
        let endLocation: Int
        if selection.length == 0 {
            endLocation = startLocation
        } else {
            endLocation = max(startLocation, min(selection.upperBound - 1, sourceLength))
        }

        let startLine = lineIndex.lineIndex(containing: startLocation)
        let endLine = lineIndex.lineIndex(containing: endLocation)
        let selectedRange = min(startLine, endLine)...max(startLine, endLine)
        var editableLines = Set(selectedRange)
        guard lineIndex.source.contains("```") || lineIndex.source.contains("~~~") else {
            return EditableRegion(lineIndexes: editableLines)
        }

        for selectedLine in selectedRange {
            if let codeBlockRange = lineIndex.fencedCodeBlockLineRange(containing: selectedLine) {
                editableLines.formUnion(codeBlockRange)
            }
        }

        return EditableRegion(lineIndexes: editableLines)
    }
}

private extension DocumentLineIndex {
    func fencedCodeBlockLineRange(containing lineIndex: Int) -> ClosedRange<Int>? {
        guard (0..<lineCount).contains(lineIndex) else { return nil }

        var openLineIndex: Int?
        var openFence: String?
        for currentLineIndex in 0..<lineCount {
            guard let line = editorLine(at: currentLineIndex, activeLineIndex: -1),
                  let fence = Self.fenceMarker(in: line.source)
            else { continue }

            if let start = openLineIndex {
                if fence == openFence {
                    let blockRange = start...currentLineIndex
                    return blockRange.contains(lineIndex) ? blockRange : nil
                }
            } else {
                openLineIndex = currentLineIndex
                openFence = fence
            }
        }

        if let start = openLineIndex {
            let blockRange = start...max(start, lineCount - 1)
            return blockRange.contains(lineIndex) ? blockRange : nil
        }
        return nil
    }

    static func fenceMarker(in source: String) -> String? {
        let indentation = source.prefix { $0 == " " || $0 == "\t" }
        guard indentation.count <= 3 else { return nil }

        let content = source.dropFirst(indentation.count)
        if content.hasPrefix("```") {
            return "```"
        }
        if content.hasPrefix("~~~") {
            return "~~~"
        }
        return nil
    }
}

private extension Array where Element == Int {
    func binarySearch(_ value: Int) -> Bool {
        var lowerBound = 0
        var upperBound = count - 1
        while lowerBound <= upperBound {
            let midpoint = (lowerBound + upperBound) / 2
            if self[midpoint] == value {
                return true
            }
            if self[midpoint] < value {
                lowerBound = midpoint + 1
            } else {
                upperBound = midpoint - 1
            }
        }
        return false
    }
}

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