use std::random::random;

/*
    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},
    random_float,
};
use hurrycurry_client_lib::Game;
use hurrycurry_protocol::{
    DemandIndex, Hand, Message, PacketS, PlayerClass, PlayerID, Score,
    glam::{IVec2, Vec2},
};
use log::debug;

#[derive(Debug, Clone, Default)]
pub struct Customer {
    config: CustomerConfig,
    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 {
        demand: DemandIndex,
        chair: IVec2,
        facing: Vec2,
        timeout: f32,
        origin: IVec2,
        check: u8,
        pinned: bool,
    },
    Eating {
        demand: DemandIndex,
        table: IVec2,
        progress: f32,
        chair: IVec2,
        origin: IVec2,
    },
    Finishing {
        table: IVec2,
        origin: IVec2,
        cooldown: f32,
    },
    Exiting {
        path: Path,
    },
}

impl Customer {
    pub fn new(config: CustomerConfig) -> Self {
        Customer {
            config,
            state: CustomerState::default(),
        }
    }
}
impl BotAlgo for Customer {
    fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput {
        let Some(playerdata) = game.players.get(&me) else {
            return BotInput::default();
        };
        let pos = playerdata.movement.position;
        self.state.tick(me, &self.config, pos, game, dt)
    }
}
impl CustomerState {
    fn tick(
        &mut self,
        me: PlayerID,
        config: &CustomerConfig,
        pos: Vec2,
        game: &Game,
        dt: f32,
    ) -> BotInput {
        match self {
            CustomerState::New => {
                if !game.data.demands.is_empty() {
                    let chairs = game
                        .tiles
                        .iter()
                        .filter(|(_, t)| game.data.tile_name(t.kind) == "chair")
                        .map(|(p, _)| *p)
                        .collect::<Vec<_>>();
                    if let Some(&chair) = chairs.get(random::<usize>(..) % chairs.len().max(1))
                        && let Some(path) = find_path(&game.walkable, pos.as_ivec2(), chair)
                    {
                        debug!("{me:?} -> entering");
                        *self = CustomerState::Entering {
                            path,
                            chair,
                            origin: pos.as_ivec2(),
                            ticks: 0,
                        };
                    }
                }
                BotInput::default()
            }
            CustomerState::Entering {
                path,
                chair,
                origin,
                ticks,
            } => {
                *ticks += 1;
                let check = *ticks % 10 == 0;
                if path.is_done() {
                    let demand = DemandIndex(random::<usize>(..) % game.data.demands.len());
                    let requested_item = game.data.demands[demand.0].input;
                    debug!("{me:?} -> waiting");
                    let timeout = 90. + random_float() * 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
                                .tile_placeable_items
                                .get(&t.kind)
                                .is_none_or(|p| p.contains(&requested_item))
                        }) {
                            facing = off.as_vec2();
                        }
                    }
                    *self = CustomerState::Waiting {
                        chair: *chair,
                        timeout,
                        demand,
                        facing,
                        origin: *origin,
                        check: 0,
                        pinned: false,
                    };
                    let message_item = if config.unknown_order {
                        game.data
                            .get_item_by_name("unknown-order")
                            .unwrap_or(requested_item)
                    } else {
                        requested_item
                    };
                    BotInput {
                        extra: vec![PacketS::Communicate {
                            message: Some(Message::Item(message_item)),
                            timeout: Some(timeout),
                            player: me,
                            pin: Some(false),
                        }],
                        ..Default::default()
                    }
                } 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;
                    BotInput::default()
                } else if path.is_stuck() {
                    if let Some(path) = find_path(&game.walkable, pos.as_ivec2(), *origin) {
                        *self = CustomerState::Exiting { path };
                    }
                    BotInput::default()
                } else {
                    BotInput {
                        direction: path.next_direction(pos, dt) * 0.6,
                        ..Default::default()
                    }
                }
            }
            CustomerState::Waiting {
                chair,
                demand,
                timeout,
                origin,
                check,
                pinned,
                facing,
            } => {
                *timeout -= dt;
                *check += 1;
                if *timeout <= 0. {
                    let path = find_path(&game.walkable, pos.as_ivec2(), *origin)
                        .expect("no path to exit");
                    debug!("{me:?} -> exiting");
                    *self = CustomerState::Exiting { path };
                    return BotInput {
                        extra: vec![
                            PacketS::Communicate {
                                message: None,
                                timeout: Some(0.),
                                player: me,
                                pin: Some(false),
                            },
                            PacketS::ApplyScore(Score {
                                points: -1,
                                demands_failed: 1,
                                ..Default::default()
                            }),
                            PacketS::Effect {
                                name: "angry".to_string(),
                                player: me,
                            },
                        ],
                        ..Default::default()
                    };
                } else if *check > 10 {
                    let demand_data = &game.data.demands[demand.0];
                    *check = 0;

                    if !*pinned {
                        let mut pin = false;
                        if config.unknown_order {
                            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;
                            return BotInput {
                                extra: vec![PacketS::Communicate {
                                    player: me,
                                    message: Some(Message::Item(demand_data.input)),
                                    timeout: Some(*timeout),
                                    pin: Some(true),
                                }],
                                ..Default::default()
                            };
                        }
                    }

                    let 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
                            }
                        });
                    if let Some(pos) = demand_pos {
                        debug!("{me:?} -> eating");
                        let points = game.data.demands[demand.0].points;
                        *self = CustomerState::Eating {
                            demand: *demand,
                            table: pos,
                            progress: 0.,
                            chair: *chair,
                            origin: *origin,
                        };
                        return BotInput {
                            extra: vec![
                                PacketS::Communicate {
                                    message: None,
                                    timeout: Some(0.),
                                    player: me,
                                    pin: Some(false),
                                },
                                PacketS::Communicate {
                                    message: None,
                                    timeout: Some(0.),
                                    player: me,
                                    pin: Some(true),
                                },
                                PacketS::Effect {
                                    name: "satisfied".to_string(),
                                    player: me,
                                },
                                PacketS::Interact {
                                    pos: Some(pos),
                                    player: me,
                                    hand: Hand(0),
                                },
                                PacketS::ApplyScore(Score {
                                    demands_completed: 1,
                                    points,
                                    ..Default::default()
                                }),
                                PacketS::Interact {
                                    pos: None,
                                    player: me,
                                    hand: Hand(0),
                                },
                            ],
                            ..Default::default()
                        };
                    }
                }
                BotInput {
                    direction: dir_input(pos, *chair, *facing),
                    ..Default::default()
                }
            }

            CustomerState::Eating {
                demand,
                table,
                progress,
                chair,
                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,
                    };
                    return BotInput {
                        extra: vec![PacketS::ReplaceHand {
                            player: me,
                            item: demand.output,
                            hand: Hand(0),
                        }],
                        ..Default::default()
                    };
                }
                BotInput {
                    direction: dir_input(pos, *chair, (*table - *chair).as_vec2()),
                    ..Default::default()
                }
            }
            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.walkable, pos.as_ivec2(), *origin) {
                        *self = CustomerState::Exiting { path };
                    }
                    BotInput::default()
                } else {
                    let direction = (table.as_vec2() + 0.5) - pos;
                    if *cooldown < 0. {
                        *cooldown += 1.;
                        BotInput {
                            extra: vec![
                                PacketS::Interact {
                                    player: me,
                                    pos: Some(*table),
                                    hand: Hand(0),
                                },
                                PacketS::Interact {
                                    player: me,
                                    pos: None,
                                    hand: Hand(0),
                                },
                            ],
                            direction,
                            ..Default::default()
                        }
                    } else {
                        BotInput {
                            direction,
                            ..Default::default()
                        }
                    }
                }
            }
            CustomerState::Exiting { path } => {
                if path.is_done() || path.is_stuck() {
                    debug!("{me:?} -> leave");
                    BotInput {
                        leave: true,
                        ..Default::default()
                    }
                } else {
                    BotInput {
                        direction: path.next_direction(pos, dt) * 0.6,
                        ..Default::default()
                    }
                }
            }
        }
    }
}

fn dir_input(pos: Vec2, target: IVec2, facing: Vec2) -> Vec2 {
    let diff = (target.as_vec2() + 0.5) - pos;
    (if diff.length() > 0.3 {
        diff.normalize()
    } else {
        diff * 0.5
    }) + facing.clamp_length_max(1.) * 0.3
}
