/*
* ASE file reader
* Author: MakhamDev
* File spec from: https://github.com/aseprite/aseprite/blob/main/docs/ase-file-specs.md
*/

enum _BIN_TYPE {
	byte,
	word,
	short,
	dword,
	long,
	fixed,
	float,
	double,
	qword,
	long64,
	
	string,
	point,
	size,
	rect,
	color,
	pixel,
}

//ASE blend mode
//Normal         = 0
//Multiply       = 1
//Screen         = 2
//Overlay        = 3
//Darken         = 4
//Lighten        = 5
//Color Dodge    = 6
//Color Burn     = 7
//Hard Light     = 8
//Soft Light     = 9
//Difference     = 10
//Exclusion      = 11
//Hue            = 12
//Saturation     = 13
//Color          = 14
//Luminosity     = 15
//Addition       = 16
//Subtract       = 17
//Divide         = 18

globalvar __ase_format_header; __ase_format_header = [
	[_BIN_TYPE.dword,	"File size"],
	[_BIN_TYPE.word,	"Magic number"],
	[_BIN_TYPE.word,	"Frame amount"],
	[_BIN_TYPE.word,	"Width"],
	[_BIN_TYPE.word,	"Height"],
	[_BIN_TYPE.word,	"Color depth"], //32: RGBA, 16: Grey, 8: Index
	[_BIN_TYPE.dword,	"Flags"], 
	[_BIN_TYPE.word,	"Speed"], //DEPRECATED
	[_BIN_TYPE.dword,	"0"], 
	[_BIN_TYPE.dword,	"0"], 
	[_BIN_TYPE.byte,	"Palette entry"], //For indexed sprite, index in palette that consider a transparent color.
	[_BIN_TYPE.byte,	"Ignore", 3],
	[_BIN_TYPE.word,	"Number of colors"],
	[_BIN_TYPE.byte,	"Pixel width"],  //If zero, then pixel ratio is 1:1
	[_BIN_TYPE.byte,	"Pixel height"], //If zero, then pixel ratio is 1:1
	[_BIN_TYPE.short,	"Grid X"],
	[_BIN_TYPE.short,	"Grid Y"],
	[_BIN_TYPE.word,	"Grid width"], //If zero, no grid
	[_BIN_TYPE.word,	"Grid height"], //If zero, no grid
	[_BIN_TYPE.byte,	"Unused", 84],
];

globalvar __ase_format_frame; __ase_format_frame = [
	[_BIN_TYPE.dword,	"Length"],
	[_BIN_TYPE.word,	"Magic number"],
	[_BIN_TYPE.word,	"Chunk amount"], //If 0xFFFF, use "Chunk amount new"
	[_BIN_TYPE.word,	"Duration"], //In millisec
	[_BIN_TYPE.byte,	"Unused", 2],
	[_BIN_TYPE.dword,	"Chunk amount new"],
];

globalvar __ase_format_chunk; __ase_format_chunk = [
	[_BIN_TYPE.dword,	"Length"],
	[_BIN_TYPE.word,	"Type"],
];

globalvar __ase_format_chunk_old_palette; __ase_format_chunk_old_palette = [
	[_BIN_TYPE.word,	"Packet amount"],
];

globalvar __ase_format_chunk_old_palette_packet; __ase_format_chunk_old_palette_packet = [
	[_BIN_TYPE.byte,	"Entries skip index"],
	[_BIN_TYPE.byte,	"Color amount"],
	[_BIN_TYPE.color,	"Colors", "Color amount"],
];

globalvar __ase_format_chunk_layer; __ase_format_chunk_layer = [
	[_BIN_TYPE.word,	"Flag"], //1: Visible, 2: Editable, 4:Lock, 8:BG
	[_BIN_TYPE.word,	"Layer type"], //0: Normal, 1: Group, 2: Tilemap
	[_BIN_TYPE.word,	"Child level"],
	[_BIN_TYPE.word,	"Ignore"],
	[_BIN_TYPE.word,	"Ignore"],
	[_BIN_TYPE.word,	"Blend mode"],
	[_BIN_TYPE.byte,	"Opacity"],
	[_BIN_TYPE.byte,	"Unused", 3],
	[_BIN_TYPE.string,	"Name"],
	[_BIN_TYPE.dword,	"Tileset index", 1, function(chunk) { return chunk[$ "Layer type"] == 2; }],
];

