// Copyright Marcus Del Favero 2025
// Licensed under the GNU AGPLv3 with an exception, see `README.md` for details
use std::{mem, collections::VecDeque, time::Instant};

use egui::{Context, Window, Order, Frame, Margin, Color32};
use glam::Vec2;

use super::health_bar::{HealthBar, HUD_HEIGHT};

use crate::app::config::action::{Action, ActionEvent, ActionState};
use crate::app::gui::{style::{LABEL_SIZE, LABEL_COLOUR}, layout_job_helper::LayoutJobHelper};
use crate::net::utils::security::Security;
use crate::app::playing::model::client::Client;
use crate::protocol::message::text_io::{TextIoString, ClientTextInput, ChatType, ServerTextOutput};
use crate::world::colour::Colour;
use crate::utils::maths::{self, decay::Decay};

pub struct TextIo { // Chat and commands
	state: Option<Prompt>,
	current_line: TextIoString,
	history: VecDeque<(ServerTextOutput, Instant)>,

	pending: Vec<ClientTextInput>,

	prompt: Prompt,
	rendered_open: f32,

	prev_height: f32,
	prev_lwinsize: Vec2,

	ping: u64,
	next_ping: f32,
}

#[derive(Clone, Copy)]
enum Prompt {
	TeamChat,
	GlobalChat,
	Command,
}

impl Prompt {
	fn add(self, job: LayoutJobHelper, text: &str) -> LayoutJobHelper {
		let (prompt, prompt_colour, text_colour) = match self {
			Prompt::TeamChat => ("> ", WHITE, LIGHT_GREY),
			Prompt::GlobalChat => ("Global > ", YELLOW, LIGHT_GREY),
			Prompt::Command => ("% ", GREEN, LIGHT_GREEN),
		};

		job.add(prompt, prompt_colour).add(text, text_colour)
	}
}

const HISTORY_MAX_LEN: usize = 64;
const HISTORY_MAX_LEN_CLOSED: usize = 6;

const TEXT_SIZE: f32 = LABEL_SIZE + 1.0;

const LIGHT_GREY: Color32 = LABEL_COLOUR;
const WHITE: Color32 = Color32::WHITE;
const GREEN: Color32 = Color32::from_rgb(0x5f, 0xff, 0x5f);
const LIGHT_GREEN: Color32 = Color32::from_rgb(0xaf, 0xff, 0xaf);
pub const RED: Color32 = Color32::from_rgb(0xff, 0x5f, 0x5f);
const LIGHT_RED: Color32 = Color32::from_rgb(0xff, 0xaf, 0xaf);

/*
 * Want to make the red extra clear for the body of admin messages so users can
 * more clearly distinguish actual admin messages from users pretending to be
 * the admin.
 */
const LIGHT_RED_ADMIN: Color32 = Color32::from_rgb(0xff, 0x7f, 0x7f);
pub const YELLOW: Color32 = Color32::from_rgb(0xff, 0xff, 0x5f);
const LIGHT_YELLOW: Color32 = Color32::from_rgb(0xff, 0xff, 0xaf);
const BLUE: Color32 = Color32::from_rgb(0x5f, 0xaf, 0xff);

// Fade out and max times when the text input isn't open
const CLOSED_MAX_TIME: f32 = 60.0;
const CLOSED_FADE_OUT_TIME: f32 = 2.0;

impl TextIo {
	pub fn new() -> TextIo {
		TextIo {
			state: None,
			current_line: TextIoString::new(),
			history: VecDeque::with_capacity(HISTORY_MAX_LEN),

			pending: Vec::new(),

			prompt: Prompt::TeamChat,
			rendered_open: 0.0, // Initially closed

			prev_height: f32::NEG_INFINITY,
			prev_lwinsize: Vec2::NEG_INFINITY,

			ping: 0,
			next_ping: 0.0,
		}
	}

	pub fn action(&mut self, action: ActionEvent) {
		let ActionEvent::Action(action, ActionState::Pressed) = action else { return; };

		self.state = match (self.state, action) {
			(None, Action::TeamChatOpen) => Some(Prompt::TeamChat),
			(None, Action::GlobalChatOpen) => Some(Prompt::GlobalChat),
			(None, Action::CommandOpen) => Some(Prompt::Command),
			(Some(_), Action::GuiLeave) => None,
			_ => return,
		};

		if let Some(state) = self.state {
			self.prompt = state;
		}
	}

	pub fn open(&self) -> bool {
		self.state.is_some()
	}

	pub fn push_history(&mut self, msg: ServerTextOutput) {
		if self.history.len() >= HISTORY_MAX_LEN { self.history.pop_front(); }
		self.history.push_back((msg, Instant::now()));
	}

