/* ************************************************************************** */
/*                                                                            */
/*                                                        ::::::::            */
/*   mlx_xpm42.c                                        :+:    :+:            */
/*                                                     +:+                    */
/*   By: W2Wizard <main@w2wizard.dev>                 +#+                     */
/*                                                   +#+                      */
/*   Created: 2021/12/28 03:42:29 by W2Wizard      #+#    #+#                 */
/*   Updated: 2022/06/27 19:58:33 by lde-la-h      ########   odam.nl         */
/*                                                                            */
/* ************************************************************************** */

#include "MLX42/MLX42_Int.h"

/**
 * XPM is an obscure image format which can't seem to make up its mind
 * whether it wants to be written in C code or not.
 * 
 * https://en.wikipedia.org/wiki/X_PixMap
 * 
 * This might anger some but instead I decided to write my own
 * image format, very similar to XPM2, which seems to be the better
 * option between the 3 versions. The only difference is in the
 * header which carries the file type, width, height, color count
 * and finally color type aka 'c' for RGBA8 or 'm' for monochrome
 * output.
 * 
 * The changes, in my opinion, very much simplify the XPM format
 * into something literally anybody can use without much guessing
 * as to what does what.
 * 
 * Additionally with the C style format, the idea is that you simply include
 * it directly into the compilation of the program (since it's just C).
 * 
 * As convenient as this is, I just find it hideous especially the XPM3 variant.
 * By sticking to the XPM style format, conversion should be very easy and 
 * straightforward to this format however.
 */

//= Private =//

/**
 * Parses HEX color channel e.g: "0F"
 * 
 * @param channel The 2 character string to parse.
 * @return Int value of the channel.
 */
static uint8_t mlx_parse_hex_channel(char* channel) 
{
	char temp_chan[] = {channel[0], channel[1], '\0'};
	return (strtol(temp_chan, NULL, 16));
}

/**
 * Parses the XPM color value entry e.g: ".X #00FF00FF"
 * into the color table while also verifying the format.
 * 
 * @param xpm The XPM.
 * @param line The line to parse.
 * @param ctable The color hash table.
 * @param s Size of the hash table
 * @return True or false depending on if it successfully parsed the line.
 */
static bool mlx_insert_xpm_entry(xpm_t* xpm, char* line, uint32_t* ctable, size_t s)
{
	// NOTE: uintptr because windows likes to complain...
	// Verify the length of the Pixel string by checking backwards for the first
	// occurrence of a space and then check the distance by comparing with cpp.
	if (((uintptr_t)strrchr(line, ' ') - (uintptr_t)line) != (uint64_t)xpm->cpp)
		return (false);
	if (!isspace(line[xpm->cpp]) || line[xpm->cpp + 1] != '#' || !isalnum(line[xpm->cpp + 2]))
		return (false);

	uint32_t color = 0;
	size_t start_offset = xpm->cpp + 2;
	color |= mlx_parse_hex_channel(line + start_offset) << 24;
	color |= mlx_parse_hex_channel(line + start_offset + 2) << 16;
	color |= mlx_parse_hex_channel(line + start_offset + 4) << 8;
	color |= mlx_parse_hex_channel(line + start_offset + 6);
	
	int32_t index = mlx_fnv_hash(line, xpm->cpp) % s;
	ctable[index] = xpm->mode == 'm' ? mlx_rgba_to_mono(color) : color;
	return (true);
}

/**
 * Retrieves the pixel data line by line and then processes each pixel
 * by hashing the characters and looking it up from the color table.
 * 
 * @param xpm The XPM.
 * @param file The filepath to the XPM42 file.
 * @param ctable The color hash table.
 * @param s Size of the hash table.
 * @return True or false depending on if it successfully parsed the line.
 */