globalvar __ase_format_chunk_cel; __ase_format_chunk_cel = [
	[_BIN_TYPE.word,	"Layer index"],
	[_BIN_TYPE.short,	"X"],
	[_BIN_TYPE.short,	"Y"],
	[_BIN_TYPE.byte,	"Opacity"],
	[_BIN_TYPE.word,	"Cel type"], //0: Raw image, 1: Linked, 2: Compressed image, 3: Compressed tilemap
	[_BIN_TYPE.byte,	"Unused", 7],
];

globalvar __ase_format_chunk_cel_raw_image; __ase_format_chunk_cel_raw_image = [
	[_BIN_TYPE.word,	"Width"],
	[_BIN_TYPE.word,	"Height"],
	[_BIN_TYPE.pixel,	"Pixels", function(chunk) { return chunk[$ "Width"] * chunk[$ "Width"]; }],
];

globalvar __ase_format_chunk_cel_linked; __ase_format_chunk_cel_linked = [
	[_BIN_TYPE.word,	"Frame position"],
];

globalvar __ase_format_chunk_cel_compress_image; __ase_format_chunk_cel_compress_image = [
	[_BIN_TYPE.word,	"Width"],
	[_BIN_TYPE.word,	"Height"],
	//[_BIN_TYPE.long,	"Raw cel", function(chunk) { return chunk[$ "Width"] * chunk[$ "Width"]; }],
];

globalvar __ase_format_chunk_cel_compress_tilemap; __ase_format_chunk_cel_compress_tilemap = [
	[_BIN_TYPE.word,	"Width"],
	[_BIN_TYPE.word,	"Height"],
	[_BIN_TYPE.word,	"Bits per tile"],
	[_BIN_TYPE.dword,	"Bitmask for tile ID"],
	[_BIN_TYPE.dword,	"X flip"],
	[_BIN_TYPE.dword,	"Y flip"],
	[_BIN_TYPE.dword,	"90CW rotation"],
	[_BIN_TYPE.byte,	"Unused", 10],
	//[_BIN_TYPE.tile,	"Tiles", function(chunk) { return chunk[$ "Width"] * chunk[$ "Width"]; }],
];

globalvar __ase_format_chunk_cel_extra; __ase_format_chunk_cel_extra = [
	[_BIN_TYPE.dword,	"Flag"],
	[_BIN_TYPE.fixed,	"X"],
	[_BIN_TYPE.fixed,	"Y"],
	[_BIN_TYPE.fixed,	"Width"],
	[_BIN_TYPE.fixed,	"Height"],
	[_BIN_TYPE.byte,	"Unused", 16],
];

globalvar __ase_format_chunk_color_profile; __ase_format_chunk_color_profile = [
	[_BIN_TYPE.word,	"Type"], //0: no profile, 1: sRGB, 2: ICC
	[_BIN_TYPE.word,	"Flag"], //1: Fix gamma
	[_BIN_TYPE.fixed,	"Fixed gamma"],
	[_BIN_TYPE.byte,	"Unused", 8],
	[_BIN_TYPE.dword,	"ICC Data length", 1, function(chunk) { return chunk[$ "Type"] == 2; }],
	[_BIN_TYPE.byte,	"ICC Data", "ICC Data length", function(chunk) { return chunk[$ "Type"] == 2; }],
];

globalvar __ase_format_chunk_file; __ase_format_chunk_file = [
	[_BIN_TYPE.dword,	"Entries"],
	[_BIN_TYPE.byte,	"Unused", 8],
];

globalvar __ase_format_chunk_file_entry; __ase_format_chunk_file_entry = [
	[_BIN_TYPE.dword,	"ID"],
	[_BIN_TYPE.byte,	"File type"], //0: External palette, 1: External tileset, 2: Extension anme
	[_BIN_TYPE.byte,	"Unused", 7],
	[_BIN_TYPE.string,	"File name"],
];

globalvar __ase_format_chunk_tag; __ase_format_chunk_tag = [
	[_BIN_TYPE.word,	"Tag amount"],
	[_BIN_TYPE.byte,	"Unused", 8],
];

globalvar __ase_format_chunk_tag_entry; __ase_format_chunk_tag_entry = [
	[_BIN_TYPE.word,	"Frame start"],
	[_BIN_TYPE.word,	"Frame end"],
	[_BIN_TYPE.byte,	"Loop"], //0: Forward, 1: Backward, 2: Ping pong, 3: Ping pong reverse
	[_BIN_TYPE.word,	"Repeat amount"], //0: Infinite, N: N-times
	[_BIN_TYPE.byte,	"Unused", 6],
	[_BIN_TYPE.color,	"Color"],
	[_BIN_TYPE.byte,	"Extra"],
	[_BIN_TYPE.string,	"Name"],
]

