/*
    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/>.

*/

pub mod book;
pub mod entities;
pub mod filter_demands;
pub mod index;
pub mod registry;

use anyhow::{Result, anyhow, bail};
use clap::Parser;
use filter_demands::filter_demands_and_recipes;
use hurrycurry_protocol::{
    Demand, Gamedata, ItemIndex, MapMetadata, Recipe, TileIndex,
    book::Book,
    glam::{IVec2, Vec2},
};
use log::debug;
use serde::{Deserialize, Serialize};
use std::{
    collections::{BTreeMap, BTreeSet, HashMap, HashSet},
    time::Duration,
};

use crate::{
    entities::EntityDecl,
    registry::{ItemTileRegistry, filter_unused_tiles_and_items},
};

#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub enum RecipeDeclAction {
    #[default]
    Never,
    Passive,
    Active,
    Instant,
    Demand,
}

#[rustfmt::skip]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RecipeDecl {
    tile: Option<String>,
    #[serde(default)] inputs: Vec<String>,
    #[serde(default)] outputs: Vec<String>,
    #[serde(default)] action: RecipeDeclAction,
    #[serde(default)] warn: bool,
    revert_duration: Option<f32>,
    duration: Option<f32>,
    points: Option<i64>,
    group: Option<String>,
    #[serde(default)] group_hidden: bool,
}

#[rustfmt::skip]
#[derive(Debug, Clone, Deserialize)]
pub struct MapDecl {
    map: Vec<String>,
    tiles: HashMap<char, String>,
    #[serde(default)] recipes: Option<String>,
    #[serde(default)] hand_count: Option<usize>,
    #[serde(default)] entities: Vec<EntityDecl>,
    #[serde(default)] score_baseline: i64,
    #[serde(default)] default_timer: Option<u64>,
    #[serde(default)] flags: ServerdataFlags,
}

