#include <math.h>
#include <string.h>

#include <exec/memory.h>
#include <exec/types.h>

#include <clib/exec_protos.h>

#include <pragmas/exec_pragmas.h>

#include "scale.h"

/* Function protoytypes */
void scale_region( struct PixelRegion *srcPR, struct PixelRegion *destPR, BOOL cubic_interpolation );
double cubic( double dx, int jm1, int j, int jp1, int jp2 );
void pixel_region_get_row( struct PixelRegion *srcPR, int src_row, int orig_width, UBYTE *src, int num_rows );
void pixel_region_set_row( struct PixelRegion *destPR, int dest_row, int width, UBYTE *dest );

void scale_region( struct PixelRegion *srcPR, struct PixelRegion *destPR, BOOL cubic_interpolation )
{
	unsigned char *src_m1, *src, *src_p1, *src_p2;
	unsigned char *s_m1, *s, *s_p1, *s_p2;
	unsigned char *dest, *d;
	double *row, *r;
	int src_row, src_col;
	int bytes, b;
	int width, height;
	int orig_width, orig_height;
	double x_rat, y_rat;
	double x_cum, y_cum;
	double x_last, y_last;
	double *x_frac, y_frac, tot_frac;
	float dx, dy;
	int i, j;
	int frac;
	int advance_dest_x, advance_dest_y;
	int minus_x, plus_x, plus2_x;
	ScaleType scale_type;

	orig_width = srcPR->w;
	orig_height = srcPR->h;

	width = destPR->w;
	height = destPR->h;

	/* Some calculations */
	bytes = destPR->bytes;

	/* The data pointers */
	src_m1 = AllocVec( orig_width * bytes, MEMF_PUBLIC | MEMF_CLEAR );
	src = AllocVec( orig_width * bytes, MEMF_PUBLIC | MEMF_CLEAR );
	src_p1 = AllocVec( orig_width * bytes, MEMF_PUBLIC | MEMF_CLEAR );
	src_p2 = AllocVec( orig_width * bytes, MEMF_PUBLIC | MEMF_CLEAR );
	dest = AllocVec( width * bytes, MEMF_PUBLIC | MEMF_CLEAR );

	/* Find the ratios of old x to new x and old y to new y */
	x_rat = (double)orig_width / (double)width;
	y_rat = (double)orig_height / (double)height;

	/* Determine the scale type */
	if ( x_rat < 1.0 && y_rat < 1.0 )
	{
		scale_type = MagnifyX_MagnifyY;
	}
	else if ( x_rat < 1.0 && y_rat >= 1.0 )
	{
		scale_type = MagnifyX_MinifyY;
	}
	else if ( x_rat >= 1.0 && y_rat < 1.0 )
	{
		scale_type = MinifyX_MagnifyY;
	}
	else
	{
		scale_type = MinifyX_MinifyY;
	}

	/* Allocate an array to help with the calculations */
	row = (double *)AllocVec( sizeof( double ) * width * bytes, MEMF_PUBLIC | MEMF_CLEAR );
	x_frac = (double *)AllocVec( sizeof( double ) * ( width + orig_width ), MEMF_PUBLIC | MEMF_CLEAR );

	/* Initialize the pre-calculated pixel fraction array */
	src_col = 0;
	x_cum = (double)src_col;
	x_last = x_cum;

	for ( i = 0; i < width + orig_width; i++ )
	{
		if ( x_cum + x_rat <= ( src_col + 1 + EPSILON ) )
		{
			x_cum += x_rat;
			x_frac[i] = x_cum - x_last;
		}
		else
		{
			src_col ++;
			x_frac[i] = src_col - x_last;
		}

		x_last += x_frac[i];
	}

	/* Clear the "row" array */
	memset( row, 0, sizeof( double ) * width * bytes );

	/* Counters */
	src_row = 0;
	y_cum = (double)src_row;
	y_last = y_cum;

	/* Get the first src row */
	pixel_region_get_row( srcPR, src_row, orig_width, src, 1 );

	/* Get the next two if possible */
	if ( src_row < ( orig_height - 1 ) )
	{
		pixel_region_get_row( srcPR, ( src_row + 1 ), orig_width, src_p1, 1 );
	}

	if ( ( src_row + 1 ) < ( orig_height - 1 ) )
	{
		pixel_region_get_row( srcPR, ( src_row + 2 ), orig_width, src_p2, 1 );
	}

	/* Scale the selected region */
	i = height;

	while ( i )
	{
		src_col = 0;
		x_cum = (double)src_col;

		/* Determine the fraction of the src pixel we are using for y */
		if ( y_cum + y_rat <= ( src_row + 1 + EPSILON ) )
		{
			y_cum += y_rat;
			dy = y_cum - src_row;
			y_frac = y_cum - y_last;

			advance_dest_y = TRUE;
		}
		else
		{
			y_frac = ( src_row + 1 ) - y_last;
			dy = 1.0;

			advance_dest_y = FALSE;
		}

		y_last += y_frac;

		s = src;
		s_m1 = ( src_row > 0 ) ? src_m1 : src;
		s_p1 = ( src_row < ( orig_height - 1 ) ) ? src_p1 : src;
		s_p2 = ( ( src_row + 1 ) < ( orig_height - 1 ) ) ? src_p2 : s_p1;

		r = row;

		frac = 0;
		j = width;

		while ( j )
		{
			if ( x_cum + x_rat <= ( src_col + 1 + EPSILON ) )
			{
				x_cum += x_rat;
				dx = x_cum - src_col;

				advance_dest_x = TRUE;
			}
			else
			{
				dx = 1.0;

				advance_dest_x = FALSE;
			}

			tot_frac = x_frac[ frac++ ] * y_frac;

			minus_x = ( src_col > 0 ) ? -bytes : 0;
			plus_x = ( src_col < ( orig_width - 1 ) ) ? bytes : 0;
			plus2_x = ( ( src_col + 1 ) < ( orig_width - 1 ) ) ? bytes * 2 : plus_x;

			if ( cubic_interpolation )
			{
				switch ( scale_type )
				{
					case MagnifyX_MagnifyY:
						for ( b = 0; b < bytes; b++ )
						{
							r[b] += cubic( dy, (int)cubic( dx, s_m1[ b + minus_x ], s_m1[b], s_m1[ b + plus_x ], s_m1[ b + plus2_x ] ),
								(int)cubic( dx, s[ b + minus_x ], s[b],	s[ b + plus_x ], s[ b + plus2_x ] ),
								(int)cubic( dx, s_p1[ b + minus_x ], s_p1[b], s_p1[ b + plus_x ], s_p1[ b + plus2_x ] ),
								(int)cubic( dx, s_p2[ b + minus_x ], s_p2[b], s_p2[ b + plus_x ], s_p2[ b + plus2_x ] ) ) * tot_frac;
						}
					break;

					case MagnifyX_MinifyY:
						for ( b = 0; b < bytes; b++ )
						{
							r[b] += cubic( dx, s[ b + minus_x ], s[b], s[ b + plus_x ], s[ b + plus2_x ] ) * tot_frac;
						}
					break;

					case MinifyX_MagnifyY:
						for ( b = 0; b < bytes; b++ )
						{
							r[b] += cubic( dy, s_m1[b], s[b], s_p1[b], s_p2[b] ) * tot_frac;
						}
					break;

					case MinifyX_MinifyY:
						for ( b = 0; b < bytes; b++ )
						{
							r[b] += s[b] * tot_frac;
						}
					break;
				}
			}
			else
			{
				switch ( scale_type )
				{
					case MagnifyX_MagnifyY:
						for ( b = 0; b < bytes; b++ )
						{
							r[b] += ( ( 1 - dy ) * ( ( 1 - dx ) * s[b] + dx * s[ b + plus_x ] ) +
							dy  * ( ( 1 - dx ) * s_p1[b] + dx * s_p1[ b + plus_x ] ) ) * tot_frac;
						}
					break;

					case MagnifyX_MinifyY:
						for ( b = 0; b < bytes; b++ )
						{
							r[b] += ( s[b] * ( 1 - dx ) + s[ b + plus_x ] * dx ) * tot_frac;
						}
					break;

					case MinifyX_MagnifyY:
						for ( b = 0; b < bytes; b++ )
						{
							r[b] += ( s[b] * ( 1 - dy ) + s_p1[b] * dy ) * tot_frac;
						}
					break;

					case MinifyX_MinifyY:
						for ( b = 0; b < bytes; b++ )
						{
							r[b] += s[b] * tot_frac;
						}
					break;
				}
			}

			if ( advance_dest_x )
			{
				r += bytes;
				j--;
			}
			else
			{
				s_m1 += bytes;
				s += bytes;
				s_p1 += bytes;
				s_p2 += bytes;
				src_col++;
			}
		}

		if ( advance_dest_y )
		{
			tot_frac = 1.0 / ( x_rat * y_rat );

			/* Copy "row" to "dest" */
			d = dest;
			r = row;
			j = width;

			while ( j-- )
			{
				b = bytes;

				while (b--)
				{
					*d++ = (unsigned char)( *r++ * tot_frac );
				}
			}

			/* Set the pixel region span */
			pixel_region_set_row( destPR, ( height - i ), width, dest );

			/* Clear the "row" array */
			memset( row, 0, sizeof( double ) * width * bytes );

			i--;
		}
		else
		{
			/* Shuffle pointers */
			s = src_m1;
			src_m1 = src;
			src = src_p1;
			src_p1 = src_p2;
			src_p2 = s;
			src_row++;

			if ( ( src_row + 1 ) < ( orig_height - 1 ) )
			{
				pixel_region_get_row( srcPR, ( src_row + 2 ), orig_width, src_p2, 1 );
			}
		}
	}

	/* Free up temporary arrays */
  FreeVec( row );
  FreeVec( x_frac );
  FreeVec( src_m1 );
  FreeVec( src );
  FreeVec( src_p1 );
  FreeVec( src_p2 );
  FreeVec( dest );
}

