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

*/

use crate::registry::ItemTileRegistry;
use anyhow::{Result, anyhow};
use hurrycurry_protocol::{Demand, DemandIndex, Recipe};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};

#[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,
}

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

#[derive(Debug, Clone, Default)]
pub struct RecipeGroup {
    pub repr: BTreeSet<DemandIndex>,
    pub demands: BTreeSet<DemandIndex>,
}

#[allow(clippy::type_complexity)]
pub(crate) fn load_recipes(
    recipes_in: Vec<RecipeDecl>,
    reg: &ItemTileRegistry,
) -> Result<(Vec<Recipe>, Vec<Demand>, BTreeMap<String, RecipeGroup>)> {
    let mut recipes = Vec::new();
    let mut demands = Vec::new();
    let mut recipe_groups = BTreeMap::<String, RecipeGroup>::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 {
            let group = recipe_groups.entry(g).or_default();
            let d = DemandIndex(demands.len());
            group.demands.insert(d);
            if !r.group_hidden {
                group.repr.insert(d);
            }
        }
        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))
}
