""" This code is based on the Eclipse/Lemminx XML language server implementation:
https://github.com/eclipse/lemminx/tree/master/org.eclipse.lemminx/src/main/java/org/eclipse/lemminx/dom

Only the minimum subset of the XML dialect used by Galaxy tool wrappers is supported.
"""

from typing import (
    Callable,
    List,
)

from lsprotocol.types import (
    Position,
    Range,
)
from pygls.workspace import Document

from .constants import (
    _LAN,
    NEW_LINE,
    WHITESPACE_CHARS,
)


def convert_document_offset_to_line(document: Document, offset: int) -> int:
    """Converts the given offset in the document to the corresponding line.

    Args:
        document (Document): The source document.
        offset (int): The character offset inside the document.

    Returns:
        int: The resulting document line.
    """
    line = max(document.source.count(NEW_LINE, 0, offset), 0)
    return line


def convert_document_offset_to_position(document: Document, offset: int) -> Position:
    """Converts the given offset in the document to a line/character based Position.

    Args:
        document (Document): The source document.
        offset (int): The character offset inside the document.

    Returns:
        Position: The resulting Position with line and character offset.
    """
    line = convert_document_offset_to_line(document, offset)
    line_offset = max(document.source.rfind(NEW_LINE, 0, offset), 0)
    character = offset - line_offset
    if line > 0:
        character -= 1
    return Position(line=line, character=character)


def convert_document_offsets_to_range(document: Document, start_offset: int, end_offset: int) -> Range:
    """Converts the given start and end offset positions in the document to a
    position Range based on line numbers.

    Args:
        start_offset (int): The start offset of the range
        end_offset (int): The end offset of the range

    Returns:
        Range: The resulting Range with the correct line number and character offset
    """
    return Range(
        start=convert_document_offset_to_position(document, start_offset),
        end=convert_document_offset_to_position(document, end_offset),
    )


class MultiLineStream:
    """Represents a multi-line stream of characters."""

    def __init__(self, source: str, position: int = 0) -> None:
        self._source = source
        self._position = position
        self._len = len(source)

    def eos(self) -> bool:
        """Indicates that the stream is at the end position."""
        return self._len <= self._position

    def get_source(self) -> str:
        """Gets the source text."""
        return self._source

    def pos(self) -> int:
        """Gets the current stream position."""
        return self._position

    def advance(self, n: int) -> None:
        """Advances the stream given number of positions."""
        self._position += n

    def go_to_end(self) -> None:
        """Sets the stream position to the end of the stream."""
        self._position = self._len

    def peek_char(self, n: int = 0) -> int:
        """Gets the value of the next position in the stream without advancing it."""
        try:
            return ord(self._source[self._position + n])
        except IndexError:
            return -1

    def advance_if_char(self, ch: int) -> bool:
        """If the next character in the stream matches the given character, the stream advances one position."""
        if ch == self.peek_char():
            self._position += 1
            return True
        return False

    def advance_if_chars(self, ch: List[int]) -> bool:
        """If the next characters in the stream matches the given sequence of characters, the stream advances
        the length of the sequence."""
        ch_length = len(ch)
        if self._position + ch_length > self._len:
            return False
        i = 0
        for i in range(ch_length):
            if self._source[self._position + i] != chr(ch[i]):
                return False
        self.advance(i)
        return True

    def advance_until_char(self, ch: int) -> bool:
        """Advances the stream until it founds a character matching the given."""
        while self._position < self._len:
            if ord(self._source[self._position]) == ch:
                return True
            self.advance(1)
        return False

    def advance_until_chars(self, ch: List[int]) -> bool:
        """Advances the stream until it founds a list of character matching the given."""
        ch_length = len(ch)
        while self._position + ch_length <= self._len:
            i = 0
            while i < ch_length and self._source[self._position + i] == chr(ch[i]):
                i += 1
            if i == ch_length:
                return True
            self.advance(i or 1)
        self.go_to_end()
        return False

    def advance_while_char_in(self, list: List[int]) -> int:
        """Advances the stream if the characters are like any of the characters in the given list."""
        pos_now = self._position
        while self._position < self._len and ord(self._source[self._position]) in list:
            self._position += 1
        return self._position - pos_now

    def advance_until_char_or_new_tag(self, ch: int) -> bool:
        """Advances the stream until it finds the given character or the '<' (new tag character)."""
        while self._position < self._len:
            if self.peek_char() in [ch, _LAN]:
                return True
            self.advance(1)
        return False

    def advance_until_chars_or_new_tag(self, ch: List[int]) -> bool:
        """Advances the stream until it finds the given sequence of characters or the '<' (new tag character)."""
        ch_length = len(ch)
        while self._position + ch_length <= self._len:
            i = 0
            if self.peek_char() == _LAN:
                return True
            while i < ch_length and self._source[self._position + i] == chr(ch[i]):
                i += 1
            if i == ch_length:
                return True
            self.advance(i or 1)
        self.go_to_end()
        return False

    def advance_while_char(self, predicate: Callable[[str], bool]) -> int:
        """Advances the stream while the given condition is True."""
        pos_now = self._position
        while self._position < self._len and predicate(self._source[self._position]):
            self._position += 1
        return self._position - pos_now

    def skip_whitespace(self) -> bool:
        """Advances the stream while any kind of white space character is found."""
        n = self.advance_while_char_in(WHITESPACE_CHARS)
        return n > 0