globalvar __ase_format_chunk_palette; __ase_format_chunk_palette = [
	[_BIN_TYPE.dword,	"Color amount"],
	[_BIN_TYPE.dword,	"First index"],
	[_BIN_TYPE.dword,	"Last index"],
	[_BIN_TYPE.byte,	"Unused", 8],
];

globalvar __ase_format_chunk_palette_entry; __ase_format_chunk_palette_entry = [
	[_BIN_TYPE.word,	"Flag"], //1: Has name
	[_BIN_TYPE.byte,	"Red"],
	[_BIN_TYPE.byte,	"Green"],
	[_BIN_TYPE.byte,	"Blue"],
	[_BIN_TYPE.byte,	"Alpha"],
	[_BIN_TYPE.string,	"Name", 1, function(chunk) { return chunk[$ "Flag"] & (1 << 0); }],
];

globalvar __ase_format_chunk_user_data; __ase_format_chunk_user_data = [
	[_BIN_TYPE.dword,	"Flag"], //1: Text, 2: Color, 4: Properties
	[_BIN_TYPE.string,	"Name", 1,  function(chunk) { return chunk[$ "Flag"] & (1 << 0); }],
	[_BIN_TYPE.byte,	"Red", 1,   function(chunk) { return chunk[$ "Flag"] & (1 << 1); }],
	[_BIN_TYPE.byte,	"Green", 1, function(chunk) { return chunk[$ "Flag"] & (1 << 1); }],
	[_BIN_TYPE.byte,	"Blue", 1,  function(chunk) { return chunk[$ "Flag"] & (1 << 1); }],
	[_BIN_TYPE.byte,	"Alpha", 1, function(chunk) { return chunk[$ "Flag"] & (1 << 1); }],
];

globalvar __ase_format_chunk_user_data_prop; __ase_format_chunk_user_data_prop = [
	[_BIN_TYPE.dword,	"Length"],
	[_BIN_TYPE.dword,	"Prop amount"],
]

/* TODO: Use data read */

globalvar __ase_format_chunk_slice; __ase_format_chunk_slice = [
	[_BIN_TYPE.dword,	"Slice key amount"],
	[_BIN_TYPE.dword,	"Flag"], //1: 9 slice, 2: pivot
	[_BIN_TYPE.dword,	"Reserved"], 
	[_BIN_TYPE.string,	"Name"], 
];

globalvar __ase_format_chunk_slice_key; __ase_format_chunk_slice_key = [
	[_BIN_TYPE.dword,	"Frame number"],
	[_BIN_TYPE.long,	"X"],
	[_BIN_TYPE.long,	"Y"],
	[_BIN_TYPE.dword,	"Width"],
	[_BIN_TYPE.dword,	"Height"],
];

globalvar __ase_format_chunk_slice_nine; __ase_format_chunk_slice_nine = [
	[_BIN_TYPE.long,	"Center X"],
	[_BIN_TYPE.long,	"Center Y"],
	[_BIN_TYPE.dword,	"Center width"],
	[_BIN_TYPE.dword,	"Center height"],
];

globalvar __ase_format_chunk_slice_pivot; __ase_format_chunk_slice_pivot = [
	[_BIN_TYPE.long,	"Pivot X"],
	[_BIN_TYPE.long,	"Pivot Y"],
];

globalvar __ase_format_chunk_tileset; __ase_format_chunk_tileset = [
	[_BIN_TYPE.dword,	"ID"],
	[_BIN_TYPE.dword,	"Flag"], //1: Link to external file, 2: Include tile in this file, 4: Use ID 0 as empty tiles.
	[_BIN_TYPE.dword,	"Tile amount"],
	[_BIN_TYPE.word,	"Tile width"],
	[_BIN_TYPE.word,	"Tile height"],
	[_BIN_TYPE.short,	"Base index"],
	[_BIN_TYPE.byte,	"Reserved", 14],
	[_BIN_TYPE.string,	"Name"],
	[_BIN_TYPE.dword,	"ID of external file", 1, function(chunk) { return chunk[$ "Flag"] & (1 << 1); }],
	[_BIN_TYPE.dword,	"Tileset ID",		   1, function(chunk) { return chunk[$ "Flag"] & (1 << 1); }],
	[_BIN_TYPE.dword,	"Data length",		   1, function(chunk) { return chunk[$ "Flag"] & (1 << 2); }],
	[_BIN_TYPE.pixel,	"Compressed image", "Data length", function(chunk) { return chunk[$ "Flag"] & (1 << 2); }],
];

