// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
mod world;
mod decorations;
mod hud;
mod camera;
mod sounds;
mod gui;
mod story_text;

pub use camera::Camera;
pub use hud::{health_bar::{HealthBar, HealthBarRenderer}, world_label::WorldLabel};
pub use sounds::{Sounds, Sfx, LoopingSounds, LoopingSfx};
pub use story_text::StoryTextInfo;

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

use glium::{Frame, DrawParameters};
use winit::{event::{WindowEvent, MouseScrollDelta}, dpi::PhysicalSize};
use glam::{UVec2, Vec2};

use decorations::Decorations;
use camera::DecayMode;
use hud::Hud;
use world::{SmoothWorld, WorldRenderer};

use super::{TimeArgs, model::Model, controller::Controller};

use crate::app::{App, config::{Config, action::{Action, ActionEvent, ActionState}}};
use crate::app::playing::model::changes::Hit;
use crate::utils::blending::ALPHA_BLENDING;

pub(super) struct View<'a> {
	pub(super) smooth_world: SmoothWorld,
	pub(super) camera: Camera,
	pub(super) sounds: Sounds,
	config: Rc<RefCell<Config>>,
	params: DrawParameters<'a>,
	decorations: Decorations,
	world_renderer: WorldRenderer,
	hud: Hud,
	render_hud: bool,
	winsize_changed: bool,

	/// If this is true, then normal spectating functionality of being able to
	/// move the camera and follow players is disabled.
	///
	/// This is because without this feature when the player fails a level, they
	/// were likely pressing keys that move the player. Those keys being held
	/// down will also move the camera while spectating for the brief period
	/// before the level restarts, which looks a little weird.
	spectating_fixed_camera: bool,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ResetType {
	/**
	 * When the player joins a new game, with the previous state being in the GUI
	 * or if the game has just started.
	 */
	Full,

	/**
	 * When the world is reset while the player is still playing, such as
	 * resetting the world after losing a level.
	 */
	Partial,
}

impl<'a> View<'a> {
	pub fn new(app: &mut App) -> View<'a> {
		let smooth_world = SmoothWorld::new();
		let winsize = { let PhysicalSize { width, height } = app.window.inner_size(); UVec2::new(width, height) };
		let camera = Camera::new(winsize);

		let player_gfx = app.player_gfx.get(&app.display, &app.fs);
		let world_renderer = WorldRenderer::new(&app.display, &app.fs, Rc::clone(&app.config), Rc::clone(&player_gfx));
		let decorations = Decorations::new(Rc::clone(&app.config), &app.display, &app.fs, player_gfx);