double cubic( double dx, int jm1, int j, int jp1, int jp2 )
{
	double dx1, dx2, dx3;
	double h1, h2, h3, h4;
	double result;

	/* Constraint parameter = -1 */
	dx1 = fabs( dx );
	dx2 = dx1 * dx1;
	dx3 = dx2 * dx1;
	h1 = dx3 - 2 * dx2 + 1;
	result = h1 * j;

	dx1 = fabs( dx - 1.0 );
	dx2 = dx1 * dx1;
	dx3 = dx2 * dx1;
	h2 = dx3 - 2 * dx2 + 1;
	result += h2 * jp1;

	dx1 = fabs( dx - 2.0 );
	dx2 = dx1 * dx1;
	dx3 = dx2 * dx1;
	h3 = -dx3 + 5 * dx2 - 8 * dx1 + 4;
	result += h3 * jp2;

	dx1 = fabs( dx + 1.0 );
	dx2 = dx1 * dx1;
	dx3 = dx2 * dx1;
	h4 = -dx3 + 5 * dx2 - 8 * dx1 + 4;
	result += h4 * jm1;

	if ( result < 0.0 ) result = 0.0;
  if ( result > 255.0 ) result = 255.0;

  return result;
}

void pixel_region_get_row( struct PixelRegion *srcPR, int src_row, int orig_width, UBYTE *src, int num_rows )
{
	CopyMem( &srcPR->buf[ src_row * orig_width * srcPR->bytes ], src, orig_width * srcPR->bytes );
}

void pixel_region_set_row( struct PixelRegion *destPR, int dest_row, int width, UBYTE *dest )
{
	CopyMem( dest, &destPR->buf[ dest_row * width * destPR->bytes ], width * destPR->bytes );
}
