use crate::reskit::utility;
use std::{error::Error, ops::RangeInclusive};
use std::fs;
use std::fs::File;
use std::io::Write;
use image::{ GenericImageView, DynamicImage };

/**
 * Get a particular RGB component adjusted for the Mega Drive VDP colour ramp.
 * See colour ramp at https://plutiedev.com/vdp-color-ramp
 */
fn rgb_component_to_ramp( component: u16 ) -> Result<u16, Box<dyn Error>> {
	let ramp: [(RangeInclusive<u16>, (u16,u16)); 7] = [
		(0..=52,    (0x0, 0x2)),
		(52..=87,   (0x2, 0x4)),
		(87..=116,  (0x4, 0x6)),
		(116..=144, (0x6, 0x8)),
		(144..=172,	(0x8, 0xA)),
		(172..=206, (0xA, 0xC)),
		(206..=255, (0xC, 0xE))
	];

	for i in 0..ramp.len() {
		let ( ramp_range, ( cram_round_down, cram_round_up ) ) = &ramp[ i ];
		if ramp_range.contains( &component ) {
			let range_midpoint = ramp_range.start() + ( ( ramp_range.end() - ramp_range.start() ) / 2 );
			return if component >= range_midpoint {
				Ok( *cram_round_up )
			} else {
				Ok( *cram_round_down )
			}
		}
	}

	Err( "rgb component not in range 0-255" )?
}

pub fn color_to_palette( r: u16, g: u16, b: u16, palette: &mut [u16; 16] ) -> Result<u32, Box<dyn Error>> {
	let final_val =
		( rgb_component_to_ramp( b )? << 8 ) |
		( rgb_component_to_ramp( g )? << 4 ) |
		( rgb_component_to_ramp( r )? );

	// Does the color already exist?
	for i in 0..palette.len() {
		if palette[ i ] == final_val {
			return Ok( i as u32 );
		}
	}

	// Place the colour in the next open slot
	for i in 1..palette.len() {
		if palette[ i ] == 0 {
			palette[ i ] = final_val;
			return Ok( i as u32 );
		}
	}

	utility::print_error( "attempted to insert greater than 15 colours in a palette" );
	Err( "no more room in this palette!" )?
}

fn get_pixel( image: &DynamicImage, palette: &mut [u16; 16], x: u32, y: u32 ) -> Result<u32, Box<dyn Error>> {
	let ( max_x, max_y ) = image.dimensions();

	if x >= max_x || y >= max_y {
		return Ok( 0 );
	}

	let pixel = image.get_pixel( x, y );
	color_to_palette( pixel[ 0 ].into(), pixel[ 1 ].into(), pixel[ 2 ].into(), palette )
}

pub fn output_bin( output_filename: &str, palette: [u16; 16], body: Vec<u8> ) -> Result<(), Box<dyn Error>> {
	let mut output_palette: Vec< u8 > = Vec::new();
	for i in 0..palette.len() {
		let bytes = palette[ i ].to_be_bytes();
		for i in 0..2 {
			output_palette.push( bytes[ i ] );
		}
	}

	let output_try = File::create( output_filename );
	if let Ok( mut output_file ) = output_try {
		output_file.write( &output_palette ).unwrap();
		output_file.write( &body ).unwrap();

		Ok( () )
	} else {
		return Err( format!( "could not open filename for output {}", output_filename ).as_str() )?;
	}
}

