#![crate_type = "rlib"]

use std::array::TryFromSliceError;
use std::fmt::Display;
use std::path::Path;
use miniz_oxide::deflate::compress_to_vec;
use miniz_oxide::inflate::{decompress_to_vec, DecompressError};

pub const FILE_VERSION: u8 = 1u8;
pub const HEADER_SIZE: usize = 6;

#[derive(Debug)]
pub enum IngrisError
{
	Unzip(DecompressError),
	IOError(std::io::Error),
	BufferError(TryFromSliceError),
	Version(),
}

impl From<std::io::Error> for IngrisError
{
	fn from(value: std::io::Error) -> Self
	{
		IngrisError::IOError(value)
	}
}

impl From<DecompressError> for IngrisError
{
	fn from(value: DecompressError) -> Self
	{
		IngrisError::Unzip(value)
	}
}

impl From<TryFromSliceError> for IngrisError
{
	fn from(value: TryFromSliceError) -> Self
	{
		IngrisError::BufferError(value)
	}
}

impl Display for IngrisError
{
	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
	{
		match self
		{
			IngrisError::Unzip(err) => err.fmt(f),
			IngrisError::IOError(err) => err.fmt(f),
			IngrisError::BufferError(err) => err.fmt(f),
			IngrisError::Version() => write!(f, "unsupported file version."),
		}
	}
}

impl std::error::Error for IngrisError {}

/// Haupt Struct.
/// Enthält die Funktionen zum Lesen und Schreiben der Bilder.
/// Ingris ist ein einfaches Bildformat, das speziell für die Verwendung in Spielen entwickelt wurde.
/// Es verwendet zur Optimierung 4 oder 8 Bit Farbtiefe und hat einen minimalen Header.
/// Mit 4 bit (max. 16 Farben) ist die Komprimierung besser, aber die Dekodierung etwas langsamer.
/// ## Ein Bild erstellen und speichern
/// ```ignore
/// let image = Image::new(FILE_VERSION, width, height, bytes);
/// Ingris::write(path, &image).unwrap();
/// ```
/// ## Ein Bild aus einer Datei lesen
/// ```ignore
/// Ingris::read(path).unwrap();
/// ```
pub struct Ingris();

impl Ingris
{
	/// Ließt ein komprimiertes Bild aus einem Byte-Buffer.
	/// * 4bit: Es wird eine etwas schnellerer Algorithmus verwendet, wenn die Anzahl der Bytes durch 4 teilbar ist indem immer ein u32 gelesen wird.
	pub fn from_bytes(buffer: Vec<u8>) -> Result<Image, IngrisError>
	{
		let buffer = decompress_to_vec(&buffer)?;
		let mut image = Self::header(&buffer[0..HEADER_SIZE].try_into()?)?;
		if image.version != FILE_VERSION { return Err(IngrisError::Version()) }
		image.bytes = if image.bits == 4
		{
			let mut bytes: Vec<u8>;
			if image.size() % 4 == 0
			{
				// fast Mode
				let mut data: Vec<u32> = Vec::with_capacity(image.size() / std::mem::size_of::<u32>());
				for b in buffer[HEADER_SIZE..].chunks_exact(2)
				{
					data.push((
						((b[0] as u32 >> 4) << 0) +
						((b[0] as u32 & 0x0F) << 8) +
						((b[1] as u32 >> 4) << 16) +
						((b[1] as u32 & 0x0F) << 24)).to_le());
				}
				bytes = to_u8_vec(data);
			}
			else
			{
				// slow Mode
				bytes = Vec::with_capacity(image.size());
				for b in &buffer[HEADER_SIZE..]
				{
					bytes.push(b >> 4);
					bytes.push(b & 0x0F);
				}
			}
			bytes
		}
		else
		{
			buffer[HEADER_SIZE..].to_owned()
		};
		Ok(image)
	}

	/// Ließt ein Bild von einer Datei.
	pub fn read<P: AsRef<Path>>(path: P) -> Result<Image, IngrisError>
	{
		Self::from_bytes(std::fs::read(path)?)
	}

