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

const MAX_ENTRIES: usize = 1000;

/// Cached icon data shared via Arc to avoid cloning pixel buffers.
pub type IconData = ( Arc<Vec<u8>>, u32, u32 );

/// Cache key. The size component is essential — a path-only key
/// would let the first decode (e.g. the launcher's hard-coded 48 px)
/// win for every later request, regardless of how big the
/// homescreen grid (`cell × ICON_FRACTION`) or the desktop dock
/// (`cell × DOCK_ICON_FRACTION`) actually wants the icon. The
/// renderer then upscales the cached 48 px raster with linear
/// filtering, which is what made small APK-extracted PNGs (Signal,
/// any Android app) read as blurry while native Linux icons that
/// happened to ship a higher source resolution looked fine.
type IconKey = ( PathBuf, u32 );

pub struct IconCache
{
	entries: HashMap<IconKey, IconData>,
	order:   Vec<IconKey>,
	pending: Vec<mpsc::Receiver<( IconKey, IconData )>>,
}

impl IconCache
{
	pub fn new() -> Self
	{
		Self
		{
			entries: HashMap::with_capacity( MAX_ENTRIES ),
			order:   Vec::with_capacity( MAX_ENTRIES ),
			pending: Vec::new(),
		}
	}

	/// Decode `jobs` on a worker thread; the results land in the cache
	/// the next time it is consulted, so a later `get_or_load_sized` for
	/// the same key is a hit instead of a main-thread decode.
	pub fn prewarm( &mut self, jobs: Vec<( PathBuf, u32 )> )
	{
		let jobs: Vec<( PathBuf, u32 )> = jobs.into_iter()
			.filter( |k| !self.entries.contains_key( k ) )
			.collect();
		if jobs.is_empty() { return; }
		let ( tx, rx ) = mpsc::channel();
		let spawned = std::thread::Builder::new()
			.name( "icon-prewarm".into() )
			.spawn( move ||
			{
				for ( path, size ) in jobs
				{
					if let Some( ( rgba, w, h ) ) = decode_icon_file( &path, size )
					{
						if tx.send( ( ( path, size ), ( Arc::new( rgba ), w, h ) ) ).is_err() { return; }
					}
				}
			} )
			.is_ok();
		if spawned { self.pending.push( rx ); }
	}

	fn absorb_pending( &mut self )
	{
		if self.pending.is_empty() { return; }
		let mut ready = Vec::new();
		self.pending.retain( |rx|
		{
			loop
			{
				match rx.try_recv()
				{
					Ok( item ) => ready.push( item ),
					Err( mpsc::TryRecvError::Empty ) => return true,
					Err( mpsc::TryRecvError::Disconnected ) => return false,
				}
			}
		} );
		for ( key, data ) in ready { self.insert( key, data ); }
	}

	fn insert( &mut self, key: IconKey, data: IconData )
	{
		if self.entries.contains_key( &key ) { return; }
		if self.entries.len() >= MAX_ENTRIES
		{
			if let Some( oldest ) = self.order.first().cloned()
			{
				self.entries.remove( &oldest );
				self.order.remove( 0 );
			}
		}
		self.entries.insert( key.clone(), data );
		self.order.push( key );
	}

	/// Decode `path` to RGBA at the requested `size` (or hit the cache
	/// when an entry for `( path, size )` already exists).
	///
	/// SVGs render at `size` natively (resvg). Raster sources (PNG /
	/// JPG / WebP / …) are resampled to fit `size` with Lanczos3 at
	/// decode time so the GLES blit can use a 1:1 sample-to-pixel
	/// mapping instead of upscaling a small APK-extracted icon. XPM is
	/// kept native-size — the parser is here for legacy 32 / 48 px
	/// pixmaps and Lanczos on a hand-painted XPM blurs the deliberate
	/// crispness of the original; callers that hit XPM at a non-native
	/// size are rare enough that it isn't worth the resample.
	pub fn get_or_load_sized( &mut self, path: &Path, size: u32 ) -> Option<IconData>
	{
		let key: IconKey = ( path.to_path_buf(), size );
		self.absorb_pending();

		if let Some( data ) = self.entries.get( &key )
		{
			if let Some( pos ) = self.order.iter().position( |k| k == &key )
			{
				let k = self.order.remove( pos );
				self.order.push( k );
			}
			// Arc clone = cheap pointer copy, the pixel buffer is shared.
			return Some( data.clone() );
		}

		let ( rgba, w, h ) = decode_icon_file( path, size )?;
		let data: IconData = ( Arc::new( rgba ), w, h );
		self.insert( key, data.clone() );
		Some( data )
	}
}

fn decode_icon_file( path: &Path, target_size: u32 ) -> Option<( Vec<u8>, u32, u32 )>
{
	if !path.exists()
	{
		return None;
	}
	let ext = path.extension()?.to_str()?;
	match ext
	{
		"svg" => decode_svg( path, target_size ),
		"xpm" => decode_xpm( path ),
		_     => decode_raster( path, target_size )
			.or_else( || sniff_and_decode_svg( path, target_size ) ),
	}
}

