import {
    get_assister_ids,
    get_attacker_ids, get_data, get_event_count, get_event,
    get_kill_ticks,
    get_map, get_player_entity_id,
    get_player_name, get_player_steam_id, get_player_user_id, get_victim_ids, get_weapon,
    parse_demo,
    XY
} from '@demostf/tf-demos-viewer';
import viewer from "@demostf/tf-demos-viewer";

function getCacheBuster(): string {
    const url = self.location.href;
    return url.substring(url.indexOf('?'));
}

export async function parseDemo(bytes: Uint8Array, progressCallback: (progress: number) => void): Promise<ParsedDemo> {
    await viewer(`/tf-demo-viewer.wasm${getCacheBuster()}`);
    const state = parse_demo(bytes, progressCallback);

    let playerCount = state.player_count;
    let buildingCount = state.building_count;
    let projectileCount = state.projectile_count;
    let hasCart = state.has_cart;
    let boundaries = state.boundaries;
    let interval_per_tick = state.interval_per_tick;
    let tickCount = state.tick_count;
    let kill_ticks = get_kill_ticks(state);
    let attackers = get_attacker_ids(state);
    let assisters = get_assister_ids(state);
    let victims = get_victim_ids(state);

    let playerInfo = [];

    for (let i = 0; i < playerCount; i++) {
        playerInfo.push({
            name: get_player_name(state, i),
            steamId: get_player_steam_id(state, i),
            entityId: get_player_entity_id(state, i),
            userId: get_player_user_id(state, i),
        })
    }

    let kills = [];
    for (let i = 0; i < kill_ticks.length; i++) {
        kills.push({
            tick: kill_ticks[i],
            attacker: attackers[i],
            assister: assisters[i],
            victim: victims[i],
            weapon: get_weapon(state, i),
        })
    }

    let map = get_map(state);

    let events = kills.map((kill: Kill) => {
        return {
            tick: kill.tick,
            type: "kill",
            kill
        } as Event
    });
    let event_count = get_event_count(state);
    for (let i = 0; i < event_count; i++) {
        let event = get_event(state, i);
        event = JSON.parse(event);
        if (event) {
            events.push(event);
        }
    }
    events.sort((a, b) => a.tick - b.tick);

    let data = get_data(state);

    return new ParsedDemo(
        playerCount,
        buildingCount,
        projectileCount,
        hasCart,
        {
            boundary_min: {
                x: boundaries.boundary_min.x,
                y: boundaries.boundary_min.y,
            },
            boundary_max: {
                x: boundaries.boundary_max.x,
                y: boundaries.boundary_max.y,
            }
        },
        {
            map,
            interval_per_tick
        },
        data,
        kills,
        playerInfo,
        events,
        tickCount,
    );
}

export interface PlayerInfo {
    entityId: number,
    name: string,
    steamId: string,
    userId: number,
}

export enum Team {
    Other = 0,
    Spectator = 1,
    Red = 2,
    Blue = 3,
}

export enum Class {
    Other = 0,
    Scout = 1,
    Sniper = 2,
    Solder = 3,
    Demoman = 4,
    Medic = 5,
    Heavy = 6,
    Pyro = 7,
    Spy = 8,
    Engineer = 9,
}

export enum BuildingType {
    TeleporterEntrance = 0,
    TeleporterExit = 1,
    Dispenser = 2,
    Level1Sentry = 3,
    Level2Sentry = 4,
    Level3Sentry = 5,
    MiniSentry = 6,
    Unknown = 7,
}

export enum ProjectileType {
    Rocket = 0,
    HealingArrow = 1,
    Sticky = 2,
    Pipe = 3,
    Flare = 4,
    LooseCannon = 5,
    Unknown = 7,
}

export interface WorldBoundaries {
    boundary_min: {
        x: number,
        y: number
    },
    boundary_max: {
        x: number,
        y: number
    }
}

export interface PlayerState {
    position: {
        x: number,
        y: number
    },
    angle: number,
    health: number,
    team: Team,
    playerClass: Class,
    info: PlayerInfo,
    class_data: PlayerClassState,
    ubered: boolean,
    cloaked: boolean,
}

