// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! JSON schema for the theme document: raw deserialisation types, colour
//! (de)serialiser, and conversion to the public runtime types.
//!
//! This module is the single place where impedance between "JSON as designers
//! and tools write it" and "the Rust types widgets consume" lives. Every
//! public type in `theme::paint`, `theme::shadow`, `theme::surface`,
//! `theme::text_style` and `theme::slots` gets a private `Raw*` counterpart
//! here with the minimum serde annotations to match the JSON shape, and a
//! `From<Raw*> for *` conversion that builds the public value.
//!
//! Keeping the derives off the public types means the public API stays
//! free of serde dependencies and the JSON format can evolve (add fields,
//! accept aliases, reject unknown keys) without touching consumer code.

use std::collections::HashMap;
use std::path::{ Path, PathBuf };

use serde::{ Deserialize, Deserializer, Serializer };
use serde::de::Error as DeError;

use crate::types::Color;

use super::document::{ Mode, ThemeDocument };
use super::fonts::{ FontFamilyDef, FontSource };
use super::palette::default_window_controls;
use super::slots::{ Slot, SlotStore };
use super::
{
	LauncherSpec, ThemeError, ThemeMode, WallpaperSpec,
	WindowControlsSpec,
};

mod raw;
mod refs;
#[ cfg( test ) ]
mod tests;

use raw::*;
use refs::*;

// ─── Color serialiser ────────────────────────────────────────────────────────

/// Encode / decode [`Color`] as a hex or `rgba(…)` string.
///
/// Accepted input forms:
///
/// * `#RRGGBB` or `RRGGBB` — opaque, 8 bits per channel.
/// * `#RRGGBBAA` or `RRGGBBAA` — with straight alpha, 8 bits per channel.
/// * `rgba(R, G, B, A)` — R/G/B as integers `0..=255` (or floats), A as a
///   float in `0.0..=1.0`. Whitespace around commas is tolerated.
/// * `rgb(R, G, B)` — same as above with implicit A = 1.0.
///
/// Output form (serialisation): `#RRGGBB` when alpha is 1.0, `#RRGGBBAA`
/// otherwise.
pub mod color_serde
{
	use super::*;

	/// Parse one of the accepted color syntaxes into a [`Color`].
	pub fn parse( s: &str ) -> Result<Color, String>
	{
		let t = s.trim();
		if t.starts_with( "rgba(" ) || t.starts_with( "rgb(" )
		{
			parse_functional( t )
		}
		else
		{
			parse_hex( t )
		}
	}

	fn parse_hex( s: &str ) -> Result<Color, String>
	{
		let h = s.trim_start_matches( '#' );
		let ( r, g, b, a ) = match h.len()
		{
			6 =>
			{
				let r = u8::from_str_radix( &h[0..2], 16 ).map_err( |_| bad( s ) )?;
				let g = u8::from_str_radix( &h[2..4], 16 ).map_err( |_| bad( s ) )?;
				let b = u8::from_str_radix( &h[4..6], 16 ).map_err( |_| bad( s ) )?;
				( r, g, b, 0xFF )
			}
			8 =>
			{
				let r = u8::from_str_radix( &h[0..2], 16 ).map_err( |_| bad( s ) )?;
				let g = u8::from_str_radix( &h[2..4], 16 ).map_err( |_| bad( s ) )?;
				let b = u8::from_str_radix( &h[4..6], 16 ).map_err( |_| bad( s ) )?;
				let a = u8::from_str_radix( &h[6..8], 16 ).map_err( |_| bad( s ) )?;
				( r, g, b, a )
			}
			_ => return Err( bad( s ) ),
		};
		Ok( Color
		{
			r: r as f32 / 255.0,
			g: g as f32 / 255.0,
			b: b as f32 / 255.0,
			a: a as f32 / 255.0,
		})
	}

	fn parse_functional( s: &str ) -> Result<Color, String>
	{
		let ( with_alpha, inner ) = if let Some( rest ) = s.strip_prefix( "rgba(" )
		{
			( true, rest.strip_suffix( ')' ).ok_or_else( || bad( s ) )? )
		}
		else if let Some( rest ) = s.strip_prefix( "rgb(" )
		{
			( false, rest.strip_suffix( ')' ).ok_or_else( || bad( s ) )? )
		}
		else
		{
			return Err( bad( s ) );
		};
		let parts: Vec<&str> = inner.split( ',' ).map( str::trim ).collect();
		let expected = if with_alpha { 4 } else { 3 };
		if parts.len() != expected { return Err( bad( s ) ); }

		let r = parse_channel( parts[0] ).ok_or_else( || bad( s ) )?;
		let g = parse_channel( parts[1] ).ok_or_else( || bad( s ) )?;
		let b = parse_channel( parts[2] ).ok_or_else( || bad( s ) )?;
		let a = if with_alpha
		{
			parts[3].parse::<f32>().map_err( |_| bad( s ) )?.clamp( 0.0, 1.0 )
		}
		else
		{
			1.0
		};

		Ok( Color { r: r / 255.0, g: g / 255.0, b: b / 255.0, a })
	}