static bool mlx_read_data(xpm_t* xpm, FILE* file, uint32_t* ctable, size_t s)
{
	size_t line_len;
	char* line = NULL;

	for (int64_t y_xpm = 0; y_xpm < xpm->texture.height; y_xpm++)
	{
		if (!mlx_getline(&line, &line_len, file))
			return (free(line), false);
		if (line[line_len - 1] == '\n')
			line_len--;
		if (line_len != xpm->texture.width * xpm->cpp)
			return (free(line), false);

		// NOTE: Copy pixel by pixel as we need to retrieve the hash table.
		for (int64_t x_xpm = 0, x_line = 0; x_xpm < xpm->texture.width; x_xpm++, x_line += xpm->cpp)
		{
			uint8_t* pixelstart = &xpm->texture.pixels[(y_xpm * xpm->texture.width + x_xpm) * BPP];
			mlx_draw_pixel(pixelstart, ctable[mlx_fnv_hash(&line[x_line], xpm->cpp) % s]);
		}
	}
	free(line);
	return (true);
}

/**
 * For quick lookups we basically create a stack allocated lookup
 * table with every ascii character in it. This should help avoid a O(n)
 * case and give us a O(1) for very fast look ups.
 * 
 * Downside is we still need to iterate over each pixel to solve its color.
 * So I hope this makes it at least a bit faster.
 * 
 * TODO: This buffer might be way to big! Do actual collision checks, 
 * for now just straight up raw dog this.
 */
static bool mlx_read_table(xpm_t* xpm, FILE* file)
{
	char* line = NULL;
	size_t line_len;
	uint32_t ctable[UINT16_MAX] = {0};

	for (int32_t i = 0; i < xpm->color_count; i++)
	{
		if (!mlx_getline(&line, &line_len, file))
			return (free(line), false);
		if (!mlx_insert_xpm_entry(xpm, line, ctable, (sizeof(ctable) / BPP)))
			return (free(line), false);
	}
	free(line);
	return (mlx_read_data(xpm, file, ctable, (sizeof(ctable) / BPP)));
}

/**
 * Reads the XPM42 file header which usually consists of a
 * file type declaration of "!XPM42" followed by the next line
 * containing image information such as width, height, unique color
 * count and finally the color mode. Which is either c for Color or
 * m for Monochrome.
 */
static bool mlx_read_xpm_header(xpm_t* xpm, FILE *file)
{
	int32_t	flagc;
	char	buffer[64] = {0};

	// Check file type dec...
	if (!fgets(buffer, sizeof(buffer), file))
		return (false);
	if (strncmp(buffer, "!XPM42\n", sizeof(buffer)) != 0)
		return (false);

	// Get header info ...
	if (!fgets(buffer, sizeof(buffer), file))
		return (false);
	flagc = sscanf(buffer, "%i %i %i %i %c\n", &xpm->texture.width, &xpm->texture.height, &xpm->color_count, &xpm->cpp, &xpm->mode);
	if (flagc < 4 || xpm->texture.width > INT16_MAX || xpm->texture.height > INT16_MAX || \
		!(xpm->mode == 'c' || xpm->mode == 'm') || xpm->cpp > 10)
		return (false);
	xpm->texture.bytes_per_pixel = BPP;
	xpm->texture.pixels = calloc(xpm->texture.width * xpm->texture.height, sizeof(int32_t));
	return (xpm->texture.pixels != NULL ? mlx_read_table(xpm, file) : false);
}

//= Public =//

xpm_t* mlx_load_xpm42(const char* path)
{
	FILE* file;
	xpm_t* xpm = NULL;

	MLX_NONNULL(path);
	if (!strstr(path, ".xpm42"))
		return ((void*)mlx_error(MLX_INVEXT));
	if (!(file = fopen(path, "r")))
		return ((void*)mlx_error(MLX_INVFILE));
	if (!(xpm = calloc(1, sizeof(xpm_t))))
		return ((void*)mlx_error(MLX_MEMFAIL));
	if (!mlx_read_xpm_header(xpm, file))
	{
		mlx_freen(2, xpm->texture.pixels, xpm);
		mlx_error(MLX_INVXPM);
		xpm = NULL;
	}
	fclose(file);
	return (xpm);
}

void mlx_delete_xpm42(xpm_t* xpm)
{
	MLX_NONNULL(xpm);
	free(xpm->texture.pixels);
	free(xpm);
}