/// Some packages ship SVG payloads under raster extensions (Proton Mail's
/// `pixmaps/proton-mail.png` is one). Peek the first bytes and retry as SVG.
fn sniff_and_decode_svg( path: &Path, target_size: u32 ) -> Option<( Vec<u8>, u32, u32 )>
{
	use std::io::Read;
	let mut buf = [0u8; 256];
	let n   = std::fs::File::open( path ).ok()?.read( &mut buf ).ok()?;
	let head = &buf[..n];
	let start = head.iter().position( |b| !b.is_ascii_whitespace() ).unwrap_or( n );
	let body  = &head[start..];
	if body.starts_with( b"<?xml" ) || body.starts_with( b"<svg" ) || body.starts_with( b"<!DOCTYPE svg" )
	{
		decode_svg( path, target_size )
	} else {
		None
	}
}

/// Decode a PNG / JPG / WebP / etc. and resample to `target_size` with
/// Lanczos3, preserving aspect ratio (most app icons are square but
/// some Linux .desktop entries ship non-square art). Aspect-preserving
/// `image::DynamicImage::resize` fits the larger dimension to
/// `target_size`; for a square source, both dimensions land on
/// `target_size` exactly. `target_size = 1` is the floor — `image`
/// rejects 0×0 outputs.
fn decode_raster( path: &Path, target_size: u32 ) -> Option<( Vec<u8>, u32, u32 )>
{
	let img = image::open( path ).ok()?;
	let ( sw, sh ) = ( img.width(), img.height() );
	let target     = target_size.max( 1 );

	// Fast path — source is already at the requested footprint.
	// Skips a Lanczos pass that would otherwise re-rasterise an
	// already-correctly-sized icon (idempotent in theory, slightly
	// soft in practice because Lanczos blurs ever so slightly even
	// at scale 1.0 due to the kernel's sinc tail).
	if sw == target && sh == target
	{
		let rgba = img.to_rgba8();
		return Some( ( rgba.into_raw(), sw, sh ) );
	}

	// `resize` (not `resize_exact`) keeps the source's aspect ratio:
	// the larger side hits `target`, the shorter side scales by the
	// same factor. A 256×192 source asked for 65 → 65×48; a square
	// 96×96 source asked for 50 → 50×50. The renderer uses the
	// returned w/h, so non-square output composes correctly.
	let resized = img.resize(
		target,
		target,
		image::imageops::FilterType::Lanczos3,
	);
	let rgba    = resized.to_rgba8();
	let ( w, h ) = ( rgba.width(), rgba.height() );
	Some( ( rgba.into_raw(), w, h ) )
}

/// Minimal XPM3 parser. Supports `#RRGGBB`, `#RRRRGGGGBBBB`, `None` and a small
/// set of common named colors. Sufficient for legacy pixmaps like `mini.xterm`.
fn decode_xpm( path: &Path ) -> Option<( Vec<u8>, u32, u32 )>
{
	let content = std::fs::read_to_string( path ).ok()?;

	// Extract all double-quoted strings, in order. XPM stores its data as a
	// C array of strings, so the data we care about is exactly these strings.
	let mut strings: Vec<String> = Vec::new();
	let bytes = content.as_bytes();
	let mut i = 0;
	while i < bytes.len()
	{
		if bytes[i] == b'"'
		{
			let start = i + 1;
			let mut j = start;
			while j < bytes.len() && bytes[j] != b'"'
			{
				if bytes[j] == b'\\' && j + 1 < bytes.len()
				{
					j += 2;
				} else {
					j += 1;
				}
			}
			if j < bytes.len()
			{
				strings.push( String::from_utf8_lossy( &bytes[start..j] ).into_owned() );
				i = j + 1;
				continue;
			}
		}
		i += 1;
	}

	if strings.is_empty()
	{
		return None;
	}

	// Header: "<width> <height> <ncolors> <chars_per_pixel>"
	let header_parts: Vec<&str> = strings[0].split_whitespace().collect();
	if header_parts.len() < 4
	{
		return None;
	}
	let width: usize = header_parts[0].parse().ok()?;
	let height: usize = header_parts[1].parse().ok()?;
	let ncolors: usize = header_parts[2].parse().ok()?;
	let cpp: usize = header_parts[3].parse().ok()?;

	if strings.len() < 1 + ncolors + height || cpp == 0
	{
		return None;
	}

	// Color table: each entry is "<chars> c <color>" (we only honor 'c').
	let mut palette: HashMap<String, [u8; 4]> = HashMap::new();
	for k in 0..ncolors
	{
		let line = &strings[1 + k];
		if line.len() < cpp
		{
			return None;
		}
		let key = line[..cpp].to_string();
		let rest = &line[cpp..];
		// Find the 'c' (color) key. Falls back to first color value if absent.
		let color = parse_xpm_color_value( rest ).unwrap_or( [0, 0, 0, 0] );
		palette.insert( key, color );
	}

	// Pixel rows.
	let mut rgba = Vec::with_capacity( width * height * 4 );
	for row_idx in 0..height
	{
		let row = &strings[1 + ncolors + row_idx];
		if row.len() < width * cpp
		{
			return None;
		}
		for col in 0..width
		{
			let key = &row[col * cpp..col * cpp + cpp];
			let px = palette.get( key ).copied().unwrap_or( [0, 0, 0, 0] );
			rgba.extend_from_slice( &px );
		}
	}

	Some( ( rgba, width as u32, height as u32 ) )
}