function read_format_type(_bin, datType, outMap) {
	switch(datType) {
		case _BIN_TYPE.byte:	return bin_read_byte(_bin);
		case _BIN_TYPE.word:	return bin_read_word(_bin);
		case _BIN_TYPE.short:	return bin_read_short(_bin);
		case _BIN_TYPE.dword:	return bin_read_dword(_bin);
		case _BIN_TYPE.long:	return bin_read_long(_bin);
		case _BIN_TYPE.fixed:	return bin_read_fixed(_bin);
		case _BIN_TYPE.float:	return bin_read_float(_bin);
		case _BIN_TYPE.double:	return bin_read_double(_bin);
		case _BIN_TYPE.qword:	return bin_read_qword(_bin);
		case _BIN_TYPE.long64:	return bin_read_long64(_bin);
		
		case _BIN_TYPE.string:	return bin_read_string(_bin);
		case _BIN_TYPE.point:	return bin_read_point(_bin);
		case _BIN_TYPE.size:	return bin_read_size(_bin);
		case _BIN_TYPE.rect:	return bin_read_rect(_bin);
		case _BIN_TYPE.color:	return bin_read_color(_bin);
		case _BIN_TYPE.pixel:	return bin_read_pixel(_bin, outMap[$ "Color depth"]);
	}
	
	return 0;
}

function read_format(_bin, format, outMap) {
	var datType = array_safe_get_fast(format, 0, 0);
	var key     = array_safe_get_fast(format, 1, "");
	var amount  = array_safe_get_fast(format, 2, 1);
	
	     if(is_string(amount)) amount = struct_has(outMap, amount)? outMap[$ amount] : 1;
	else if(is_method(amount)) amount = amount(outMap);
		
	if(amount == 1) {
		var val = read_format_type(_bin, datType, outMap);
		outMap[$ key] = val;
		return val;
		
	} else {
		var a = array_create(amount);
		for( var i = 0; i < amount; i++ )
			a[i] = read_format_type(_bin, datType, outMap);
		outMap[$ key] = a;
		return a;
	}
}

function read_format_array(_bin, formatArr, outMap) {
	for( var i = 0, n = array_length(formatArr); i < n; i++ ) {
		var _form = formatArr[i];
		if(array_length(_form) >= 4 && !_form[3](outMap)) 
			continue;
			
		var pos = file_bin_position(_bin);
		var val = read_format(_bin, _form, outMap);
		//printIf(global.FLAG.ase_import, "Pos " + dec_to_hex(pos) + " - " + dec_to_hex(file_bin_position(_bin)));
		
		if(_form[1] == "Type")	printIf(global.FLAG.ase_import, $"\t{_form[1]}:\t 0x{dec_to_hex(val, 4)}");
		else					printIf(global.FLAG.ase_import, $"\t{_form[1]}:\t {string(val)}");
	}
}

function read_ase(path) { //// MAIN READ
	printIf(global.FLAG.ase_import, $"===== Reading: {path} =====");
	
	var file = file_bin_open(path, 0);
	if(file < 0) { noti_warning($"ASE file read error."); return noone; }
	
	file_bin_seek(file, 0);
	
	fileMap = {};
	read_format_array(file, __ase_format_header, fileMap);
	
	var frames   = [];
	var frameAmo = struct_try_get(fileMap, "Frame amount", 0);
	
	for( var i = 0; i < frameAmo; i++ ) {
		printIf(global.FLAG.ase_import, $"\n=== Reading frame {i} ===");
		array_push(frames, read_ase_frame(file));
	}
	
	fileMap[$ "Frames"] = frames; 
	file_bin_close(file);
	
	return fileMap;
}

