/*
    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 hurrycurry_client_lib::{gamedata_index::GamedataIndex, Involvement, Item};
use hurrycurry_protocol::{Gamedata, ItemLocation, PacketC, PlayerID, Recipe, Score, TileIndex};
use log::info;
use std::collections::VecDeque;

#[allow(clippy::too_many_arguments)]
pub fn interact(
    data: &Gamedata,
    edge: bool,
    tile: Option<TileIndex>,
    player: Option<PlayerID>,
    this: &mut Option<Item>,
    this_loc: ItemLocation,
    other: &mut Option<Item>,
    other_loc: ItemLocation,
    score: &mut Score,
    score_changed: &mut bool,
    automated: bool,
    packet_out: &mut VecDeque<PacketC>,
) {
    let _ = automated; //? what was this used for??
    if other.is_none() {
        if let Some(item) = this {
            if let Some(active) = &mut item.active {
                let recipe = &data.recipe(active.recipe);
                if recipe.supports_tile(tile) {
                    if let Recipe::Active { outputs, speed, .. } = recipe {
                        if edge {
                            active.speed += speed;
                        } else {
                            active.speed -= speed;
                            active.speed = active.speed.max(0.); // in case of "release without press" when items cool on active tile
                        }
                        if active.position >= 1. {
                            let this_had_item = this.is_some();
                            let other_had_item = other.is_some();
                            *other = outputs[0].map(|kind| Item { kind, active: None });
                            *this = outputs[1].map(|kind| Item { kind, active: None });
                            produce(
                                this_had_item,
                                other_had_item,
                                this,
                                this_loc,
                                other,
                                other_loc,
                                score_changed,
                                packet_out,
                            );
                        } else {
                            packet_out.push_back(PacketC::SetProgress {
                                player,
                                item: this_loc,
                                position: active.position,
                                speed: active.speed,
                                warn: active.warn,
                            });
                        }
                        return;
                    }
                }
            }
        }
    }
    if !edge {
        return;
    }
    for (ri, recipe) in data.recipes() {
        if !recipe.supports_tile(tile) {
            continue;
        }
        match recipe {
            Recipe::Active { input, speed, .. } => {
                if other.is_none() {
                    if let Some(item) = this {
                        if item.kind == *input && item.active.is_none() {
                            info!("start active recipe {ri:?}");
                            item.active = Some(Involvement {
                                player,
                                recipe: ri,
                                speed: *speed,
                                position: 0.,
                                warn: false,
                            });
                        }
                    }
                }
                if this.is_none() {
                    if let Some(item) = &other {
                        if item.kind == *input && item.active.is_none() {
                            let mut item = other.take().unwrap();
                            info!("start active recipe {ri:?}");
                            item.active = Some(Involvement {
                                player,
                                recipe: ri,
                                speed: *speed,
                                position: 0.,
                                warn: false,
                            });
                            *this = Some(item);
                            score.active_recipes += 1;
                            packet_out.push_back(PacketC::MoveItem {
                                from: other_loc,
                                to: this_loc,
                            });
                            packet_out.push_back(PacketC::SetProgress {
                                player,
                                item: this_loc,
                                position: 0.,
                                speed: *speed,
                                warn: false,
                            });
                            return;
                        }
                    }
                }
            }
            Recipe::Instant {
                inputs,
                outputs,
                points: pd,
                ..
            } => {
                let on_tile = this.as_ref().map(|i| i.kind);
                let in_hand = other.as_ref().map(|i| i.kind);
                let ok = inputs[0] == on_tile && inputs[1] == in_hand;
                let ok_rev = inputs[1] == on_tile && inputs[0] == in_hand;
                if ok || ok_rev {
                    info!("instant recipe {ri:?} reversed={ok_rev}");
                    let ok_rev = ok_rev as usize;
                    let this_had_item = this.is_some();
                    let other_had_item = other.is_some();
                    *other = outputs[1 - ok_rev].map(|kind| Item { kind, active: None });
                    *this = outputs[ok_rev].map(|kind| Item { kind, active: None });
                    score.points += pd;
                    score.instant_recipes += 1;
                    *score_changed = true;
                    produce(
                        this_had_item,
                        other_had_item,
                        this,
                        this_loc,
                        other,
                        other_loc,
                        score_changed,
                        packet_out,
                    );
                    return;
                }
            }
            _ => (),
        }
    }

    let can_place = tile.is_none_or(|tile| {
        other.as_ref().is_some_and(|other| {
            data.tile_placeable_items
                .get(&tile)
                .is_none_or(|pl| pl.contains(&other.kind))
        })
    });

    if can_place && this.is_none() {
        if let Some(item) = other.take() {
            *this = Some(item);
            packet_out.push_back(PacketC::MoveItem {
                from: other_loc,
                to: this_loc,
            });
            return;
        }
    }
    if other.is_none() {
        if let Some(item) = this.take() {
            *other = Some(item);
            packet_out.push_back(PacketC::MoveItem {
                from: this_loc,
                to: other_loc,
            });
        }
    }
}

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

#[allow(clippy::too_many_arguments)]
pub fn tick_slot(
    dt: f32,
    data: &Gamedata,
    data_index: &GamedataIndex,
    tile: Option<TileIndex>,
    slot: &mut Option<Item>,
    slot_loc: ItemLocation,
    score: &mut Score,
    score_changed: &mut bool,
    packet_out: &mut VecDeque<PacketC>,
) {
    if let Some(item) = slot {
        if let Some(a) = &mut item.active {
            let r = &data.recipe(a.recipe);
            let prev_speed = a.speed;

            if r.supports_tile(tile) {
                if a.speed <= 0. {
                    if let Recipe::Passive { speed, .. } = &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;
                packet_out.push_back(PacketC::ClearProgress { item: slot_loc });
                return;
            }
            if a.position >= 1. {
                if let Recipe::Passive { output, warn, .. } = &data.recipe(a.recipe) {
                    *slot = output.map(|kind| Item { kind, active: None });
                    score.passive_recipes += 1;
                    *score_changed = true;
                    packet_out.push_back(PacketC::SetProgress {
                        player: None,
                        warn: *warn,
                        item: slot_loc,
                        position: 1.,
                        speed: 0.,
                    });
                    packet_out.push_back(PacketC::SetItem {
                        location: slot_loc,
                        item: slot.as_ref().map(|i| i.kind),
                    });
                    return;
                };
            }

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

            if a.speed != prev_speed {
                packet_out.push_back(PacketC::SetProgress {
                    player: None,
                    position: a.position,
                    speed: a.speed,
                    warn: a.warn,
                    item: slot_loc,
                });
            }
        } else if let Some(recipes) = data_index.recipe_passive_by_input.get(&item.kind) {
            for &ri in recipes {
                let recipe = data.recipe(ri);
                if recipe.supports_tile(tile) {
                    if let Recipe::Passive {
                        input, warn, speed, ..
                    } = recipe
                    {
                        if *input == item.kind {
                            item.active = Some(Involvement {
                                player: None,
                                recipe: ri,
                                position: 0.,
                                warn: *warn,
                                speed: *speed,
                            });
                            packet_out.push_back(PacketC::SetProgress {
                                player: None,
                                position: 0.,
                                speed: *speed,
                                warn: *warn,
                                item: slot_loc,
                            });
                            return;
                        }
                    }
                }
            }
        }
    }
}

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