/*
    Hurry Curry! - a game about cooking
    Copyright (C) 2025 Hurry Curry! Contributors

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, version 3 of the License only.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.

*/
/// <reference lib="dom" />

import { init_locale } from "./locale.ts";
import { MovementBase, collide_player_player, update_movement } from "./movement.ts";
import { particle_splash, tick_particles } from "./particles.ts";
import { Gamedata, ItemIndex, ItemLocation, Message, MessageTimeout, PacketC, PacketS, PlayerClass, PlayerID, Score, TileIndex } from "./protocol.ts";
import { V2, lerp_exp_v2_mut, normalize, lerp_exp } from "./util.ts";
import { draw_ingame, draw_wait } from "./visual.ts";

const KEY_INTERACT_LEFT = "KeyJ"
const KEY_INTERACT_RIGHT = "KeyL"
const KEY_BOOST = "KeyK"
const KEY_UP = "KeyW"
const KEY_DOWN = "KeyS"
const KEY_LEFT = "KeyA"
const KEY_RIGHT = "KeyD"
const KEY_CHAT = "Enter"
const KEY_CLOSE = "Escape"
const KEY_ZOOM = "KeyI"
const HANDLED_KEYS = [KEY_INTERACT_LEFT, KEY_BOOST, KEY_CHAT, KEY_CLOSE, KEY_DOWN, KEY_UP, KEY_LEFT, KEY_RIGHT]

export let ctx: CanvasRenderingContext2D;
export let canvas: HTMLCanvasElement;
let ws: WebSocket;
document.addEventListener("DOMContentLoaded", async () => {
    await init_locale()
    const ws_uri = globalThis.location.protocol.endsWith("s:")
        ? `wss://${globalThis.location.host}/`
        : `ws://${globalThis.location.hostname}:27032/`
    ws = new WebSocket(ws_uri)
    ws.onerror = console.error
    ws.onmessage = m => {
        packet(JSON.parse(m.data) as PacketC);
    }
    ws.onclose = () => console.log("close")
    ws.onopen = () => {
        console.log("open")
        send({ type: "join", name: "test", character: { color: Math.floor(Math.random() * 100), hairstyle: 0, headwear: 0 }, class: "chef" })
    }

    canvas = document.createElement("canvas");
    document.body.append(canvas)
    ctx = canvas.getContext("2d")!
    resize()
    globalThis.addEventListener("resize", resize)
    draw()

    document.addEventListener("keydown", ev => keyboard(ev, true))
    document.addEventListener("keyup", ev => keyboard(ev, false))
    document.addEventListener("contextmenu", ev => ev.preventDefault())
    const tick_int = setInterval(tick_update, 1000 / 25);
    ws.addEventListener("close", () => clearInterval(tick_int))
})

export interface ItemData {
    kind: ItemIndex,
    x: number,
    y: number,
    tracking?: V2,
    active?: Involvement
    remove_anim?: number
}
export interface Involvement {
    position: number,
    speed: number,
    warn: boolean
}
export interface ItemSlot {
    item?: ItemData,
}
export interface PlayerData extends MovementBase {
    id: number,
    name: string,
    hands: ItemSlot[],
    direction: V2,
    class: PlayerClass,
    character: number,
    anim_position: V2,
    message_persist?: MessageData,
    message_pinned?: MessageData,
    message?: MessageData,
}

export interface TileData extends ItemSlot {
    x: number, y: number // tile position
    position: V2, // center position
    kind: TileIndex
}
export type MessageStyle = "hint" | "normal" | "error" | "pinned"
export interface MessageData {
    style: MessageStyle
    inner: Message
    anim_position: V2,
    anim_size: number,
    timeout?: MessageTimeout,
}

export const players = new Map<PlayerID, PlayerData>()
export const tiles = new Map<string, TileData>()
export const items_removed = new Set<ItemData>()
export const server_hints = new Map<string, MessageData>()

