from BaseClasses import Item, Location
from typing import Tuple, Union, Set, List, Dict
import string
import pkgutil


class TerrariaItem(Item):
    game = "Terraria"


class TerrariaLocation(Location):
    game = "Terraria"


def add_token(
    tokens: List[Tuple[int, int, Union[str, int, None]]],
    token: Union[str, int],
    token_index: int,
):
    if token == "":
        return
    if type(token) == str:
        tokens.append((token_index, 0, token.rstrip()))
    elif type(token) == int:
        tokens.append((token_index, 1, token))


IDENT = 0
NUM = 1
SEMI = 2
HASH = 3
AT = 4
NOT = 5
AND = 6
OR = 7
LPAREN = 8
RPAREN = 9
END_OF_LINE = 10

CHAR_TO_TOKEN_ID = {
    ";": SEMI,
    "#": HASH,
    "@": AT,
    "~": NOT,
    "&": AND,
    "|": OR,
    "(": LPAREN,
    ")": RPAREN,
}

TOKEN_ID_TO_CHAR = {id: char for char, id in CHAR_TO_TOKEN_ID.items()}


def tokens(rule: str) -> List[Tuple[int, int, Union[str, int, None]]]:
    token_list = []
    token = ""
    token_index = 0
    escaped = False
    for index, char in enumerate(rule):
        if escaped:
            if token == "":
                token_index = index
            token += char
            escaped = False
        elif char == "\\":
            if type(token) == int:
                add_token(token_list, token, token_index)
                token = ""
            escaped = True
        elif char == "/" and token.endswith("/"):
            add_token(token_list, token[:-1], token_index)
            return token_list
        elif token == "" and char.isspace():
            pass
        elif token == "" and char.isdigit():
            token_index = index
            token = int(char)
        elif type(token) == int and char.isdigit():
            token = token * 10 + int(char)
        elif (id := CHAR_TO_TOKEN_ID.get(char)) != None:
            add_token(token_list, token, token_index)
            token_list.append((index, id, None))
            token = ""
        else:
            if token == "":
                token_index = index
            token += char

    add_token(token_list, token, token_index)
    return token_list


NAME = 0
NAME_SEMI = 1
FLAG_OR_SEMI = 2
POST_FLAG = 3
FLAG = 4
FLAG_ARG = 5
FLAG_ARG_END = 6
COND_OR_SEMI = 7
POST_COND = 8
COND = 9
LOC = 10
FN = 11
POST_FN = 12
FN_ARG = 13
FN_ARG_END = 14
END = 15
GOAL = 16

POS_FMT = [
    "name or `#`",
    "`;`",
    "flag or `;`",
    "`;`, `|`, or `(`",
    "flag",
    "text or number",
    "`)`",
    "name, `#`, `@`, `~`, `(`, or `;`",
    "`;`, `&`, `|`, or `)`",
    "name, `#`, `@`, `~`, or `(`",
    "name",
    "name",
    "`(`, `;`, `&`, `|`, or `)`",
    "text or number",
    "`)`",
    "end of line",
    "goal",
]

RWD_NAME = 0
RWD_NAME_SEMI = 1
RWD_FLAG = 2
RWD_FLAG_SEMI = 3
RWD_END = 4
RWD_LABEL = 5

RWD_POS_FMT = ["name or `#`", "`;`", "flag", "`;`", "end of line", "name"]


def unexpected(line: int, char: int, id: int, token, pos, pos_fmt, file):
    if id == IDENT or id == NUM:
        token_fmt = f"`{token}`"
    elif id == END_OF_LINE:
        token_fmt = "end of line"
    else:
        token_fmt = f"`{TOKEN_ID_TO_CHAR[id]}`"

    raise Exception(
        f"in `{file}`, found {token_fmt} at {line + 1}:{char + 1}; expected {pos_fmt[pos]}"
    )


COND_ITEM = 0
COND_LOC = 1
COND_FN = 2
COND_GROUP = 3


