/*
    Hurry Curry! - a game about cooking
    Copyright (C) 2026 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/>.

*/

import { float, Gamedata, int, ItemIndex, PacketC } from "../protocol.ts";
import { System } from "../system.ts";

import item_sprites_raw from "../assets/item_sprites.ini"
import tile_sprites_raw from "../assets/tile_sprites.ini"

let sprites_raw: { [key: string]: string[] } = {}

export class SSprites extends System {
    items: Map<number, Sprite> = new Map()
    tiles: Map<number, Sprite> = new Map()

    override packet(p: PacketC): void {
        if (p.type == "game_data")
            this.load(p)
    }

    load(data: Gamedata) {
        if (!Object.keys(sprites_raw).length)
            sprites_raw = parse_sprite_file(item_sprites_raw + tile_sprites_raw)
        for (let i = 0; i < data.tile_names.length; i++)
            this.tiles.set(i, load_sprite(data.tile_names[i]))
        for (let i = 0; i < data.item_names.length; i++)
            this.items.set(i, load_sprite(data.item_names[i]))
    }
    draw_item(ctx: CanvasRenderingContext2D, i: ItemIndex) { this.items.get(i)!.draw(ctx) }
    draw_tile(ctx: CanvasRenderingContext2D, i: ItemIndex) { this.tiles.get(i)!.draw(ctx) }
}

interface SpritePart {
    fill?: string
    stroke?: string,
    line_width: number,
    line_join: number,
    path: Path2D
    path_raw: string,
}
class Sprite {
    constructor(public parts: SpritePart[] = []) { }
    draw(ctx: CanvasRenderingContext2D) {
        for (const p of this.parts) {
            if (p.fill) ctx.fillStyle = p.fill
            if (p.stroke) ctx.strokeStyle = p.stroke
            ctx.lineWidth = p.line_width
            ctx.lineJoin = (["miter", "round", "bevel"] as const)[p.line_join]
            if (p.fill) ctx.fill(p.path)
            if (p.stroke) ctx.stroke(p.path)
        }
    }
}

function parse_sprite_file(f: string): { [key: string]: string[] } {
    let item = ""
    const out: { [key: string]: string[] } = {}
    for (const line of f.split("\n")) {
        if (line.startsWith("#")) continue
        else if (!line.trim().length) continue
        else if (line.startsWith("[") && line.endsWith("]"))
            item = line.substring(1, line.length - 1)
        else {
            out[item] = out[item] ?? []
            out[item].push(line.trim())
        }
    }
    return out
}

function fallback_sprite(name: string): Sprite {
    console.warn(`missing sprite for "${name}"`);
    const path_raw = "m -.5 -.5 h 1 v 1 h -1 z"
    return new Sprite([{ path: new Path2D(path_raw), path_raw, fill: "#f0f7", line_width: 0.05, line_join: 1 }])
}

function calc_color_shade(color: string): string {
    const prefix = color.startsWith("rgba") ? "rgba(" : "rgb("
    if (!color.startsWith(prefix) || !color.endsWith(")")) throw "a"
    color = color.substring(prefix.length, color.length - ")".length)
    const [r, g, b, a] = color.split(",")
    const [r2, g2, b2] = [r, g, b]
        .map(e => parseInt(e))
        .map(e => e / 1.3)
        .map(e => Math.max(Math.min(e, 255), 0) | 0)

    return a != undefined
        ? `rgba(${r2}, ${g2}, ${b2}, ${a})`
        : `rgb(${r2}, ${g2}, ${b2})`
}

