/*
    Hurry Curry! - 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 serde::{Deserialize, Serialize};
use std::{
    collections::BTreeSet,
    fmt::Display,
    sync::{LazyLock, Mutex},
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct Item {
    container: Option>,
    name: String,
    container_dispose_tile: Option,
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum RecipeAction {
    #[default]
    Never,
    Passive,
    Active,
    Instant,
    Demand,
}
#[rustfmt::skip]
#[derive(Debug, Clone, Deserialize,Default, Serialize,PartialEq, Eq, PartialOrd, Ord)]
pub struct Recipe {
    #[serde(default)] tile: Option,
    #[serde(default)] inputs: Vec,
    #[serde(default)] outputs: Vec,
    #[serde(default)] action: RecipeAction,
    #[serde(default)] warn: bool,
    #[serde(default)] revert_duration: Option>,
    #[serde(default)] duration: Option>,
    #[serde(default)] points: Option,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
#[repr(transparent)]
#[serde(transparent)]
struct OrdAnyway(T);
impl Ord for OrdAnyway {
    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
        self.partial_cmp(other).unwrap()
    }
}
impl Eq for OrdAnyway {}
static ALL_ITEMS: Mutex> = Mutex::new(BTreeSet::new());
static ALL_RECIPES: Mutex>> = Mutex::new(BTreeSet::new());
fn out(r: Recipe- ) {
    for i in r.inputs.clone() {
        ALL_ITEMS.lock().unwrap().insert(i.clone());
    }
    for o in r.outputs.clone() {
        ALL_ITEMS.lock().unwrap().insert(o.clone());
    }
    ALL_RECIPES.lock().unwrap().insert(r);
}
fn finish() {
    println!(
        "{}",
        serde_yml::to_string(
            &ALL_RECIPES
                .lock()
                .unwrap()
                .iter()
                .map(|r| Recipe {
                    action: r.action,
                    inputs: r.inputs.iter().map(|i| i.to_string()).collect(),
                    outputs: r.outputs.iter().map(|o| o.to_string()).collect(),
                    points: r.points,
                    tile: r.tile.clone(),
                    warn: r.warn,
                    duration: r.duration,
                    revert_duration: r.revert_duration,
                })
                .collect::>()
        )
        .unwrap()
    );
}
fn auto_trash() {
    let all_items = ALL_ITEMS.lock().unwrap().clone();
    for i in all_items {
        if i.container_dispose_tile.is_some() {
            continue;
        }
        if let Some(c) = &i.container {
            let cdt = c.container_dispose_tile.as_ref().unwrap();
            out(Recipe {
                action: RecipeAction::Instant,
                inputs: vec![i.clone()],
                outputs: i.container.clone().into_iter().map(|i| *i).collect(),
                tile: Some(cdt.to_owned()),
                ..Default::default()
            });
        } else {
            out(Recipe {
                action: RecipeAction::Instant,
                inputs: vec![i],
                outputs: vec![],
                tile: Some("trash".to_owned()),
                ..Default::default()
            });
        }
    }
}
fn auto_burn() {
    let all_items = ALL_ITEMS.lock().unwrap().clone();
    for i in all_items {
        if i.container_dispose_tile.is_some() {
            continue;
        }
        if i.container.is_some() || i.name == "burned" {
            continue;
        } else {
            out(Recipe {
                action: RecipeAction::Passive,
                inputs: vec![i],
                outputs: vec![Item {
                    name: "burned".to_owned(),
                    ..Default::default()
                }],
                duration: Some(OrdAnyway(1.5)),
                revert_duration: Some(OrdAnyway(1.5)),
                warn: true,
                tile: Some("stove".to_owned()),
                ..Default::default()
            })
        }
    }
}
impl Item {
    fn r#as(mut self, s: &str) -> Item {
        self.name = s.to_owned();
        self
    }
    fn tr(&self, container: Option>) -> Item {
        let o = Item {
            name: self.name.to_owned(),
            container,
            ..Default::default()
        };
        if self.container == o.container {
            return o;
        }
        match (self.container.clone(), o.container.clone()) {
            (None, None) => (),
            (Some(old_c), Some(new_c)) => out(Recipe {
                action: RecipeAction::Instant,
                inputs: vec![self.clone(), *new_c],
                outputs: vec![*old_c, o.clone()],
                ..Default::default()
            }),
            (None, Some(new_c)) => out(Recipe {
                action: RecipeAction::Instant,
                inputs: vec![*new_c, self.clone()],
                outputs: vec![o.clone()],
                ..Default::default()
            }),
            (Some(old_c), None) => out(Recipe {
                action: RecipeAction::Instant,
                inputs: vec![self.clone()],
                outputs: vec![o.clone(), *old_c],
                ..Default::default()
            }),
        }
        return o;
    }
}
impl Display for Item {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(c) = &self.container {
            write!(f, "{c}:")?;
        }
        write!(f, "{}", self.name)
    }
}
static FP: LazyLock> = LazyLock::new(move || {
    Box::new(Item {
        container: None,
        name: "foodprocessor".to_owned(),
        container_dispose_tile: Some("trash".to_string()),
    })
});
static POT: LazyLock> = LazyLock::new(move || {
    Box::new(Item {
        container: None,
        name: "pot".to_owned(),
        container_dispose_tile: Some("trash".to_string()),
        ..Default::default()
    })
});
static PAN: LazyLock> = LazyLock::new(move || {
    Box::new(Item {
        container: None,
        name: "pan".to_owned(),
        container_dispose_tile: Some("trash".to_string()),
        ..Default::default()
    })
});
static PL: LazyLock> = {
    LazyLock::new(move || {
        Box::new(Item {
            container: None,
            name: "plate".to_owned(),
            container_dispose_tile: Some("trash".to_string()),
            ..Default::default()
        })
    })
};
static GL: LazyLock> = {
    LazyLock::new(move || {
        Box::new(Item {
            container: None,
            name: "glass".to_owned(),
            container_dispose_tile: Some("sink".to_string()),
            ..Default::default()
        })
    })
};
fn from_crate(s: &str) -> Item {
    let item = Item {
        name: s.to_owned(),
        ..Default::default()
    };
    out(Recipe {
        action: RecipeAction::Instant,
        inputs: vec![],
        outputs: vec![item.clone()],
        tile: Some(format!("{s}-crate")),
        points: Some(-1),
        ..Default::default()
    });
    out(Recipe {
        action: RecipeAction::Instant,
        inputs: vec![item.clone()],
        outputs: vec![],
        tile: Some(format!("{s}-crate")),
        points: Some(1),
        ..Default::default()
    });
    return item;
}
fn cut(s: &Item) -> Item {
    cut_two(s, false)
}
fn cut_two(s: &Item, two: bool) -> Item {
    let o = Item {
        name: format!("sliced-{}", s.name),
        container: s.container.clone(),
        ..Default::default()
    };
    out(Recipe {
        action: RecipeAction::Active,
        inputs: vec![s.clone()],
        outputs: if two {
            vec![o.clone(), o.clone()]
        } else {
            vec![o.clone()]
        },
        tile: Some("cuttingboard".to_owned()),
        duration: Some(OrdAnyway(2.)),
        ..Default::default()
    });
    o
}
const COOK_D: f32 = 20.;
fn cook(s: &Item, d: f32) -> Item {
    let o = Item {
        name: format!("cooked-{}", s.name),
        container: s.container.clone(),
        ..Default::default()
    };
    out(Recipe {
        action: RecipeAction::Passive,
        duration: Some(OrdAnyway(d)),
        revert_duration: Some(OrdAnyway(d * 2.)),
        tile: Some("stove".to_owned()),
        inputs: vec![s.clone()],
        outputs: vec![o.clone()],
        ..Default::default()
    });
    out(Recipe {
        action: RecipeAction::Passive,
        duration: Some(OrdAnyway(d / 3.)),
        revert_duration: Some(OrdAnyway(d / 2.)),
        tile: Some("stove".to_owned()),
        inputs: vec![o.clone()],
        outputs: vec![Item {
            name: "burned".to_owned(),
            container: Some(POT.clone()),
            ..Default::default()
        }],
        warn: true,
        ..Default::default()
    });
    return o;
}
const SEAR_D: f32 = 15.;
fn sear(s: &Item, d: f32) -> Item {
    let s = s.tr(Some(PAN.clone()));
    let o = Item {
        name: format!("seared-{}", s.name),
        container: s.container.clone(),
        ..Default::default()
    };
    out(Recipe {
        action: RecipeAction::Passive,
        duration: Some(OrdAnyway(d)),
        revert_duration: Some(OrdAnyway(d * 2.)),
        tile: Some("stove".to_owned()),
        inputs: vec![s.clone()],
        outputs: vec![o.clone()],
        ..Default::default()
    });
    out(Recipe {
        action: RecipeAction::Passive,
        duration: Some(OrdAnyway(d / 3.)),
        revert_duration: Some(OrdAnyway(d / 3.)),
        tile: Some("stove".to_owned()),
        inputs: vec![o.clone()],
        outputs: vec![Item {
            name: "burned".to_owned(),
            container: Some(PAN.clone()),
            ..Default::default()
        }],
        warn: true,
        ..Default::default()
    });
    return o;
}
const BAKE_D: f32 = 25.;
fn bake(s: &Item, d: f32) -> Item {
    let o = Item {
        name: format!("sliced-{}", s.name),
        container: s.container.clone(),
        ..Default::default()
    };
    out(Recipe {
        action: RecipeAction::Passive,
        duration: Some(OrdAnyway(d)),
        revert_duration: Some(OrdAnyway(d * 2.)),
        tile: Some("oven".to_owned()),
        inputs: vec![s.clone()],
        outputs: vec![o.clone()],
        ..Default::default()
    });
    out(Recipe {
        action: RecipeAction::Passive,
        duration: Some(OrdAnyway(d / 2.)),
        revert_duration: Some(OrdAnyway(d / 4.)),
        tile: Some("oven".to_owned()),
        inputs: vec![o.clone()],
        outputs: vec![Item {
            name: "burned".to_owned(),
            ..Default::default()
        }],
        warn: true,
        ..Default::default()
    });
    return o;
}
fn freeze(s: &Item) -> Item {
    let o = Item {
        name: format!("frozen-{}", s.name),
        container: s.container.clone(),
        ..Default::default()
    };
    out(Recipe {
        action: RecipeAction::Passive,
        duration: Some(OrdAnyway(25.)),
        tile: Some("freezer".to_owned()),
        inputs: vec![s.clone()],
        outputs: vec![o.clone()],
        ..Default::default()
    });
    return o;
}
fn process(s: &Item) -> Item {
    let o = Item {
        name: format!("processed-{}", s.name),
        container: s.container.clone(),
        ..Default::default()
    };
    out(Recipe {
        action: RecipeAction::Passive,
        duration: Some(OrdAnyway(5.)),
        inputs: vec![s.clone()],
        outputs: vec![o.clone()],
        ..Default::default()
    });
    return o;
}
fn container_add(base: &Item, add: &Item) -> Item {
    let o = Item {
        name: "!!!".to_string(),
        container: base.container.clone(),
        ..Default::default()
    };
    out(Recipe {
        action: RecipeAction::Instant,
        inputs: vec![base.clone(), add.clone()],
        outputs: if let Some(c) = add.container.clone() {
            vec![o.clone(), *c]
        } else {
            vec![o.clone()]
        },
        ..Default::default()
    });
    return o;
}
fn combine(c: Item, items: Vec<&Item>) -> Item {
    if items.len() == 1 {
        return items[0].tr(Some(Box::new(c.clone())));
    }
    let mut open = BTreeSet::from_iter(items.iter().map(|i| {
        i.tr(Some(Box::new(c.clone())));
        vec![i.name.clone()]
    }));
    let mut seen = BTreeSet::new();
    let mut result = None;
    while let Some(cur) = open.pop_first() {
        for &new_item in &items {
            if cur.contains(&new_item.name) {
                continue;
            }
            let rkey = format!("{}#{}", cur.join(","), new_item);
            if seen.contains(&rkey) {
                continue;
            }
            seen.insert(rkey);
            let mut parts = cur.clone();
            parts.push(new_item.name.clone());
            parts.sort();
            open.insert(parts.clone());
            let i = Item {
                name: cur.join(","),
                container: Some(Box::new(c.clone())),
                ..Default::default()
            };
            let o = Item {
                name: parts.join(","),
                container: Some(Box::new(c.clone())),
                ..Default::default()
            };
            if parts.len() == items.len() {
                result = Some(o.clone())
            };
            out(Recipe {
                action: RecipeAction::Instant,
                inputs: vec![i, new_item.clone()],
                outputs: if let Some(c) = new_item.container.clone() {
                    vec![o, *c]
                } else {
                    vec![o]
                },
                ..Default::default()
            })
        }
    }
    return result.unwrap();
}
fn edible(i: Item) {
    out(Recipe {
        action: RecipeAction::Demand,
        inputs: vec![i],
        outputs: vec![], // TODO
        duration: Some(OrdAnyway(10.)),
        ..Default::default()
    })
}
fn either(a: Item, b: Item) -> Item {
    if a.name != b.name {
        panic!("either options are named differently");
    }
    if a.container != b.container {
        panic!("either options are contained differently");
    }
    return a;
}
fn sink_fill() -> Item {
    let o = Item {
        name: "water".to_owned(),
        container: Some(GL.clone()),
        ..Default::default()
    };
    out(Recipe {
        action: RecipeAction::Active,
        inputs: vec![*GL.clone()],
        outputs: vec![o.clone()],
        tile: Some("sink".to_owned()),
        duration: Some(OrdAnyway(1.)),
        ..Default::default()
    });
    return o;
}
fn main() {
    out(Recipe {
        action: RecipeAction::Active,
        duration: Some(OrdAnyway(2.)),
        tile: Some("sink".to_owned()),
        inputs: vec![Item {
            name: "dirty-plate".to_owned(),
            ..Default::default()
        }],
        outputs: vec![*PL.clone()],
        ..Default::default()
    });
    let tomato = from_crate("tomato");
    let steak = from_crate("steak");
    let flour = from_crate("flour");
    let leek = from_crate("leek");
    let rice = from_crate("rice");
    let fish = from_crate("fish");
    let coconut = from_crate("coconut");
    let strawberry = from_crate("strawberry");
    let cheese = from_crate("cheese");
    let lettuce = from_crate("lettuce");
    // // Buns
    let dough = process(&flour.tr(Some(FP.clone()))).r#as("dough").tr(None);
    let bun = bake(&dough, BAKE_D).r#as("bun");
    edible(bun.tr(Some(PL.clone())));
    // Steak
    let seared_steak = sear(&steak, SEAR_D);
    edible(combine(*PL.clone(), vec![&seared_steak, &bun]));
    edible(combine(*PL.clone(), vec![&seared_steak]));
    // Salad
    edible(combine(*PL.clone(), vec![&cut(&tomato), &cut(&lettuce)]));
    edible(combine(*PL.clone(), vec![&cut(&lettuce)]));
    // Burger
    let b_patty = sear(&cut(&steak).r#as("patty"), SEAR_D);
    edible(combine(
        *PL.clone(),
        vec![&cut(&bun), &b_patty, &cut(&cheese)],
    ));
    edible(combine(
        *PL.clone(),
        vec![&cut(&bun), &b_patty, &cut(&cheese), &cut(&lettuce)],
    ));
    edible(combine(
        *PL.clone(),
        vec![&cut(&bun), &b_patty, &cut(&tomato), &cut(&lettuce)],
    ));
    edible(combine(
        *PL.clone(),
        vec![&cut(&bun), &b_patty, &cut(&cheese), &cut(&tomato)],
    ));
    edible(combine(
        *PL.clone(),
        vec![&cut(&bun), &cut(&cheese), &cut(&lettuce), &cut(&tomato)],
    ));
    edible(combine(
        *PL.clone(),
        vec![&cut(&bun), &cut(&lettuce), &cut(&tomato)],
    ));
    // Soup
    let tomato_juice = process(&tomato.tr(Some(FP.clone()))).r#as("tomato-juice");
    let leek_tj_pot = combine(*POT.clone(), vec![&leek, &tomato_juice]);
    let tomato_soup_plate = cook(&leek_tj_pot, COOK_D)
        .r#as("tomato-soup")
        .tr(Some(PL.clone()));
    edible(tomato_soup_plate);
    // Rice and nigiri
    let nigiri = container_add(&cut(&fish), &cook(&rice.tr(Some(POT.clone())), COOK_D))
        .r#as("nigiri")
        .tr(Some(PL.clone()));
    edible(nigiri);
    // coconut milk and strawberry puree
    let strawberry_puree = process(&strawberry.tr(Some(FP.clone()))).r#as("strawberry-puree");
    let milk = process(&coconut.tr(Some(FP.clone()))).r#as("milk");
    let strawberry_shake = either(
        process(&container_add(&milk, &strawberry).r#as("milk,strawberry"))
            .r#as("strawberry-shake"),
        process(&container_add(&strawberry_puree, &coconut).r#as("coconut,strawberry-puree"))
            .r#as("strawberry-shake"),
    );
    // Icecream
    edible(
        freeze(&strawberry_shake)
            .r#as("strawberry-icecream")
            .tr(Some(PL.clone())),
    );
    // Mochi
    let rice_flour = process(&rice.tr(Some(FP.clone()))).r#as("rice-flour");
    let mochi_dough = cook(&rice_flour.tr(Some(POT.clone())), 5.).r#as("mochi-dough");
    let strawberry_mochi = container_add(&strawberry, &mochi_dough).r#as("strawberry-mochi");
    edible(strawberry_mochi);
    // Drinks
    edible(strawberry_shake.tr(Some(GL.clone())));
    edible(tomato_juice.tr(Some(GL.clone())));
    edible(sink_fill());
    // Curry
    let curry_with_rice = combine(
        *PL.clone(),
        vec![
            &cook(&&rice.tr(Some(POT.clone())), COOK_D),
            &cook(&combine(*POT.clone(), vec![&milk, &tomato, &leek]), COOK_D).r#as("curry"),
        ],
    );
    edible(curry_with_rice);
    auto_trash();
    auto_burn();
    finish();
}