export type PlayerClassState = MedicState | SpyState | null;

export enum MedigunType {
    Uber = 0,
    Kritzkrieg = 1,
    Quickfix = 2,
    Vaccinator = 3,
}

export interface MedicState {
    charge: number,
    medigun: MedigunType
    target: number,
}

export interface SpyState {
    disguise_team: Team,
    disguise_class: Class,
    cloak: number,
}

export interface BuildingState {
    position: {
        x: number,
        y: number
    },
    angle: number,
    health: number,
    level: number,
    team: Team,
    buildingType: BuildingType,
    buildProgress: number,
    shield: boolean,
}

export interface ProjectileState {
    position: {
        x: number,
        y: number
    },
    angle: number,
    team: Team,
    projectileType: ProjectileType,
    critical: boolean,
}

export interface Header {
    interval_per_tick: number,
    map: string
}

export interface Kill {
    tick: number,
    attacker: number,
    assister: number,
    victim: number,
    weapon: string,
}

export interface CartState {
    position: {
        x: number,
        y: number
    },
}

function unpack_f32(val: number, min: number, max: number): number {
    const ratio = val / (Math.pow(2, 16) - 1);
    return ratio * (max - min) + min;
}

function unpack_angle(val: number): number {
    const ratio = val / (Math.pow(2, 8) - 1);
    return ratio * 360;
}

export class ParsedDemo {
    public readonly playerCount: number;
    public readonly buildingCount: number;
    public readonly projectileCount: number;
    public readonly hasCart: boolean;
    public readonly world: WorldBoundaries;
    public readonly data: Uint8Array;
    public readonly header: Header;
    public readonly tickCount: number;
    public readonly kills: Kill[];
    public readonly playerInfo: PlayerInfo[];
    public readonly events: Event[];

    constructor(
        playerCount: number,
        buildingCount: number,
        projectileCount: number,
        hasCart: boolean,
        world: WorldBoundaries,
        header: Header,
        data: Uint8Array,
        kills: Kill[],
        playerInfo: PlayerInfo[],
        events: Event[],
        tickCount: number
    ) {
        this.playerCount = playerCount;
        this.buildingCount = buildingCount;
        this.projectileCount = projectileCount;
        this.hasCart = hasCart;
        this.world = world;
        this.header = header;
        this.data = data;
        this.kills = kills;
        this.playerInfo = playerInfo;
        this.events = events;
        this.tickCount = tickCount;
    }

    getPlayer(tick: number, playerIndex: number): PlayerState {
        if (playerIndex >= this.playerCount) {
            throw new Error("Player out of bounds");
        }

        const base = ((playerIndex * this.tickCount) + tick) * PLAYER_PACK_SIZE;
        return unpackPlayer(this.data, base, this.world, this.playerInfo[playerIndex]);
    }

    getBuilding(tick: number, buildingIndex: number): BuildingState {
        if (buildingIndex >= this.buildingCount) {
            throw new Error("Building out of bounds");
        }

        const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) + ((buildingIndex * this.tickCount) + tick) * BUILDING_PACK_SIZE;
        return unpackBuilding(this.data, base, this.world);
    }

    getProjectile(tick: number, projectileIndex: number): ProjectileState {
        if (projectileIndex >= this.projectileCount) {
            throw new Error("Projectile out of bounds");
        }

        const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) +
            (this.buildingCount * this.tickCount * BUILDING_PACK_SIZE) +
            ((projectileIndex * this.tickCount) + tick) * PROJECTILE_PACK_SIZE;
        return unpackProjectile(this.data, base, this.world);
    }

    getCart(tick: number): CartState | null {
        if (this.hasCart) {
            const base = (this.playerCount * this.tickCount * PLAYER_PACK_SIZE) +
                (this.buildingCount * this.tickCount * BUILDING_PACK_SIZE) +
                (this.projectileCount * this.tickCount * PROJECTILE_PACK_SIZE) +
                (tick * CART_PACK_SIZE);
            return unpackCart(this.data, base, this.world);
        } else {
            return null;
        }
    }
}