	/// Schreibt ein Bild in eine Datei.
	/// * 4bit: Es wird eine etwas schnellerer Algorithmus verwendet, wenn die Anzahl der Bytes durch 4 teilbar ist indem immer ein u32 geschrieben wird.
	pub fn write<P: AsRef<Path>>(path: P, image: &Image) -> Result<(), IngrisError>
	{
		debug_assert!(image.size() == image.bytes.len(), "image size dont match byte length.");
		debug_assert!(image.bits == 4 || image.bits == 8, "invalid bit depth.");

		let mut buffer: Vec<u8> = Vec::with_capacity(image.size() + HEADER_SIZE);
		buffer.push(FILE_VERSION);
		buffer.extend(image.width.to_le_bytes());
		buffer.extend(image.height.to_le_bytes());
		buffer.push(image.bits);
		if image.bits == 4
		{
			if image.size() % 8 == 0
			{
				// fast Mode
				let mut dbuf: Vec<u32> = Vec::with_capacity(image.size() / std::mem::size_of::<u32>());
				for slice in image.bytes.chunks_exact(8)
				{
					let byte: u32 =
						((slice[0] as u32) << 4) +
						((slice[1] as u32) << 0) +
						((slice[2] as u32) << 12) +
						((slice[3] as u32) << 8) +
						((slice[4] as u32) << 20) +
						((slice[5] as u32) << 16) +
						((slice[6] as u32) << 28) +
						((slice[7] as u32) << 24);
					dbuf.push(byte.to_le());
				}
				buffer.extend_from_slice(&to_u8_vec(dbuf));
			}
			else
			{
				// slow Mode
				for slice in image.bytes.chunks_exact(2)
				{
					let byte = (slice[0] << 4) + slice[1];
					buffer.push(byte);
				}
			}
		}
		else
		{
			buffer.extend_from_slice(&image.bytes);
		}
		std::fs::write(path, compress_to_vec(&buffer, 10))?;
		Ok(())
	}

	fn header(header: &[u8; 6]) -> Result<Image, IngrisError>
	{
		let version = header[0];
		let width = u16::from_le_bytes(header[1..3].try_into()?);
		let height = u16::from_le_bytes(header[3..5].try_into()?);
		let bits: u8 = header[5].into();
		Ok(Image { version, width, height, bits, bytes: Default::default() })
	}
}

/// Konvertiert ein Vektor eines Types in einen Byte Vektor
fn to_u8_vec<T>(mut vec: Vec<T>) -> Vec<u8>
{
	let length = vec.len() * std::mem::size_of::<T>();
	let ptr = vec.as_mut_ptr() as *mut u8;
	std::mem::forget(vec);

	unsafe { Vec::from_raw_parts(ptr, length, length) }
}

/// Struct für die Bilddaten.
/// Enthält eine Versionsnummer, die Breite und Höhe des Bildes, die Bit-Tiefe und die indizierten Bilddaten.
#[derive(PartialEq)]
pub struct Image
{
	pub version: u8,
	pub width: u16,
	pub height: u16,
	bits: u8,
	bytes: Vec<u8>
}

impl Image
{
	/// Erstellt ein neues Bild.
	/// Die Bittiefe wird automatisch ermittelt und auf 4 oder 8 Bit gesetzt.
	/// Die Bilddaten müssen in einem Vektor mit der Größe `width * height` sein.
	pub fn new(version: u8, width: u16, height: u16, bytes: Vec<u8>) -> Self
	{
		if bytes.len() != (width as usize * height as usize) {panic!("image size dont match byte length.")}	
		let mut bits = 4;
		for b in &bytes
		{
			if *b > 0x0F
			{
				bits = 8;
				break;
			}
		}
		Self
		{
			version,
			width,
			height,
			bits,
			bytes
		}
	}

	/// Erstellt ein neues Bild ohne Überprüfung der Bittiefe.
	/// # Safety
	/// Der Aufrufer muss sicher stellen, dass die Abmessung zu den Bilddaten passt und die Bittiefe korrekt ist.
	pub unsafe fn new_unchecked(version: u8, width: u16, height: u16, bits: u8, bytes: Vec<u8>) -> Self
	{
		debug_assert!(width as usize * height as usize == bytes.len(), "image size dont match byte length.");
		debug_assert!(bits == 4 || bits == 8, "invalid bit depth.");
		Self
		{
			version,
			width,
			height,
			bits,
			bytes
		}
	}

	/// Die Bit-Tiefe des Bildes. Entweder 4 oder 8 bit.
	pub fn bit_depth(&self) -> u8
	{
		self.bits
	}

