// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
mod smooth;
mod ammo_crate;
mod blocks;
mod border;
mod bullet;
mod checkpoint;
mod flag;
mod forcefield;
mod player;
mod powerup;
mod special_area;

pub use smooth::{SmoothWorld, LocalPlayer};
pub use powerup::PowerupRenderer;
pub use forcefield::RenderedForcefield;

use std::{rc::Rc, cell::RefCell};

use glium::{Display, Frame, DrawParameters, StencilTest, StencilOperation};
use glutin::surface::WindowSurface;
use glam::Mat4;

use ammo_crate::AmmoCrateRenderer;
use blocks::BlocksRenderer;
use border::BorderRenderer;
use bullet::BulletRenderer;
use checkpoint::CheckpointRenderer;
use flag::FlagRenderer;
use forcefield::ForcefieldRenderer;
use player::PlayerRenderer;
use special_area::SpecialAreaRenderer;

use crate::world::{World, blocks::Blocks};

use crate::app::{config::Config, filesystem::Filesystem, player_gfx::PlayerGfx, playing::{model::Model, view::{Camera, ResetType}}};
use crate::utils::blending::{ALPHA_BLENDING_FUNC, ADDITIVE_BLENDING_FUNC};

const MIN_TINT: f32 = 0.4375; // Brightness of the initial tint
const TINT_TIME: f32 = 0.15625;

pub struct WorldRenderer {
	border: BorderRenderer,
	players: PlayerRenderer,
	bullets: BulletRenderer,
	ammo_crates: AmmoCrateRenderer,
	powerups: PowerupRenderer,
	forcefields: ForcefieldRenderer,
	blocks: BlocksRenderer,
	special_areas: SpecialAreaRenderer,
	flags: FlagRenderer,
	checkpoints: CheckpointRenderer,
}

impl WorldRenderer {
	pub fn new(display: &Display<WindowSurface>, fs: &Filesystem, config: Rc<RefCell<Config>>, player_gfx: Rc<PlayerGfx>) -> WorldRenderer {
		WorldRenderer {
			border: BorderRenderer::new(display, fs),
			players: PlayerRenderer::new(display, player_gfx),
			bullets: BulletRenderer::new(display, fs),
			ammo_crates: AmmoCrateRenderer::new(display, fs),
			powerups: PowerupRenderer::new(display, fs),
			forcefields: ForcefieldRenderer::new(config, display, fs),
			blocks: BlocksRenderer::new(display, fs),
			special_areas: SpecialAreaRenderer::new(display, fs),
			flags: FlagRenderer::new(display, fs),
			checkpoints: CheckpointRenderer::new(display, fs),
		}
	}

	pub fn reset(&mut self, display: &Display<WindowSurface>, model: &Model, typ: ResetType) {
		let world = model.get_world();
		if typ == ResetType::Full {
			// If only needing to do a partial reset (like restarting a level), these don't change so no need to reset them
			self.border.reset(world);
			self.blocks.reset(world.get_blocks());
			self.special_areas.reset(world.get_special_areas(), display);
		}
		self.checkpoints.reset(world.get_checkpoints(), world.get_active_checkpoint(), display);
	}

	pub fn get_powerup_renderer(&self) -> &PowerupRenderer {
		&self.powerups
	}

	pub fn render_border(&self, frame: &mut Frame, params: &DrawParameters, matrix: &Mat4) {
		self.border.render(frame, params, matrix);
	}

	pub fn render_special_areas(&mut self, display: &Display<WindowSurface>, frame: &mut Frame, params: &mut DrawParameters, blocks: &Blocks, camera: &Camera, matrix: &Mat4) {
		/*
		 * Uses the stencil test to avoid special areas from being visible under
		 * blocks. This looks pretty ugly as blocks are now partially transparent.
		 */

		// Disables rendering blocks
		params.color_mask = [false; 4].into();

		// Sets the stencil buffer value to 1
		params.stencil.reference_value_counter_clockwise = 1;
		params.stencil.reference_value_clockwise = 1;
		params.stencil.depth_pass_operation_counter_clockwise = StencilOperation::Replace;
		params.stencil.depth_pass_operation_clockwise = StencilOperation::Replace;

		self.blocks.render(display, frame, params, blocks, camera, false);

		// Reverts all settings
		params.color_mask = [true; 4].into();
		params.stencil.reference_value_counter_clockwise = 0;
		params.stencil.reference_value_clockwise = 0;
		params.stencil.depth_pass_operation_counter_clockwise = StencilOperation::Keep;
		params.stencil.depth_pass_operation_clockwise = StencilOperation::Keep;

		// Keeps fragments only if they are equal to zero (the reference value), which means not part of the blocks (previously set to 1)
		params.stencil.test_counter_clockwise = StencilTest::IfEqual { mask: u32::MAX };
		params.stencil.test_clockwise = StencilTest::IfEqual { mask: u32::MAX };

		self.special_areas.render(frame, params, matrix);

		// Reverts the settings
		params.stencil.test_counter_clockwise = StencilTest::AlwaysPass;
		params.stencil.test_clockwise = StencilTest::AlwaysPass;
	}

	#[allow(clippy::too_many_arguments)]
	pub fn render(&mut self, dt: f32,
		display: &Display<WindowSurface>, frame: &mut Frame, params: &mut DrawParameters,
		time_diff: f32, world: &World, smooth_world: &mut SmoothWorld, forcefields: &[RenderedForcefield], camera: &Camera,
	) {
		let matrix = camera.get_matrix();
		let time_shift = smooth_world.get_total_time_shift() + time_diff;

		params.blend.color = ADDITIVE_BLENDING_FUNC;
		self.checkpoints.render(dt, world.get_active_checkpoint(), frame, params, matrix);
		params.blend.color = ALPHA_BLENDING_FUNC;
		self.ammo_crates.render(world.get_ammo_crates(), display, frame, params, matrix, time_shift);
		params.blend.color = ADDITIVE_BLENDING_FUNC; // The powerups are basically "lightning balls" so additive blending can be used (also makes texture generation easier)
		self.powerups.render(world.get_powerups(), display, frame, params, matrix, time_shift);
		params.blend.color = ALPHA_BLENDING_FUNC;

		self.bullets.render(smooth_world.get_bullets(), forcefields, display, frame, params, matrix, time_diff);
		self.flags.render(world.get_flags(), display, frame, params, matrix, |id| smooth_world.get_player(id).map(|player| player.get_pos(time_diff)), time_shift);

		let followed_id = smooth_world.followed_player_id();
		let mut followed_info = None;
		for (id, player) in smooth_world.player_iter_mut() {
			let tint = if player.last_hurt < TINT_TIME {
				(player.last_hurt / TINT_TIME) * (1.0 - MIN_TINT) + MIN_TINT
			} else {
				1.0
			};

			player.hbar.update(&player.player, dt);
			if Some(id) == followed_id {
				followed_info = Some((tint, time_diff));
			} else {
				self.players.push(player, tint, time_diff);
			}
			player.last_hurt += dt; // Want to update the tint after rendering so the darkest tint is rendered once
		}

		// Renders the followed player above all other players
		if let Some((tint, time_diff)) = followed_info && let Some((_id, player)) = smooth_world.followed_player() {
			self.players.push(player, tint, time_diff);
		}

		self.players.render(display, frame, params, matrix);

		self.blocks.render(display, frame, params, world.get_blocks(), camera, true);

		if !forcefields.is_empty() {
			self.forcefields.update(dt);
			self.forcefields.render(forcefields, display, frame, params, matrix);
		}
	}
}