pub fn output_inc( output_filename: &str, palette: [u16; 16], body: Vec<u8> ) -> Result<(), Box<dyn Error>> {
	let mut output_palette: Vec< u8 > = Vec::new();
	for i in 0..palette.len() {
		let bytes = palette[ i ].to_be_bytes();
		for i in 0..2 {
			output_palette.push( bytes[ i ] );
		}
	}

	let mut output_c: String = String::new();
	let mut output_h: String = String::new();

	// Set output_h based on image_filename
	output_h += "#pragma once\n\n";
	output_h += &format!( "extern const unsigned char {}[];\n", output_filename );
	output_h += &format!( "extern const unsigned int {}_len;\n", output_filename );

	let mut row_counter = 12;
	// Spray palette
	output_c += &format!( "const unsigned char {}[] = {{\n", output_filename );
	for i in 0..32 {
		if row_counter == 0 {
			row_counter = 12;
			output_c += "\n";
		} else {
			row_counter = row_counter - 1;
		}

		output_c += &format!( "0x{:X},", output_palette[ i ] );
	}

	for i in 0..body.len() {
		if row_counter == 0 {
			row_counter = 12;
			output_c += "\n";
		} else {
			row_counter = row_counter - 1;
		}

		output_c += &format!( "0x{:X}", body[ i ] );
		if i != ( body.len() - 1 ) {
			output_c += ",";
		}
	}

	output_c += "\n};\n";
	output_c += &format!( "const unsigned int {}_len = {}\n", output_filename, output_palette.len() + body.len() );

	fs::write( output_filename.to_string() + ".h", output_h ).expect( "Could not write header file" );
	fs::write( output_filename.to_string() + ".c", output_c ).expect( "Could not write source file" );

	Ok( () )
}

pub fn image_to_tiles( img: &DynamicImage, palette: &mut [u16; 16], tile_order: &str ) -> Result<Vec<u8>, Box<dyn Error>> {
	let ( mut max_x, mut max_y ) = img.dimensions();
	if max_x % 8 != 0 { max_x = ( 8 * ( max_x / 8 ) ) + ( 8 - ( max_x % 8 ) ); }
	if max_y % 8 != 0 {	max_y = ( 8 * ( max_y / 8 ) ) + ( 8 - ( max_y % 8 ) ); }

	let mut body: Vec<u8> = Vec::new();

	if tile_order == "sprite" {
		/*
		 * Sprite order:
		 * 1 3
		 * 2 4
		 */
		 for x in ( 0..max_x ).step_by( 8 ) {
			for y in ( 0..max_y ).step_by( 8 ) {
				for cell_y in 0..8 {
					let mut series: u32 = 0;

					for cell_x in 0..8 {
						let nibble: u32 = get_pixel( &img, palette, cell_x + x, cell_y + y )? << ( ( 7 - cell_x ) * 4 );
						series = series | nibble;
					}

					let bytes = series.to_be_bytes();
					for i in 0..4 {
						body.push( bytes[ i ] );
					}
				}
			}
		}
	} else {
		/*
		 * Tile order:
		 * 1 2
		 * 3 4
		 */
		for y in ( 0..max_y ).step_by( 8 ) {
			for x in ( 0..max_x ).step_by( 8 ) {
				for cell_y in 0..8 {
					let mut series: u32 = 0;

					for cell_x in 0..8 {
						let nibble: u32 = get_pixel( &img, palette, cell_x + x, cell_y + y )? << ( ( 7 - cell_x ) * 4 );
						series = series | nibble;
					}

					let bytes = series.to_be_bytes();
					for i in 0..4 {
						body.push( bytes[ i ] );
					}
				}
			}
		}
	}

	Ok( body )
}

pub fn generate( image_filename: &str, output_filename: &str, output_mode: &str, tile_order: &str ) -> Result<(), Box<dyn Error>> {
	let img = image::open( image_filename );
	if let Ok( img ) = img {
		let mut palette: [u16; 16] = [ 0; 16 ];
		let body = image_to_tiles( &img, &mut palette, tile_order )?;

		if output_mode == "bin" {
			if let Err( err ) = output_bin( output_filename, palette, body ) {
				utility::print_error( &format!( "{}", err ) );
			}

			utility::print_good( format!( "converted file {}", image_filename ).as_str() );
		} else if output_mode == "inc" {
			if let Err( err ) = output_inc( output_filename, palette, body ) {
				utility::print_error( &format!( "{}", err ) );
			}

			utility::print_good( format!( "converted file {}", image_filename ).as_str() );
		} else {
			utility::print_error( format!( "invalid output mode {}", output_mode ).as_str() );
		}
	} else {
		utility::print_error( format!( "could not open filename {}", image_filename ).as_str() );
	}

	Ok( () )
}