/*
	Copyright (C) 2016-2026 Mark Tyler

	This program is free software; you can redistribute it and/or modify
	it under the terms of the GNU General Public License as published by
	the Free Software Foundation; either version 3 of the License, or
	(at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
	GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program in the file COPYING.
*/

#include "private.h"



static int find_closest_color (
	mtColor		const * const	pal,
	int			const	ncols,
	int			const	r,
	int			const	g,
	int			const	b,
	long long			& low
	)
{
	int idx = 0;

	low = LONG_MAX;

	for ( int i=0; i<ncols; i++ )
	{
		// Calculate color differences
		long long const dr = r - pal[i].red;
		long long const dg = g - pal[i].green;
		long long const db = b - pal[i].blue;

		// Weighted Euclidean Distance
		long long const gap =
			(3 * dr * dr) +
			(4 * dg * dg) +
			(2 * db * db);

		if ( gap < low )
		{
			low = gap;
			idx = i;
		}
	}

	return idx;
}


static void simple_dither (
	mtColor		const * const	pal,
	int			const	ncols,
	unsigned char	const * const	mem_src,
	unsigned char		* const	mem_dest,
	int			const	w,
	int			const	h,
	int			const	dither_percent
	)
{
	double const dpercent = mtkit_int_bound( dither_percent, 0, 100 )/100.0;
	unsigned char		* dest = mem_dest;
	unsigned char	const	* src = mem_src;

	for ( int j = 0; j < h; j++ )		// Convert RGB to indexed
	{
		for ( int i = 0; i < w; i++ )
		{
			int const r = *src++;
			int const g = *src++;
			int const b = *src++;

			// Find closest palette color (best_i)
			long long best_d;
			int const best_i = find_closest_color ( pal, ncols, r,
				g, b, best_d );

			int k;
			if ( best_d == 0 )
			{
				k = best_i;
			}
			else
			{
				// Compute reflected point for better averaging
				int const ref_r = 2 * r - (int)pal[best_i].red;
				int const ref_g = 2 * g - (int)pal[best_i].green;
				int const ref_b = 2 * b - (int)pal[best_i].blue;

				long long second_d;
				int const second_i = find_closest_color ( pal,
					ncols, ref_r, ref_g, ref_b, second_d );

				// Compute distance to average of best and second
				int avg_rd = r - ((int)pal[best_i].red +
					(int)pal[second_i].red) / 2;
				int avg_gd = g - ((int)pal[best_i].green +
					(int)pal[second_i].green) / 2;
				int avg_bd = b - ((int)pal[best_i].blue +
					(int)pal[second_i].blue) / 2;
				double const avg_d =
					3 * avg_rd * avg_rd +
					4 * avg_gd * avg_gd +
					2 * avg_bd * avg_bd;

				if ( avg_d >= (double)best_d * dpercent )
				{
					k = best_i;
					// No improvement from dithering; use
					// solid color
				}
				else
				{
					// Dither in checkerboard pattern
					int kt[2] = { best_i, second_i };
					if ( best_i > second_i )
					{
						kt[0] = second_i;
						kt[1] = best_i;
					}
					int const dither = (i + j) & 1;
					k = kt[dither];
				}
			}

			*dest++ = (unsigned char)k;
		}
	}
}

/*
	Atkinson dither including serpentine and % for amount of dither.
	MT 2026-03-02
*/

