/*
    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::{Involvement, Item, gamedata_index::GamedataIndex};
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()
        && let Some(item) = this
        && let Some(active) = &mut item.active
    {
        let recipe = &data.recipe(active.recipe);
        if recipe.supports_tile(tile)
            && 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()
                    && let Some(item) = this
                    && item.kind == *input
                    && item.active.is_none()
                {
                    info!("start active {ri}");
                    item.active = Some(Involvement {
                        player,
                        recipe: ri,
                        speed: *speed,
                        position: 0.,
                        warn: false,
                    });
                }
                if this.is_none()
                    && let Some(item) = &other
                    && item.kind == *input
                    && item.active.is_none()
                {
                    let mut item = other.take().unwrap();
                    info!("start active {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 {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()
        && 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()
        && 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.
                    && 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.
                && 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)
                    && let Recipe::Passive {
                        input, warn, speed, ..
                    } = recipe
                    && *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),
        });
    }
}
