/*
    Undercooked - a game about cooking
    Copyright 2024 metamuffin
    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 .
*/
use crate::{
    interaction::Recipe,
    protocol::{DemandIndex, ItemIndex, RecipeIndex, TileIndex},
};
use anyhow::{anyhow, bail, Result};
use glam::{IVec2, Vec2};
use serde::{Deserialize, Serialize};
use std::{
    collections::{HashMap, HashSet},
    fs::File,
    sync::RwLock,
};
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub enum Action {
    #[default]
    Never,
    Passive,
    Active,
    Instant,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RecipeDecl {
    #[serde(default)]
    tile: Option,
    #[serde(default)]
    inputs: Vec,
    #[serde(default)]
    outputs: Vec,
    #[serde(default)]
    action: Action,
    #[serde(default)]
    warn: bool,
    #[serde(default)]
    revert_duration: Option,
    #[serde(default)]
    duration: Option,
}
#[derive(Debug, Clone, Deserialize)]
pub struct InitialMap {
    map: Vec,
    tiles: HashMap,
    items: HashMap,
    collider: Vec,
    walkable: Vec,
    chef_spawn: char,
    customer_spawn: char,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DemandDecl {
    from: String,
    to: String,
    duration: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Demand {
    pub from: ItemIndex,
    pub to: ItemIndex,
    pub duration: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Gamedata {
    #[serde(skip)]
    pub recipes: Vec,
    #[serde(skip)]
    pub demands: Vec,
    pub item_names: Vec,
    pub tile_names: Vec,
    pub tile_collide: Vec,
    pub tile_interact: Vec,
    #[serde(skip)]
    pub initial_map: HashMap)>,
    pub chef_spawn: Vec2,
    pub customer_spawn: Vec2,
}
#[derive(Debug, Deserialize, Default)]
pub struct DataIndex {
    pub maps: HashSet,
    pub demands: HashSet,
    pub recipes: HashSet,
}
impl DataIndex {
    pub fn reload(&mut self) -> anyhow::Result<()> {
        *self = serde_yaml::from_reader(File::open("data/index.yaml")?)?;
        Ok(())
    }
    pub fn generate(&self, spec: String) -> anyhow::Result {
        let [demands, map, recipes] = spec
            .split("-")
            .collect::>()
            .try_into()
            .map_err(|_| anyhow!("data specification malformed"))?;
        if !self.demands.contains(demands) {
            bail!("unknown demands: {demands:?}");
        }
        if !self.maps.contains(map) {
            bail!("unknown map: {map:?}");
        }
        if !self.recipes.contains(recipes) {
            bail!("unknown recipes: {recipes:?}");
        }
        let demands_path = format!("data/demands/{demands}.yaml");
        let map_path = format!("data/maps/{map}.yaml");
        let recipes_path = format!("data/recipes/{recipes}.yaml");
        let demands_in = serde_yaml::from_reader(File::open(demands_path).unwrap()).unwrap();
        let map_in = serde_yaml::from_reader(File::open(map_path).unwrap()).unwrap();
        let recipes_in = serde_yaml::from_reader(File::open(recipes_path).unwrap()).unwrap();
        Ok(Gamedata::build(recipes_in, map_in, demands_in)?)
    }
}
impl Gamedata {
    pub fn build(
        recipes_in: Vec,
        map_in: InitialMap,
        demands_in: Vec,
    ) -> Result {
        let item_names = RwLock::new(Vec::new());
        let tile_names = RwLock::new(Vec::new());
        let mut recipes = Vec::new();
        let mut demands = Vec::new();
        for r in recipes_in {
            let r2 = r.clone();
            let mut inputs = r
                .inputs
                .into_iter()
                .map(|i| ItemIndex(register(&item_names, i)));
            let mut outputs = r
                .outputs
                .into_iter()
                .map(|o| ItemIndex(register(&item_names, o)));
            let tile = r.tile.map(|t| TileIndex(register(&tile_names, t)));
            match r.action {
                Action::Never => {}
                Action::Passive => recipes.push(Recipe::Passive {
                    duration: r.duration.expect("duration for passive missing"),
                    warn: r.warn,
                    tile,
                    revert_duration: r.revert_duration,
                    input: inputs.next().expect("passive recipe without input"),
                    output: outputs.next(),
                }),
                Action::Active => recipes.push(Recipe::Active {
                    duration: r.duration.expect("duration for active missing"),
                    tile,
                    input: inputs.next().expect("active recipe without input"),
                    outputs: [outputs.next(), outputs.next()],
                }),
                Action::Instant => {
                    recipes.push(Recipe::Instant {
                        tile,
                        inputs: [inputs.next(), inputs.next()],
                        outputs: [outputs.next(), outputs.next()],
                    });
                }
            }
            assert_eq!(inputs.next(), None, "{r2:?}");
            assert_eq!(outputs.next(), None, "{r2:?}");
        }
        for d in demands_in {
            demands.push(Demand {
                from: ItemIndex(register(&item_names, d.from)),
                to: ItemIndex(register(&item_names, d.to)),
                duration: d.duration,
            })
        }
        let mut chef_spawn = Vec2::new(0., 0.);
        let mut customer_spawn = Vec2::new(0., 0.);
        let mut initial_map = HashMap::new();
        for (y, line) in map_in.map.iter().enumerate() {
            for (x, tile) in line.trim().chars().enumerate() {
                let pos = IVec2::new(x as i32, y as i32);
                if tile == map_in.chef_spawn {
                    chef_spawn = pos.as_vec2() + Vec2::splat(0.5);
                }
                if tile == map_in.customer_spawn {
                    customer_spawn = pos.as_vec2() + Vec2::splat(0.5);
                }
                let tilename = map_in
                    .tiles
                    .get(&tile)
                    .ok_or(anyhow!("tile {tile} is undefined"))?
                    .clone();
                let itemname = map_in.items.get(&tile).cloned();
                let tile = TileIndex(register(&tile_names, tilename));
                let item = itemname.map(|i| ItemIndex(register(&item_names, i)));
                initial_map.insert(pos, (tile, item));
            }
        }
        let item_names = item_names.into_inner().unwrap();
        let tile_names = tile_names.into_inner().unwrap();
        let tile_collide = tile_names
            .iter()
            .map(|i| !map_in.walkable.contains(i))
            .collect();
        let tile_interact = tile_names
            .iter()
            .map(|i| !map_in.collider.contains(i) && !map_in.walkable.contains(i))
            .collect();
        Ok(Gamedata {
            demands,
            tile_collide,
            tile_interact,
            recipes,
            initial_map,
            item_names,
            tile_names,
            chef_spawn,
            customer_spawn,
        })
    }
}
fn register(db: &RwLock>, name: String) -> usize {
    let mut db = db.write().unwrap();
    if let Some(index) = db.iter().position(|e| e == &name) {
        index
    } else {
        let index = db.len();
        db.push(name);
        index
    }
}
impl Gamedata {
    pub fn tile_name(&self, index: TileIndex) -> &String {
        &self.tile_names[index.0]
    }
    pub fn is_tile_colliding(&self, index: TileIndex) -> bool {
        self.tile_collide[index.0]
    }
    pub fn is_tile_interactable(&self, index: TileIndex) -> bool {
        self.tile_interact[index.0]
    }
    pub fn item_name(&self, index: ItemIndex) -> &String {
        &self.item_names[index.0]
    }
    pub fn recipe(&self, index: RecipeIndex) -> &Recipe {
        &self.recipes[index.0]
    }
    pub fn demand(&self, index: DemandIndex) -> &Demand {
        &self.demands[index.0]
    }
    pub fn get_tile_by_name(&self, name: &str) -> Option {
        self.tile_names
            .iter()
            .position(|t| t == name)
            .map(TileIndex)
    }
    pub fn get_item_by_name(&self, name: &str) -> Option {
        self.item_names
            .iter()
            .position(|t| t == name)
            .map(ItemIndex)
    }
    pub fn recipes(&self) -> impl Iterator-  {
        self.recipes
            .iter()
            .enumerate()
            .map(|(i, e)| (RecipeIndex(i), e))
    }
}