/*
    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 super::{Entity, EntityContext};
use crate::{message::TrError, trm};
use anyhow::Result;
use hurrycurry_protocol::{
    glam::IVec2, ItemIndex, Message, PacketC, PlayerID, Recipe, RecipeIndex, TileIndex,
};
use log::{debug, warn};
pub struct Tutorial {
    pub player: PlayerID,
    target: ItemIndex,
    next_update_due: f32,
    had_aquired_target: bool,
    current_hint: Option<(Option, Message)>,
    delete_timer: f32,
    pub ended: bool,
}
impl Tutorial {
    pub fn new(player: PlayerID, item: ItemIndex) -> Self {
        Self {
            ended: false,
            player,
            next_update_due: 0.,
            target: item,
            current_hint: None,
            delete_timer: 1.5,
            had_aquired_target: false,
        }
    }
}
impl Entity for Tutorial {
    fn finished(&self) -> bool {
        self.ended
    }
    fn destructor(&mut self, c: EntityContext<'_>) {
        if let Some((position, _)) = self.current_hint {
            c.packet_out.push_back(PacketC::ServerHint {
                position,
                player: self.player,
                message: None,
            });
        }
        c.packet_out.push_back(PacketC::TutorialEnded {
            player: self.player,
            item: self.target,
            success: false,
        });
    }
    fn tick(&mut self, c: EntityContext<'_>) -> Result<()> {
        if self.ended {
            return Ok(());
        }
        const TARGET_DT: f32 = 0.2;
        self.next_update_due -= c.dt;
        if self.next_update_due > 0. {
            return Ok(());
        }
        self.next_update_due += TARGET_DT;
        let mut hint = StepContext {
            ent: &c,
            had_aquired_target: &mut self.had_aquired_target,
            player: self.player,
            recursion_abort: 0,
        }
        .fulfil_demand(self.target)
        .err();
        if hint.is_none() {
            self.delete_timer -= TARGET_DT;
            if self.delete_timer <= 0. {
                self.ended = true;
                hint = None;
                c.packet_out.push_back(PacketC::TutorialEnded {
                    item: self.target,
                    player: self.player,
                    success: true,
                });
            } else {
                hint = Some((None, trm!("s.tutorial.finished")));
            }
        }
        if hint != self.current_hint {
            if let Some((position, _)) = self.current_hint.take() {
                c.packet_out.push_back(PacketC::ServerHint {
                    position,
                    player: self.player,
                    message: None,
                });
            }
            if let Some((position, message)) = hint.clone() {
                c.packet_out.push_back(PacketC::ServerHint {
                    player: self.player,
                    position,
                    message: Some(message),
                });
            }
            self.current_hint = hint;
        }
        Ok(())
    }
    fn interact(
        &mut self,
        _c: EntityContext<'_>,
        _pos: Option,
        _player: PlayerID,
    ) -> Result {
        Ok(false)
    }
}
struct StepContext<'a> {
    ent: &'a EntityContext<'a>,
    had_aquired_target: &'a mut bool,
    recursion_abort: usize,
    player: PlayerID,
}
impl<'a> StepContext<'a> {
    fn is_hand_item(&self, item: ItemIndex) -> bool {
        self.ent
            .game
            .players
            .get(&self.player)
            .map_or(false, |p| p.item.as_ref().map_or(false, |i| i.kind == item))
    }
    pub fn find_demand(&self, item: ItemIndex) -> Option {
        self.ent
            .game
            .players
            .iter()
            .find_map(|(_, pl)| match &pl.communicate_persist {
                Some((Message::Item(i), _)) if *i == item => Some(pl.movement.position.as_ivec2()),
                _ => None,
            })
    }
    fn find_recipe_with_output(&self, item: ItemIndex) -> Option {
        self.ent
            .game
            .data
            .recipes
            .iter()
            .enumerate()
            .find(|(_, r)| r.outputs().contains(&item))
            .map(|(i, _)| RecipeIndex(i))
    }
    fn find_item_on_map(&self, item: ItemIndex) -> Option {
        self.ent
            .game
            .tiles
            .iter()
            .find(|(_, t)| t.item.as_ref().map_or(false, |t| t.kind == item))
            .map(|(p, _)| *p)
    }
    fn find_tile(&self, tile: TileIndex) -> Option {
        self.ent
            .game
            .tiles
            .iter()
            .find(|(_, t)| t.kind == tile)
            .map(|(p, _)| *p)
    }
    fn aquire_placed_item(&mut self, item: ItemIndex) -> Result, Message)> {
        debug!(
            "aquire placed item {:?}",
            self.ent.game.data.item_names[item.0]
        );
        if let Some(pos) = self.find_item_on_map(item) {
            return Ok(pos);
        }
        self.aquire_item(item)?;
        Err((None, trm!("s.tutorial.put_away")))
    }
    fn prevent_burning(&self) -> Result<(), (Option, Message)> {
        if let Some((pos, tile)) = self.ent.game.tiles.iter().find(|(_, t)| {
            t.item.as_ref().map_or(false, |t| {
                t.active.as_ref().map_or(false, |i| i.warn && i.speed > 0.)
            })
        }) {
            Err((
                Some(*pos),
                match self.ent.game.data.tile_name(tile.kind).as_str() {
                    "stove" | "oven" => trm!("s.tutorial.prevent_burning"),
                    _ => trm!("s.tutorial.take_now"),
                },
            ))
        } else {
            Ok(())
        }
    }
    fn fulfil_demand(&mut self, item: ItemIndex) -> Result<(), (Option, Message)> {
        if self.ent.game.data.item_name(item) == "unknown-order" {
            return if let Some(pos) = self.find_demand(item) {
                Err((Some(pos), trm!("s.tutorial.accept_order")))
            } else {
                Ok(())
            };
        }
        if !*self.had_aquired_target {
            self.prevent_burning()?;
            self.aquire_item(item)?;
            *self.had_aquired_target = true;
        }
        if self
            .ent
            .game
            .players
            .get(&self.player)
            .map_or(false, |p| p.item.as_ref().map_or(false, |i| i.kind == item))
        {
            if let Some(pos) = self.find_demand(item) {
                Err((Some(pos), trm!("s.tutorial.serve")))
            } else {
                Ok(())
            }
        } else {
            Ok(())
        }
    }
    fn aquire_item(&mut self, item: ItemIndex) -> Result<(), (Option, Message)> {
        debug!("aquire item {:?}", self.ent.game.data.item_names[item.0]);
        self.recursion_abort += 1;
        if self.recursion_abort > 32 {
            warn!("too much recursion");
            return Err((None, trm!("s.tutorial.error")));
        }
        if self.is_hand_item(item) {
            return Ok(());
        }
        if let Some(pos) = self.find_item_on_map(item) {
            return Err((Some(pos), trm!("s.tutorial.pickup")));
        }
        if let Some(recipe) = self.find_recipe_with_output(item) {
            let r = &self.ent.game.data.recipes[recipe.0];
            match r {
                Recipe::Instant {
                    tile: Some(tile),
                    inputs: [None, None],
                    ..
                } => {
                    if let Some(pos) = self.find_tile(*tile) {
                        return Err((Some(pos), trm!("s.tutorial.take", i = item)));
                    }
                }
                Recipe::Instant {
                    tile: None,
                    inputs: [Some(a), Some(b)],
                    ..
                } => {
                    let apos = self.aquire_placed_item(*a)?;
                    self.aquire_item(*b)?;
                    let aname = self.ent.game.data.item_name(*a);
                    let bname = self.ent.game.data.item_name(*b);
                    return Err((
                        Some(apos),
                        if aname.starts_with("plate:") || bname.starts_with("plate:") {
                            trm!("s.tutorial.interact_plate")
                        } else {
                            trm!("s.tutorial.interact")
                        },
                    ));
                }
                Recipe::Instant {
                    tile: None,
                    inputs: [Some(input), None],
                    ..
                } => {
                    self.aquire_item(*input)?;
                    return Err((None, trm!("s.tutorial.interact_empty")));
                }
                Recipe::Active {
                    tile: Some(tile),
                    input,
                    speed,
                    ..
                } => {
                    for (pos, tile) in self.ent.game.tiles.iter().filter(|(_, t)| t.kind == *tile) {
                        if let Some(item) = &tile.item {
                            if item.kind == *input {
                                return Err((Some(*pos), trm!("s.tutorial.hold_interact")));
                            }
                        }
                    }
                    if let Some(pos) = self.find_tile(*tile) {
                        self.aquire_item(*input)?;
                        return Err((
                            Some(pos),
                            if self.ent.game.data.tile_name(*tile) == "cuttingboard" {
                                trm!("s.tutorial.active_cuttingboard")
                            } else {
                                trm!("s.tutorial.active", s = format!("{:.01}", 1. / speed))
                            },
                        ));
                    }
                }
                Recipe::Passive {
                    tile: Some(tile),
                    input,
                    ..
                } => {
                    for (pos, tile) in self.ent.game.tiles.iter().filter(|(_, t)| t.kind == *tile) {
                        if let Some(item) = &tile.item {
                            if item.kind == *input {
                                return Err((Some(*pos), trm!("s.tutorial.wait_finish")));
                            }
                        }
                    }
                    if let Some(pos) = self.find_tile(*tile) {
                        self.aquire_item(*input)?;
                        return Err((Some(pos), trm!("s.tutorial.put_on", t = *tile)));
                    }
                }
                Recipe::Passive {
                    tile: None, input, ..
                } => {
                    let pos = self.aquire_placed_item(*input)?;
                    return Err((Some(pos), trm!("s.tutorial.wait_finish")));
                }
                _ => warn!("recipe too hard {r:?}"),
            }
        }
        warn!(
            "stuck at making item {:?}",
            self.ent.game.data.item_names[item.0]
        );
        Err((None, trm!("s.tutorial.error")))
    }
}