/*
    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::{Game, Involvement, Item};
use hurrycurry_locale::{TrError, tre};
use hurrycurry_protocol::{Effect, ItemLocation, PacketC, Recipe};
use log::info;
use std::collections::{BTreeSet, VecDeque};

impl Game {
    pub fn interact(
        &mut self,
        this_loc: ItemLocation,
        other_loc: ItemLocation,
        edge: bool,
    ) -> Result<(), TrError> {
        let automated = this_loc.is_tile() && other_loc.is_tile();
        let other_player_id = match other_loc {
            ItemLocation::Player(pid, _) => Some(pid),
            _ => None,
        };
        let this_tile_parts = match this_loc {
            ItemLocation::Tile(t) => self
                .tiles
                .get(&t)
                .map(|t| t.parts.clone())
                .unwrap_or_default(),
            _ => Vec::new(),
        };

        let (this_slot, other_slot) = match (this_loc, other_loc) {
            (ItemLocation::Tile(p1), ItemLocation::Tile(p2)) => {
                if p1 == p2 {
                    return Err(tre!("s.error.self_interact"));
                }
                let [Some(x), Some(y)] = self.tiles.get_disjoint_mut([&p1, &p2]) else {
                    return Err(tre!("s.error.no_tile"));
                };
                (&mut x.item, &mut y.item)
            }
            (ItemLocation::Tile(p1), ItemLocation::Player(p2, h2)) => {
                let Some(x) = self.tiles.get_mut(&p1) else {
                    return Err(tre!("s.error.no_tile"));
                };
                let Some(y) = self.players.get_mut(&p2) else {
                    return Err(tre!("s.error.no_player"));
                };
                let Some(y) = y.items.get_mut(h2.0) else {
                    return Err(tre!("s.error.no_hand"));
                };
                (&mut x.item, y)
            }
            (ItemLocation::Player(p1, h1), ItemLocation::Tile(p2)) => {
                let Some(x) = self.players.get_mut(&p1) else {
                    return Err(tre!("s.error.no_player"));
                };
                let Some(x) = x.items.get_mut(h1.0) else {
                    return Err(tre!("s.error.no_hand"));
                };
                let Some(y) = self.tiles.get_mut(&p2) else {
                    return Err(tre!("s.error.no_tile"));
                };
                (x, &mut y.item)
            }
            (ItemLocation::Player(p1, h1), ItemLocation::Player(p2, h2)) => {
                if p1 == p2 {
                    return Err(tre!("s.error.self_interact"));
                }
                let [Some(x), Some(y)] = self.players.get_disjoint_mut([&p1, &p2]) else {
                    return Err(tre!("s.error.no_tile"));
                };
                let Some(x) = x.items.get_mut(h1.0) else {
                    return Err(tre!("s.error.no_hand"));
                };
                let Some(y) = y.items.get_mut(h2.0) else {
                    return Err(tre!("s.error.no_hand"));
                };
                (x, y)
            }
        };

        if other_slot.is_none()
            && let Some(item) = this_slot
            && let Some(inv) = &mut item.active
        {
            let recipe = &self.data.recipe(inv.recipe);
            if this_tile_parts.iter().any(|p| recipe.supports_tile(*p))
                && let Recipe::Active { outputs, speed, .. } = recipe
            {
                if edge {
                    inv.players.extend(other_player_id);
                } else if let Some(player) = other_player_id {
                    inv.players.remove(&player);
                }
                inv.speed = speed * inv.players.len() as f32;

                if inv.position >= 1. {
                    let this_had_item = this_slot.is_some();
                    let other_had_item = other_slot.is_some();
                    *other_slot = outputs[0].map(|kind| Item { kind, active: None });
                    *this_slot = outputs[1].map(|kind| Item { kind, active: None });
                    self.item_locations_index.remove(&this_loc);
                    self.item_locations_index.remove(&other_loc);
                    if this_slot.is_some() {
                        self.item_locations_index.insert(this_loc);
                    }
                    if other_slot.is_some() {
                        self.item_locations_index.insert(other_loc);
                    }
                    produce_events(
                        &mut self.events,
                        this_had_item,
                        other_had_item,
                        this_slot,
                        this_loc,
                        other_slot,
                        other_loc,
                    );
                } else {
                    self.events.push_back(PacketC::SetProgress {
                        players: inv.players.clone(),
                        item: this_loc,
                        position: inv.position,
                        speed: inv.speed,
                        warn: inv.warn,
                    });
                }
                return Ok(());
            }
        }
        if !edge {
            return Ok(());
        }
        for (ri, recipe) in self.data.recipes() {
            if !this_tile_parts.iter().any(|p| recipe.supports_tile(*p)) {
                continue;
            }
            match recipe {
                Recipe::Active { input, speed, .. } => {
                    if other_slot.is_none()
                        && let Some(item) = this_slot
                        && item.kind == *input
                        && item.active.is_none()
                    {
                        info!("Active {ri} started");
                        item.active = Some(Involvement {
                            players: other_player_id.into_iter().collect(),
                            recipe: ri,
                            speed: *speed,
                            position: 0.,
                            warn: false,
                        });
                    }
                    if this_slot.is_none()
                        && let Some(item) = &other_slot
                        && item.kind == *input
                        && item.active.is_none()
                    {
                        let mut item = other_slot.take().unwrap();
                        info!("Active {ri} started");
                        item.active = Some(Involvement {
                            players: other_player_id.into_iter().collect(),
                            recipe: ri,
                            speed: *speed,
                            position: 0.,
                            warn: false,
                        });
                        self.item_locations_index.remove(&other_loc);
                        self.item_locations_index.insert(this_loc);
                        *this_slot = Some(item);
                        self.score.active_recipes += 1;
                        self.events.push_back(PacketC::MoveItem {
                            from: other_loc,
                            to: this_loc,
                        });
                        self.events.push_back(PacketC::SetProgress {
                            players: other_player_id.into_iter().collect(),
                            item: this_loc,
                            position: 0.,
                            speed: *speed,
                            warn: false,
                        });
                        return Ok(());
                    }
                }
                Recipe::Instant {
                    inputs,
                    outputs,
                    points: pd,
                    ..
                } => {
                    let on_tile = this_slot.as_ref().map(|i| i.kind);
                    let in_hand = other_slot.as_ref().map(|i| i.kind);
                    let ok = inputs[0] == on_tile && inputs[1] == in_hand;
                    let ok_flip = inputs[1] == on_tile && inputs[0] == in_hand;
                    if ok || ok_flip {
                        info!("Instant {ri} (flip={ok_flip})");
                        let flip = ok_flip as usize;
                        let this_had_item = this_slot.is_some();
                        let other_had_item = other_slot.is_some();
                        *other_slot = outputs[1 - flip].map(|kind| Item { kind, active: None });
                        *this_slot = outputs[flip].map(|kind| Item { kind, active: None });
                        self.item_locations_index.remove(&this_loc);
                        self.item_locations_index.remove(&other_loc);
                        if this_slot.is_some() {
                            self.item_locations_index.insert(this_loc);
                        }
                        if other_slot.is_some() {
                            self.item_locations_index.insert(other_loc);
                        }
                        self.score.points += pd;
                        self.score.instant_recipes += 1;
                        if *pd != 0 {
                            self.events.push_back(PacketC::Effect {
                                effect: Effect::Points { amount: *pd },
                                location: this_loc,
                            });
                            self.events.push_back(PacketC::Score(self.score.clone()));
                        }
                        produce_events(
                            &mut self.events,
                            this_had_item,
                            other_had_item,
                            this_slot,
                            this_loc,
                            other_slot,
                            other_loc,
                        );
                        return Ok(());
                    }
                }
                _ => (),
            }
        }

        let can_place = automated
            || other_slot
                .as_ref()
                .is_some_and(|other| self.data.can_place_item(&this_tile_parts, other.kind));

        if can_place
            && this_slot.is_none()
            && let Some(item) = other_slot.take()
        {
            self.item_locations_index.remove(&other_loc);
            self.item_locations_index.insert(this_loc);
            *this_slot = Some(item);
            self.events.push_back(PacketC::MoveItem {
                from: other_loc,
                to: this_loc,
            });
            return Ok(());
        }
        if other_slot.is_none()
            && let Some(item) = this_slot.take()
        {
            self.item_locations_index.remove(&this_loc);
            self.item_locations_index.insert(other_loc);
            *other_slot = Some(item);
            self.events.push_back(PacketC::MoveItem {
                from: this_loc,
                to: other_loc,
            });
        }

        Ok(())
    }

    pub fn tick_slot(&mut self, loc: ItemLocation, dt: f32) -> Result<(), TrError> {
        let parts = match loc {
            ItemLocation::Tile(t) => self
                .tiles
                .get(&t)
                .map(|t| t.parts.clone())
                .unwrap_or_default(),
            _ => Vec::new(),
        };
        let slot = match loc {
            ItemLocation::Tile(p) => {
                let Some(x) = self.tiles.get_mut(&p) else {
                    return Err(tre!("s.error.no_tile"));
                };
                &mut x.item
            }
            ItemLocation::Player(p, h) => {
                let Some(x) = self.players.get_mut(&p) else {
                    return Err(tre!("s.error.no_player"));
                };
                let Some(x) = x.items.get_mut(h.0) else {
                    return Err(tre!("s.error.no_hand"));
                };
                x
            }
        };

        if let Some(item) = slot {
            if let Some(a) = &mut item.active {
                let r = &self.data.recipe(a.recipe);
                let prev_speed = a.speed;

                if r.tile().is_none() || parts.iter().any(|p| r.supports_tile(*p)) {
                    if a.speed <= 0.
                        && let Recipe::Passive { speed, .. } = &self.data.recipe(a.recipe)
                    {
                        a.speed = *speed;
                    }
                } else if let Some(revert_speed) = r.revert_speed() {
                    a.speed = -revert_speed
                } else {
                    a.speed = 0.;
                }

                if a.position < 0. {
                    item.active = None;
                    self.events.push_back(PacketC::ClearProgress { item: loc });
                    return Ok(());
                }
                if a.position >= 1.
                    && let Recipe::Passive { output, warn, .. } = &self.data.recipe(a.recipe)
                {
                    *slot = output.map(|kind| Item { kind, active: None });
                    self.score.passive_recipes += 1;
                    self.events.push_back(PacketC::Score(self.score.clone()));
                    self.events.push_back(PacketC::SetProgress {
                        players: BTreeSet::new(),
                        warn: *warn,
                        item: loc,
                        position: 1.,
                        speed: 0.,
                    });
                    self.events.push_back(PacketC::SetItem {
                        location: loc,
                        item: slot.as_ref().map(|i| i.kind),
                    });
                    return Ok(());
                };

                a.position += dt * a.speed;
                a.position = a.position.min(1.);

                if a.speed != prev_speed {
                    self.events.push_back(PacketC::SetProgress {
                        players: a.players.clone(),
                        position: a.position,
                        speed: a.speed,
                        warn: a.warn,
                        item: loc,
                    });
                }
            } else if let Some(recipes) = self.data_index.recipe_passive_by_input.get(&item.kind) {
                for &ri in recipes {
                    let recipe = self.data.recipe(ri);
                    if parts.iter().any(|p| recipe.supports_tile(*p))
                        && let Recipe::Passive {
                            input, warn, speed, ..
                        } = recipe
                        && *input == item.kind
                    {
                        item.active = Some(Involvement {
                            players: BTreeSet::new(),
                            recipe: ri,
                            position: 0.,
                            warn: *warn,
                            speed: *speed,
                        });
                        self.events.push_back(PacketC::SetProgress {
                            players: BTreeSet::new(),
                            position: 0.,
                            speed: *speed,
                            warn: *warn,
                            item: loc,
                        });
                        return Ok(());
                    }
                }
            }
        }
        Ok(())
    }
}

pub enum TickEffect {
    Progress {
        speed: f32,
        position: f32,
        warn: bool,
    },
    ClearProgress,
    Produce,
}

#[allow(clippy::too_many_arguments)]
fn produce_events(
    events: &mut VecDeque<PacketC>,
    this_had_item: bool,
    other_had_item: bool,
    this: &Option<Item>,
    this_loc: ItemLocation,
    other: &Option<Item>,
    other_loc: ItemLocation,
) {
    info!("Produce {this_loc} <~ {other_loc}");
    if this_had_item {
        events.push_back(PacketC::SetItem {
            location: this_loc,
            item: None,
        });
    }
    if other_had_item {
        events.push_back(PacketC::MoveItem {
            from: other_loc,
            to: this_loc,
        });
        events.push_back(PacketC::SetItem {
            location: this_loc,
            item: None,
        });
    }
    if let Some(i) = &other {
        events.push_back(PacketC::SetItem {
            location: this_loc,
            item: Some(i.kind),
        });
        events.push_back(PacketC::MoveItem {
            from: this_loc,
            to: other_loc,
        })
    }
    if let Some(i) = &this {
        events.push_back(PacketC::SetItem {
            location: this_loc,
            item: Some(i.kind),
        });
    }
}
