/* warpcore.c
 * This file is part of Amsam photo cropping program
 * Copyright 2021, 2023, 2024, 2025, 2026 Nandakumar Edamana
 * Distributed under GNU General Public License, version 3
 */ 

// Nandakumar Edamana
// Started on 2021-08-29 porting PHP code started on 2021-08-28
// 2021-08-30 00:36 - finished porting the PHP version to C.
// 2021-08-31 07:24 - fixed: the output is ragged (had to use fabs(), not abs())
// 2021-08-31 evening - speeding up using memcpy() and pixel stride
// 2021-09-03 - changes in the warping algorithm

#include <assert.h>
#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "core.h"
#include "warpcore.h"

// For debugging
void print_pixel(LinearPixel pixel) {
	fprintf(stderr, "pixel: %f %f %f %f\n", pixel.rgb[0], pixel.rgb[1],pixel.rgb[2],pixel.rgb[3]);
}

double max_dblarr(double * arr, size_t n)
{
	assert(n > 0);

	double max = arr[0];

	for(size_t i = 1; i < n; i++)
		if(arr[i] > max)
			max = arr[i];

	return max;
}

void die_file_format()
{
	fprintf(stderr, "Error: Unsupported file format or corrupt file.\n");
	exit(EXIT_FAILURE);
}

void die_io()
{
	fprintf(stderr, "IO error.\n");
	exit(EXIT_FAILURE);
}

// TODO do better than calling fgetc() thousands of times
int ppmfp_getint(FILE * fp) {
	int   out = 0;

	_Bool read = 0;
	_Bool newline = 1;

	char buf[MAX_INTSTR];
	int  bufpos = 0;
	int   c;

	while( EOF != (c = fgetc(fp)) ) {
		if(bufpos >= MAX_INTSTR) {
			fprintf(stderr, "Error: Buffer overflow.\n");
			exit(EXIT_FAILURE);
		}
	
		if(newline) {
			if(c == '#') {
				do {
					c = fgetc(fp);
					
					if(c == EOF)
						die_file_format();
				} while(c != '\r' && c != '\n');
			}

			newline = 0;
		}

		if(c == '\r' || c == '\n') {
			newline = 1;
		}
		
		if(isdigit(c)) {
			buf[bufpos++] = c;
		}
		else if(isspace(c)) {
			buf[bufpos] = '\0';
			
			if(bufpos > 0) {
				out = atoi(buf);
				read = 1;
				break;
			}
		}
		else {
			die_file_format();
		}
	}
	
	if(!read)
		die_file_format();
	
	return out;
}

LinearPixel nan_img_get_linear_pixel(Image * himg, int x, int y)
{
	assert(x >= 0 && x <= himg->width);
	assert(y >= 0 && y <= himg->height);

	LinearPixel pixel;
	
	memcpy(pixel.rgb,
		himg->buf + (y * himg->width + x) * PIXSTRIDE,
		sizeof(pixel.rgb));
	
	return pixel;
}

// XXX Keep SCALING_AA_RADIUS in sync with this
LinearPixel nan_img_get_linear_pixel_for_scaling(Image * himg, double x, double y)
{
	if(!SCALING_AA_ENABLE)
		return nan_img_get_linear_pixel(himg, x, y);

	// just drop the decimal part; we'll deal with it below
	int xround = x;
	int yround = y;

	size_t inpcount = 0;
	double inputs[3][4];
	
	for(int xi = xround; xi <= xround + 1; xi++) {
		for(int yi = yround; yi <= yround + 1; yi++) {
			if(xi >= himg->width || yi >= himg->height)
				continue;

			// pixperc was originally based on distance rather than area
			
			/* Calculate the area of the floating source pixel in
			 * this pixel ((xi, yi)) in the integral source 4-pixel matrix 
			 */
			double px1 = (xi == xround)? x: xi;
			double py1 = (yi == yround)? y: yi;
			double px2 = (xi == xround)? xi + 1: x + 1;
			double py2 = (yi == yround)? yi + 1: y + 1;
			double pw  = px2 - px1;
			double ph  = py2 - py1;
			
			assert(pw >= 0 && pw <= 1);
			assert(ph >= 0 && ph <= 1);
			
			double par = pw * ph;
			assert(par <= 1);

			double maxarea = 1.0;
			double pixperc = par/maxarea;
			assert(pixperc >= 0 && pixperc <= 1);

			LinearPixel pix = nan_img_get_linear_pixel(himg, xi, yi);
			for(size_t ch = 0; ch < 3; ch++)
				inputs[ch][inpcount] = pix.rgb[ch] * pixperc;
			
			inpcount++;
		}
	}

	assert(inpcount <= 4);

	LinearPixel pix;

	for(size_t ch = 0; ch < 3; ch++) {
		double sum = 0;
		for(size_t i = 0; i < inpcount; i++)
			sum += inputs[ch][i];

		// TODO FIXME enable, but consider floating point errors
		//assert(sum >= 0 && sum <= 1);
		assert(sum >= 0 && (sum - 1) <= 0.01);
		pix.rgb[ch] = sum;
	}

	return pix;
}

