/*
    Hurry Curry! - a game about cooking
    Copyright (C) 2025 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, BotInput,
    pathfinding::{Path, find_path_to_neighbour},
};
use hurrycurry_client_lib::Game;
use hurrycurry_protocol::{
    ItemIndex, Message, PlayerID, Recipe, RecipeIndex, TileIndex, glam::IVec2,
};
use log::{debug, warn};

#[derive(Default)]
pub struct Simple {
    path: Option<(Path, IVec2, f32)>,
    cooldown: f32,
}

pub struct Context<'a, State> {
    pub game: &'a Game,
    pub me: PlayerID,
    pub own_position: IVec2,
    pub state: &'a mut State,
    pub recursion_abort: usize,
}

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

impl BotAlgo for Simple {
    fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput {
        let Some(player) = game.players.get(&me) else {
            return BotInput::default();
        };
        let pos = player.movement.position;

        if self.cooldown > 0. {
            self.cooldown -= dt;
            return BotInput::default();
        }

        if let Some((path, target, down)) = &mut self.path {
            let direction = path.next_direction(pos, dt);
            let arrived = path.is_done();
            let target = *target;
            if arrived {
                *down -= dt;
                if *down < 0. {
                    self.path = None;
                    self.cooldown = 0.2;
                }
            }
            return BotInput {
                direction,
                boost: false,
                interact: if arrived { Some(target) } else { None },
                ..Default::default()
            };
        }

        Context {
            game,
            own_position: pos.as_ivec2(),
            me,
            state: self,
            recursion_abort: 0,
        }
        .update()
        .ok();
        debug!("target={:?}", self.path.as_ref().map(|a| a.1));

        BotInput::default()
    }
}

pub trait State {
    fn cooldown(&mut self, duration: f32);
    fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32);
    fn get_empty_tile_priority(&self) -> &'static [&'static str];
}
impl State for Simple {
    fn cooldown(&mut self, duration: f32) {
        self.cooldown = duration;
    }
    fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32) {
        self.path = Some((path, tile, duration));
    }
    fn get_empty_tile_priority(&self) -> &'static [&'static str] {
        &["counter", "counter-window"]
    }
}

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
                                    .tile_placeable_items
                                    .get(&t.kind)
                                    .is_none_or(|placable| placable.contains(item))
                            })
                        })
                        .map(|off| pos + off)
                        .map(|pos| (*item, pos, timeout.remaining))
                }
                _ => None,
            })
            .collect()
    }
    pub fn find_recipe_with_output(&self, item: ItemIndex) -> Option<RecipeIndex> {
        self.game
            .data
            .recipes
            .iter()
            .enumerate()
            .find(|(_, r)| r.outputs().contains(&item))
            .map(|(i, _)| RecipeIndex(i))
    }
    pub fn find_item_on_map(&self, item: ItemIndex) -> Option<IVec2> {
        self.game
            .tiles
            .iter()
            .find(|(_, t)| t.item.as_ref().is_some_and(|t| t.kind == item))
            .map(|(p, _)| *p)
    }
    pub fn find_tile(&self, tile: TileIndex) -> Option<IVec2> {
        self.game
            .tiles
            .iter()
            .find(|(_, t)| t.kind == tile)
            .map(|(p, _)| *p)
    }
    pub fn find_occupied_table_or_floor(&self) -> Option<IVec2> {
        self.game
            .tiles
            .iter()
            .find(|(_, t)| {
                t.item.is_some()
                    && matches!(
                        self.game.data.tile_names[t.kind.0].as_str(),
                        "table" | "floor"
                    )
            })
            .map(|(p, _)| *p)
    }
    pub fn find_empty_interactable_tile_by_name(&self, name: &str) -> Option<IVec2> {
        self.game
            .tiles
            .iter()
            .find(|(_, t)| {
                !self.game.data.tile_placeable_items.contains_key(&t.kind)
                    && t.item.is_none()
                    && self.game.data.tile_names[t.kind.0] == name
            })
            .map(|(p, _)| *p)
    }

    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_empty_interactable_tile(&self) -> Option<IVec2> {
        for p in self.state.get_empty_tile_priority() {
            if let Some(t) = self.find_empty_interactable_tile_by_name(p) {
                return Some(t);
            }
        }
        warn!("all counters filled up");
        self.game
            .tiles
            .iter()
            .find(|(_, t)| !self.game.data.tile_placeable_items.contains_key(&t.kind)) // TODO filter by placable item
            .map(|(p, _)| *p)
    }
    pub fn clear_tile(&mut self, pos: IVec2) -> LogicRes {
        debug!("clear tile {pos}");
        self.assert_hand_is_clear()?;
        self.interact_with(pos, 0.)
    }
    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()?;
        }
        Ok(())
    }
    pub fn dispose_hand(&mut self) -> LogicRes {
        debug!("dispose hand");
        if let Some(pos) = self.find_empty_interactable_tile() {
            self.interact_with(pos, 0.)?;
            warn!("no path to empty space ");
            Err(())
        } else {
            warn!("no empty space left");
            Err(())
        }
    }
    pub fn interact_with(&mut self, tile: IVec2, duration: f32) -> LogicRes {
        if let Some(path) = find_path_to_neighbour(&self.game.walkable, self.own_position, tile) {
            self.state.queue_segment(path, tile, duration);
            Err(())
        } else {
            Ok(())
        }
    }
}

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(item) {
            return Ok(pos);
        }
        self.aquire_item(item)?;
        self.dispose_hand()?;
        Err(())
    }
    pub fn aquire_item(&mut self, item: ItemIndex) -> LogicRes {
        debug!("aquire item {:?}", self.game.data.item_names[item.0]);
        self.recursion_abort += 1;
        if self.recursion_abort > 32 {
            warn!("too much recursion");
            return Err(());
        }
        if self.is_hand_item(item) {
            return Ok(());
        }
        if let Some(pos) = self.find_item_on_map(item) {
            self.assert_hand_is_clear()?;
            self.interact_with(pos, 0.)?;
            return Ok(());
        }
        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.)?;
                    }
                }
                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.)?;
                }
                Recipe::Instant {
                    tile: None,
                    inputs: [Some(input), None],
                    ..
                } => {
                    self.aquire_item(*input)?;
                    if let Some(pos) = self.find_empty_interactable_tile() {
                        self.interact_with(pos, 0.)?;
                    } 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)?;
                    }
                }
                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.state.cooldown(0.5);
                                return Err(()); // waiting for it to finish
                            } else {
                                self.assert_tile_is_clear(pos)?;
                            }
                        }
                        self.aquire_item(*input)?;
                        self.interact_with(pos, 0.)?;
                    }
                }
                Recipe::Passive {
                    tile: None, input, ..
                } => {
                    self.aquire_placed_item(*input)?;
                    debug!("waiting for passive to finish");
                    self.state.cooldown(0.5);
                    return Err(());
                }
                _ => warn!("recipe too hard {r:?}"),
            }
        }
        warn!(
            "stuck at making item {:?}",
            self.game.data.item_names[item.0]
        );
        Err(())
    }
    pub fn update(&mut self) -> LogicRes {
        if let Some((item, table, _)) = self.find_demands().pop() {
            if self.game.data.item_name(item) == "unknown-order" {
                self.interact_with(table, 0.)?;
            } else {
                self.assert_tile_is_clear(table)?;
                self.aquire_item(item)?;
                self.interact_with(table, 0.)?;
            }
        }
        Ok(())
    }
}
