/*
    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::{BotAlgo, PacketSink, step::StepState};
use hurrycurry_game_core::Game;
use hurrycurry_protocol::{
    Hand, ItemIndex, ItemLocation, Message, PlayerID, Recipe, RecipeIndex, TileIndex,
    glam::{IVec2, Vec2},
};
use log::{debug, warn};
use std::fmt::Write;

pub struct Simple {
    step: StepState,
    me: PlayerID,
    choosen_order: Option<ItemIndex>,
}

pub struct Context<'a, State> {
    pub game: &'a Game,
    pub me: PlayerID,
    pub state: &'a mut State,
    pub complexity_abort: usize,
    pub stack: Vec<ItemIndex>,
}

type LogicRes<Out = ()> = Result<Out, ()>;

impl Simple {
    pub fn new(me: PlayerID) -> Self {
        Self {
            me,
            step: StepState::new_idle(me, "idle".to_string()),
            choosen_order: None,
        }
    }
}
impl BotAlgo for Simple {
    fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) {
        if self.step.is_busy() {
            return self.step.tick(&mut out, game, dt);
        }

        Context {
            game,
            me: self.me,
            state: self,
            complexity_abort: 0,
            stack: vec![],
        }
        .update()
        .ok();
    }
}

pub trait State {
    fn queue_step(&mut self, step: StepState);
}
impl State for Simple {
    fn queue_step(&mut self, step: StepState) {
        self.step = step;
    }
}

impl<S> Context<'_, S> {
    pub fn is_hand_item(&self, item: ItemIndex) -> bool {
        self.game
            .players
            .get(&self.me)
            .is_some_and(|p| p.items[0].as_ref().is_some_and(|i| i.kind == item))
    }
    pub fn is_hand_occupied(&self) -> bool {
        self.game
            .players
            .get(&self.me)
            .map(|p| p.items[0].is_some())
            .unwrap_or(false)
    }
    /// Returns (requested_item, table_pos, remaining_time)
    pub fn find_demands(&self) -> Vec<(ItemIndex, IVec2, f32)> {
        self.game
            .players
            .iter()
            .filter_map(|(_, pl)| match &pl.communicate_persist {
                Some((Message::Item(item), timeout)) => {
                    let pos = pl.movement.position.as_ivec2();
                    [IVec2::X, IVec2::Y, -IVec2::X, -IVec2::Y]
                        .into_iter()
                        .find(|off| {
                            self.game
                                .tiles
                                .get(&(pos + *off))
                                .is_some_and(|t| self.game.data.can_place_item(&t.parts, *item))
                        })
                        .map(|off| pos + off)
                        .map(|pos| (*item, pos, timeout.remaining))
                }
                _ => None,
            })
            .collect()
    }
    pub fn find_burning(&self) -> Option<IVec2> {
        self.game
            .item_locations_index
            .iter()
            .filter_map(|p| match p {
                ItemLocation::Tile(p) => Some(*p),
                _ => None,
            })
            .find(|t| {
                self.game.tiles[t]
                    .item
                    .as_ref()
                    .is_some_and(|i| i.active.as_ref().is_some_and(|i| i.warn && i.speed > 0.))
            })
    }
    pub fn find_recipe_with_output(&self, item: ItemIndex) -> Option<RecipeIndex> {
        self.game
            .data
            .recipes
            .iter()
            .enumerate()
            .find(|(_, r)| r.outputs().any(|i| i == item))
            .map(|(i, _)| RecipeIndex(i))
    }
    pub fn find_item_on_map(&self, allow_table: bool, item: ItemIndex) -> Option<IVec2> {
        self.game
            .item_locations_index
            .iter()
            .filter_map(|p| match p {
                ItemLocation::Tile(p) => Some(*p),
                _ => None,
            })
            .filter(|p| {
                allow_table
                    || !self.game.tiles[p]
                        .parts
                        .iter()
                        .any(|t| self.game.data.tile_name(*t) == "table")
            })
            .find(|p| {
                self.game.tiles[p]
                    .item
                    .as_ref()
                    .is_some_and(|t| t.kind == item)
            })
    }
    pub fn find_tile(&self, tile: TileIndex) -> Option<IVec2> {
        let pos = self.my_position().as_ivec2();
        self.game
            .tile_index
            .get(&tile)?
            .iter()
            .min_by_key(|p| p.manhattan_distance(pos))
            .copied()
    }
    pub fn find_occupied_table(&self) -> Option<IVec2> {
        let pos = self.my_position().as_ivec2();
        let tile = self.game.data.get_tile_by_name("table")?;
        self.game
            .tile_index
            .get(&tile)?
            .iter()
            .filter(|pos| self.game.tiles[pos].item.is_some())
            .min_by_key(|p| p.manhattan_distance(pos))
            .copied()
    }
    pub fn my_position(&self) -> Vec2 {
        self.game.players[&self.me].movement.position
    }
    pub fn find_free_tool_by_name(&self, name: &str) -> Option<IVec2> {
        let pos = self.my_position().as_ivec2();
        let tile = self.game.data.get_tile_by_name(name)?;
        self.game
            .tile_index
            .get(&tile)?
            .iter()
            .filter(|pos| self.game.tiles[pos].item.is_none())
            .min_by_key(|p| p.manhattan_distance(pos))
            .copied()
    }
    pub fn find_stash_tile_by_name(&self, name: &str) -> Option<IVec2> {
        let pos = self.my_position().as_ivec2();
        let tile = self.game.data.get_tile_by_name(name)?;
        self.game
            .tile_index
            .get(&tile)?
            .iter()
            .filter(|pos| {
                self.game.tiles[pos].item.is_none()
                // ensure there is no tool or other part that blocks access
                    && self.game.tiles[pos]
                        .parts
                        .iter()
                        .all(|p| matches!(self.game.data.tile_name(*p), "counter" | "counter-window" | "floor"))
            })
            .min_by_key(|p| p.manhattan_distance(pos))
            .copied()
    }
    pub fn is_tile_occupied(&self, pos: IVec2) -> bool {
        self.game
            .tiles
            .get(&pos)
            .map(|t| t.item.is_some())
            .unwrap_or(true)
    }
}
impl<S: State> Context<'_, S> {
    pub fn find_stash_tile(&self, prefer_cw: bool) -> Option<IVec2> {
        if prefer_cw && let Some(t) = self.find_stash_tile_by_name("counter-window") {
            return Some(t);
        }
        if let Some(t) = self.find_stash_tile_by_name("counter") {
            return Some(t);
        }
        warn!("all counters filled up");
        None
    }
    pub fn clear_tile(&mut self, pos: IVec2) -> LogicRes {
        debug!("clear tile {pos}");
        self.assert_hand_is_clear()?;
        self.interact_with(pos, 0., "clear tile")
    }
    pub fn assert_tile_is_clear(&mut self, pos: IVec2) -> LogicRes {
        if self.is_tile_occupied(pos) {
            self.clear_tile(pos)?;
        }
        Ok(())
    }
    pub fn assert_hand_is_clear(&mut self) -> LogicRes {
        if self.is_hand_occupied() {
            self.dispose_hand(false, "clear hand")?;
        }
        Ok(())
    }
    pub fn dispose_hand(&mut self, prefer_cw: bool, label: &str) -> LogicRes {
        debug!("dispose hand");
        if let Some(pos) = self.find_stash_tile(prefer_cw) {
            self.interact_with(pos, 0., label)?;
            warn!("no path to empty space");
            Err(())
        } else {
            warn!("no empty space left");
            Err(())
        }
    }
    pub fn interact_with(&mut self, tile: IVec2, duration: f32, label: &str) -> LogicRes {
        if let Some(step) = StepState::new_segment(
            self.game,
            self.me,
            Hand(0),
            tile,
            duration,
            self.stack_label(label),
        ) {
            self.state.queue_step(step);
            Err(())
        } else {
            Ok(())
        }
    }
    pub fn wait(&mut self, delay: f32, label: &str) -> LogicRes {
        self.state
            .queue_step(StepState::new_wait(self.me, delay, self.stack_label(label)));
        return Err(());
    }
    fn stack_label(&self, inner: &str) -> String {
        let mut o = String::new();
        for &i in &self.stack {
            writeln!(o, "-> {}", self.game.data.item_name(i)).unwrap();
        }
        write!(o, "{inner}").unwrap();
        o
    }
}

impl Context<'_, Simple> {
    pub fn aquire_placed_item(&mut self, item: ItemIndex) -> LogicRes<IVec2> {
        debug!("aquire placed item {:?}", self.game.data.item_names[item.0]);
        if let Some(pos) = self.find_item_on_map(false, item) {
            return Ok(pos);
        }
        self.aquire_item(item)?;
        self.dispose_hand(false, "put down item")?;
        Err(())
    }
    pub fn aquire_item(&mut self, item: ItemIndex) -> LogicRes {
        self.stack.push(item);
        debug!("aquire item {:?}", self.game.data.item_names[item.0]);
        self.complexity_abort += 1;
        if self.complexity_abort > 32 {
            warn!("too much complexity");
            self.wait(0.5, "complexity limit")?;
        }
        if self.is_hand_item(item) {
            self.stack.pop();
            return Ok(());
        }
        if let Some(pos) = self.find_item_on_map(false, item) {
            self.assert_hand_is_clear()?;
            self.interact_with(pos, 0., "pick item")?;
        } else if let Some(recipe) = self.find_recipe_with_output(item) {
            let r = &self.game.data.recipes[recipe.0];
            match r {
                Recipe::Instant {
                    tile: Some(tile),
                    inputs: [None, None],
                    ..
                } => {
                    if let Some(pos) = self.find_tile(*tile) {
                        self.assert_tile_is_clear(pos)?;
                        self.assert_hand_is_clear()?;
                        self.interact_with(pos, 0., "instant from empty")?;
                    }
                }
                Recipe::Instant {
                    tile: None,
                    inputs: [Some(a), Some(b)],
                    ..
                } => {
                    let apos = self.aquire_placed_item(*a)?;
                    self.aquire_item(*b)?;
                    self.interact_with(apos, 0., "instant (B)")?;
                }
                Recipe::Instant {
                    tile: None,
                    inputs: [Some(input), None],
                    ..
                } => {
                    self.aquire_item(*input)?;
                    if let Some(pos) = self.find_stash_tile(false) {
                        self.interact_with(pos, 0., "instant (A)")?;
                    } else {
                        warn!("no empty space left")
                    }
                }
                Recipe::Active {
                    tile: Some(tile),
                    input,
                    speed,
                    ..
                } => {
                    if let Some(pos) = self.find_tile(*tile) {
                        self.assert_tile_is_clear(pos)?;
                        self.aquire_item(*input)?;
                        self.interact_with(pos, 1. / speed + 0.2, "active")?;
                    }
                }
                Recipe::Passive {
                    tile: Some(tile),
                    input,
                    ..
                } => {
                    if let Some(pos) = self.find_tile(*tile) {
                        if let Some(item) = &self.game.tiles.get(&pos).unwrap().item {
                            if item.kind == *input {
                                debug!("waiting for passive to finish at {pos}");
                                self.wait(0.5, "waiting for passive")?;
                            } else {
                                self.assert_tile_is_clear(pos)?;
                            }
                        }
                        self.aquire_item(*input)?;
                        self.interact_with(pos, 0., "pick passive output")?;
                    }
                }
                Recipe::Passive {
                    tile: None, input, ..
                } => {
                    self.aquire_placed_item(*input)?;
                    debug!("waiting for passive to finish");
                    self.wait(0.5, "wait for passive")?;
                }
                _ => warn!("recipe too hard {r:?}"),
            }
            warn!(
                "stuck at making item {:?} via {r:?}",
                self.game.data.item_names[item.0]
            );
        }
        Err(())
    }
    pub fn update(&mut self) -> LogicRes {
        if let Some(pos) = self.find_burning() {
            self.assert_hand_is_clear()?;
            self.interact_with(pos, 0., "prevent burning")?;
        } else if let Some(item) = self.state.choosen_order {
            if !self.find_demands().iter().any(|(i, _, _)| *i == item) {
                self.state.choosen_order = None;
            }
            self.aquire_item(item)?;
            self.state.choosen_order = None;
            self.dispose_hand(true, "drop for delivery")?;
        } else {
            if let Some((item, _, _)) = self
                .find_demands()
                .iter()
                .filter(|(i, _, _)| self.game.data.item_name(*i) != "unknown-order")
                .filter(|(i, _, _)| self.find_item_on_map(false, *i).is_none())
                .min_by_key(|(_, _, d)| (*d * 1000.) as i64)
            {
                self.state.choosen_order = Some(*item)
            } else {
                self.wait(1., "waiting for orders")?;
            }
        }
        Ok(())
    }
}
