/*
    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 super::{Entity, EntityContext};
use anyhow::Result;
use hurrycurry_locale::{TrError, trm};
use hurrycurry_protocol::{
    ItemIndex, ItemLocation, Message, PacketC, PlayerID, Recipe, RecipeIndex, TileIndex,
    glam::IVec2,
};
use log::{debug, warn};

pub struct Tutorial {
    pub player: PlayerID,
    state: TutorialState,
    next_update_due: f32,
    current_hint: Option<(Option<IVec2>, Message)>,
    delete_timer: f32,
    pub ended: bool,
}

pub enum TutorialState {
    Item {
        item: ItemIndex,
        had_aquired_target: bool,
    },
    Button {
        tile: TileIndex,
        pressed: bool,
    },
}

impl Tutorial {
    fn new(player: PlayerID, target: TutorialState) -> Self {
        Self {
            ended: false,
            player,
            next_update_due: 0.,
            state: target,
            current_hint: None,
            delete_timer: 1.5,
        }
    }
    pub fn new_item(player: PlayerID, item: ItemIndex) -> Self {
        Self::new(
            player,
            TutorialState::Item {
                item,
                had_aquired_target: false,
            },
        )
    }
    pub fn new_button(player: PlayerID, tile: TileIndex) -> Self {
        Self::new(
            player,
            TutorialState::Button {
                tile,
                pressed: 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: match &self.state {
                TutorialState::Item { item, .. } => *item,
                _ => ItemIndex(0), // TODO
            },
            success: false,
        });
    }
    fn interact(
        &mut self,
        c: EntityContext<'_>,
        target: Option<ItemLocation>,
        player: PlayerID,
    ) -> Result<bool, TrError> {
        if player == self.player {
            match (&mut self.state, target) {
                (TutorialState::Button { tile, pressed }, Some(ItemLocation::Tile(pos))) => {
                    if let Some(t) = c.game.tiles.get(&pos) {
                        if t.parts.contains(tile) {
                            *pressed = true;
                        }
                    }
                }
                _ => (),
            };
        }
        Ok(false)
    }

    fn tick(&mut self, c: EntityContext<'_>) -> Result<(), TrError> {
        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 = match &mut self.state {
            TutorialState::Item {
                item,
                had_aquired_target,
            } => StepContext {
                ent: &c,
                had_aquired_target,
                player: self.player,
                recursion_abort: 0,
            }
            .fulfil_demand(*item)
            .err(),
            TutorialState::Button { tile, pressed } => find_button(&c, *tile, *pressed),
        };
        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: match &self.state {
                        TutorialState::Item { item, .. } => *item,
                        _ => ItemIndex(0),
                    },
                    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 find_button(
    ent: &EntityContext,
    tile: TileIndex,
    pressed: bool,
) -> Option<(Option<IVec2>, Message)> {
    if pressed {
        return None;
    }
    ent.game
        .tile_index
        .get(&tile)
        .map(|pos| pos.iter().next())
        .flatten()
        .map(|p| {
            (
                Some(*p),
                match ent.game.data.tile_name(tile) {
                    "book" => trm!("s.tutorial.button.book"),
                    "map-selector" => trm!("s.tutorial.button.map_selector"),
                    _ => trm!("s.tutorial.interact"),
                },
            )
        })
}

struct StepContext<'a> {
    ent: &'a EntityContext<'a>,
    had_aquired_target: &'a mut bool,
    recursion_abort: usize,
    player: PlayerID,
}

impl StepContext<'_> {
    fn is_hand_item(&self, item: ItemIndex) -> bool {
        self.ent
            .game
            .players
            .get(&self.player)
            .is_some_and(|p| p.items.iter().flatten().any(|i| i.kind == item))
    }
    pub fn find_demand(&self, item: ItemIndex) -> Option<IVec2> {
        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<RecipeIndex> {
        self.ent
            .game
            .data
            .recipes
            .iter()
            .enumerate()
            .find(|(_, r)| r.outputs().any(|i| i == item))
            .map(|(i, _)| RecipeIndex(i))
    }
    fn find_item_on_map(&self, item: ItemIndex) -> Option<IVec2> {
        self.ent
            .game
            .tiles
            .iter()
            .find(|(_, t)| t.item.as_ref().is_some_and(|t| t.kind == item))
            .map(|(p, _)| *p)
    }
    fn find_tile(&self, tile: TileIndex) -> Option<IVec2> {
        self.ent
            .game
            .tile_index
            .get(&tile)
            .and_then(|s| s.iter().next().copied())
    }
    fn aquire_placed_item(&mut self, item: ItemIndex) -> Result<IVec2, (Option<IVec2>, 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<IVec2>, Message)> {
        if let Some((pos, tile)) = self.ent.game.tiles.iter().find(|(_, t)| {
            t.item
                .as_ref()
                .is_some_and(|t| t.active.as_ref().is_some_and(|i| i.warn && i.speed > 0.))
        }) {
            Err((
                Some(*pos),
                if tile
                    .parts
                    .iter()
                    .any(|p| matches!(self.ent.game.data.tile_name(*p), "stove" | "oven"))
                {
                    trm!("s.tutorial.prevent_burning")
                } else {
                    trm!("s.tutorial.take_now")
                },
            ))
        } else {
            Ok(())
        }
    }
    fn fulfil_demand(&mut self, item: ItemIndex) -> Result<(), (Option<IVec2>, 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)
            .is_some_and(|p| p.items.iter().flatten().any(|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<IVec2>, 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,
                    ..
                } => {
                    if let Some(tiles) = self.ent.game.tile_index.get(tile) {
                        for pos in tiles {
                            let Some(tile) = self.ent.game.tiles.get(pos) else {
                                continue;
                            };
                            if let Some(item) = &tile.item
                                && 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) == "cutting-board" {
                                trm!("s.tutorial.active_cutting_board")
                            } else {
                                trm!("s.tutorial.active", s = format!("{:.01}", 1. / speed))
                            },
                        ));
                    }
                }
                Recipe::Passive {
                    tile: Some(tile),
                    input,
                    ..
                } => {
                    if let Some(tiles) = self.ent.game.tile_index.get(tile) {
                        for pos in tiles {
                            let Some(tile) = self.ent.game.tiles.get(pos) else {
                                continue;
                            };
                            if let Some(item) = &tile.item
                                && item.kind == *input
                            {
                                return Err((None, 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, ..
                } => {
                    self.aquire_placed_item(*input)?;
                    return Err((None, 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")))
    }
}