class Condition:
    def __init__(
        self,
        # True = positive, False = negative
        sign: bool,
        # See the `COND_*` constants
        type: int,
        # Condition name or list
        condition: Union[str, Tuple[Union[bool, None], List["Condition"]]],
        argument: Union[str, int, None],
    ):
        self.sign = sign
        self.type = type
        self.condition = condition
        self.argument = argument


class Rule:
    def __init__(
        self,
        name: str,
        # Name to arg
        flags: Dict[str, Union[str, int, None]],
        # True = or, False = and, None = N/A
        operator: Union[bool, None],
        conditions: List[Condition],
    ):
        self.name = name
        self.flags = flags
        self.operator = operator
        self.conditions = conditions


def validate_conditions(
    rule: str,
    rule_indices: dict,
    conditions: List[Condition],
):
    for condition in conditions:
        if condition.type == COND_ITEM:
            if condition.condition not in rule_indices:
                raise Exception(
                    f"item `{condition.condition}` in `{rule}` is not defined"
                )
        elif condition.type == COND_LOC:
            if condition.condition not in rule_indices:
                raise Exception(
                    f"location `{condition.condition}` in `{rule}` is not defined"
                )
        elif condition.type == COND_FN:
            if condition.condition not in {
                "npc",
                "calamity",
                "grindy",
                "pickaxe",
                "hammer",
                "mech_boss",
                "minions",
                "getfixedboi",
            }:
                raise Exception(
                    f"function `{condition.condition}` in `{rule}` is not defined"
                )
        elif condition.type == COND_GROUP:
            _, conditions = condition.condition
            validate_conditions(rule, rule_indices, conditions)


def mark_progression(
    conditions: List[Condition],
    progression: Set[str],
    rules: list,
    rule_indices: dict,
    loc_to_item: dict,
):
    for condition in conditions:
        if condition.type == COND_ITEM:
            prog = condition.condition in progression
            progression.add(loc_to_item[condition.condition])
            rule = rules[rule_indices[condition.condition]]
            if (
                not prog
                and "Achievement" not in rule.flags
                and "Location" not in rule.flags
                and "Item" not in rule.flags
            ):
                mark_progression(
                    rule.conditions, progression, rules, rule_indices, loc_to_item
                )
        elif condition.type == COND_LOC:

            mark_progression(
                rules[rule_indices[condition.condition]].conditions,
                progression,
                rules,
                rule_indices,
                loc_to_item,
            )
        elif condition.type == COND_GROUP:
            _, conditions = condition.condition
            mark_progression(conditions, progression, rules, rule_indices, loc_to_item)


