import {
    Component,
    MarkdownPostProcessorContext,
    MarkdownRenderChild,
    Notice,
    parseYaml
} from "obsidian";
import type InitiativeTracker from "../main";
import { Creature } from "../utils/creature";

import EncounterUI from "./ui/Encounter.svelte";
import EncounterTable from "./ui/EncounterTable.svelte";

type RawCreatureArray = string | Array<string | { [key: number]: string }>;
type RawCreature = string | { [key: number]: string };
type RawPlayers = boolean | "none" | string[];
interface EncounterParameters {
    name?: string;
    players?: RawPlayers;
    party?: string;
    hide?: "players" | "creatures" | string[];
    creatures?: RawCreatureArray;
    xp?: number;
}
interface CreatureStats {
    name: string;
    ac: number;
    hp: number;
    modifier: number;
    xp: number;
    display?: string;
}

export const equivalent = (
    creature: Creature | CreatureStats,
    existing: Creature | CreatureStats
) => {
    return (
        creature.name == existing.name &&
        creature.display == existing.display &&
        creature.ac == existing.ac &&
        creature.hp == existing.hp &&
        creature.modifier == existing.modifier &&
        creature.xp == existing.xp
    );
};

export interface ParsedParams {
    name: string;
    players: string[];
    party: string;
    hide: string[];
    creatures: Map<Creature, string | number>;
    xp: number;
    playerLevels: number[];
}

export class EncounterParser {
    constructor(public plugin: InitiativeTracker) {}
    async parse(params: EncounterParameters): Promise<ParsedParams> {
        const name = params.name;
        const party = params.party ?? this.plugin.data.defaultParty;
        const players: string[] = this.parsePlayers(params);
        const hide = this.parseHide(params);
        const rawMonsters = params.creatures ?? [];

        let creatures = await this.parseRawCreatures(rawMonsters);

        const xp = params.xp ?? null;
        const playerLevels = this.plugin.data.players
            .map((p) => p.level)
            .filter((p) => p);

        return {
            name,
            players,
            party,
            hide,
            creatures,
            xp,
            playerLevels
        };
    }
    parseHide(params: EncounterParameters): string[] {
        if (!("hide" in (params ?? {}))) return [];
        if (typeof params.hide == "string")
            return ["creatures", "players"].filter((v) => params.hide == v);
        if (Array.isArray(params.hide))
            return ["creatures", "players"].filter((v) =>
                params.hide.includes(v)
            );

        return [];
    }
    parsePlayers(params: EncounterParameters) {
        const partyName = params.party ?? this.plugin.data.defaultParty;
        const playersToReturn: string[] = [];
        const players = params.players;
        if (
            partyName &&
            this.plugin.data.parties.find(
                (p) => p.name.toLowerCase() == partyName.toLowerCase()
            )
        ) {
            const party = this.plugin.data.parties.find(
                (p) => p.name.toLowerCase() == partyName.toLowerCase()
            );
            playersToReturn.push(...party.players);
        }
        if (players == "none" || players == false) {
            playersToReturn.splice(0, playersToReturn.length);
        } else if (players == true) {
            playersToReturn.push(
                ...this.plugin.data.players.map((p) => p.name)
            );
        } else if (!players && !params.party) {
        } else if (typeof players == "string") {
            playersToReturn.push(players);
        } else if (Array.isArray(players)) {
            playersToReturn.push(
                ...(this.plugin.data.players ?? [])
                    .map((p) => p.name)
                    .filter((p) =>
                        (players as string[])
                            .map((n) => n.toLowerCase())
                            .includes(p.toLowerCase())
                    )
            );
        }
        return Array.from(new Set(playersToReturn));
    }
    async parseRawCreatures(rawMonsters: RawCreatureArray) {
        const creatureMap: Map<Creature, number | string> = new Map();
        if (rawMonsters && Array.isArray(rawMonsters)) {
            for (const raw of rawMonsters) {
                const { creature, number = 1 } =
                    this.parseRawCreature(raw) ?? {};
                if (!creature) continue;

                const stats = {
                    name: creature.name,
                    display: creature.display,
                    ac: creature.ac,
                    hp: creature.hp,
                    modifier: creature.modifier,
                    xp: creature.xp
                };
                const existing = [...creatureMap].find(([c]) =>
                    equivalent(c, stats)
                );
                if (!existing) {
                    creatureMap.set(creature, number);
                } else {
                    let amount;
                    if (!isNaN(Number(number)) && !isNaN(Number(existing[1]))) {
                        amount =
                            (Number(number) as number) +
                            (existing[1] as number);
                    } else {
                        amount = `${number} + ${existing[1]}`;
                    }

                    creatureMap.set(existing[0], amount);
                }
            }
        }
        return creatureMap;
    }
    parseRawCreature(raw: RawCreature) {
        if (!raw) return {};
        let monster: string | string[] | Record<string, any>,
            number = 1;

        if (typeof raw == "string") {
            const match = raw.match(/(\d+)?:?\s?(.+)/) ?? [];
            number = isNaN(Number(match[1] ?? null))
                ? number
                : Number(match[1]);
            monster = match[2];
        } else if (typeof raw == "object") {
            let entries = Object.entries(raw).flat();
            number = entries[0];
            monster = entries[1];
        }

        if (!monster) return {};

        if (
            typeof number == "string" &&
            !this.plugin.canUseDiceRoller &&
            /\d+d\d+/.test(number)
        ) {
            number = 1;
        }
        if (!isNaN(Number(number))) number = Number(number);
        if (!number || (typeof number == "number" && number < 1)) number = 1;

        let name: string,
            display: string,
            hp: number,
            ac: number,
            mod: number,
            xp: number;

        if (typeof monster == "string") {
            name = monster.split(/,\s?/)[0];
            [hp, ac, mod, xp] = monster
                .split(/,\s?/)
                .slice(1)
                .map((v) => (isNaN(Number(v)) ? null : Number(v)));
        } else if (Array.isArray(monster)) {
            if (typeof monster[0] == "string") {
                //Hobgoblin, Jim
                name = monster[0];
                display = monster[1];
            } else if (Array.isArray(monster[0])) {
                //[Hobgoblin, Jim]
                name = monster[0][0];
                display = monster[0][1];
            }
            [hp, ac, mod, xp] = monster
                .slice(1)
                .map((v) => (isNaN(Number(v)) ? null : Number(v)));
        } else if (typeof monster == "object") {
            ({ creature: name, name: display, hp, ac, mod, xp } = monster);
        }

        if (!name || typeof name != "string") return {};
        let existing = this.plugin.bestiary.find((c) => c.name == name);
        let creature = existing
            ? Creature.from(existing)
            : new Creature({ name });

        creature.display = display ?? creature.name;
        creature.hp = hp ?? creature.hp;
        creature.ac = ac ?? creature.ac;
        creature.modifier = mod ?? creature.modifier;
        creature.xp = xp ?? creature.xp;

        return { creature, number };
    }
}

