# Extracted and mutilated from https://github.com/lordwelch/wsfmt
# Which was extracted and mutliated from https://github.com/golang/go/tree/master/src/text/template/parse
from __future__ import annotations

import calendar
import os
import unicodedata
from enum import Enum, auto
from typing import Any, Callable


class ItemType(Enum):
    Error = auto()  # Error occurred; value is text of error
    EOF = auto()
    Text = auto()  # Text
    LeftParen = auto()
    Number = auto()  # Simple number
    IssueNumber = auto()  # Preceded by a # Symbol
    RightParen = auto()
    Space = auto()  # Run of spaces separating arguments
    Dot = auto()
    LeftBrace = auto()
    RightBrace = auto()
    LeftSBrace = auto()
    RightSBrace = auto()
    Symbol = auto()
    Skip = auto()  # __ or -- no title, issue or series information beyond
    Operator = auto()
    Calendar = auto()
    InfoSpecifier = auto()  # Specifies type of info e.g. v1 for 'volume': 1
    ArchiveType = auto()
    Honorific = auto()
    Keywords = auto()
    FCBD = auto()
    ComicType = auto()
    Publisher = auto()
    C2C = auto()


braces = [
    ItemType.LeftBrace,
    ItemType.LeftParen,
    ItemType.LeftSBrace,
    ItemType.RightBrace,
    ItemType.RightParen,
    ItemType.RightSBrace,
]

eof = chr(0)

key = {
    "fcbd": ItemType.FCBD,
    "freecomicbookday": ItemType.FCBD,
    "cbr": ItemType.ArchiveType,
    "cbz": ItemType.ArchiveType,
    "cbt": ItemType.ArchiveType,
    "cb7": ItemType.ArchiveType,
    "rar": ItemType.ArchiveType,
    "zip": ItemType.ArchiveType,
    "tar": ItemType.ArchiveType,
    "7z": ItemType.ArchiveType,
    "annual": ItemType.ComicType,
    "book": ItemType.ComicType,
    "volume": ItemType.InfoSpecifier,
    "vol.": ItemType.InfoSpecifier,
    "vol": ItemType.InfoSpecifier,
    "v": ItemType.InfoSpecifier,
    "of": ItemType.InfoSpecifier,
    "dc": ItemType.Publisher,
    "marvel": ItemType.Publisher,
    "covers": ItemType.InfoSpecifier,
    "c2c": ItemType.C2C,
    "mr": ItemType.Honorific,
    "ms": ItemType.Honorific,
    "mrs": ItemType.Honorific,
    "dr": ItemType.Honorific,
}


class Item:
    def __init__(self, typ: ItemType, pos: int, val: str) -> None:
        self.typ: ItemType = typ
        self.pos: int = pos
        self.val: str = val

    def __repr__(self) -> str:
        return f"{self.val}: index: {self.pos}: {self.typ}"


class Lexer:
    def __init__(self, string: str) -> None:
        self.input: str = string  # The string being scanned
        self.state: Callable[[Lexer], Callable | None] | None = None  # The next lexing function to enter
        self.pos: int = -1  # Current position in the input
        self.start: int = 0  # Start position of this item
        self.lastPos: int = 0  # Position of most recent item returned by nextItem
        self.paren_depth: int = 0  # Nesting depth of ( ) exprs
        self.brace_depth: int = 0  # Nesting depth of { }
        self.sbrace_depth: int = 0  # Nesting depth of [ ]
        self.items: list[Item] = []

    # Next returns the next rune in the input.
    def get(self) -> str:
        if int(self.pos) >= len(self.input) - 1:
            self.pos += 1
            return eof

        self.pos += 1
        return self.input[self.pos]

    # Peek returns but does not consume the next rune in the input.
    def peek(self) -> str:
        if int(self.pos) >= len(self.input) - 1:
            return eof

        return self.input[self.pos + 1]

    def backup(self) -> None:
        self.pos -= 1

    # Emit passes an item back to the client.
    def emit(self, t: ItemType) -> None:
        self.items.append(Item(t, self.start, self.input[self.start : self.pos + 1]))
        self.start = self.pos + 1

    # Ignore skips over the pending input before this point.
    def ignore(self) -> None:
        self.start = self.pos

    # Accept consumes the next rune if it's from the valid se:
    def accept(self, valid: str) -> bool:
        if self.get() in valid:
            return True

        self.backup()
        return False

    # AcceptRun consumes a run of runes from the valid set.
    def accept_run(self, valid: str) -> None:
        while self.get() in valid:
            pass

        self.backup()

    def scan_number(self) -> bool:
        digits = "0123456789"

        self.accept_run(digits)
        if self.accept("."):
            if self.accept(digits):
                self.accept_run(digits)
            else:
                self.backup()
        if self.accept("s"):
            if not self.accept("t"):
                self.backup()
        elif self.accept("nr"):
            if not self.accept("d"):
                self.backup()
        elif self.accept("t"):
            if not self.accept("h"):
                self.backup()

        return True

    # Runs the state machine for the lexer.
    def run(self) -> None:
        self.state = lex_filename
        while self.state is not None:
            self.state = self.state(self)


# Errorf returns an error token and terminates the scan by passing
# Back a nil pointer that will be the next state, terminating self.nextItem.
def errorf(lex: Lexer, message: str) -> Callable[[Lexer], Callable | None] | None:
    lex.items.append(Item(ItemType.Error, lex.start, message))
    return None


