/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2025-2025. All rights resvered.
 */
package commonmark4cj.commonmark

/**
 * A source span references a snippet of text from the source input.
 * <p>
 * It has a starting position (line and column index) and a length of how many characters it spans.
 * <p>
 * For example, this CommonMark source text:
 * <pre><code>
 * &gt; foo
 * </code></pre>
 * The {@link BlockQuote} node would have this source span: line 0, column 0, length 5.
 * <p>
 * The {@link Paragraph} node inside it would have: line 0, column 2, length 3.
 * <p>
 * If a block has multiple lines, it will have a source span for each line.
 * <p>
 * Note that the column index and length are measured in Java characters (UTF-16 code units). If you're outputting them
 * to be consumed by another programming language, e.g. one that uses UTF-8 strings, you will need to translate them,
 * otherwise characters such as emojis will result in incorrect positions.
 */
public class SourceSpan <: Equatable<SourceSpan> & ToString {
    private let lineIndex: Int
    private let columnIndex: Int
    private let inputIndex: Int
    private let length: Int

    public static func of(line: Int, col: Int, input: Int, length: Int): SourceSpan {
        return SourceSpan(line, col, input, length)
    }

    /**
     * @deprecated Use {{@link #of(Int, Int, Int, Int)}} instead to also specify input index. Using the deprecated one
     * will set {@link #inputIndex} to 0.
     */
    // @Deprecated
    public static func of(lineIndex: Int, columnIndex: Int, length: Int): SourceSpan {
        return of(lineIndex, columnIndex, 0, length)
    }

    private SourceSpan(lineIndex: Int, columnIndex: Int, inputIndex: Int, length: Int) {
        if (lineIndex < 0) {
            throw IllegalArgumentException("lineIndex ${lineIndex} must be >= 0")
        }
        if (columnIndex < 0) {
            throw IllegalArgumentException("columnIndex ${columnIndex} must be >= 0")
        }
        if (inputIndex < 0) {
            throw IllegalArgumentException("inputIndex ${inputIndex} must be >= 0")
        }
        if (length < 0) {
            throw IllegalArgumentException("length ${length} must be >= 0")
        }
        this.lineIndex = lineIndex
        this.columnIndex = columnIndex
        this.inputIndex = inputIndex
        this.length = length
    }

    /**
     * @return 0-based line index, e.g. 0 for first line, 1 for the second line, etc
     */
    public func getLineIndex(): Int {
        return lineIndex
    }

    /**
     * @return 0-based index of column (character on line) in source, e.g. 0 for the first character of a line, 1 for
     * the second character, etc
     */
    public func getColumnIndex(): Int {
        return columnIndex
    }

    /**
     * @return 0-based index in whole input
     */
    public func getInputIndex(): Int {
        return inputIndex
    }

    /**
     * @return length of the span in characters
     */
    public func getLength(): Int {
        return length
    }

    public func subSpan(beginIndex: Int): SourceSpan {
        return subSpan(beginIndex, length)
    }

    public func subSpan(beginIndex: Int, endIndex: Int): SourceSpan {
        if (beginIndex < 0) {
            throw IndexOutOfBoundsException("beginIndex ${beginIndex} + must be >= 0")
        }
        if (beginIndex > length) {
            throw IndexOutOfBoundsException("beginIndex ${beginIndex} must be <= length ${length}")
        }
        if (endIndex < 0) {
            throw IndexOutOfBoundsException("endIndex ${endIndex} + must be >= 0")
        }
        if (endIndex > length) {
            throw IndexOutOfBoundsException("endIndex ${endIndex} must be <= length ${length}")
        }
        if (beginIndex > endIndex) {
            throw IndexOutOfBoundsException("beginIndex ${beginIndex} must be <= endIndex ${endIndex}")
        }
        if (beginIndex == 0 && endIndex == length) {
            return this
        }
        return SourceSpan(lineIndex, columnIndex + beginIndex, inputIndex + beginIndex, endIndex - beginIndex)
    }

    public operator func ==(that: SourceSpan): Bool {
        if (refEq(this, that)) {
            return true
        }
        return lineIndex == that.lineIndex && columnIndex == that.columnIndex && inputIndex == that.inputIndex &&
            length == that.length
    }
    public operator func !=(that: SourceSpan): Bool {
        !(this == that)
    }

    // @Override
    // public func hashCode() : Int  {
    //     return Objects.hash(lineIndex, columnIndex, inputIndex, length)
    // }

    public func toString(): String {
        return "SourceSpan{line=${lineIndex}, column=${columnIndex}, input=${inputIndex}, length=${length}}"
    }
}

/**
 * A list of source spans that can be added to. Takes care of merging adjacent source spans.
 */
public class SourceSpans {
    private var sourceSpans: ?ArrayList<SourceSpan> = None

    public static func empty(): SourceSpans {
        return SourceSpans()
    }

    @Frozen
    public func getSourceSpans(): ArrayList<SourceSpan> {
        return sourceSpans ?? ArrayList()
    }

    @Frozen
    public func addAllFrom(nodes: Collection<Node>): Unit {
        for (node in nodes) {
            addAll(node.getSourceSpans())
        }
    }

    @Frozen
    public func addAllFromTexts(nodes: ReadOnlyList<Text>): Unit {
        for (node in nodes) {
            addAll(node.getSourceSpans())
        }
    }

    @Frozen
    public func addAll(other: ReadOnlyList<SourceSpan>): Unit {
        if (other.isEmpty()) {
            return
        }

        let sourceSpans = sourceSpans ?? ArrayList()
        this.sourceSpans = sourceSpans

        if (sourceSpans.isEmpty()) {
            sourceSpans.add(all: other)
        } else {
            let lastIndex: Int = sourceSpans.size - 1
            let a: SourceSpan = sourceSpans[lastIndex]
            let b: SourceSpan = other[0]
            if (a.getInputIndex() + a.getLength() == b.getInputIndex()) {
                sourceSpans[lastIndex] = SourceSpan.of(a.getLineIndex(), a.getColumnIndex(), a.getInputIndex(),
                    a.getLength() + b.getLength())
                for (i in 1..other.size) {
                    sourceSpans.add(other[i])
                }
            } else {
                sourceSpans.add(all: other)
            }
        }
    }
}

/**
 * SourcePosition within a {@link Scanner}. This is intentionally kept opaque so as not to expose the internal structure of
 * the Scanner.
 */
public class SourcePosition {
    public SourcePosition(public let lineIndex: Int, public let index: Int) {}
}

extend SourcePosition <: ToString {
    public func toString(): String {
        return "{lineIndex=${lineIndex}, index=${index}}"
    }
}

/**
 * Whether to include {@link org.commonmark.node.SourceSpan} or not while parsing,
 * see {@link Parser.Builder#includeSourceSpans(IncludeSourceSpans)}.
 */
public enum IncludeSourceSpans <: Equatable<IncludeSourceSpans> {
    /**
     * Do not include source spans.
     */
    | NONE
    /**
     * Include source spans on {@link org.commonmark.node.Block} nodes.
     */
    | BLOCKS
    /**
     * Include source spans on block nodes and inline nodes.
     */
    | BLOCKS_AND_INLINES

    public operator func ==(other: IncludeSourceSpans): Bool {
        match ((this, other)) {
            case (BLOCKS_AND_INLINES, BLOCKS_AND_INLINES) => true
            case (BLOCKS, BLOCKS) => true
            case (NONE, NONE) => true
            case _ => false
        }
    }
    public prop need: Bool {
        get() {
            match (this) {
                case NONE => false
                case _ => true
            }
        }
    }
}