// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
mod reducer;
pub mod underpower;

use std::{array, collections::BTreeMap};

use strum::{EnumCount, IntoEnumIterator};

use reducer::{EffectReducer, EffectReducerBehaviour, Extend, Stack};
use underpower::{Underpower, UnderpowerEffect, UnderpowerCommand};

use crate::server::common::config::EffectReduction;
use crate::world::{World, event::WorldEvent, player::PlayerId, effects::{Effect, EffectInfo}};
use crate::protocol::message::text_io::{self, TextIoString};
use crate::utils::ToStr;

#[derive(Clone)]
struct ControllerInfo {
	powerup: EffectReducerBehaviour,
	zone: EffectInfo,
	zone_colliding: bool,
	underpower: Option<Underpower>,
	next_expected: EffectInfo,
	next_expected_no_underpower: f32,
}

impl ControllerInfo {
	fn new(reduction_behaviour: EffectReduction) -> ControllerInfo {
		ControllerInfo {
			powerup: match reduction_behaviour {
				EffectReduction::Extend => EffectReducerBehaviour::from(Extend::default()),
				EffectReduction::Stack => EffectReducerBehaviour::from(Stack::default()),
			},
			zone: EffectInfo::default(),
			zone_colliding: false,
			underpower: None,
			next_expected: EffectInfo::default(),
			next_expected_no_underpower: 0.0,
		}
	}
}

#[derive(Clone)]
pub struct EffectController {
	effects: BTreeMap<PlayerId, [ControllerInfo; Effect::COUNT]>,
	reduction_behaviour: EffectReduction,
}

impl EffectController {
	pub fn new(reduction_behaviour: EffectReduction) -> EffectController {
		EffectController { effects: BTreeMap::new(), reduction_behaviour }
	}

	fn get_all_controller_info(&mut self, id: PlayerId) -> &mut [ControllerInfo; Effect::COUNT] {
		self.effects.entry(id).or_insert_with(|| array::from_fn(|_| ControllerInfo::new(self.reduction_behaviour)))
	}

	fn get_controller_info(&mut self, id: PlayerId, effect: Effect) -> &mut ControllerInfo {
		&mut self.get_all_controller_info(id)[effect as usize]
	}

	pub fn underpower_command(&mut self, id: PlayerId, cmd: UnderpowerCommand) -> TextIoString {
		fn get_effects(effect: Option<UnderpowerEffect>) -> Vec<UnderpowerEffect> {
			effect.map_or_else(|| UnderpowerEffect::iter().collect(), |effect| vec![effect])
		}

		let all_info = self.get_all_controller_info(id);
		match cmd {
			UnderpowerCommand::Clear(effect) => {
				let mut changed = false;
				for effect in get_effects(effect) {
					let info = &mut all_info[effect.to_effect() as usize];
					changed |= info.underpower.is_some();
					info.underpower = None;
				}

				match effect {
					_ if !changed => text_io::from_const!("Already cleared."),
					None => text_io::from_const!("Removed underpower from all effects."),
					Some(effect) => TextIoString::new_truncate(format!("Removed underpower from {}.", effect.to_str())),
				}
			},
			UnderpowerCommand::Status(effect) => TextIoString::new_truncate(
				get_effects(effect)
					.into_iter()
					.map(|effect| format!("{} = {}", effect.to_str(), all_info[effect.to_effect() as usize].underpower.map_or(1.0, Underpower::get)))
					.collect::<Vec<_>>()
					.join("\n")
			),
			UnderpowerCommand::Change(effect, power) => {
				all_info[effect.to_effect() as usize].underpower = Some(power);
				TextIoString::new_truncate(format!("Updated underpower of {} to {}.", effect.to_str(), power.get()))
			},
		}
	}

	pub fn powerup_collected(&mut self, id: PlayerId, effect: Effect, powerup: EffectInfo) {
		self.get_controller_info(id, effect).powerup.powerup_collected(powerup);
	}

	pub fn in_zone(&mut self, id: PlayerId, effect: Effect, zone: EffectInfo) {
		let info = self.get_controller_info(id, effect);
		info.zone = zone;
		info.zone_colliding = true;
	}

	pub fn reset_player_effects(&mut self, id: PlayerId) {
		if let Some(effects) = self.effects.get_mut(&id) {
			EffectController::reset_effects(effects);
		}
	}

	pub fn reset_all_effects(&mut self) {
		for effects in self.effects.values_mut() {
			EffectController::reset_effects(effects);
		}
	}

	fn reset_effects(effects: &mut [ControllerInfo; Effect::COUNT]) {
		for info in effects {
			info.powerup.reset();
			info.zone_colliding = false;
			info.zone.time = 0.0;
		}
	}

	pub fn update(&mut self, world: &World, events: &mut Vec<WorldEvent>, dt: f32) {
		self.effects.retain(|&id, effects| {
			// Removes old players
			if !world.has_player(id) { return false; }

			let mut has_active = false;
			for effect in Effect::iter() {
				let info = &mut effects[effect as usize];

				let time = if info.zone_colliding || info.underpower.is_some() {
					f32::INFINITY
				} else {
					info.powerup.get_time().max(info.zone.time).max(0.0)
				};

				/*
				 * Effect zones are allowed to have a time of zero to indicate that
				 * the effect wears of immediately after the player leaves.
				 *
				 * Need to explicitly get this power during collision to avoid
				 * EffectInfo::get_power from returning None since the time is zero.
				 */
				let zone_power =
					if info.zone_colliding { Some(info.zone.power) }
					else { info.zone.get_power() };

				let power = EffectController::get_power(effect, [info.powerup.get_power(effect), zone_power, info.underpower.map(Underpower::get)]);
				let power_no_underpower = EffectController::get_power(effect, [info.powerup.get_power(effect), zone_power, None]);

				/*
				 * Pushes an event for the updated effect.
				 *
				 * I want to do this as few times as possible to minimise bandwidth,
				 * as this event gets bundled up into a WorldUpdate that gets
				 * serialised and sent over the network.
				 *
				 * To do this as few times as possible, I am keeping track of a
				 * prediction of what the next effect info should be. If this
				 * prediction is ever wrong, then the event is sent out.
				 *
				 * The code differs below because the prediction being wrong is
				 * either if the times are different (these times are never negative
				 * so this avoids possible false positives) or if the powers are
				 * different. However, the powers being different isn't sufficient
				 * because if the time is zero, the value of the power doesn't
				 * matter at all.
				 */
				if info.next_expected.time != time || (info.next_expected.power != power && time > 0.0) {
					events.push(WorldEvent::PlayerEffect(id, effect, EffectInfo { time, power }, info.next_expected_no_underpower != power_no_underpower));
				}

				info.powerup.update(dt);
				if !info.zone_colliding { info.zone.time -= dt; }
				info.next_expected.time = (time - dt).max(0.0);
				info.next_expected.power = power;
				info.next_expected_no_underpower = power_no_underpower;

				has_active |= time > 0.0;
				info.zone_colliding = false; // Resets it for the next update
			}
			has_active
		});
	}

	fn get_power(effect: Effect, powers: [Option<f32>; 3]) -> f32 {
		powers
			.into_iter()
			.flatten()
			.reduce(|a, b| reducer::stack_power(effect, a, b))
			.unwrap_or_default()
	}
}