# Scans the elements inside action delimiters.
def lex_filename(lex: Lexer) -> Callable[[Lexer], Callable | None] | None:
    r = lex.get()
    if r == eof:
        if lex.paren_depth != 0:
            return errorf(lex, "unclosed left paren")

        if lex.brace_depth != 0:
            return errorf(lex, "unclosed left paren")
        lex.emit(ItemType.EOF)
        return None
    elif is_space(r):
        if r == "_" and lex.peek() == "_":
            lex.get()
            lex.emit(ItemType.Skip)
        else:
            return lex_space
    elif r == ".":
        r = lex.peek()
        if r < "0" or "9" < r:
            lex.emit(ItemType.Dot)
            return lex_filename

        lex.backup()
        return lex_number
    elif r == "'":
        r = lex.peek()
        if r in "0123456789":
            return lex_number
        lex.emit(ItemType.Text)  # TODO: Change to Text
    elif "0" <= r <= "9":
        lex.backup()
        return lex_number
    elif r == "#":
        if "0" <= lex.peek() <= "9":
            return lex_number
        lex.emit(ItemType.Symbol)
    elif is_operator(r):
        if r == "-" and lex.peek() == "-":
            lex.get()
            lex.emit(ItemType.Skip)
        else:
            return lex_operator
    elif is_alpha_numeric(r):
        lex.backup()
        return lex_text
    elif r == "(":
        lex.emit(ItemType.LeftParen)
        lex.paren_depth += 1
    elif r == ")":
        lex.emit(ItemType.RightParen)
        lex.paren_depth -= 1
        if lex.paren_depth < 0:
            return errorf(lex, "unexpected right paren " + r)

    elif r == "{":
        lex.emit(ItemType.LeftBrace)
        lex.brace_depth += 1
    elif r == "}":
        lex.emit(ItemType.RightBrace)
        lex.brace_depth -= 1
        if lex.brace_depth < 0:
            return errorf(lex, "unexpected right brace " + r)

    elif r == "[":
        lex.emit(ItemType.LeftSBrace)
        lex.sbrace_depth += 1
    elif r == "]":
        lex.emit(ItemType.RightSBrace)
        lex.sbrace_depth -= 1
        if lex.sbrace_depth < 0:
            return errorf(lex, "unexpected right brace " + r)
    elif is_symbol(r):
        lex.emit(ItemType.Symbol)
    else:
        return errorf(lex, "unrecognized character in action: " + r)

    return lex_filename


def lex_operator(lex: Lexer) -> Callable:
    lex.accept_run("-|:;")
    lex.emit(ItemType.Operator)
    return lex_filename


# LexSpace scans a run of space characters.
# One space has already been seen.
def lex_space(lex: Lexer) -> Callable:
    while is_space(lex.peek()):
        lex.get()

    lex.emit(ItemType.Space)
    return lex_filename


# Lex_text scans an alphanumeric.
def lex_text(lex: Lexer) -> Callable:
    while True:
        r = lex.get()
        if is_alpha_numeric(r):
            if r.isnumeric():  # E.g. v1
                word = lex.input[lex.start : lex.pos]
                if word.casefold() in key and key[word.casefold()] == ItemType.InfoSpecifier:
                    lex.backup()
                    lex.emit(key[word.casefold()])
                    return lex_filename
        else:
            if r == "'" and lex.peek() == "s":
                lex.get()
            else:
                lex.backup()
            word = lex.input[lex.start : lex.pos + 1]
            if word.casefold() == "vol" and lex.peek() == ".":
                lex.get()
            word = lex.input[lex.start : lex.pos + 1]

            if word.casefold() in key:
                lex.emit(key[word.casefold()])
            elif cal(word):
                lex.emit(ItemType.Calendar)
            else:
                lex.emit(ItemType.Text)
            break

    return lex_filename


def cal(value: str) -> set[Any]:
    month_abbr = [i for i, x in enumerate(calendar.month_abbr) if x == value.title()]
    month_name = [i for i, x in enumerate(calendar.month_name) if x == value.title()]
    day_abbr = [i for i, x in enumerate(calendar.day_abbr) if x == value.title()]
    day_name = [i for i, x in enumerate(calendar.day_name) if x == value.title()]
    return set(month_abbr + month_name + day_abbr + day_name)


def lex_number(lex: Lexer) -> Callable[[Lexer], Callable | None] | None:
    if not lex.scan_number():
        return errorf(lex, "bad number syntax: " + lex.input[lex.start : lex.pos])
    # Complex number logic removed. Messes with math operations without space

    if lex.input[lex.start] == "#":
        lex.emit(ItemType.IssueNumber)
    elif not lex.input[lex.pos].isdigit():
        # Assume that 80th is just text and not a number
        lex.emit(ItemType.Text)
    else:
        lex.emit(ItemType.Number)

    return lex_filename


def is_space(character: str) -> bool:
    return character in "_ \t"


# IsAlphaNumeric reports whether r is an alphabetic, digit, or underscore.
def is_alpha_numeric(character: str) -> bool:
    return character.isalpha() or character.isnumeric()


def is_operator(character: str) -> bool:
    return character in "-|:;/\\"


def is_symbol(character: str) -> bool:
    return unicodedata.category(character)[0] in "PS"


def Lex(filename: str) -> Lexer:
    lex = Lexer(string=os.path.basename(filename))
    lex.run()
    return lex