import typing as t
from enum import Enum

from . import exceptions as ex


class TokenType(Enum):
    COMMENT = 0
    RAW_VARIABLE = 1
    SECTION = 2
    INVERTED_SECTION = 3
    END_SECTION = 4
    DELIMITER = 5
    PARTIAL = 6
    VARIABLE = 8
    LITERAL = 9

    def __lt__(self, other: t.Any) -> bool:
        """
        From: https://stackoverflow.com/a/39269589
        """
        if self.__class__ is other.__class__:
            return self.value < other.value
        return NotImplemented


class TokenTuple(t.NamedTuple):
    type: TokenType
    data: str
    offset: int  # used by partials to compute indentation.


def mustache_tokenizer(
    text: str,
    tags: t.Tuple[str, str] = (R"{{", R"}}"),
) -> t.List[TokenTuple]:
    # Different tokenizers to deal with the stupid delimiter swaps
    # text = text.encode()
    res_list = []
    start_tag, end_tag = tags
    end_literal = "}" + end_tag
    end_switch = "=" + end_tag
    cursor_loc = 0
    newline_offset = 0
    seen_tag_in_current_line = False

    while cursor_loc < len(text):
        next_tag_loc = text.find(start_tag, cursor_loc)
        # If we're at the tag location, yield it
        if cursor_loc == next_tag_loc:
            end_tag_to_search = end_tag

            tag_type_loc = cursor_loc + len(start_tag)
            offset = 1
            # '!': 'comment',
            # '#': 'section',
            # '^': 'inverted section',
            # '/': 'end',
            # '>': 'partial',
            # '=': 'set delimiter?',
            # '{': 'no escape?',
            # '&': 'no escape'
            if tag_type_loc >= len(text):
                raise ex.MystaceError("Tag not closed.")
            elif text[tag_type_loc] == "!":
                new_token_type = TokenType.COMMENT
            elif text[tag_type_loc] == "#":
                new_token_type = TokenType.SECTION
            elif text[tag_type_loc] == "^":
                new_token_type = TokenType.INVERTED_SECTION
            elif text[tag_type_loc] == "/":
                new_token_type = TokenType.END_SECTION
            elif text[tag_type_loc] == ">":
                new_token_type = TokenType.PARTIAL
            elif text[tag_type_loc] == "=":
                new_token_type = TokenType.DELIMITER
                end_tag_to_search = end_switch
                offset = 1
            elif text[tag_type_loc] in ("{", "&"):
                new_token_type = TokenType.RAW_VARIABLE
                if text[tag_type_loc] == "{":
                    end_tag_to_search = end_literal
            else:
                # Just a variable
                new_token_type = TokenType.VARIABLE
                offset = 0

            # Search for end tag starting after the start tag and any offset
            search_start = cursor_loc + len(start_tag) + offset
            end_loc = text.find(end_tag_to_search, search_start)

            if end_loc == -1:
                raise ex.MystaceError("Tag not closed.")

            # This handles the "inline indentation" case, where a partial
            # doesn't get indented if it is not the first tag on a line
            if seen_tag_in_current_line:
                effective_newline_offset = 0
            else:
                effective_newline_offset = newline_offset

            # yield
            token_content = text[cursor_loc + len(start_tag) + offset : end_loc].strip()
            res_list.append(
                TokenTuple(
                    new_token_type,
                    token_content,
                    effective_newline_offset,
                )
            )
            cursor_loc = len(end_tag_to_search) + end_loc
            seen_tag_in_current_line = True

            # Handle delimiter changes
            if new_token_type == TokenType.DELIMITER:
                # Parse new delimiters from token content
                parts = token_content.split()
                if len(parts) != 2:
                    raise ex.DelimiterError(
                        f"Delimiter tag must contain exactly 2 delimiters, got {len(parts)}"
                    )
                start_tag, end_tag = parts
                # Update all the computed end tags
                end_literal = "}" + end_tag
                end_switch = "=" + end_tag

        # Otherwise, yield the next literal, ending at newlines as-necessary
        else:
            next_literal_end = len(text)
            next_newline_loc = text.find("\n", cursor_loc)

            if next_newline_loc != -1:
                next_literal_end = min(next_literal_end, next_newline_loc + 1)

            if next_tag_loc != -1:
                next_literal_end = min(next_literal_end, next_tag_loc)

            literal_text = text[cursor_loc:next_literal_end]

            res_list.append(TokenTuple(TokenType.LITERAL, literal_text, newline_offset))

            if literal_text.endswith("\n"):
                newline_offset = 0
                seen_tag_in_current_line = False
            else:
                newline_offset += len(literal_text)

            cursor_loc = next_literal_end
    return res_list