#[derive(Parser)]
struct TileArgs {
    tile_name: String,
    #[clap(short = 'c', long)]
    collider: bool,
    #[clap(short = 'x', long)]
    exclusive: bool,
    #[clap(short = 'w', long)]
    walkable: bool,
    #[clap(long)]
    book: bool,
    #[clap(long)]
    chef_spawn: bool,
    #[clap(long)]
    customer_spawn: bool,
    #[clap(short = 'i', long)]
    item: Option<String>,
    #[clap(long)]
    conveyor: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DemandDecl {
    from: String,
    to: Option<String>,
    duration: f32,
    points: i64,
}

#[derive(Debug, Clone, Default)]
#[rustfmt::skip]
pub struct Serverdata {
    pub initial_map: HashMap<IVec2, (TileIndex, Option<ItemIndex>)>,
    pub chef_spawn: Vec2,
    pub customer_spawn: Option<Vec2>,
    pub score_baseline: i64,
    pub default_timer: Option<Duration>,
    pub book: Book,
    pub flags: ServerdataFlags,
    pub entity_decls: Vec<EntityDecl>,
    pub recipe_groups: BTreeMap<String, BTreeSet<ItemIndex>>,
}

#[rustfmt::skip]
#[derive(Debug, Clone, Default, Deserialize)]
pub struct ServerdataFlags {
    #[serde(default)] pub disable_unknown_orders: bool,
}

fn build_data(
    maps: &HashMap<String, MapMetadata>,
    map_name: String,
    map_in: MapDecl,
    recipes_in: Vec<RecipeDecl>,
) -> Result<(Gamedata, Serverdata)> {
    debug!("Preparing gamedata for {map_name}");
    let reg = ItemTileRegistry::default();

    let (mut recipes, mut demands, recipe_groups) = load_recipes(recipes_in, &reg)?;

    let mut tile_specs = BTreeMap::new();
    for (char, tile_spec_raw) in map_in.tiles {
        let mut toks = shlex::split(&tile_spec_raw).ok_or(anyhow!("tile spec quoting invalid"))?;
        toks.insert(0, "tile-spec".to_string()); // exe name
        tile_specs.insert(char, TileArgs::try_parse_from(toks)?);
    }

    let mut entities = Vec::new();
    let mut chef_spawn = None;
    let mut customer_spawn = None;
    let mut initial_map = HashMap::new();
    let mut tiles_used = HashSet::new();
    let mut items_used = HashSet::new();
    let mut tile_walkable = HashSet::new();
    let mut exclusive_tiles = BTreeMap::<TileIndex, HashSet<ItemIndex>>::new();
    let mut tile_placeable_items = BTreeMap::new();
    let mut tile_interactable_empty = HashSet::new();
    for (y, line) in map_in.map.iter().enumerate() {
        for (x, char) in line.chars().enumerate() {
            if char == ' ' {
                continue; // space is empty space
            }
            let pos = IVec2::new(x as i32, y as i32);

            let tile_spec = tile_specs
                .get(&char)
                .ok_or(anyhow!("tile {char} is undefined"))?;

            let tile = reg.register_tile(tile_spec.tile_name.clone());
            tiles_used.insert(tile);
            let item = tile_spec.item.clone().map(|i| reg.register_item(i));
            items_used.extend(item);
            initial_map.insert(pos, (tile, item));

            if tile_spec.chef_spawn {
                chef_spawn = Some(pos.as_vec2() + Vec2::splat(0.5));
            }
            if tile_spec.customer_spawn {
                customer_spawn = Some(pos.as_vec2() + Vec2::splat(0.5));
            }
            if tile_spec.walkable {
                tile_walkable.insert(tile);
            }
            if tile_spec.walkable || tile_spec.collider || tile_spec.exclusive {
                exclusive_tiles.entry(tile).or_default().extend(item);
            }
            if tile_spec.book {
                entities.push(EntityDecl::Book { pos });
                tile_interactable_empty.insert(tile); // if it doesnt have a dedicated tile all of its kind will be interactable
            }
            if let Some(off) = &tile_spec.conveyor {
                let (x, y) = off
                    .split_once(",")
                    .ok_or(anyhow!("conveyor offset invalid format"))?;
                let dir = IVec2::new(x.parse()?, y.parse()?);
                entities.push(EntityDecl::Conveyor {
                    from: pos,
                    speed: None,
                    to: pos + dir,
                });
            }
        }
    }

    let chef_spawn = chef_spawn.ok_or(anyhow!("map has no chef spawn"))?;

    for tile in tile_specs.values() {
        if !tiles_used.contains(&reg.register_tile(tile.tile_name.clone())) {
            bail!("tile {:?} is unused", tile.tile_name)
        }
    }

    for mut e in map_in.entities.clone() {
        if let EntityDecl::Customers { unknown_order, .. } = &mut e {
            *unknown_order = reg.register_item("unknown-order".to_owned())
        }
        entities.push(e);
    }
    debug!("{} entites created", entities.len());

    filter_demands_and_recipes(&tiles_used, &items_used, &mut demands, &mut recipes);

    let mut maps = maps
        .iter()
        .filter(|(_, v)| v.players > 0)
        .map(|(k, v)| (k.to_owned(), v.to_owned()))
        .collect::<Vec<(String, MapMetadata)>>();
    maps.sort_unstable_by_key(|(_, m)| m.difficulty);
    maps.sort_by_key(|(_, m)| m.players);

    for (tile, used_items) in exclusive_tiles {
        let whitelist = recipes
            .iter()
            .filter(|r| r.tile() == Some(tile))
            .flat_map(|e| e.inputs())
            .chain(used_items)
            .collect();
        let int_empty = recipes
            .iter()
            .any(|r| r.tile() == Some(tile) && r.inputs().is_empty());
        tile_placeable_items.insert(tile, whitelist);
        if int_empty {
            tile_interactable_empty.insert(tile);
        }
    }

    let (item_names, tile_names) = reg.finish();

    let default_timer = if map_name.ends_with("lobby") {
        None
    } else {
        Some(Duration::from_secs(map_in.default_timer.unwrap_or(420)))
    };

    let mut data = Gamedata {
        current_map: map_name,
        maps,
        tile_walkable,
        tile_placeable_items,
        tile_interactable_empty,
        recipes,
        item_names,
        demands,
        tile_names,
        bot_algos: vec![
            "waiter".to_string(),
            "simple".to_string(),
            "dishwasher".to_string(),
            "frank".to_string(),
        ],
        hand_count: map_in.hand_count.unwrap_or(1),
    };
    let mut serverdata = Serverdata {
        initial_map,
        chef_spawn,
        flags: map_in.flags,
        customer_spawn,
        default_timer,
        book: Book::default(),
        score_baseline: map_in.score_baseline,
        entity_decls: entities,
        recipe_groups,
    };
    filter_unused_tiles_and_items(&mut data, &mut serverdata);

    Ok((data, serverdata))
}

#[allow(clippy::type_complexity)]
fn load_recipes(
    recipes_in: Vec<RecipeDecl>,
    reg: &ItemTileRegistry,
) -> Result<(
    Vec<Recipe>,
    Vec<Demand>,
    BTreeMap<String, BTreeSet<ItemIndex>>,
)> {
    let mut recipes = Vec::new();
    let mut demands = Vec::new();
    let mut recipe_groups = BTreeMap::<String, BTreeSet<ItemIndex>>::new();

    for mut r in recipes_in {
        #[cfg(feature = "fast_recipes")]
        match r.action {
            RecipeDeclAction::Passive | RecipeDeclAction::Active => {
                if !r.warn {
                    r.duration = Some(0.5)
                }
            }
            _ => (),
        }

        let r2 = r.clone();
        let mut inputs = r.inputs.into_iter().map(|i| reg.register_item(i));
        let mut outputs = r.outputs.into_iter().map(|o| reg.register_item(o));
        let tile = r.tile.map(|t| reg.register_tile(t));
        if let Some(g) = r.group
            && !r.group_hidden
        {
            recipe_groups.entry(g).or_default().extend(inputs.clone());
        }
        match r.action {
            RecipeDeclAction::Never => {}
            RecipeDeclAction::Passive => recipes.push(Recipe::Passive {
                speed: 1. / r.duration.ok_or(anyhow!("duration for passive missing"))?,
                warn: r.warn,
                tile,
                revert_speed: r.revert_duration.map(|d| 1. / d),
                input: inputs
                    .next()
                    .ok_or(anyhow!("passive recipe without input"))?,
                output: outputs.next(),
            }),
            RecipeDeclAction::Active => recipes.push(Recipe::Active {
                speed: 1. / r.duration.ok_or(anyhow!("duration for active missing"))?,
                tile,
                input: inputs
                    .next()
                    .ok_or(anyhow!("active recipe without input"))?,
                outputs: [outputs.next(), outputs.next()],
            }),
            RecipeDeclAction::Instant => {
                recipes.push(Recipe::Instant {
                    points: r.points.take().unwrap_or(0),
                    tile,
                    inputs: [inputs.next(), inputs.next()],
                    outputs: [outputs.next(), outputs.next()],
                });
            }
            RecipeDeclAction::Demand => demands.push(Demand {
                input: inputs.next().ok_or(anyhow!("demand needs inputs"))?,
                output: outputs.next(),
                duration: r.duration.unwrap_or(10.),
                points: 0, // assigned later when filtering
            }),
        }
        assert_eq!(inputs.next(), None, "{r2:?} inputs left over");
        assert_eq!(outputs.next(), None, "{r2:?} outputs left over");
        assert_eq!(r.points, None, "points specified where not possible")
    }

    Ok((recipes, demands, recipe_groups))
}