export let data: Gamedata = {
    item_names: [],
    tile_names: [],
    tile_interactable_empty: [],
    tile_placeable_items: {},
    maps: [],
    hand_count: 0,
    bot_algos: [],
    current_map: "",
    tile_walkable: []
}

export let global_message: MessageData | undefined = undefined
export let my_id: PlayerID = -1
export const score: Score = { active_recipes: 0, demands_completed: 0, demands_failed: 0, instant_recipes: 0, passive_recipes: 0, players: 0, points: 0, stars: 0, time_remaining: 0 }
export const camera: V2 = { x: 0, y: 0 }
export let camera_scale = 0.05;
export const interact_target_anim: V2 = { x: 0, y: 0 }
export let interact_possible_anim: number = 0
export let interact_active_anim: number = 0
export let overlay_vis_anim: number = 0
export let is_lobby = false
let interacting: V2 | undefined;
let last_server_sent_position: V2 = { x: 0, y: 0 }

function get_item_location(loc: ItemLocation): ItemSlot {
    if ("tile" in loc) return tiles.get(loc.tile.toString())!
    if ("player" in loc) return players.get(loc.player[0])!.hands[loc.player[1]]
    throw new Error("invalid item location");
}
function get_item_location_tracking(loc: ItemLocation): V2 {
    if ("tile" in loc) return tiles.get(loc.tile.toString())!.position
    if ("player" in loc) return players.get(loc.player[0])!.position
    throw new Error("invalid item location");
}