void nan_img_over_linear_pixel(Image * himg, int x, int y, LinearPixel pixel1)
{
	if (x < 0 || y < 0 || x >= himg->width || y >= himg->height)
		return;

	linear_pixel_assert(pixel1);

	LinearPixel *pixel0 = (LinearPixel*) (himg->buf + (y * himg->width + x) * PIXSTRIDE);
	linear_pixel_assert(*pixel0);

	for(int i = 0; i < 3; ++i) {
		// Straight
		//pixel0->rgb[i] = (pixel1.rgb[i] * pixel1.rgb[3]) + (pixel0->rgb[i] * (1 - pixel1.rgb[3]));

		// Premultiplied
		pixel0->rgb[i] = pixel1.rgb[i] + (pixel0->rgb[i] * (1 - pixel1.rgb[3]));
	}

	pixel0->rgb[3] = pixel1.rgb[3] + pixel0->rgb[3] * (1 - pixel1.rgb[3]);

	linear_pixel_assert(*pixel0);
}

void nan_img_set_linear_pixel(Image * himg, int x, int y, LinearPixel pixel)
{
// TODO DOC
// XXX coords can underflow or overflow while the mouse has moved out but the
// button is still pressed.
// Clipping at the source isn't as easy as it may seem;
// 1) interpolation is needed at the boundary (when (x1, y1) is inside the
//    canvas and (x2, y2) is outside, for instance)
// 2) A thick brush can leave marks on the canvas even when the mouse coords
//    are outside; the UI cannot clip the coords in this case.
/*	assert(x >= 0 && x < himg->width);*/
/*	assert(y >= 0 && y < himg->height);*/

	if (x < 0 || y < 0 || x >= himg->width || y >= himg->height)
		return;

	memcpy(himg->buf + (y * himg->width + x) * PIXSTRIDE,
		pixel.rgb,
		sizeof(pixel.rgb));
}

// TODO REM or use better calculation like nan_img_alphen_pixel()
void nan_img_darken_pixel(Image * himg, int x, int y, double intensity)
{
	if (x < 0 || y < 0 || x >= himg->width || y >= himg->height)
		return;

	LinearPixel *pixel = (LinearPixel*) (himg->buf + (y * himg->width + x) * PIXSTRIDE);
	double r = pixel->rgb[0] - intensity;
	double g = pixel->rgb[1] - intensity;
	double b = pixel->rgb[2] - intensity;
	
	if(r < 0) r = 0;
	if(g < 0) g = 0;
	if(b < 0) b = 0;
	
	pixel->rgb[0] = r;
	pixel->rgb[1] = g;
	pixel->rgb[2] = b;

	linear_pixel_assert(*pixel);
}

void nan_img_alphen_pixel(Image * himg, int x, int y, double intensity)
{
	if (x < 0 || y < 0 || x >= himg->width || y >= himg->height)
		return;

	LinearPixel *pixel = (LinearPixel*) (himg->buf + (y * himg->width + x) * PIXSTRIDE);

	double changefac = 1 - intensity;

	for(int i = 0; i < 4; ++i)
		pixel->rgb[i] *= changefac;
	
	linear_pixel_assert(*pixel);
}

// TODO gamma correction
Image * nan_img_open(const char * path)
{
	FILE * fp = fopen(path, "r");
	if(!fp) {
		perror(NULL);
		exit(EXIT_FAILURE);
	}
	
	switch(fgetc(fp)) {
	case 'P': break;
	case EOF: die_io();
	default: die_file_format();
	}
	
	switch(fgetc(fp)) {
	case '3': break;
	case EOF: die_io();
	default: die_file_format();
	}
	
	int width  = ppmfp_getint(fp);
	int height = ppmfp_getint(fp);
	int depth  = ppmfp_getint(fp);
	
	Image * himg = nan_img_new(width, height, depth);
	
	for(int y = 0; y < himg->height; y++) {
		for(int x = 0; x < himg->width; x++) {
			LinearPixel pix;
		
			for(size_t ch = 0; ch < 3; ch++) {
				int incolor = ppmfp_getint(fp);
				pix.rgb[ch] = ((double) incolor) / himg->depth;
			}

			nan_img_set_linear_pixel(himg, x, y, pix);		
		}
	}

	return himg;
}

// TODO FIXME this is a bottleneck with thousands of printf() calls.
// TODO gamma correction
void nan_img_write(Image * himg, const char * path)
{
	FILE * fp = fopen(path, "w");
	if(!fp) {
		perror(NULL);
		exit(EXIT_FAILURE);
	}
	
	fprintf(fp, "P3\n%d %d\n%d\n", himg->width, himg->height, himg->depth); // TODO err
	for(size_t i = 0; i < himg->width * himg->height; i++) {
		double rgb[PIXSTRIDE];
		memcpy(rgb, himg->buf + i * PIXSTRIDE, sizeof(rgb));

		int r = rgb[0] * himg->depth;
		int g = rgb[1] * himg->depth;
		int b = rgb[2] * himg->depth;

		fprintf(fp, "%d %d %d\n", r, g, b); // TODO err
	}
	
	fclose(fp);
}

Pixel nan_pixel_new(double r, double g, double b, double a) {
	Pixel px;
	px.rgb[0] = r;
	px.rgb[1] = g;
	px.rgb[2] = b;
	px.rgb[3] = a;
	
	return px;
}