	/// Die Bilddaten.
	pub fn bytes(&self) -> &[u8]
	{
		&self.bytes
	}
	
	/// Die Abmessungen des Bildes.
	#[inline]
	pub fn size(&self) -> usize
	{
		self.width as usize * self.height as usize
	}
}

#[cfg(test)]
mod tests
{
	use super::*;
	use std::{fs::File, io::BufWriter, sync::Mutex};

	const IMAGE_1: &[u8] = &[0, 0, 1, 2,  6, 6, 4, 5,  8, 8, 2, 15,  2, 29, 30, 31];

	static PANORAMA: Mutex<Option<PNGImage>> = Mutex::new(None);

	#[test]
	fn basic_encoding()
	{
		let image = Image::new(FILE_VERSION, 4, 4, IMAGE_1.into());

		let path = Path::new("image.ing");
		Ingris::write(path, &image).expect("failed");

		let readed_image = Ingris::read(path).expect("failed");

		assert_eq!(readed_image.width, image.width);
		assert_eq!(readed_image.height, image.height);
		assert_eq!(readed_image.bits, image.bits);
		assert_eq!(readed_image.bytes, image.bytes);
	}

	#[test]
	fn example_image_bit4()
	{
		with_portrait(|png|
		{
			let path_out = Path::new("Portrait_4bit-out.png");
			let mut image = Image::new(FILE_VERSION, png.width as u16, png.height as u16, png.bytes.clone());

			image.bits = 4;
			let out_path = Path::new("Portrait_4bit-out.ing");
			Ingris::write(out_path, &image).expect("failed");

			let image = Ingris::read(out_path).expect("failed");
			let file = File::create(path_out).unwrap();
			let ref mut w = BufWriter::new(file);
			let mut encoder = png::Encoder::new(w, image.width as u32, image.height as u32);
			encoder.set_compression(png::Compression::Best);
			encoder.set_color(png::ColorType::Indexed);
			encoder.set_depth(png::BitDepth::Eight);
			encoder.set_trns(&png.alpha);
			encoder.set_palette(&png.palette);
			let mut writer = encoder.write_header().unwrap();
			writer.write_image_data(&image.bytes()).unwrap();
		});
	}

	#[test]
	fn example_image_bit8()
	{
		with_portrait(|png|
		{
			let path_out = Path::new("Portrait_8bit-out.png");
			let mut image = Image::new(FILE_VERSION, png.width as u16, png.height as u16, png.bytes.clone());
	
			image.bits = 8;
			let out_path = Path::new("Portrait_8bit-out.ing");
			Ingris::write(out_path, &image).expect("failed");
	
			let image = Ingris::read(out_path).expect("failed");
			let file = File::create(path_out).unwrap();
			let ref mut w = BufWriter::new(file);
			let mut encoder = png::Encoder::new(w, image.width as u32, image.height as u32);
			encoder.set_compression(png::Compression::Best);
			encoder.set_color(png::ColorType::Indexed);
			encoder.set_depth(png::BitDepth::Eight);
			encoder.set_trns(&png.alpha);
			encoder.set_palette(&png.palette);
			let mut writer = encoder.write_header().unwrap();
			writer.write_image_data(&image.bytes()).unwrap();
		});
	}

	fn with_portrait<F>(f: F)
	where F: FnOnce(&PNGImage)
	{
		let mut p = PANORAMA.lock().unwrap();
		if p.is_none()
		{
			let path = Path::new("Portrait_low.png");
			let decoder = png::Decoder::new(File::open(path).expect( path.to_str().unwrap()));
			let mut reader = decoder.read_info().unwrap();
			let mut buf = vec![0; reader.output_buffer_size()];
			let img = reader.next_frame(&mut buf).unwrap();
			let alpha = reader.info().trns.as_ref().unwrap().clone();
			let palette = reader.info().palette.as_ref().unwrap().clone();

			*p = Some(PNGImage { width: img.width, height: img.height, bytes: buf, alpha: alpha.to_vec(), palette: palette.to_vec() });
		}
		f(p.as_ref().unwrap());
	}

	struct PNGImage
	{
		pub width: u32,
		pub height: u32,
		pub bytes: Vec<u8>,
		pub alpha: Vec<u8>,
		pub palette: Vec<u8>,
	}
}