function read_ase_frame(file) {
	var frame = {};
	
	read_format_array(file, __ase_format_frame, frame);
	
	var chunks   = [];
	var chunkAmo = struct_try_get(frame, "Chunk amount", 0);
	
	if(chunkAmo == 0xFFFF)
		chunkAmo = struct_try_get(frame, "Chunk amount new", chunkAmo);
	
	for( var i = 0; i < chunkAmo; i++ ) {
		printIf(global.FLAG.ase_import, $"\n=== Reading chunk {i} ===");
		array_push(chunks, read_ase_chunk(file));
	}
	
	frame[$ "Chunks"] = chunks; 
	
	return frame;
}

function read_ase_chunk(file) {
	var chunk    = {};
	var startPos = file_bin_position(file);
	
	read_format_array(file, __ase_format_chunk, chunk);
	
	var skipPos = startPos + chunk[$ "Length"];
	
	switch(chunk[$ "Type"]) {
		case 0x0004: //old palette
		case 0x0011: //old palette
			printIf(global.FLAG.ase_import, "\n -- Reading chunk [Old palette] -- "); 
			read_format_array(file, __ase_format_chunk_old_palette, chunk);
			var cc = [];
			for( var i = 0; i < chunk[$ "Packet amount"]; i++ ) {
				cc[i] = {};
				read_format_array(file, __ase_format_chunk_old_palette_packet, cc[i]);
			}
			chunk[$ "Packets"] = cc;
			break;
			
		case 0x2004: //layer
			printIf(global.FLAG.ase_import, "\n -- Reading chunk [Layer] -- "); 
			read_format_array(file, __ase_format_chunk_layer, chunk);
			break;
			
		case 0x2005: //cel
			printIf(global.FLAG.ase_import, "\n -- Reading chunk [Cel] -- "); 
			read_format_array(file, __ase_format_chunk_cel, chunk);
			
			var type = chunk[$ "Cel type"];
			switch(type) {
				case 0 :  read_format_array(file, __ase_format_chunk_cel_raw_image, chunk); break;
				case 1 :  read_format_array(file, __ase_format_chunk_cel_linked,    chunk); break;
				
				case 2 : 
					read_format_array(file, __ase_format_chunk_cel_compress_image, chunk);
					chunk[$ "Surface"] = noone;
					
					var compressLength = (skipPos - file_bin_position(file));
					var _compBuff = buffer_create(compressLength * buffer_sizeof(buffer_u8), buffer_grow, 1);
					buffer_seek(_compBuff, buffer_seek_start, 0);
					
					repeat(compressLength) {
						var byte = file_bin_read_byte(file);
						buffer_write(_compBuff, buffer_u8, byte);
					}
					
					var _rawBuff = buffer_decompress(_compBuff);
					if(_rawBuff != -1) chunk[$ "Buffer"] = _rawBuff;
					printIf(global.FLAG.ase_import, $"    Buffer size: {compressLength}");
					buffer_delete(_compBuff);
					break;
					
				case 3 : 
					read_format_array(file, __ase_format_chunk_cel_compress_tilemap, chunk);
					//TILE READ
					break;
			}
			break;
			
		case 0x2006: break; //cel extra
			
		case 0x2007: //color profile
			printIf(global.FLAG.ase_import, "\n -- Reading chunk [Color profile] -- "); 
			read_format_array(file, __ase_format_chunk_color_profile, chunk);
			break;
			
		case 0x2008: break; //external file
		case 0x2009: break; //mask DEPRECATED
		case 0x2017: break; //path
			
		case 0x2018: //tag
			printIf(global.FLAG.ase_import, "\n -- Reading chunk [Tag] -- "); 
			read_format_array(file, __ase_format_chunk_tag, chunk);
			var amo = chunk[$ "Tag amount"]
			var tags = [];
			repeat(amo) {
				var m = {};	
				read_format_array(file, __ase_format_chunk_tag_entry, m);
				array_push(tags, m);
			}
			chunk[$ "Tags"] = tags;
			break;
			
		case 0x2019: //palette
			printIf(global.FLAG.ase_import, "\n -- Reading chunk [Palette] -- "); 
			read_format_array(file, __ase_format_chunk_palette, chunk);
			var cc = [];
			for( var i = 0; i < chunk[$ "Color amount"]; i++ ) {
				cc[i] = {};
				read_format_array(file, __ase_format_chunk_palette_entry, cc[i]);
			}
			chunk[$ "Palette"] = cc;
			break;
			
		case 0x2020: break; //user data
		case 0x2022: break; //slice
		case 0x2023: break; //tileset
	}
	
	file_bin_seek(file, skipPos - file_bin_position(file));
	
	return chunk;
}