function send(p: PacketS) {
    if (p.type != "movement") console.log("send", p);
    ws.send(JSON.stringify(p))
}
function packet(p: PacketC) {
    if (!["movement", "update_map"].includes(p.type))
        console.log(p);
    switch (p.type) {
        case "version":
            console.log(`Protocol version: ${p.major}.${p.minor}`);
            break;
        case "joined":
            my_id = p.id
            break;
        case "data":
            data = p.data
            break;
        case "add_player": {
            players.set(p.id, {
                id: p.id,
                hands: new Array(data.hand_count).fill(null).map(() => ({})),
                position: { x: p.position[0], y: p.position[1], },
                anim_position: { x: p.position[0], y: p.position[1] },
                character: p.character.color,
                class: p.class,
                name: p.name,
                rot: 0,
                facing: { x: 0, y: 1 },
                vel: { x: 0, y: 0 },
                direction: { x: 0, y: 0 },
                stamina: 0,
                boosting: false,
            })
            break;
        }
        case "remove_player":
            players.delete(p.id)
            break;
        case "movement": {
            const pl = players.get(p.player)!
            const pos = { x: p.pos[0], y: p.pos[1] }
            // const dist = length(sub_v2(pl.position, pos));a
            // TODO this is actually not a good idea if latency is too high
            // if (p.player == my_id && dist < 3) return; // we know better where we are
            if (p.player == my_id) return last_server_sent_position = pos
            pl.position.x = pos.x
            pl.position.y = pos.y
            pl.boosting = p.boost
            pl.rot = p.rot
            break;
        }
        case "move_item": {
            const from = get_item_location(p.from)
            const to = get_item_location(p.to)

            to.item = from.item
            to.item!.tracking = get_item_location_tracking(p.to)
            from.item = undefined

            break;
        }
        case "set_item": {
            const slot = get_item_location(p.location)
            const slotpos = get_item_location_tracking(p.location)
            if (slot.item !== undefined && slot.item !== null) items_removed.add(slot.item)
            delete slot.item
            if (p.item !== undefined && p.item !== null) slot.item = { kind: p.item, x: slotpos.x, y: slotpos.y, tracking: slotpos }
            break;
        }
        case "clear_progress": {
            delete get_item_location(p.item).item!.active
            break;
        }
        case "set_progress": {
            const slot = get_item_location(p.item)
            if (!slot.item) return
            slot.item.active = {
                position: p.position,
                speed: p.speed,
                warn: p.warn
            }
            break;
        }
        case "update_map":
            if (p.kind !== undefined && p.kind !== null)
                tiles.set(p.tile.toString(), {
                    x: p.tile[0],
                    y: p.tile[1],
                    position: { x: p.tile[0] + 0.5, y: p.tile[1] + 0.5 },
                    kind: p.kind
                })
            else
                tiles.delete(p.tile.toString())
            break;
        case "communicate": {
            const player = players.get(p.player)!
            if (p.message) {
                const message = {
                    inner: p.message,
                    anim_size: player.message_persist?.anim_size ?? 0,
                    anim_position: player.anim_position,
                    timeout: p.timeout ?? { initial: 5, remaining: 5, pinned: false },
                    style: "normal" as const
                };
                if (p.timeout) player.message_persist = message
                else player.message = message
                if (p.timeout?.pinned) player.message_pinned = {
                    inner: p.message,
                    anim_size: player.message_pinned?.anim_size ?? 0,
                    anim_position: { x: 20, y: 0 },
                    style: "pinned",
                    timeout: p.timeout
                }
            } else {
                delete player.message_persist
                delete player.message_pinned
            }
            break;
        }
        case "score":
            score.demands_completed = p.demands_completed
            score.demands_failed = p.demands_failed
            score.points = p.points
            score.time_remaining = p.time_remaining ?? null
            break;
        case "server_message":
            global_message = {
                inner: p.message,
                style: p.error ? "error" : "normal",
                anim_size: 0.,
                anim_position: { x: 0, y: 0 },
                timeout: { initial: 5, remaining: 5, pinned: false }
            }
            break;
        case "set_ingame":
            console.log(`ingame ${p.state}`);
            is_lobby = p.lobby
            send({ type: "ready" })
            break;
        case "movement_sync": {
            const me = players.get(my_id)
            if (me) {
                me.position.x = last_server_sent_position.x // ensuring object has same ref
                me.position.y = last_server_sent_position.y
            }
            break;
        }
        case "server_hint":
            if (p.player != my_id) return;
            if (p.message) server_hints.set(p.position + "", {
                inner: p.message,
                anim_size: 0.,
                anim_position: p.position ?
                    { x: p.position[0] + 0.5, y: p.position[1] + 0.5 } :
                    players.get(my_id)!.anim_position,
                style: "hint"
            })
            else server_hints.delete(p.position + "")
            break;
        case "tutorial_ended":
            break;
        case "environment":
            break
        case "effect":
            break;
        case "flush_map":
            break;
        case "menu":
            switch (p.menu) {
                case "score":
                    global_message = {
                        timeout: { initial: 5, remaining: 5, pinned: false },
                        inner: { text: `Score: ${JSON.stringify(p.data, null, 4)}` },
                        anim_position: { x: 0, y: 0 },
                        anim_size: 0,
                        style: "normal"
                    };
                    break
                default: console.warn("unknown menu");
            }
            break;
        default:
            console.warn("unknown packet", p);
    }
}

export let chat: null | HTMLInputElement = null;

const QUICK_COMMANDS: { [key: string]: string } = {
    "Numpad1": "/start -s junior",
    "Numpad2": "/start -s debug2",
    "Numpad3": "/start -s sophomore",
    "Numpad4": "/start -s debug",
    "Numpad5": "/start -s 5star",
    "Numpad6": "/start campaign/lobby",
    "Numpad8": "/start-tutorial plate:seared-patty,sliced-bun",
    "Numpad9": "/start-tutorial plate:bun",
    "Numpad7": "/end-tutorial",
    "Numpad0": "/end",
    "NumpadEnter": "/r",
}