class EncounterComponent {
    instance: EncounterUI;
    constructor(
        public params: ParsedParams,
        public encounterEl: HTMLElement,
        public plugin: InitiativeTracker
    ) {
        this.display();
    }
    async display() {
        this.instance = new EncounterUI({
            target: this.encounterEl,
            props: {
                plugin: this.plugin,
                name: this.params.name,
                party: this.params.party,
                players: this.params.players,
                playerLevels: this.params.playerLevels,
                creatures: this.params.creatures,
                xp: this.params.xp,
                hide: this.params.hide
            }
        });
    }
}

export class EncounterBlock extends MarkdownRenderChild {
    parser = new EncounterParser(this.plugin);
    constructor(
        public plugin: InitiativeTracker,
        public src: string,
        public containerEl: HTMLElement,
        public table = false
    ) {
        super(containerEl);
    }
    onload(): void {
        if (this.table) {
            this.postprocessTable();
        } else {
            this.postprocess();
        }
    }
    async postprocess() {
        const encounters = this.src.split("---") ?? [];
        const containerEl = this.containerEl.createDiv("encounter-container");
        const empty = containerEl.createSpan({
            text: "No encounters created. Please check your syntax and try again."
        });

        for (let encounter of encounters) {
            if (!encounter?.trim().length) continue;
            try {
                const params: EncounterParameters = parseYaml(encounter);
                new EncounterComponent(
                    await this.parser.parse(params),
                    containerEl.createDiv("encounter-instance"),
                    this.plugin
                );
                empty.detach();
            } catch (e) {
                console.error(e);
                new Notice(
                    "Initiative Tracker: here was an issue parsing: \n\n" +
                        encounter
                );
            }
        }
        this.registerEvent(
            this.plugin.app.workspace.on("initiative-tracker:unload", () => {
                this.containerEl.empty();
                this.containerEl.createEl("pre").createEl("code", {
                    text: `\`\`\`encounter\n${this.src}\`\`\``
                });
            })
        );
    }
    async postprocessTable() {
        const encounterSource = this.src.split("---") ?? [];
        const containerEl = this.containerEl.createDiv("encounter-container");
        const empty = containerEl.createSpan({
            text: "No encounters created. Please check your syntax and try again."
        });

        const encounters: ParsedParams[] = [];

        for (let encounter of encounterSource) {
            if (!encounter?.trim().length) continue;
            try {
                const params: EncounterParameters = parseYaml(encounter);
                encounters.push(await this.parser.parse(params));
            } catch (e) {
                console.error(e);
                new Notice(
                    "Initiative Tracker: here was an issue parsing: \n\n" +
                        encounter
                );
            }
        }
        if (encounters.length) {
            empty.detach();
            new EncounterTable({
                target: this.containerEl,
                props: {
                    encounters,
                    plugin: this.plugin
                }
            });
        }
        this.registerEvent(
            this.plugin.app.workspace.on("initiative-tracker:unload", () => {
                this.containerEl.empty();
                this.containerEl.createEl("pre").createEl("code", {
                    text: `\`\`\`encounter-table\n${this.src}\`\`\``
                });
            })
        );
    }
}