const PLAYER_PACK_SIZE = 9;
const BUILDING_PACK_SIZE = 8;
const PROJECTILE_PACK_SIZE = 6;
const CART_PACK_SIZE = 4;

function unpackPlayer(bytes: Uint8Array, base: number, world: WorldBoundaries, info: PlayerInfo): PlayerState {
    const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x);
    const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y);
    const team_class_health = bytes[base + 4] + (bytes[base + 5] << 8);
    const angle = unpack_angle(bytes[base + 6]);
    const ubered = (team_class_health & 1) == 1;
    const health = (team_class_health >> 1) & 511;
    const team = (team_class_health >> 14) as Team;
    const playerClass = ((team_class_health >> 10) & 15) as Class;
    const class_bits = [bytes[base + 7], bytes[base + 8]];
    let class_data = null;
    let cloaked = false;
    switch (playerClass) {
        case Class.Medic:
            class_data = {
                charge: class_bits[0],
                medigun: (class_bits[1] >> 6),
                target: class_bits[1] & 31,
            }
            break;
        case Class.Spy:
            class_data = {
                disguise_team: class_bits[0] >> 6,
                disguise_class: class_bits[0] >> 2 & 15,
                cloak: class_bits[1] >> 1,
            }
            cloaked = (class_bits[1] & 1) === 1;
            break;
    }

    return {
        position: {x, y},
        angle,
        health,
        team,
        playerClass,
        info,
        class_data,
        ubered,
        cloaked,
    }
}

function unpackBuilding(bytes: Uint8Array, base: number, world: WorldBoundaries): BuildingState {
    const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x);
    const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y);
    const team_type_health = bytes[base + 4] + (bytes[base + 5] << 8);
    const angle = unpack_angle(bytes[base + 6]);
    const health = (team_type_health >> 1) & 511;
    const shield = (team_type_health & 1) == 1;
    const team = (((team_type_health >> 13) & 1) === 0) ? Team.Blue : Team.Red;
    const level = (team_type_health >> 14);
    const buildingType = ((team_type_health >> 10) & 7) as BuildingType;
    const buildProgress = bytes[base + 7];

    return {
        position: {x, y},
        angle,
        health,
        team,
        buildingType,
        level,
        buildProgress,
        shield,
    }
}

function unpackProjectile(bytes: Uint8Array, base: number, world: WorldBoundaries): ProjectileState {
    const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x);
    const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y);
    const team_type = bytes[base + 4];
    const team = (((team_type >> 4) & 1) === 0) ? Team.Blue : Team.Red;
    const projectileType = ((team_type >> 5) & 7) as ProjectileType;
    const critical = bytes[base + 5] >> 7 == 1;
    const angle = unpack_angle(bytes[base + 5] << 1);

    return {
        position: {x, y},
        angle,
        team,
        projectileType,
        critical,
    }
}

function unpackCart(bytes: Uint8Array, base: number, world: WorldBoundaries): CartState {
    const x = unpack_f32(bytes[base] + (bytes[base + 1] << 8), world.boundary_min.x, world.boundary_max.x);
    const y = unpack_f32(bytes[base + 2] + (bytes[base + 3] << 8), world.boundary_min.y, world.boundary_max.y);

    return {
        position: {x, y},
    }
}

export type KillEvent = {
    type: "kill";
    tick: number,
    kill: Kill,
}

export type UberEvent = {
    type: "uber";
    tick: number,
    user_id: number,
    target_id: number,
}

export type RawBuildingType = "dispenser" | "teleporter" | "sentrygun";

export type BuildingDestroyedEvent = {
    type: "building_destroyed";
    tick: number,
    attacker_id: number,
    assister_id: number,
    victim_id: number,
    weapon: string,
    building_type: RawBuildingType,
}

export type Event = KillEvent | UberEvent | BuildingDestroyedEvent;