		View {
			smooth_world,
			camera,
			sounds: Sounds::new(&app.fs, Rc::clone(&app.config)),
			config: Rc::clone(&app.config),
			params: DrawParameters {
				blend: ALPHA_BLENDING,
				.. DrawParameters::default()
			},
			decorations,
			world_renderer,
			hud: Hud::new(&app.display, &app.fs, winsize),
			render_hud: true,
			winsize_changed: true,
			spectating_fixed_camera: false,
		}
	}

	pub fn reset(&mut self, app: &App, model: &Model, typ: ResetType) {
		let size = {
			let size = app.window.inner_size();
			UVec2::new(size.width, size.height)
		};

		if typ == ResetType::Full {
			self.hud.reset(size);
			self.render_hud = true;
		}

		self.smooth_world.reset(model);
		let (pos, vel) = if let Some((_, player)) = self.smooth_world.followed_player() {
			(player.get_pos(0.0), player.get_vel())
		} else {
			(Vec2::ZERO, Vec2::ZERO)
		};
		self.camera.reset(pos, vel, size, typ);
		self.update_sounds(0.0, 0.0);
		self.decorations.reset(&app.display, size, &self.camera, typ);
		self.world_renderer.reset(&app.display, model, typ);

		/*
		 * Only fix the camera's position while spectating if playing a level
		 * *and* the player wasn't initially spectating (which can happen if
		 * command line arguments configure the player to spectate, in which being
		 * able to move the camera is very useful).
		 */
		self.spectating_fixed_camera = model.get_client().is_level() && model.get_world().has_player(model.get_id());
	}

	pub fn text_input_action(&mut self, action: ActionEvent) {
		if self.render_hud {
			/*
			 * Doesn't do this if the HUD is hidden, as the player will no longer
			 * be able to move with no visible feedback to why that's the case.
			 */
			self.hud.action(action);
		}
	}

	pub fn action(&mut self, action: ActionEvent) {
		self.camera.action(action);

		if matches!(action, ActionEvent::Action(Action::ToggleHud, ActionState::Pressed)) {
			self.render_hud = !self.render_hud;
		}

		if !self.spectating_fixed_camera && let Some(new_camera_pos) = self.smooth_world.action(action) {
			self.camera.move_to(new_camera_pos);
		}
	}

	pub fn following_player(&self) -> bool {
		self.smooth_world.followed_player().is_some()
	}

	pub fn get_hud(&self) -> &Hud { &self.hud }
	pub fn get_hud_mut(&mut self) -> &mut Hud { &mut self.hud }
	pub fn spectating_fixed_camera(&self) -> bool { self.spectating_fixed_camera }

	pub fn window_event(&mut self, event: &WindowEvent, story_text: bool) {
		if let WindowEvent::Resized(size) = event {
			let winsize = UVec2::new(size.width, size.height);
			self.camera.resize_event(winsize);
			self.hud.resize_event(winsize);
			self.decorations.resize_event(winsize);
			self.winsize_changed = true;
		} else if !story_text {
			match event {
				WindowEvent::MouseWheel { delta: MouseScrollDelta::LineDelta(_, dy), .. } => self.camera.mouse_scroll_event(*dy),
				WindowEvent::MouseWheel { delta: MouseScrollDelta::PixelDelta(pos), .. } => self.camera.mouse_scroll_event(pos.y as f32 * 0.0625),
				WindowEvent::KeyboardInput { event, .. } => self.hud.key_event(event),
				_ => (),
			}
		}
	}

	pub fn on_hit(&mut self, hit: &Hit) {
		self.decorations.on_hit(hit);

		if hit.killed {
			if self.smooth_world.followed_player_id().is_some_and(|id| Some(id) == hit.player_id) {
				self.sounds.play(Sfx::Kill);
			} else {
				self.sounds.play_spatial(Sfx::Kill, hit.player_pos, hit.player_vel);
			}
		} else {
			self.sounds.play_spatial(Sfx::Hit, hit.player_pos, hit.player_vel);
		}
	}

	pub fn clear_particles(&mut self) { self.decorations.clear_particles(); }

	pub fn render(&mut self, app: &mut App, frame: &mut Frame, model: &mut Model, controller: &mut Controller, time: &TimeArgs, story_text: &mut StoryTextInfo) {
		if !self.spectating_fixed_camera && !self.following_player() && let Some(px_dpos) = controller.get_drag() {
			let world_dpos = self.camera.rel_pixel_to_world_coords(px_dpos);
			self.camera.move_target_dpos(world_dpos, time.dt);
		}

		/*
		 * Need to subtract dt from the time difference to make the camera move
		 * smoothly when the frame rate varies.
		 *
		 * Not too sure why this is needed, but one reason I suspect is because
		 * the calculation involves the velocity for calculating how the camera's
		 * position should exponentially decay towards the player, using the
		 * previous player's movement from last frame to this frame.
		 *
		 */
		let decay_mode = if let Some((_, player)) = self.smooth_world.followed_player() {
			self.camera.set_target(player.get_pos(time.time_diff - time.dt), player.get_vel());
			DecayMode::Player
		} else if self.spectating_fixed_camera {
			DecayMode::Player
		} else {
			self.camera.move_target_dir(controller.pcontrol.get_move_dir(), time.dt);
			DecayMode::Free
		};

		let moved_by = self.camera.update(decay_mode, time.dt);

		let matrix = self.camera.get_matrix();

		// Updates the player's cursor position with the new player position, to ensure the player is always rendered as looking towards the cursor
		if let Some(player) = self.smooth_world.current_player_mut() {
			controller.pcontrol.mouse_move_event(self.camera.pixel_to_abs_world_coords(controller.cursor_pos) - player.get_pos(time.time_diff));
			player.control(&controller.pcontrol);
		}

		// First renders the background sky, special areas and border
		self.decorations.render_sky(time.dt, &app.display, &self.camera, moved_by, frame, &mut self.params);
		self.world_renderer.render_special_areas(&app.display, frame, &mut self.params, model.get_world().get_blocks(), &self.camera, matrix);
		self.world_renderer.render_border(frame, &self.params, matrix);

		let forcefields = self.smooth_world.get_forcefields(time.time_diff);

		// Second renders the particles (including the death effect particles), they are decorative so they can go below everything else
		// Note that updating must come BEFORE adding new particles to avoid any new thrust particles getting a "head start" and being off-centre
		self.decorations.update_particles(time.dt);
		self.decorations.add_particles(time.dt, time.time_diff, model.get_world(), &self.smooth_world, &forcefields);
		self.decorations.render_particles(time.dt, &app.display, frame, &mut self.params, matrix);

		// Third renders everything in the world
		self.world_renderer.render(time.dt, &app.display, frame, &mut self.params, time.time_diff, model.get_world(), &mut self.smooth_world, &forcefields, &self.camera);

		// Fourth renders the health bars, ammo status and scores, which are above everything else
		let config = self.config.borrow();
		let scale_factor = app.scale_factor();
		let mut labels = Vec::new();

		if config.accessibility.powerup_labels {
			let lwinsize = self.camera.get_window_size().as_vec2() / scale_factor;
			for powerup in model.get_world().get_powerups() {
				labels.push(WorldLabel::new_powerup(powerup, self.world_renderer.get_powerup_renderer(), &self.camera, lwinsize));
			}
		}

		let render_hud = self.render_hud && !story_text.open();
		if render_hud {
			self.hud.push_health_bars_and_labels(&self.smooth_world, &self.camera, time.time_diff, scale_factor, &mut labels);
		}

		if render_hud || (config.accessibility.powerup_labels && config.accessibility.powerup_labels_hud_hidden) {
			// Must have labels under the other HUD elements
			gui::render(app, frame, |ctx| {
				for label in &labels {
					label.render(ctx);
				}
			});

			if render_hud {
				self.hud.render_glium(app, frame, &mut self.params, &self.smooth_world, scale_factor);
			}
		}

		if render_hud || story_text.open() {
			if self.winsize_changed {
				app.gui.do_extra_pass();
			}
			gui::render(app, frame, |ctx| {
				if render_hud {
					self.hud.render_egui(ctx, time, model, &self.smooth_world, scale_factor);
				}

				story_text.render(ctx);
			});
		}

		drop(config);

		self.update_sounds(time.dt, time.time_diff);
		self.winsize_changed = false;
	}

	fn update_sounds(&mut self, dt: f32, time_diff: f32) {
		let mut looping_sounds = LoopingSounds::new();
		self.smooth_world.update_sounds(dt, time_diff, &mut looping_sounds);
		self.sounds.update(dt, self.camera.get_pos(), self.camera.get_size().min_element(), self.camera.jolted(), looping_sounds);
	}
}