def read_data() -> Tuple[
    # Goal to rule index that ends that goal's range and the locations required
    List[Tuple[int, Set[str]]],
    # Rules
    List[Rule],
    # Rule to rule index
    Dict[str, int],
    # Label to rewards
    Dict[str, List[str]],
    # Reward to flags
    Dict[str, Set[str]],
    # Item name to ID
    Dict[str, int],
    # Location name to ID
    Dict[str, int],
    # NPCs
    List[str],
    # Pickaxe to pick power
    Dict[str, int],
    # Hammer to hammer power
    Dict[str, int],
    # Mechanical bosses
    List[str],
    # Calamity final bosses
    List[str],
    # Progression rules
    Set[str],
    # Armor to minion count,
    Dict[str, int],
    # Accessory to minion count,
    Dict[str, int],
]:
    next_id = 0x7E0000
    item_name_to_id = {}

    goals = []
    goal_indices = {}
    rules = []
    rule_indices = {}
    loc_to_item = {}

    npcs = []
    pickaxes = {}
    hammers = {}
    mech_boss_loc = []
    mech_bosses = []
    final_boss_loc = []
    final_bosses = []
    armor_minions = {}
    accessory_minions = {}

    progression = set()

    for line, rule in enumerate(
        pkgutil.get_data(__name__, "Rules.dsv").decode().splitlines()
    ):
        goal = None
        name = None
        flags = {}

        sign = True
        operator = None
        outer = []
        conditions = []

        pos = NAME
        for char, id, token in tokens(rule):
            if pos == NAME:
                if id == IDENT:
                    name = token
                    pos = NAME_SEMI
                elif id == HASH:
                    pos = GOAL
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == NAME_SEMI:
                if id == SEMI:
                    pos = FLAG_OR_SEMI
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == FLAG_OR_SEMI:
                if id == IDENT:
                    flag = token
                    pos = POST_FLAG
                elif id == SEMI:
                    pos = COND_OR_SEMI
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == POST_FLAG:
                if id == SEMI:
                    if flag is not None:
                        if flag in flags:
                            raise Exception(
                                f"set flag `{flag}` at {line + 1}:{char + 1} that was already set"
                            )
                        flags[flag] = None
                        flag = None
                    pos = COND_OR_SEMI
                elif id == OR:
                    pos = FLAG
                elif id == LPAREN:
                    pos = FLAG_ARG
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == FLAG:
                if id == IDENT:
                    if flag is not None:
                        if flag in flags:
                            raise Exception(
                                f"set flag `{flag}` at {line + 1}:{char + 1} that was already set"
                            )
                        flags[flag] = None
                        flag = None
                    flag = token
                    pos = POST_FLAG
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == FLAG_ARG:
                if id == IDENT or id == NUM:
                    if flag in flags:
                        raise Exception(
                            f"set flag `{flag}` at {line + 1}:{char + 1} that was already set"
                        )
                    flags[flag] = token
                    flag = None
                    pos = FLAG_ARG_END
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == FLAG_ARG_END:
                if id == RPAREN:
                    pos = POST_FLAG
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == COND_OR_SEMI:
                if id == IDENT:
                    conditions.append(Condition(sign, COND_ITEM, token, None))
                    sign = True
                    pos = POST_COND
                elif id == HASH:
                    pos = LOC
                elif id == AT:
                    pos = FN
                elif id == NOT:
                    sign = not sign
                    pos = COND
                elif id == LPAREN:
                    outer.append((sign, None, conditions))
                    conditions = []
                    sign = True
                    pos = COND
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == POST_COND:
                if id == SEMI:
                    if outer:
                        raise Exception(
                            f"found `;` at {line + 1}:{char + 1} after unclosed `(`"
                        )
                    pos = END
                elif id == AND:
                    if operator is True:
                        raise Exception(
                            f"found `&` at {line + 1}:{char + 1} in group containing `|`"
                        )
                    operator = False
                    pos = COND
                elif id == OR:
                    if operator is False:
                        raise Exception(
                            f"found `|` at {line + 1}:{char + 1} in group containing `&`"
                        )
                    operator = True
                    pos = COND
                elif id == RPAREN:
                    if not outer:
                        raise Exception(
                            f"found `)` at {line + 1}:{char + 1} without matching `(`"
                        )
                    condition = operator, conditions
                    sign, operator, conditions = outer.pop()
                    conditions.append(Condition(sign, COND_GROUP, condition, None))
                    sign = True
                    pos = POST_COND
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == COND:
                if id == IDENT:
                    conditions.append(Condition(sign, COND_ITEM, token, None))
                    sign = True
                    pos = POST_COND
                elif id == HASH:
                    pos = LOC
                elif id == AT:
                    pos = FN
                elif id == NOT:
                    sign = not sign
                elif id == LPAREN:
                    outer.append((sign, operator, conditions))
                    conditions = []
                    sign = True
                    operator = None
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == LOC:
                if id == IDENT:
                    conditions.append(Condition(sign, COND_LOC, token, None))
                    sign = True
                    pos = POST_COND
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == FN:
                if id == IDENT:
                    function = token
                    pos = POST_FN
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == POST_FN:
                if id == LPAREN:
                    pos = FN_ARG
                elif id == SEMI:
                    conditions.append(Condition(sign, COND_FN, function, None))
                    pos = END
                elif id == AND:
                    conditions.append(Condition(sign, COND_FN, function, None))
                    sign = True
                    if operator is True:
                        raise Exception(
                            f"found `&` at {line + 1}:{char + 1} in group containing `|`"
                        )
                    operator = False
                    pos = COND
                elif id == OR:
                    conditions.append(Condition(sign, COND_FN, function, None))
                    sign = True
                    if operator is False:
                        raise Exception(
                            f"found `|` at {line + 1}:{char + 1} in group containing `&`"
                        )
                    operator = True
                    pos = COND
                elif id == RPAREN:
                    conditions.append(Condition(sign, COND_FN, function, None))
                    if not outer:
                        raise Exception(
                            f"found `)` at {line + 1}:{char + 1} without matching `(`"
                        )
                    condition = operator, conditions
                    sign, operator, conditions = outer.pop()
                    conditions.append(Condition(sign, COND_GROUP, condition, None))
                    sign = True
                    pos = POST_COND
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == FN_ARG:
                if id == IDENT or id == NUM:
                    conditions.append(Condition(sign, COND_FN, function, token))
                    sign = True
                    pos = FN_ARG_END
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == FN_ARG_END:
                if id == RPAREN:
                    pos = POST_COND
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")
            elif pos == END:
                unexpected(line, char, id, token, pos)
            elif pos == GOAL:
                if id == IDENT:
                    goal = token
                    pos = END
                else:
                    unexpected(line, char, id, token, pos, POS_FMT, "Rules.dsv")

        if pos != NAME and pos != FLAG_OR_SEMI and pos != COND_OR_SEMI and pos != END:
            unexpected(line, char + 1, END_OF_LINE, None, pos, POS_FMT, "Rules.dsv")

        if name:
            if name in rule_indices:
                raise Exception(
                    f"rule `{name}` on line `{line + 1}` shadows a previous rule"
                )
            rule_indices[name] = len(rules)
            rules.append(Rule(name, flags, operator, conditions))

            for flag in flags:
                if flag not in {
                    "Location",
                    "Item",
                    "Goal",
                    "Early",
                    "Achievement",
                    "Grindy",
                    "Fishing",
                    "Npc",
                    "Pickaxe",
                    "Hammer",
                    "Minions",
                    "Armor Minions",
                    "Mech Boss",
                    "Final Boss",
                    "Getfixedboi",
                    "Not Getfixedboi",
                    "Calamity",
                    "Not Calamity",
                    "Not Calamity Getfixedboi",
                }:
                    raise Exception(
                        f"rule `{name}` on line `{line + 1}` has unrecognized flag `{flag}`"
                    )

            if "Item" in flags:
                item_name = flags["Item"] or f"Post-{name}"
                if item_name in item_name_to_id:
                    raise Exception(
                        f"item `{item_name}` on line `{line + 1}` shadows a previous item"
                    )
                item_name_to_id[item_name] = next_id
                next_id += 1
                loc_to_item[name] = item_name
            else:
                loc_to_item[name] = name

            if "Npc" in flags:
                npcs.append(name)

            if (power := flags.get("Pickaxe")) is not None:
                pickaxes[name] = power

            if (power := flags.get("Hammer")) is not None:
                hammers[name] = power

            if "Mech Boss" in flags:
                mech_bosses.append(flags["Item"] or f"Post-{name}")
                mech_boss_loc.append(name)

            if "Final Boss" in flags:
                final_bosses.append(flags["Item"] or f"Post-{name}")
                final_boss_loc.append(name)

            if (minions := flags.get("Armor Minions")) is not None:
                armor_minions[name] = minions

            if (minions := flags.get("Minions")) is not None:
                accessory_minions[name] = minions

        if goal:
            if goal in goal_indices:
                raise Exception(
                    f"goal `{goal}` on line `{line + 1}` shadows a previous goal"
                )
            goal_indices[goal] = len(goals)
            goals.append((len(rules), set()))

    for rule in rules:
        if "Goal" in rule.flags:
            if (name := rule.flags.get("Goal")) is not None:
                goal_name = name
            else:
                goal_name = (
                    rule.name.translate(str.maketrans("", "", string.punctuation))
                    .replace(" ", "_")
                    .lower()
                )

            _, items = goals[goal_indices[goal_name]]
            items.add(rule.name)

    _, mech_boss_items = goals[goal_indices["mechanical_bosses"]]
    mech_boss_items.update(mech_boss_loc)

    _, final_boss_items = goals[goal_indices["calamity_final_bosses"]]
    final_boss_items.update(final_boss_loc)

    for rule in rules:
        validate_conditions(rule.name, rule_indices, rule.conditions)

    for rule in rules:
        prog = False
        if (
            "Npc" in rule.flags
            or "Goal" in rule.flags
            or "Pickaxe" in rule.flags
            or "Hammer" in rule.flags
            or "Mech Boss" in rule.flags
            or "Final Boss" in rule.flags
            or "Minions" in rule.flags
            or "Armor Minions" in rule.flags
        ):
            progression.add(loc_to_item[rule.name])
            prog = True
        if prog or "Location" in rule.flags or "Achievement" in rule.flags:
            mark_progression(
                rule.conditions, progression, rules, rule_indices, loc_to_item
            )

    # Will be randomized via `slot_randoms` / `self.multiworld.random`
    label = None
    labels = {}
    rewards = {}

    for line in pkgutil.get_data(__name__, "Rewards.dsv").decode().splitlines():
        reward = None
        flags = set()

        pos = RWD_NAME
        for char, id, token in tokens(line):
            if pos == RWD_NAME:
                if id == IDENT:
                    reward = f"Reward: {token}"
                    pos = RWD_NAME_SEMI
                elif id == HASH:
                    pos = RWD_LABEL
                else:
                    unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
            elif pos == RWD_NAME_SEMI:
                if id == SEMI:
                    pos = RWD_FLAG
                else:
                    unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
            elif pos == RWD_FLAG:
                if id == IDENT:
                    if token in flags:
                        raise Exception(
                            f"set flag `{token}` at {line + 1}:{char + 1} that was already set"
                        )
                    flags.add(token)
                    pos = RWD_FLAG_SEMI
                else:
                    unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
            elif pos == RWD_FLAG_SEMI:
                if id == SEMI:
                    pos = RWD_END
                else:
                    unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
            elif pos == RWD_END:
                unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")
            elif pos == RWD_LABEL:
                if id == IDENT:
                    label = token
                    if label in labels:
                        raise Exception(
                            f"started label `{label}` at {line + 1}:{char + 1} that was already used"
                        )
                    labels[label] = []
                    pos = RWD_END
                else:
                    unexpected(line, char, id, token, pos, RWD_POS_FMT, "Rewards.dsv")

        if pos != RWD_NAME and pos != RWD_FLAG and pos != RWD_END:
            unexpected(line, char + 1, END_OF_LINE, None, pos)

        if reward:
            if reward in rewards:
                raise Exception(
                    f"reward `{reward}` on line `{line + 1}` shadows a previous reward"
                )
            rewards[reward] = flags

            if not label:
                raise Exception(
                    f"reward `{reward}` on line `{line + 1}` is not labeled"
                )
            labels[label].append(reward)

            if reward in item_name_to_id:
                raise Exception(
                    f"item `{reward}` on line `{line + 1}` shadows a previous item"
                )
            item_name_to_id[reward] = next_id
            next_id += 1

    item_name_to_id["Reward: Coins"] = next_id
    next_id += 1

    location_name_to_id = {}

    for rule in rules:
        if "Location" in rule.flags or "Achievement" in rule.flags:
            if rule.name in location_name_to_id:
                raise Exception(f"location `{rule.name}` shadows a previous location")
            location_name_to_id[rule.name] = next_id
            next_id += 1

    return (
        goals,
        rules,
        rule_indices,
        labels,
        rewards,
        item_name_to_id,
        location_name_to_id,
        npcs,
        pickaxes,
        hammers,
        mech_bosses,
        final_bosses,
        progression,
        armor_minions,
        accessory_minions,
    )


(
    goals,
    rules,
    rule_indices,
    labels,
    rewards,
    item_name_to_id,
    location_name_to_id,
    npcs,
    pickaxes,
    hammers,
    mech_bosses,
    final_bosses,
    progression,
    armor_minions,
    accessory_minions,
) = read_data()
