use super::*;

/// Bundles a PAKS archive as static data in the read-only section of the executable.
///
/// # Notes
///
/// * The bundled file must have a size that is an exact multiple of the PAKS block size.
/// * This is intended for archives generated ahead of time and shipped as part of the binary.
///
/// ```
/// paks::static_bundle!(EXAMPLE_PAKS = "../../tests/data/example.paks");
/// let key = [0, 0];
/// let reader = paks::BundleReader::open(&EXAMPLE_PAKS, key).unwrap();
/// ```
#[macro_export]
macro_rules! static_bundle {
	($vis:vis $name:ident = $path:expr) => {
		$vis static $name: [$crate::Block; include_bytes!($path).len() / ::core::mem::size_of::<$crate::Block>()] = unsafe {
			::core::mem::transmute(*include_bytes!($path))
		};
	}
}

/// Bundle reader.
///
/// Reader for PAKS archives that are bundled with the application.
pub struct BundleReader<'a> {
	blocks: &'a [Block],
	directory: Section,
	key: Key,
}

impl<'a> BundleReader<'a> {
	/// Opens the PAKS archive from the given blocks.
	///
	/// The archive header and encrypted directory are authenticated when opening the reader,
	/// while file descriptors are still decrypted lazily during path lookup.
	#[inline]
	pub fn open(blocks: &'a [Block], key: Key) -> Result<Self, ErrorKind> {
		open(blocks, key)
	}

	/// Returns the key used to open the PAKS archive.
	#[inline]
	pub fn key(&self) -> &Key {
		&self.key
	}

	/// Finds a descriptor by its path.
	pub fn find_desc(&self, path: &[u8]) -> Option<Descriptor> {
		let encrypted_dir = get_directory(self.blocks, &self.directory)?;
		dir::find_encrypted(encrypted_dir, path, &self.directory, &self.key)
	}

	/// Finds a file descriptor by its path.
	#[inline]
	pub fn find_file(&self, path: &[u8]) -> Option<Descriptor> {
		match self.find_desc(path) {
			Some(desc) if desc.is_file() => Some(desc),
			_ => None
		}
	}
}

impl<'a> BundleReader<'a> {
	/// Reads the contents of a file from the PAKS archive.
	pub fn read(&self, path: &[u8], key: &Key) -> Result<Vec<u8>, ErrorKind> {
		let desc = match self.find_file(path) {
			Some(desc) => desc,
			None => return Err(ErrorKind::NotFound),
		};

		self.read_data(&desc, key)
	}

	/// Reads the contents of a file from the PAKS archive into a string.
	pub fn read_to_string(&self, path: &[u8], key: &Key) -> Result<String, ErrorKind> {
		let desc = match self.find_file(path) {
			Some(desc) => desc,
			None => return Err(ErrorKind::NotFound),
		};

		let data = self.read_data(&desc, key)?;
		String::from_utf8(data).map_err(|_| ErrorKind::InvalidData)
	}

	/// Decrypts and authenticates the section.
	///
	/// The key is not required to be the same as used to open the PAKS file.
	///
	/// # Errors
	///
	/// * [`ErrorKind::InvalidInput`]: The the descriptor is not a file descriptor.
	/// * [`ErrorKind::InvalidData`]: The file's MAC is incorrect, the file is corrupted.
	#[inline]
	pub fn read_section(&self, section: &Section, key: &Key) -> Result<Vec<Block>, ErrorKind> {
		read_section(&self.blocks, section, key)
	}

	/// Decrypts the contents of the given file descriptor.
	///
	/// See [`read_section`](Self::read_section) for more information.
	#[inline]
	pub fn read_data(&self, desc: &Descriptor, key: &Key) -> Result<Vec<u8>, ErrorKind> {
		read_data(&self.blocks, desc, key)
	}

	/// Decrypts the contents of the given file descriptor into the dest buffer.
	///
	/// See [`read_section`](Self::read_section) for more information.
	#[inline]
	pub fn read_data_into(&self, desc: &Descriptor, key: &Key, byte_offset: usize, dest: &mut [u8]) -> Result<(), ErrorKind> {
		read_data_into(&self.blocks, desc, key, byte_offset, dest)
	}
}

#[inline]
fn get_directory<'a>(blocks: &'a [Block], directory: &Section) -> Option<&'a [Descriptor]> {
	let start = directory.offset as usize;
	let len = directory.size as usize;
	let size = len.checked_mul(Descriptor::BLOCKS_LEN)?;
	let end = start.checked_add(size)?;
	let blocks = blocks.get(start..end)?;
	let descriptors = unsafe {
		slice::from_raw_parts(blocks.as_ptr() as *const Descriptor, len)
	};
	Some(descriptors)
}

fn open<'a>(blocks: &'a [Block], key: Key) -> Result<BundleReader<'a>, ErrorKind> {
	if blocks.len() < Header::BLOCKS_LEN {
		return Err(ErrorKind::InvalidInput);
	}

	let header_blocks: [Block; Header::BLOCKS_LEN] = blocks[..Header::BLOCKS_LEN].try_into().unwrap();
	let mut header = Header::from(header_blocks);
	if !crypt::decrypt_header(&mut header, &key) {
		return Err(ErrorKind::InvalidData);
	}

	let encrypted_dir = match get_directory(blocks, &header.info.directory) {
		Some(encrypted_dir) => encrypted_dir,
		None => return Err(ErrorKind::InvalidInput),
	};
	let encrypted_dir_blocks = unsafe {
		slice::from_raw_parts(encrypted_dir.as_ptr() as *const Block, encrypted_dir.len() * Descriptor::BLOCKS_LEN)
	};
	if !crypt::verify_section(encrypted_dir_blocks, &header.info.directory, &key) {
		return Err(ErrorKind::InvalidData);
	}

	Ok(BundleReader {
		blocks,
		directory: header.info.directory,
		key,
	})
}
