/*
    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,
    pathfinding::{HoldLocation, Path, find_path},
    random_usize,
};
use hurrycurry_game_core::Game;
use hurrycurry_protocol::{
    DebugEvent, DemandIndex, Effect, Hand, ItemLocation, Message, PacketS, PlayerClass, PlayerID,
    Score, TileIndex,
    glam::{IVec2, Vec2},
};
use log::debug;

#[derive(Debug, Clone)]
pub struct Customer {
    me: PlayerID,
    state: CustomerState,
}

#[derive(Debug, Clone, Default)]
pub struct CustomerConfig {
    pub unknown_order: bool,
}

#[derive(Debug, Clone, Default)]
enum CustomerState {
    #[default]
    New,
    Entering {
        path: Path,
        chair: IVec2,
        origin: IVec2,
        ticks: usize,
    },
    Waiting {
        hold_location: HoldLocation,
        demand: DemandIndex,
        chair: IVec2,
        timeout: f32,
        origin: IVec2,
        check: u8,
        pinned: bool,
    },
    Eating {
        hold_location: HoldLocation,
        demand: DemandIndex,
        table: IVec2,
        progress: f32,
        origin: IVec2,
    },
    Finishing {
        table: IVec2,
        origin: IVec2,
        cooldown: f32,
    },
    Exiting {
        path: Path,
    },
    Exited,
}

impl Customer {
    pub fn new(me: PlayerID) -> Self {
        Customer {
            me,
            state: CustomerState::default(),
        }
    }
}
impl BotAlgo for Customer {
    fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) {
        self.state.tick(&mut out, self.me, game, dt);
    }
    fn is_finished(&self) -> bool {
        matches!(self.state, CustomerState::Exited)
    }
}
impl CustomerState {
    fn tick(&mut self, out: &mut PacketSink, me: PlayerID, game: &Game, dt: f32) {
        let Some(playerdata) = game.players.get(&me) else {
            return;
        };
        let pos = playerdata.movement.position;
        #[cfg(feature = "debug_events")]
        {
            out.push(PacketS::Debug(self.debug(me)));
        }
        match self {
            CustomerState::New => {
                if !game.data.demands.is_empty() {
                    let chair_ti = game.data.get_tile_by_name("chair").unwrap_or(TileIndex(0)); // TODO
                    let chairs = game
                        .tile_index
                        .get(&chair_ti)
                        .into_iter()
                        .flatten()
                        .copied()
                        .collect::<Vec<_>>(); //? maybe opt alloc
                    if let Some(&chair) =
                        chairs.get(random_usize(&mut rand::rng()) % chairs.len().max(1))
                        && let Some(path) = find_path(game, pos.as_ivec2(), chair)
                    {
                        debug!("{me:?} -> entering");
                        *self = CustomerState::Entering {
                            path,
                            chair,
                            origin: pos.as_ivec2(),
                            ticks: 0,
                        };
                    }
                }
            }
            CustomerState::Entering {
                path,
                chair,
                origin,
                ticks,
            } => {
                *ticks += 1;
                let check = *ticks % 10 == 0;
                if path.is_done() {
                    let demand =
                        DemandIndex(random_usize(&mut rand::rng()) % game.data.demands.len());
                    let requested_item = game.data.demands[demand.0].input;
                    debug!("{me:?} -> waiting");
                    let timeout = 90. + rand::random::<f32>() * 60.;
                    let mut facing = Vec2::ZERO;
                    for off in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] {
                        if game
                            .tiles
                            .get(&(off + *chair))
                            .is_some_and(|t| game.data.can_place_item(&t.parts, requested_item))
                        {
                            facing = off.as_vec2();
                        }
                    }
                    *self = CustomerState::Waiting {
                        chair: *chair,
                        timeout,
                        demand,
                        origin: *origin,
                        check: 0,
                        pinned: false,
                        hold_location: HoldLocation::new(chair.as_vec2(), facing),
                    };
                    let message_item = if !game.data.flags.disable_unknown_orders {
                        game.data
                            .get_item_by_name("unknown-order")
                            .unwrap_or(requested_item)
                    } else {
                        requested_item
                    };
                    out.push(PacketS::Movement {
                        player: me,
                        boost: false,
                        dir: Vec2::ZERO,
                        pos: None,
                    });
                    out.push(PacketS::Communicate {
                        message: Some(Message::Item(message_item)),
                        timeout: Some(timeout),
                        player: me,
                        pin: Some(false),
                    });
                } else if check
                    && path.remaining_segments() < 5
                    && game.players.iter().any(|(id, p)| {
                        p.class == PlayerClass::Customer
                            && *id != me
                            && p.movement.position.distance(chair.as_vec2() + 0.5) < 1.
                    })
                {
                    *self = CustomerState::New;
                } else if path.is_stuck(3.) {
                    if let Some(path) = find_path(game, pos.as_ivec2(), *origin) {
                        *self = CustomerState::Exiting { path };
                    }
                } else {
                    #[cfg(feature = "debug_events")]
                    out.push(PacketS::Debug(path.debug(me)));
                    path.update(out, me, pos, dt, 0.6, false);
                }
            }
            CustomerState::Waiting {
                hold_location,
                chair,
                demand,
                timeout,
                origin,
                check,
                pinned,
            } => {
                *timeout -= dt;
                *check += 1;
                if *timeout <= 0. {
                    let path = find_path(game, pos.as_ivec2(), *origin).expect("no path to exit");
                    debug!("{me:?} -> exiting");
                    out.push(PacketS::Communicate {
                        message: None,
                        timeout: Some(0.),
                        player: me,
                        pin: Some(false),
                    });
                    let points = -1;
                    out.push(PacketS::ApplyScore(Score {
                        points,
                        demands_failed: 1,
                        ..Default::default()
                    }));
                    out.push(PacketS::Effect {
                        effect: Effect::Angry,
                        location: ItemLocation::Player(me, Hand(0)),
                    });
                    out.push(PacketS::Effect {
                        effect: Effect::Points { amount: points },
                        location: ItemLocation::Player(me, Hand(0)),
                    });
                    *self = CustomerState::Exiting { path };
                    return;
                } else if *check > 10 {
                    let demand_data = &game.data.demands[demand.0];
                    *check = 0;

                    if !*pinned {
                        let mut pin = false;
                        if !game.data.flags.disable_unknown_orders {
                            game.players_spatial_index.query(pos, 3., |pid, _| {
                                if game
                                    .players
                                    .get(&pid)
                                    .is_some_and(|p| p.class.is_cheflike())
                                {
                                    pin = true
                                }
                            });
                        } else {
                            pin = true;
                        }
                        if pin {
                            *pinned = true;
                            out.push(PacketS::Communicate {
                                player: me,
                                message: Some(Message::Item(demand_data.input)),
                                timeout: Some(*timeout),
                                pin: Some(true),
                            });
                        }
                    }

                    let mut demand_pos = [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y]
                        .into_iter()
                        .find_map(|off| {
                            let pos = *chair + off;
                            if game
                                .tiles
                                .get(&pos)
                                .map(|t| {
                                    t.item
                                        .as_ref()
                                        .map(|i| i.kind == demand_data.input)
                                        .unwrap_or_default()
                                })
                                .unwrap_or_default()
                            {
                                Some(pos)
                            } else {
                                None
                            }
                        });

                    // Check for other customer with more urgent order for the item
                    if let Some(pos) = demand_pos {
                        game.players_spatial_index.query(
                            pos.as_vec2() + Vec2::splat(0.5),
                            1.5,
                            |p, _| {
                                if let Some((Message::Item(item), t)) =
                                    &game.players[&p].communicate_persist
                                    && p != me
                                    && t.remaining < *timeout
                                    && *item == demand_data.input
                                {
                                    demand_pos = None;
                                }
                            },
                        );
                    }

                    if let Some(table) = demand_pos {
                        debug!("{me:?} -> eating");
                        let points = game.data.demands[demand.0].points;
                        out.push(PacketS::Communicate {
                            message: None,
                            timeout: Some(0.),
                            player: me,
                            pin: Some(false),
                        });
                        out.push(PacketS::Communicate {
                            message: None,
                            timeout: Some(0.),
                            player: me,
                            pin: Some(true),
                        });
                        out.push(PacketS::Effect {
                            effect: Effect::Satisfied,
                            location: ItemLocation::Player(me, Hand(0)),
                        });
                        out.push(PacketS::Effect {
                            effect: Effect::Points { amount: points },
                            location: ItemLocation::Player(me, Hand(0)),
                        });
                        out.push(PacketS::Interact {
                            target: Some(ItemLocation::Tile(table)),
                            player: me,
                            hand: Hand(0),
                        });
                        out.push(PacketS::ApplyScore(Score {
                            demands_completed: 1,
                            points,
                            ..Default::default()
                        }));
                        out.push(PacketS::Interact {
                            target: None,
                            player: me,
                            hand: Hand(0),
                        });
                        *self = CustomerState::Eating {
                            hold_location: HoldLocation::new(
                                chair.as_vec2(),
                                (table - *chair).as_vec2(),
                            ),
                            demand: *demand,
                            table,
                            progress: 0.,
                            origin: *origin,
                        };
                        return;
                    }
                }
                hold_location.update(out, me, pos);
            }

            CustomerState::Eating {
                hold_location,
                demand,
                table,
                progress,
                origin,
            } => {
                let demand = &game.data.demands[demand.0];
                *progress += dt / demand.duration;
                if *progress >= 1. {
                    debug!("{me:?} -> finishing");
                    *self = CustomerState::Finishing {
                        table: *table,
                        origin: *origin,
                        cooldown: 0.5,
                    };
                    out.push(PacketS::ReplaceHand {
                        player: me,
                        item: demand.output,
                        hand: Hand(0),
                    });
                    return;
                }
                hold_location.update(out, me, pos);
            }
            CustomerState::Finishing {
                table,
                origin,
                cooldown,
            } => {
                *cooldown -= dt;
                if game
                    .players
                    .get(&me)
                    .is_some_and(|pl| pl.items[0].is_none())
                // TODO index out of bounds?
                {
                    if let Some(path) = find_path(game, pos.as_ivec2(), *origin) {
                        *self = CustomerState::Exiting { path };
                    }
                } else {
                    if *cooldown < 0. {
                        *cooldown += 1.;
                        out.push(PacketS::Interact {
                            player: me,
                            target: Some(ItemLocation::Tile(*table)),
                            hand: Hand(0),
                        });
                        out.push(PacketS::Interact {
                            player: me,
                            target: None,
                            hand: Hand(0),
                        });
                    }

                    out.push(PacketS::Movement {
                        player: me,
                        dir: (table.as_vec2() + 0.5) - pos,
                        boost: false,
                        pos: None,
                    });
                }
            }
            CustomerState::Exiting { path } => {
                if path.is_done() || path.is_stuck(3.) {
                    debug!("{me:?} -> leave");
                    *self = CustomerState::Exited
                } else {
                    #[cfg(feature = "debug_events")]
                    out.push(PacketS::Debug(path.debug(me)));
                    path.update(out, me, pos, dt, 0.6, false);
                }
            }
            CustomerState::Exited => (),
        }
    }

    #[cfg(feature = "debug_events")]
    pub fn debug(&self, me: PlayerID) -> DebugEvent {
        use crate::debug_player_color;
        use hurrycurry_protocol::{DebugEvent, DebugEventDisplay};

        let text = match self {
            CustomerState::New => format!("New"),
            CustomerState::Entering { .. } => format!("Entering"),
            CustomerState::Waiting {
                demand,
                timeout,
                pinned,
                ..
            } => format!(
                "Waiting\nD={}  t={timeout:.01}  pin={}",
                demand.0, *pinned as u8
            ),
            CustomerState::Eating {
                demand, progress, ..
            } => format!("Eating\nD={}  pr={progress:.02}", demand.0),
            CustomerState::Finishing { cooldown, .. } => {
                format!("Finishing\nc={cooldown:.01}")
            }
            CustomerState::Exiting { .. } => format!("Exiting"),
            CustomerState::Exited => format!("Exited"),
        };
        DebugEvent {
            key: format!("player-{me}"),
            timeout: 0.1,
            color: debug_player_color(me),
            display: DebugEventDisplay::Label {
                pos: ItemLocation::Player(me, Hand(0)),
                text,
            },
        }
    }
}