	fn parse_channel( s: &str ) -> Option<f32>
	{
		// Integer 0..=255 or float 0..=255.
		if let Ok( n ) = s.parse::<u16>()
		{
			if n <= 255 { return Some( n as f32 ); }
			return None;
		}
		if let Ok( f ) = s.parse::<f32>()
		{
			if ( 0.0..=255.0 ).contains( &f ) { return Some( f ); }
		}
		None
	}

	fn bad( s: &str ) -> String
	{
		format!
		(
			"invalid colour `{}` (expected `#RRGGBB`, `#RRGGBBAA` or `rgb[a](…)`)",
			s
		)
	}

	/// Canonical string form: `#RRGGBB` when opaque, `#RRGGBBAA` otherwise.
	pub fn format( c: Color ) -> String
	{
		let r = (c.r.clamp( 0.0, 1.0 ) * 255.0).round() as u8;
		let g = (c.g.clamp( 0.0, 1.0 ) * 255.0).round() as u8;
		let b = (c.b.clamp( 0.0, 1.0 ) * 255.0).round() as u8;
		let a = (c.a.clamp( 0.0, 1.0 ) * 255.0).round() as u8;
		if a == 0xFF
		{
			format!( "#{:02X}{:02X}{:02X}", r, g, b )
		}
		else
		{
			format!( "#{:02X}{:02X}{:02X}{:02X}", r, g, b, a )
		}
	}

	pub fn serialize<S>( c: &Color, ser: S ) -> Result<S::Ok, S::Error>
	where S: Serializer
	{
		ser.serialize_str( &format( *c ) )
	}

	pub fn deserialize<'de, D>( de: D ) -> Result<Color, D::Error>
	where D: Deserializer<'de>
	{
		let s = String::deserialize( de )?;
		parse( &s ).map_err( D::Error::custom )
	}
}

/// Expose the colour parser so error messages and other loaders can share
/// the same syntax understanding.
pub fn parse_color_str( s: &str ) -> Result<Color, String>
{
	color_serde::parse( s )
}

// ─── Conversion from raw to runtime types ────────────────────────────────────

fn family_from_raw( root: Option<&Path>, r: RawFontFamily ) -> FontFamilyDef
{
	FontFamilyDef
	{
		name:      r.name,
		fallbacks: r.fallbacks,
		sources:   r.sources.into_iter().map( |s| FontSource
		{
			weight: s.weight,
			style:  s.style.into(),
			path:   resolve_relative( root, &s.path ),
		}).collect(),
	}
}

fn wallpaper_from_raw( root: Option<&Path>, r: RawWallpaper ) -> WallpaperSpec
{
	WallpaperSpec { path: Some( resolve_relative( root, &r.path ) ), fit: r.fit }
}

fn window_controls_from_raw
(
	fallback_palette: Option<&super::Palette>,
	_mode: ThemeMode,
	raw: RawWindowControls,
) -> Result<WindowControlsSpec, ThemeError>
{
	// When the mode has no palette to derive sensible defaults from, we fall
	// back to neutral black-on-white defaults for the fields the author did
	// not override.
	let fallback = match fallback_palette
	{
		Some( p ) => default_window_controls( *p ),
		None      => WindowControlsSpec
		{
			bar_bg:         Color::WHITE,
			icon:           Color::BLACK,
			hover_bg:       Color::rgba( 0.0, 0.0, 0.0, 0.08 ),
			pressed_bg:     Color::rgba( 0.0, 0.0, 0.0, 0.12 ),
			close_hover_bg: Color::rgba( 0.92, 0.18, 0.18, 0.90 ),
			close_icon:     Color::WHITE,
			focus_ring:     Color::hex( 0x04, 0xD9, 0xFE ),
		},
	};
	Ok( WindowControlsSpec
	{
		bar_bg:         parse_opt( raw.bar_bg.as_deref(),         fallback.bar_bg         )?,
		icon:           parse_opt( raw.icon.as_deref(),           fallback.icon           )?,
		hover_bg:       parse_opt( raw.hover_bg.as_deref(),       fallback.hover_bg       )?,
		pressed_bg:     parse_opt( raw.pressed_bg.as_deref(),     fallback.pressed_bg     )?,
		close_hover_bg: parse_opt( raw.close_hover_bg.as_deref(), fallback.close_hover_bg )?,
		close_icon:     parse_opt( raw.close_icon.as_deref(),     fallback.close_icon     )?,
		focus_ring:     parse_opt( raw.focus_ring.as_deref(),     fallback.focus_ring     )?,
	})
}

fn parse_opt( s: Option<&str>, fallback: Color ) -> Result<Color, ThemeError>
{
	match s
	{
		Some( v ) => parse_color_str( v ).map_err( ThemeError::InvalidColor ),
		None      => Ok( fallback ),
	}
}