	pub(super) fn push_current(&mut self, s: &str) {
		for ch in s.chars() {
			match ch {
				'\r' => {
					let msg = mem::take(&mut self.current_line);

					if !msg.trim().is_empty() { // Unlikely that an empty chat/command will be needed
						match self.prompt {
							Prompt::TeamChat => self.pending.push(ClientTextInput::Chat(ChatType::Team, msg)),
							Prompt::GlobalChat => self.pending.push(ClientTextInput::Chat(ChatType::Global, msg)),
							Prompt::Command => self.pending.push(ClientTextInput::Command(msg)),
						}
					}

					self.state = None;
				},
				'\x08' /* Backspace */ => self.current_line.pop(),
				'\x00'..'\x20' | '\x7f' => (), // Prevents any control characters from being pushed (happens when pressing escape or delete)
				_ => self.current_line.try_push(ch),
			}
		}
	}

	pub fn pending_messages(&mut self) -> impl Iterator<Item = ClientTextInput> + '_ {
		self.pending.drain(..)
	}

	pub fn render(&mut self, dt: f32, ctx: &Context, lwinsize: Vec2, player_count: usize, client: &Client) {
		const MARGIN: f32 = 8.0;

		// Exponential decay for the text background
		let open = self.open() as u32 as f32;
		self.rendered_open.decay_to(open, 16.0, dt);

		// Prevents never decaying to the exact value from possibly causing rendering problems
		if self.rendered_open < 1e-4 && open == 0.0 {
			self.rendered_open = 0.0;
		} else if self.rendered_open > 1.0 - 1e-4 && open == 1.0 {
			self.rendered_open = 1.0;
		}

		let width = HealthBar::hud_x_px(lwinsize.x).0 - HUD_HEIGHT - MARGIN * 2.0;
		if width <= 0.0 { return; }

		let mut job = LayoutJobHelper::new(TEXT_SIZE).width(width);
		let now = Instant::now();

		/*
		 * Hides the prompt of ">" or "%" if the text input is closed and no chat
		 * is visible (actually just uses the largest alpha in the history).
		 */
		let mut current_line_alpha = maths::lerp(if self.current_line.is_empty() { 0.0 } else { 1.0 }, 1.0, self.rendered_open);

		for (i, (msg, time_received)) in self.history.iter().enumerate() {
			let time = now.duration_since(*time_received).as_secs_f32();
			let alpha_if_closed = if self.history.len() - i > HISTORY_MAX_LEN_CLOSED {
				0.0
			} else if time <= CLOSED_MAX_TIME - CLOSED_FADE_OUT_TIME {
				1.0
			} else if time < CLOSED_MAX_TIME {
				(CLOSED_MAX_TIME - time) / CLOSED_FADE_OUT_TIME
			} else {
				0.0
			};

			let alpha = maths::lerp(alpha_if_closed, 1.0, self.rendered_open);

			if alpha <= 0.0 { continue; }
			current_line_alpha = current_line_alpha.max(alpha);

			job = job.opacity(alpha);
			job = match msg {
				ServerTextOutput::Chat(typ, name, colour, msg) => {
					let dark_colour = colour.name_colour_dark();
					if *typ == ChatType::Team {
						job = job.add("(Team) ", Colour::name_colour_less_dark(dark_colour));
					}
					job.add(&format!("{name}: "), Colour::name_colour_normal(dark_colour)).add(msg.as_ref(), LIGHT_GREY)
				}
				ServerTextOutput::Admin(msg) => job.add("Admin: ", RED).add(msg.as_ref(), LIGHT_RED_ADMIN),
				ServerTextOutput::Server(msg) => job.add(msg.as_ref(), WHITE),
				ServerTextOutput::Command(msg) => Prompt::Command.add(job, msg.as_ref()),
				ServerTextOutput::CommandResponse(msg) => job.add(msg.as_ref(), LIGHT_YELLOW),
			}.newline();
		}

		job = job.opacity(current_line_alpha);
		job = self.prompt.add(job, &self.current_line).newline().newline();

		self.next_ping -= dt;
		if self.next_ping < 0.0 {
			self.ping = client.get_ping().as_millis() as u64;
			self.next_ping += 1.0;
		}

		job = job.opacity(self.rendered_open).add(&format!("Players: {player_count}, Spectators: {}, Ping: {} ms, ", client.get_spectator_count(), self.ping), LIGHT_GREY);
		job = match client.get_security() {
			Security::Secure => job.add("Secure connection", GREEN),
			Security::Local => job.add("Local connection", BLUE),
			Security::Insecure => job.add("INSECURE connection! ", RED).add("Chat messages can be eavesdropped.", LIGHT_RED),
		};

		let galley = ctx.fonts(|fonts| fonts.layout_job(job.build()));
		let height = galley.rect.height();

		let alpha = (0x9f as f32 * self.rendered_open).round() as u8;

		// Prevents first-frame jitter when the history length changes or window size changes
		if height != self.prev_height || lwinsize != self.prev_lwinsize {
			ctx.request_discard("");
			self.prev_height = height;
			self.prev_lwinsize = lwinsize;
		}

		Window::new("text-panel")
			.frame(Frame::none().inner_margin(Margin::same(MARGIN)).fill(Color32::from_black_alpha(alpha)))
			.fixed_pos([0.0, lwinsize.y - (height + MARGIN * 2.0)])
			.title_bar(false)
			.order(Order::Foreground)
			.show(ctx, |ui| {
				ui.set_width(width);
				ui.set_height(height);
				ui.label(galley);
			});
	}
}