export const keys_down = new Set();
function keyboard(ev: KeyboardEvent, down: boolean) {
    if (down && ev.code == KEY_CHAT) return toggle_chat()
    else if (down && ev.code == KEY_CLOSE && chat) return close_chat()
    else if (chat) return
    if (HANDLED_KEYS.includes(ev.code)) ev.preventDefault()
    if (!keys_down.has(KEY_INTERACT_LEFT) && ev.code == KEY_INTERACT_LEFT && down) set_interact(true, 0)
    if (keys_down.has(KEY_INTERACT_LEFT) && ev.code == KEY_INTERACT_LEFT && !down) set_interact(false, 0)
    if (!keys_down.has(KEY_INTERACT_RIGHT) && ev.code == KEY_INTERACT_RIGHT && down) set_interact(true, 1)
    if (keys_down.has(KEY_INTERACT_RIGHT) && ev.code == KEY_INTERACT_RIGHT && !down) set_interact(false, 1)
    if (down && ev.code in QUICK_COMMANDS) send({ player: my_id, type: "communicate", message: { text: QUICK_COMMANDS[ev.code] } })
    if (down && ev.code == "KeyE") particle_splash(get_interact_target() ?? { x: 0, y: 0 }, 0.8)
    if (down) keys_down.add(ev.code)
    else keys_down.delete(ev.code)
}

function close_chat() {
    if (!chat) return
    chat.remove()
    canvas.focus()
    chat = null;
}
function toggle_chat() {
    if (chat) {
        if (chat.value.length) send({ player: my_id, type: "communicate", message: { text: chat.value } })
        chat.remove()
        canvas.focus()
        chat = null;
    } else {
        chat = document.createElement("input")
        chat.type = "text"
        chat.placeholder = "Message"
        document.body.append(chat)
        chat.focus()
    }
}

export function can_interact_with_tile(t: TileData, hand: ItemSlot) {
    return t.item
        || (!hand.item && data.tile_interactable_empty.includes(t.kind))
        || (hand.item && !data.tile_placeable_items[t.kind])
        || (hand.item && data.tile_placeable_items[t.kind]?.includes(hand.item?.kind))
}

export function get_interact_target(): V2 | undefined {
    if (interacting) return interacting
    const me = players.get(my_id)
    if (!me) return

    if (location.hash.search("oldinteract") != -1)
        return {
            x: Math.floor(me.position.x + Math.sin(me.rot)),
            y: Math.floor(me.position.y + Math.cos(me.rot))
        }

    const rx = me.position.x + Math.sin(me.rot) * 0.7
    const ry = me.position.y + Math.cos(me.rot) * 0.7
    const bx = Math.floor(rx)
    const by = Math.floor(ry)
    let best = { x: bx, y: by }
    let best_d = 100
    for (let ox = -1; ox <= 1; ox++) for (let oy = -1; oy <= 1; oy++) {
        const t = tiles.get([bx + ox, by + oy] + "")
        if (!t) continue
        for (const hand of me.hands) {
            if (!can_interact_with_tile(t, hand)) continue
            const cx = (bx + ox + 0.5)
            const cy = (by + oy + 0.5)
            const dx = rx - cx
            const dy = ry - cy
            const pdx = me.position.x - cx
            const pdy = me.position.y - cy
            const d = Math.sqrt(dx * dx + dy * dy)
            const pd = Math.sqrt(pdx * pdx + pdy * pdy)
            if (pd < 2 && d < best_d) {
                best_d = d
                best = { x: bx + ox, y: by + oy }
            }
        }
    }
    return best
}

function set_interact(edge: boolean, hand: number) {
    if (edge) interacting = get_interact_target()
    if (interacting) send({ player: my_id, type: "interact", pos: edge ? [interacting.x, interacting.y] : undefined, hand })
    if (!edge) interacting = undefined
}

function tick_update() {
    const p = players.get(my_id)
    if (!p) return
    send({ player: my_id, type: "movement", pos: [p.position.x, p.position.y], dir: [p.direction.x, p.direction.y], boost: p.boosting })
}