fn mode_from_raw( root: Option<&Path>, r: RawMode ) -> Result<Mode, ThemeError>
{
	let mut store = SlotStore::new();
	for ( id, raw ) in r.slots
	{
		store.insert( id, Slot::from( raw ) );
	}
	let wallpaper  = r.wallpaper.map( |w| wallpaper_from_raw( root, w ) );
	let lockscreen = r.lockscreen.map( |w| wallpaper_from_raw( root, w ) );
	let launcher = match r.launcher
	{
		Some( l ) => Some( LauncherSpec
		{
			background:    parse_color_str( &l.background ).map_err( ThemeError::InvalidColor )?,
			border_radius: l.border_radius,
		}),
		None => None,
	};
	let window_controls = match r.window_controls
	{
		Some( raw ) => Some( window_controls_from_raw( None, ThemeMode::Light, raw )? ),
		None        => None,
	};
	Ok( Mode { wallpaper, lockscreen, launcher, window_controls, slots: store } )
}

// ─── Public entry points ─────────────────────────────────────────────────────

/// Parse a theme document from its JSON source text.
///
/// `root` is the directory the document was read from, used to resolve
/// relative paths (wallpaper images, font files) to absolute ones. Pass
/// `None` when parsing in-memory fixtures from tests.
///
/// Performs the colour-reference resolution pass before structural
/// deserialisation: any string of the form `@name` or `@name/AA` (where
/// `AA` is a two-digit hex alpha override) is rewritten to its literal
/// hex form by looking `name` up in the top-level `colors` object. The
/// rewritten JSON is then deserialised into [`RawThemeDocument`] and
/// converted to the runtime types as before.
pub fn parse_document_json( text: &str, root: Option<&Path> )
	-> Result<ThemeDocument, ThemeError>
{
	let mut value: serde_json::Value = serde_json::from_str( text ).map_err( |e|
		ThemeError::ParseJson( root.map( Path::to_path_buf ).unwrap_or_default(), e )
	)?;
	let colors          = extract_colors_map( &value )?;
	let gradients       = extract_gradients_map( &value )?;
	let inset_stacks    = extract_inset_stacks_map( &value )?;
	// Gradients live in objects (`{ "type": "linear", … }`) and inset stacks
	// live in arrays (`[ { offset, blur, … }, … ]`); the resolver does not
	// care about the shape, so the two sections share a single internal
	// lookup. Names must therefore be unique across both — a collision is
	// rejected up front rather than letting `@foo` resolve ambiguously.
	let mut tokens = gradients;
	for ( k, v ) in inset_stacks
	{
		if tokens.contains_key( &k )
		{
			return Err( ThemeError::InvalidColor( format!(
				"name `{}` is defined in both `gradients` and `inset_stacks`", k
			)));
		}
		tokens.insert( k, v );
	}
	// Pre-resolve `@color` references that live inside token bodies so
	// subsequent substitutions are flat clones with no recursion. Tokens
	// cannot reference each other, so we resolve against an empty token
	// map here.
	let empty_tokens = HashMap::new();
	for ( _, t ) in tokens.iter_mut()
	{
		resolve_refs( t, &colors, &empty_tokens )?;
	}
	// Drop the top-level palette sections before resolving the rest, so the
	// walk does not waste cycles on entries that are about to be discarded
	// and `RawThemeDocument`'s `deny_unknown_fields` does not reject them.
	if let serde_json::Value::Object( ref mut map ) = value
	{
		map.remove( "colors" );
		map.remove( "gradients" );
		map.remove( "inset_stacks" );
	}
	resolve_refs( &mut value, &colors, &tokens )?;

	let raw: RawThemeDocument = serde_json::from_value( value ).map_err( |e|
		ThemeError::ParseJson( root.map( Path::to_path_buf ).unwrap_or_default(), e )
	)?;
	let fonts = raw.fonts
		.into_iter()
		.map( |( k, v )| ( k, family_from_raw( root, v ) ) )
		.collect();
	let light = mode_from_raw( root, raw.modes.light )?;
	let dark  = mode_from_raw( root, raw.modes.dark  )?;
	Ok( ThemeDocument
	{
		id:   raw.theme.id,
		name: raw.theme.name,
		root: root.map( Path::to_path_buf ),
		fonts,
		light,
		dark,
	})
}

/// Load a theme document from a directory containing a `theme.json`.
pub fn load_document_from_dir( dir: &Path ) -> Result<ThemeDocument, ThemeError>
{
	let json_path = dir.join( "theme.json" );
	let text = std::fs::read_to_string( &json_path )
		.map_err( |e| ThemeError::Io( json_path.clone(), e ) )?;
	parse_document_json( &text, Some( dir ) )
}

// ─── Helpers ─────────────────────────────────────────────────────────────────

fn resolve_relative( root: Option<&Path>, rel: &str ) -> PathBuf
{
	let p = Path::new( rel );
	if p.is_absolute() { return p.to_path_buf(); }
	match root
	{
		Some( r ) => r.join( p ),
		None      => p.to_path_buf(),
	}
}
