// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
pub mod client;
pub(super) mod changes;

use std::fmt::Write;

use glam::Vec2;

use client::{Client, ClientRes, ClientError};
use changes::ClientChanges;

use super::{TimeArgs, view::{View, Sfx, ResetType}, controller::Controller};

use crate::protocol::message::{Message, LevelMessage};
use crate::world::{World, DELTA_TIME, player::{PlayerId, update::PlayerUpdate}, powerup::PowerupType};
use crate::app::App;

pub struct Model {
	id: PlayerId,
	client: Client,
	world: World,
}

pub enum ModelOutcome {
	Continue,
	Exit,
}

impl Model {
	pub(super) fn new(client_res: ClientRes) -> Model {
		let (id, client, world) = client_res;
		Model { id, client, world }
	}

	pub fn get_id(&self) -> PlayerId { self.id }
	pub fn get_client(&self) -> &Client { &self.client }
	pub fn get_client_mut(&mut self) -> &mut Client { &mut self.client }
	pub fn get_world(&self) -> &World { &self.world }

	pub(super) fn update(&mut self, app: &App, view: &mut View, controller: &mut Controller, time: &TimeArgs) -> Result<ModelOutcome, ClientError> {
		self.client.process_messages(time.dt)?;

		if let Some(level) = self.client.get_level_mut() {
			if level.take_has_completed() {
				if let Some(index) = level.get_index() {
					let mut config = app.config.borrow_mut();
					config.story_mode.completed_levels.insert(index);
					config.changed();
				}
				return Ok(ModelOutcome::Exit);
			}

			if let Some(init_world) = level.take_has_reset() {
				self.world = init_world;
				view.reset(app, self, ResetType::Partial);
				self.client.send(Message::Level(LevelMessage::Reset))?; // The server resets the acknowledgements on this message, which keeps things in sync
			}
		}

		let init_player_pos = view.smooth_world.followed_player().map(|(_, player)| player.get_pos(time.time_diff));
		if let Some(pos) = init_player_pos {
			controller.pcontrol.mouse_move_event(view.camera.pixel_to_abs_world_coords(controller.cursor_pos) - pos);
		}
		let player_update = controller.pcontrol.get_update();

		let updates = self.client.take_world_updates();
		if !updates.is_empty() {
			let mut changes = ClientChanges::new(&self.world, self.id);
			for update in &updates {
				self.world.apply_events(&update.0.pre_events, &mut changes);
				self.world.update(&update.0.players, &mut changes);
				self.world.apply_events(&update.0.post_events, &mut changes);
				self.world.validate().map_err(|err| ClientError::Limit(String::from(err)))?;
				self.client.update_timer(DELTA_TIME);
			}

			view.smooth_world.remove_old_players(&self.world);

			let ack = updates.last().unwrap().1;
			view.smooth_world.sync(&self.world, &changes, ack, updates.len(), &mut view.sounds);

			if view.smooth_world.update_followed_player() {
				if let Some((_, player)) = view.smooth_world.followed_player() {
					// Updates the camera to directly centre the player when the player previously was freely spectating
					view.camera.move_to(player.get_pos(time.time_diff));
				}
			} else if let (Some(init_pos), Some((id, player))) = (init_player_pos, view.smooth_world.followed_player()) && changes.moved.contains(&id) {
				// Updates the camera to the player, preserving the previous relative position of the player
				// Currently used when teleporting or spawning to avoid a sudden change in this relative position
				view.camera.move_by(player.get_pos(time.time_diff) - init_pos);
			}

			for hit in changes.hits {
				view.on_hit(&hit);
			}

			for (sfx, pos, vel) in changes.sounds {
				view.sounds.play_spatial(sfx, pos, vel);
			}

			for (id, typ, pos) in changes.powerup_sounds {
				let sfx = Sfx::Powerup(typ);
				if typ == PowerupType::Teleportation && Some(id) == view.smooth_world.followed_player_id() {
					view.sounds.play(sfx);
				} else {
					view.sounds.play_spatial(sfx, pos, Vec2::ZERO);
				}
			}

			if changes.items_reset {
				view.clear_particles();
			}
		}

		for sfx in self.client.take_sounds() {
			view.sounds.play(sfx);
		}

		if time.steps > 0 {
			for _ in 0..time.steps {
				view.smooth_world.update(player_update, &self.world, &mut view.sounds)?;
			}

			if self.world.has_player(self.id) {
				self.client.send(Message::PlayerUpdate(player_update, time.steps as u64))?;
				if self.client.is_networked() {
					self.client.send(view.smooth_world.create_bulk_update())?;
				}
			} else {
				/*
				 * NOTE: Even sending this out when spectating. This is intentional.
				 *
				 * The reason why I'm sending out player update information when
				 * spectating is so the server can acknowledge the progression of
				 * time. This allows me to reuse my current smooth world code and
				 * keep things looking smooth.
				 *
				 * I could spend some time writing code to improve smoothness in the
				 * case that the player is a spectator and possibly save bandwidth,
				 * but I can't be bothered.
				 *
				 * This information being sent out while spectating isn't correct
				 * for privacy reasons as a server admin running a modified server
				 * could capture information about what inputs the user is pressing.
				 * This isn't too serious but this information isn't needed.
				 */
				self.client.send(Message::PlayerUpdate(PlayerUpdate::default(), time.steps as u64))?;
			}
		}

		for msg in view.get_hud_mut().get_text_io().pending_messages() {
			self.client.send(Message::ClientTextInput(msg))?;
		}

		for msg in self.client.take_text_output() {
			view.get_hud_mut().get_text_io().push_history(msg);
		}

		self.client.flush()?;

		Ok(ModelOutcome::Continue)
	}

	pub(super) fn sanity_check(&mut self) {
		if let Some(sanity_check_data) = self.client.take_sanity_check_data() {
			let mut expected_str = String::new();
			let _ = write!(&mut expected_str, "{sanity_check_data:?}");
			let mut actual_str = String::with_capacity(expected_str.len());
			let _ = write!(&mut actual_str, "{:?}", &self.world.get_dynamic_data());

			if expected_str != actual_str {
				log::error!("world sync error (id = {}):\nexpected = {expected_str},\nactual = {actual_str}", self.id);
			} else {
				log::debug!("sanity check passed");
			}
		}
	}
}