function frame_update(dt: number) {
    const p = players.get(my_id)
    if (!p) return

    score.time_remaining -= dt

    const direction = normalize({
        x: (+keys_down.has(KEY_RIGHT) - +keys_down.has(KEY_LEFT)),
        y: (+keys_down.has(KEY_DOWN) - +keys_down.has(KEY_UP))
    })
    if (interacting) direction.x *= 0, direction.y *= 0
    p.direction = direction
    update_movement(p, dt, direction, keys_down.has("KeyK"))

    for (const [_, a] of players)
        for (const [_, b] of players)
            collide_player_player(a, b, dt)

    const update_item = (item: ItemData) => {
        if (item.tracking) lerp_exp_v2_mut(item, item.tracking, dt * 10.)
        if (item.active) item.active.position += item.active.speed * dt
    }
    let pin_xo = 0
    for (const [pid, player] of players) {
        if (pid == my_id) player.anim_position.x = player.position.x, player.anim_position.y = player.position.y
        else lerp_exp_v2_mut(player.anim_position, player.position, dt * 15)
        player.hands.forEach(h => { if (h.item) update_item(h.item) })
        if (player.message && tick_message(player.message, dt)) delete player.message
        if (player.message_persist && tick_message(player.message_persist, dt)) delete player.message_persist
        if (player.message_pinned && tick_message(player.message_pinned, dt)) delete player.message_pinned
        if (player.message_pinned) lerp_exp_v2_mut(player.message_pinned.anim_position, { x: pin_xo++, y: 0 }, dt * 5)
    }
    for (const [_, tile] of tiles)
        if (tile.item !== undefined && tile.item !== null) update_item(tile.item)

    const remove: ItemData[] = []
    for (const item of items_removed) {
        update_item(item)
        if (item.remove_anim === undefined) item.remove_anim = 0
        item.remove_anim += dt * 4.
        if (item.remove_anim > 1.) remove.push(item)
    }
    remove.forEach(i => items_removed.delete(i))

    for (const [_, h] of server_hints)
        tick_message(h, dt);

    lerp_exp_v2_mut(camera, p.position, dt * 10.)

    if (global_message && tick_message(global_message, dt)) global_message = undefined

    const it = get_interact_target() ?? { x: 0, y: 0 };
    const target_tile = tiles.get([it.x, it.y].toString())
    const me = players.get(my_id)
    const possible = target_tile && me && me.hands.find(h => can_interact_with_tile(target_tile, h))
    lerp_exp_v2_mut(interact_target_anim, it, dt * 15.)
    interact_possible_anim = lerp_exp(interact_possible_anim, +!!possible, dt * 18.)
    interact_active_anim = lerp_exp(interact_active_anim, +!!interacting, dt * 15.)

    const zoom_target = Math.min(canvas.width, canvas.height) * (keys_down.has(KEY_ZOOM) ? 0.05 : 0.1)
    camera_scale = lerp_exp(camera_scale, zoom_target, dt * 5)
    overlay_vis_anim = lerp_exp(overlay_vis_anim, +keys_down.has(KEY_ZOOM), dt * 10)

    tick_particles(dt)
}

function resize() {
    canvas.width = globalThis.innerWidth
    canvas.height = globalThis.innerHeight
}

let last_frame = performance.now()
function draw() {
    const now = performance.now()
    frame_update((now - last_frame) / 1000)
    last_frame = now;
    if (ws.readyState == ws.CONNECTING) draw_wait("Connecting...")
    else if (ws.readyState == ws.CLOSING) draw_wait("Closing...")
    else if (ws.readyState == ws.CLOSED) draw_wait("Disconnected")
    else if (ws.readyState == ws.OPEN) draw_ingame()
    else throw new Error(`ws state invalid`);
    requestAnimationFrame(draw)
}

function tick_message(m: MessageData | undefined, dt: number): boolean {
    if (!m) return true
    m.anim_size = lerp_exp(m.anim_size, m.timeout ? m.timeout.remaining > 0.3 ? 1 : 0 : 1, dt * 3)
    if (!m.timeout) return false
    m.timeout.remaining -= dt;
    return m.timeout.remaining <= 0
}