static void atkinson_dither (
	mtColor		const * const	pal,
	int			const	ncols,
	unsigned char	const * const	rgb_pixels,
	unsigned char		* const	out_indexed,
	int			const	width,
	int			const	height,
	int			const	dither_percent
	)
{
	double const dither = mtkit_int_bound ( dither_percent, PIXY_DITHER_MIN,
		PIXY_DITHER_MAX ) / 640.0;

	// Need enough space to spread error +2 pixels in either direction
	int const stride = width + 4;
	int * e_buf = (int *)calloc( size_t(stride * 3 * 3), sizeof(*e_buf) );
	if ( ! e_buf )
	{
		return;
	}

	// Pointers for current, next, and 2-rows-ahead
	int * r0 = &e_buf[0];
	int * r1 = &e_buf[size_t(stride * 3)];
	int * r2 = &e_buf[size_t(stride * 3 * 2)];

	auto distribute = [](
		int const x,
		int * const row,
		int const col_off,
		int const er,
		int const eg,
		int const eb )
	{
		// Added 2 to offset to handle potential negative col_off
		int const target = (x + 2 + col_off) * 3;
		row[target + 0] += er;
		row[target + 1] += eg;
		row[target + 2] += eb;
	};

	for ( int y = 0; y < height; y++ )
	{
		// Serpentine: even rows left-to-right, odd rows right-to-left
		bool const left_to_right = (y % 2 == 0);

		for (int x_idx = 0; x_idx < width; x_idx++)
		{
			// Calculate actual x based on scan direction
			int const x = left_to_right ? x_idx : (width - 1 - x_idx);

			int const px_idx = (y * width + x) * 3;
			// Buffer index adjusted for serpentine padding
			int const buf_idx = (x + 2) * 3;

			int const r = rgb_pixels[px_idx + 0] + r0[buf_idx + 0];
			int const g = rgb_pixels[px_idx + 1] + r0[buf_idx + 1];
			int const b = rgb_pixels[px_idx + 2] + r0[buf_idx + 2];

			int const cr = mtkit_int_bound (r, 0, 255);
			int const cg = mtkit_int_bound (g, 0, 255);
			int const cb = mtkit_int_bound (b, 0, 255);

			long long gap;
			int const pal_idx = find_closest_color ( pal, ncols, cr,
				cg, cb, gap );

			out_indexed[y * width + x] = (unsigned char)pal_idx;

			int const errR = r - pal[pal_idx].red;
			int const errG = g - pal[pal_idx].green;
			int const errB = b - pal[pal_idx].blue;

			// Apply dither scaling
			int const eR = (int)::floor ( errR * dither );
			int const eG = (int)::floor ( errG * dither );
			int const eB = (int)::floor ( errB * dither );

			if ( left_to_right )
			{
				// Standard Atkinson
				distribute(x, r0,  1, eR, eG, eB); // [x+1, y]
				distribute(x, r0,  2, eR, eG, eB); // [x+2, y]
				distribute(x, r1, -1, eR, eG, eB); // [x-1, y+1]
				distribute(x, r1,  0, eR, eG, eB); // [x, y+1]
				distribute(x, r1,  1, eR, eG, eB); // [x+1, y+1]
				distribute(x, r2,  0, eR, eG, eB); // [x, y+2]
			}
			else
			{
				// Reversed Atkinson
				distribute(x, r0, -1, eR, eG, eB); // [x-1, y]
				distribute(x, r0, -2, eR, eG, eB); // [x-2, y]
				distribute(x, r1,  1, eR, eG, eB); // [x+1, y+1]
				distribute(x, r1,  0, eR, eG, eB); // [x, y+1]
				distribute(x, r1, -1, eR, eG, eB); // [x-1, y+1]
				distribute(x, r2,  0, eR, eG, eB); // [x, y+2]
			}
		}

		// Cycle buffers
		int * rtmp = r0;
		r0 = r1;
		r1 = r2;
		r2 = rtmp;

		memset ( r2, 0, sizeof(*r2) * size_t(stride * 3) );
	}

	free ( e_buf );
}

mtPixmap * pixy_pixmap_convert_to_indexed (
	mtPixmap const	* const	pixmap,
	int		const	dither_percent,
	int		const	dither_type
	)
{
	if ( pixmap->bpp != PIXY_PIXMAP_BPP_RGB )
	{
		return NULL;
	}

	mtPixmap * const im = pixy_pixmap_new_indexed ( pixmap->width,
		pixmap->height );
	if ( ! im )
	{
		return NULL;
	}

	pixy_palette_copy ( &im->palette, &pixmap->palette );

	if ( pixy_pixmap_copy_alpha ( im, pixmap ) )
	{
		pixy_pixmap_destroy ( im );
		return NULL;
	}

	switch ( dither_type )
	{
	default:
	case PIXY_DITHER_TYPE_ATKINSON:
		atkinson_dither ( &pixmap->palette.color[0],
			pixmap->palette.size, pixmap->canvas, im->canvas,
			pixmap->width, pixmap->height, dither_percent );
		break;

	case PIXY_DITHER_TYPE_SIMPLE:
		simple_dither ( &pixmap->palette.color[0],
			pixmap->palette.size, pixmap->canvas, im->canvas,
			pixmap->width, pixmap->height, dither_percent );
		break;
	}

	return im;
}