/// Parse the part of an XPM color line after the chars key. Returns RGBA.
fn parse_xpm_color_value( rest: &str ) -> Option<[u8; 4]>
{
	let tokens: Vec<&str> = rest.split_whitespace().collect();
	let mut i = 0;
	let mut chosen: Option<&str> = None;
	while i + 1 < tokens.len()
	{
		// Color keys: c (color), m (mono), g (greyscale), s (symbolic), g4.
		// Prefer 'c'; fall back to others if 'c' is not present.
		let key = tokens[i];
		// The color value can be multiple tokens for named colors like "light blue".
		// Find the next key token to delimit the value.
		let mut j = i + 1;
		while j < tokens.len() && !is_xpm_color_key( tokens[j] )
		{
			j += 1;
		}
		let value = tokens[i + 1..j].join( " " );
		if key == "c"
		{
			return parse_color_string( &value );
		}
		if chosen.is_none() && (key == "m" || key == "g" || key == "g4")
		{
			chosen = Some( tokens[i + 1] );
		}
		i = j;
	}
	chosen.and_then( parse_color_string )
}

fn is_xpm_color_key( tok: &str ) -> bool
{
	matches!( tok, "c" | "m" | "g" | "g4" | "s" )
}

fn parse_color_string( value: &str ) -> Option<[u8; 4]>
{
	let v = value.trim();
	if v.eq_ignore_ascii_case( "none" )
	{
		return Some( [0, 0, 0, 0] );
	}
	if let Some( hex ) = v.strip_prefix( '#' )
	{
		// Supported lengths: 6 (#RRGGBB), 12 (#RRRRGGGGBBBB), 3 (#RGB).
		match hex.len()
		{
			6 =>
			{
				let r = u8::from_str_radix( &hex[0..2], 16 ).ok()?;
				let g = u8::from_str_radix( &hex[2..4], 16 ).ok()?;
				let b = u8::from_str_radix( &hex[4..6], 16 ).ok()?;
				return Some( [r, g, b, 255] );
			}
			12 =>
			{
				let r = u8::from_str_radix( &hex[0..2], 16 ).ok()?;
				let g = u8::from_str_radix( &hex[4..6], 16 ).ok()?;
				let b = u8::from_str_radix( &hex[8..10], 16 ).ok()?;
				return Some( [r, g, b, 255] );
			}
			3 =>
			{
				let r = u8::from_str_radix( &hex[0..1], 16 ).ok()? * 17;
				let g = u8::from_str_radix( &hex[1..2], 16 ).ok()? * 17;
				let b = u8::from_str_radix( &hex[2..3], 16 ).ok()? * 17;
				return Some( [r, g, b, 255] );
			}
			_ => return None,
		}
	}
	// Named colors — small subset commonly used in XPM icons.
	let v_lower = v.to_lowercase();
	let v_lower = v_lower.replace( ' ', "" );
	match v_lower.as_str()
	{
		"black"   => Some( [0,   0,   0,   255] ),
		"white"   => Some( [255, 255, 255, 255] ),
		"red"     => Some( [255, 0,   0,   255] ),
		"green"   => Some( [0,   128, 0,   255] ),
		"blue"    => Some( [0,   0,   255, 255] ),
		"yellow"  => Some( [255, 255, 0,   255] ),
		"cyan"    => Some( [0,   255, 255, 255] ),
		"magenta" => Some( [255, 0,   255, 255] ),
		"gray" | "grey" => Some( [128, 128, 128, 255] ),
		"lightgray" | "lightgrey" => Some( [211, 211, 211, 255] ),
		"darkgray" | "darkgrey"   => Some( [169, 169, 169, 255] ),
		_ => None,
	}
}

fn decode_svg(path: &Path, size: u32) -> Option<(Vec<u8>, u32, u32)> {
    let data = std::fs::read(path).ok()?;
    // Delegated to ltk so the SVG rasteriser dep lives in one place.
    // The ltk function returns an `Arc<Vec<u8>>`; this caller wants an
    // owned `Vec<u8>`, so we unwrap the Arc — the buffer was just
    // produced and has refcount 1, so `Arc::try_unwrap` succeeds.
    let ( rgba, w, h ) = ltk::decode_svg_bytes( &data, size )?;
    let bytes = std::sync::Arc::try_unwrap( rgba ).unwrap_or_else( |arc| (*arc).clone() );
    Some( ( bytes, w, h ) )
}