function variant_handler(_p: SpritePart, ps: SpritePart[], mode: string, variant: string) {
    if (mode == "sprite") {
        if (variant == "") return
        ps.push(...load_sprite(variant).parts)
    } else if (mode == "arrange_sprites") {
        if (variant == "") return
        const items = variant.search(":") == -1 ? variant.split(",") : [variant];// dont split if nesting
        for (let i = 0; i < items.length; i++) {
            const t = i / items.length * Math.PI * 2.
            const radius = Math.sqrt(items.length - 1) * 0.15
            const off_x = Math.sin(t) * radius
            const off_y = Math.cos(t) * radius
            const scale = 1. / Math.sqrt(Math.max(2, items.length))

            const item_sprite = load_sprite(items[i])
            for (const p of item_sprite.parts) {
                p.line_width *= scale
                p.path_raw = transform_path(p.path_raw, scale, off_x, off_y)
                ps.push(p)
            }
        }
    } else if (mode == "stack_sprites") {
        if (variant == "") return
        for (const item of variant.split(",")) {
            const item_sprite = load_sprite(item)
            for (const p of item_sprite.parts) {
                p.line_width *= 0.8
                p.path_raw = transform_path(p.path_raw, 0.8, 0, 0)
                ps.push(p)
            }
        }
    } else {
        console.warn(`unknown sprite variant handler: "${mode}"`);
    }
}

function load_sprite(name: string): Sprite {
    let main = ""
    let variant = ""
    if (name in sprites_raw) main = name // special case or no variant
    else {
        const [a, ...b] = name.split(":")
        main = a, variant = b.join(":")
    }
    const parts_raw = sprites_raw[main]
    if (!parts_raw) return fallback_sprite(name)
    const parts: SpritePart[] = []
    for (const pr of parts_raw) {
        const p: SpritePart = { path: new Path2D(), path_raw: "", line_width: 0.03, line_join: 1 }
        let scale = 0.25
        for (const t of pr.split("|")) {
            const [ty, ...pd_a] = t.split(" ")
            const pd = pd_a.join(" ")
            if (ty == "p") { p.path_raw = transform_path(pd, scale, 0, 0) }
            else if (ty == "f") p.fill = pd
            else if (ty == "s" && !pd.length) p.stroke = p.fill
            else if (ty == "s") p.stroke = pd
            else if (ty == "sc") scale = parseFloat(pd)
            else if (ty == "lw") p.line_width = parseFloat(pd)
            else if (ty == "lj") p.line_join = parseInt(pd)
            else if (ty == "as") p.stroke = calc_color_shade(p.fill!)
            else if (ty == "af") p.fill = calc_color_shade(p.stroke!)
            else if (ty == "v") variant_handler(p, parts, pd, variant)
            else console.warn(`unknown sprite part token "${t}"`)
        }
        if (p.path_raw.length) parts.push(p)
    }
    for (const p of parts) p.path = new Path2D(p.path_raw)
    return new Sprite(parts)
}

const PATH_COMMANDS: { [key: string]: ("x" | "y" | "dx" | "dy" | "aux")[] } = {
    "M": ["x", "y"], "m": ["dx", "dy"],
    "L": ["x", "y"], "l": ["dx", "dy"],
    "H": ["x"], "h": ["dx"],
    "V": ["y"], "v": ["dy"],
    "Z": [], "z": [],
    "C": ["x", "y", "x", "y", "x", "y"], "c": ["dx", "dy", "dx", "dy", "dx", "dy"],
    "S": ["x", "y", "x", "y"], "s": ["dx", "dy", "dx", "dy"],
    "Q": ["x", "y", "x", "y"], "q": ["dx", "dy", "dx", "dy"],
    "T": ["x", "y"], "t": ["dx", "dy"],
    "A": ["dx", "dy", "aux", "aux", "aux", "x", "y"], "a": ["dx", "dy", "aux", "aux", "aux", "dx", "dy"],
}


function transform_path(path: string, scale: float, xoff: float, yoff: float): string {
    const toks = path.split(" ")
    let i = 0
    const sc = (i: int) => toks[i] = (parseFloat(toks[i]) * scale).toString()
    const xo = (i: int) => toks[i] = (parseFloat(toks[i]) + xoff).toString()
    const yo = (i: int) => toks[i] = (parseFloat(toks[i]) + yoff).toString()
    while (i < toks.length) {
        const first = i == 0;
        const cmd = toks[i++]
        const args = PATH_COMMANDS[cmd]
        if (!args) throw `invalid path command "${cmd}" in "${path}"`
        for (const a of args) {
            switch (a) {
                case "x": sc(i); xo(i); break;
                case "y": sc(i); yo(i); break;
                case "dx": sc(i); if (first) xo(i); break;
                case "dy": sc(i); if (first) yo(i); break;
                case "aux": break;
            }
            i++
        }
    }
    return toks.join(" ")
}
