//Copyright Paul Reiche, Fred Ford. 1992-2002

/*
 *  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 2 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; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "libs/graphics/cmap.h"
#include "libs/threadlib.h"
#include "libs/timelib.h"
#include "libs/inplib.h"
#include "libs/strlib.h"
		// for GetStringAddress()
#include "libs/log.h"
#include <string.h>
#include <stdlib.h>


typedef struct xform_control
{
	int CMapIndex; // -1 means unused
	COLORMAPPTR CMapPtr;
	SIZE Ticks;
	DWORD StartTime;
	DWORD EndTime;
	Color OldCMap[NUMBER_OF_PLUTVALS];
} XFORM_CONTROL;

#define MAX_XFORMS 16
static struct
{
	XFORM_CONTROL TaskControl[MAX_XFORMS];
	volatile int Highest;
			// 'pending' is Highest >= 0
	Mutex Lock;
} XFormControl;

static int fadeAmount = FADE_NORMAL_INTENSITY;
static int fadeDelta;
static TimeCount fadeStartTime;
static sint32 fadeInterval;
static Mutex fadeLock;

#define SPARE_COLORMAPS  20

// Colormaps are rapidly replaced in some parts of the game, so
// it pays to have some spares on hand
static TFB_ColorMap *poolhead;
static int poolcount;

static TFB_ColorMap * colormaps[MAX_COLORMAPS];
static int mapcount;
static Mutex maplock;


void
InitColorMaps (void)
{
	int i;

	// init colormaps
	maplock = CreateMutex ("Colormaps Lock", SYNC_CLASS_TOPLEVEL | SYNC_CLASS_VIDEO);

	// init xform control
	XFormControl.Highest = -1;
	XFormControl.Lock = CreateMutex ("Transform Lock", SYNC_CLASS_TOPLEVEL | SYNC_CLASS_VIDEO);
	for (i = 0; i < MAX_XFORMS; ++i)
		XFormControl.TaskControl[i].CMapIndex = -1;

	fadeLock = CreateMutex ("Fade Lock", SYNC_CLASS_TOPLEVEL | SYNC_CLASS_VIDEO);
}

void
UninitColorMaps (void)
{
	TFB_ColorMap *next;

	// free spares
	for ( ; poolhead; poolhead = next)
	{
		next = poolhead->next;
		HFree (poolhead);
	}

	DestroyMutex (fadeLock);
	// uninit xform control
	DestroyMutex (XFormControl.Lock);
	
	// uninit colormaps
	DestroyMutex (maplock);
}

static inline TFB_ColorMap *
alloc_colormap (void)
		// returns an addrefed object
{
	TFB_ColorMap *map;

	if (poolhead)
	{	// have some spares
		map = poolhead;
		poolhead = map->next;
		--poolcount;
	}
	else
	{	// no spares, need a new one
		map = HMalloc (sizeof (*map));
		map->palette = AllocNativePalette ();
		if (!map->palette)
		{
			HFree (map);
			return NULL;
		}
	}
	map->next = NULL;
	map->index = -1;
	map->refcount = 1;
	map->version = 0;

	return map;
}

static TFB_ColorMap *
clone_colormap (TFB_ColorMap *from, int index)
		// returns an addrefed object
{
	TFB_ColorMap *map;

	map = alloc_colormap ();
	if (!map)
	{
		log_add (log_Warning, "FATAL: clone_colormap(): "
					"could not allocate a map");
		exit (EXIT_FAILURE);
	}
	else
	{	// fresh new map
		map->index = index;
		if (from)
			map->version = from->version;
	}
	map->version++;
	
	return map;
}

static inline void
free_colormap (TFB_ColorMap *map)
{
	if (!map)
	{
		log_add (log_Warning, "free_colormap(): tried to free a NULL map");
		return;
	}

	if (poolcount < SPARE_COLORMAPS)
	{	// return to the spare pool
		map->next = poolhead;
		poolhead = map;
		++poolcount;
	}
	else
	{	// don't need any more spares
		FreeNativePalette (map->palette);
		HFree (map);
	}
}

static inline TFB_ColorMap *
get_colormap (int index)
{
	TFB_ColorMap *map;

	map = colormaps[index];
	if (!map)
	{
		log_add (log_Fatal, "BUG: get_colormap(): map not present");
		exit (EXIT_FAILURE);
	}

	map->refcount++;
	return map;
}

static inline void
release_colormap (TFB_ColorMap *map)
{
	if (!map)
		return;

	if (map->refcount <= 0)
	{
		log_add (log_Warning, "BUG: release_colormap(): refcount not >0");
		return;
	}

	map->refcount--;
	if (map->refcount == 0)
		free_colormap (map);
}

void
TFB_ReturnColorMap (TFB_ColorMap *map)
{
	LockMutex (maplock);
	release_colormap (map);
	UnlockMutex (maplock);
}

TFB_ColorMap *
TFB_GetColorMap (int index)
{
	TFB_ColorMap *map;

	LockMutex (maplock);
	map = get_colormap (index);
	UnlockMutex (maplock);

	return map;
}

void
GetColorMapColors (Color *colors, TFB_ColorMap *map)
{
	int i;

	if (!map)
		return;

	for (i = 0; i < NUMBER_OF_PLUTVALS; ++i)
		colors[i] = GetNativePaletteColor (map->palette, i);
}

BOOLEAN
SetColorMap (COLORMAPPTR map)
{
	int start, end;
	int total_size;
	UBYTE *colors = (UBYTE*)map;
	TFB_ColorMap **mpp;
	
	if (!map)
		return TRUE;

	start = *colors++;
	end = *colors++;
	if (start > end)
	{
		log_add (log_Warning, "ERROR: SetColorMap(): "
				"starting map (%d) not less or eq ending (%d)",
				start, end);
		return FALSE;
	}
	if (start >= MAX_COLORMAPS)
	{
		log_add (log_Warning, "ERROR: SetColorMap(): "
				"starting map (%d) beyond range (0-%d)",
				start, (int)MAX_COLORMAPS - 1);
		return FALSE;
	}
	if (end >= MAX_COLORMAPS)
	{
		log_add (log_Warning, "SetColorMap(): "
				"ending map (%d) beyond range (0-%d)\n",
				end, (int)MAX_COLORMAPS - 1);
		end = MAX_COLORMAPS - 1;
	}

	total_size = end + 1;

	LockMutex (maplock);

	if (total_size > mapcount)
		mapcount = total_size;
	
	// parse the supplied PLUTs into our colormaps
	for (mpp = colormaps + start; start <= end; ++start, ++mpp)
	{
		int i;
		TFB_ColorMap *newmap;
		TFB_ColorMap *oldmap;

		oldmap = *mpp;
		newmap = clone_colormap (oldmap, start);
		
		for (i = 0; i < NUMBER_OF_PLUTVALS; ++i, colors += PLUTVAL_BYTE_SIZE)
		{
			Color color;

			color.a = 0xff;
			color.r = colors[PLUTVAL_RED];
			color.g = colors[PLUTVAL_GREEN];
			color.b = colors[PLUTVAL_BLUE];
			SetNativePaletteColor (newmap->palette, i, color);
		}

		*mpp = newmap;
		release_colormap (oldmap);
	}

	UnlockMutex (maplock);

	return TRUE;
}

/* Fade Transforms */

int
GetFadeAmount (void)
{
	int newAmount;

	LockMutex (fadeLock);

	if (fadeInterval)
	{	// have a pending fade
		TimeCount Now = GetTimeCounter ();
		sint32 elapsed;
		
		elapsed = Now - fadeStartTime;
		if (elapsed > fadeInterval)
			elapsed = fadeInterval;

		newAmount = fadeAmount + (long)fadeDelta * elapsed / fadeInterval;

		if (elapsed >= fadeInterval)
		{	// fade is over
			fadeAmount = newAmount;
			fadeInterval = 0;
		}
	}
	else
	{	// no fade pending, return the current
		newAmount = fadeAmount;
	}

	UnlockMutex (fadeLock);

	return newAmount;
}

static void
finishPendingFade (void)
{
	if (fadeInterval)
	{	// end the fade immediately
		fadeAmount += fadeDelta;
		fadeInterval = 0;
	}
}

static void
FlushFadeXForms (void)
{
	LockMutex (fadeLock);
	finishPendingFade ();
	UnlockMutex (fadeLock);
}

DWORD
FadeScreen (ScreenFadeType fadeType, SIZE TimeInterval)
{
	TimeCount TimeOut;
	int FadeEnd;

	switch (fadeType)
	{
	case FadeAllToBlack:
	case FadeSomeToBlack:
		FadeEnd = FADE_NO_INTENSITY;
		break;
	case FadeAllToColor:
	case FadeSomeToColor:
		FadeEnd = FADE_NORMAL_INTENSITY;
		break;
	case FadeAllToWhite:
	case FadeSomeToWhite:
		FadeEnd = FADE_FULL_INTENSITY;
		break;
	default:
		return (GetTimeCounter ());
	}

	// Don't make users wait for fades
	if (QuitPosted)
		TimeInterval = 0;

	LockMutex (fadeLock);

	finishPendingFade ();

	if (TimeInterval <= 0)
	{	// end the fade immediately
		fadeAmount = FadeEnd;
		// cancel any pending fades
		fadeInterval = 0;
		TimeOut = GetTimeCounter ();
	}
	else
	{
		fadeInterval = TimeInterval;
		fadeDelta = FadeEnd - fadeAmount;
		fadeStartTime = GetTimeCounter ();
		TimeOut = fadeStartTime + TimeInterval + 1;
	}

	UnlockMutex (fadeLock);

	return TimeOut;
}

/* Colormap Transforms */

static void
finish_colormap_xform (int which)
{
	SetColorMap (XFormControl.TaskControl[which].CMapPtr);
	XFormControl.TaskControl[which].CMapIndex = -1;
	// check Highest ptr
	if (which == XFormControl.Highest)
	{
		do
			--which;
		while (which >= 0 && XFormControl.TaskControl[which].CMapIndex == -1);
		
		XFormControl.Highest = which;
	}
}

static inline BYTE
blendChan (BYTE c1, BYTE c2, int weight, int scale)
{
	return c1 + ((int)c2 - c1) * weight / scale;
}

/* This gives the XFormColorMap task a timeslice to do its thing
 * Only one thread should ever be allowed to be calling this at any time
 */
BOOLEAN
XFormColorMap_step (void)
{
	BOOLEAN Changed = FALSE;
	int x;
	DWORD Now = GetTimeCounter ();

	LockMutex (XFormControl.Lock);

	for (x = 0; x <= XFormControl.Highest; ++x)
	{
		XFORM_CONTROL *control = &XFormControl.TaskControl[x];
		int index = control->CMapIndex;
		int TicksLeft = control->EndTime - Now;
		TFB_ColorMap *curmap;

		if (index < 0)
			continue; // unused slot

		LockMutex (maplock);

		curmap = colormaps[index];
		if (!curmap)
		{
			UnlockMutex (maplock);
			log_add (log_Error, "BUG: XFormColorMap_step(): no current map");
			finish_colormap_xform (x);
			continue;
		}

		if (TicksLeft > 0)
		{
#define XFORM_SCALE 0x10000
			TFB_ColorMap *newmap = NULL;
			UBYTE *newClr;
			Color *oldClr;
			int frac;
			int i;

			newmap = clone_colormap (curmap, index);

			oldClr = control->OldCMap;
			newClr = (UBYTE*)control->CMapPtr + 2;

			frac = (int)(control->Ticks - TicksLeft) * XFORM_SCALE
					/ control->Ticks;

			for (i = 0; i < NUMBER_OF_PLUTVALS; ++i, ++oldClr,
					newClr += PLUTVAL_BYTE_SIZE)
			{
				Color color;

				color.a = 0xff;
				color.r = blendChan (oldClr->r, newClr[PLUTVAL_RED],
						frac, XFORM_SCALE);
				color.g = blendChan (oldClr->g, newClr[PLUTVAL_GREEN],
						frac, XFORM_SCALE);
				color.b = blendChan (oldClr->b, newClr[PLUTVAL_BLUE],
						frac, XFORM_SCALE);
				SetNativePaletteColor (newmap->palette, i, color);
			}

			colormaps[index] = newmap;
			release_colormap (curmap);
		}

		UnlockMutex (maplock);

		if (TicksLeft <= 0)
		{	// asked for immediate xform or already done
			finish_colormap_xform (x);
		}
		
		Changed = TRUE;
	}

	UnlockMutex (XFormControl.Lock);

	return Changed;
}

static void
FlushPLUTXForms (void)
{
	int i;

	LockMutex (XFormControl.Lock);

	for (i = 0; i <= XFormControl.Highest; ++i)
	{
		if (XFormControl.TaskControl[i].CMapIndex >= 0)
			finish_colormap_xform (i);
	}
	XFormControl.Highest = -1; // all gone

	UnlockMutex (XFormControl.Lock);
}

static DWORD
XFormPLUT (COLORMAPPTR ColorMapPtr, SIZE TimeInterval)
{
	TFB_ColorMap *map;
	XFORM_CONTROL *control;
	int index;
	int x;
	int first_avail = -1;
	DWORD EndTime;
	DWORD Now;

	Now = GetTimeCounter ();
	index = *(UBYTE*)ColorMapPtr;

	LockMutex (XFormControl.Lock);
	// Find an available slot, or reuse if required
	for (x = 0; x <= XFormControl.Highest
			&& index != XFormControl.TaskControl[x].CMapIndex;
			++x)
	{
		if (first_avail == -1 && XFormControl.TaskControl[x].CMapIndex == -1)
			first_avail = x;
	}

	if (index == XFormControl.TaskControl[x].CMapIndex)
	{	// already xforming this colormap -- cancel and reuse slot
		finish_colormap_xform (x);
	}
	else if (first_avail >= 0)
	{	// picked up a slot along the way
		x = first_avail;
	}
	else if (x >= MAX_XFORMS)
	{	// flush some xforms if the queue is full
		log_add (log_Debug, "WARNING: XFormPLUT(): no slots available");
		x = XFormControl.Highest;
		finish_colormap_xform (x);
	}
	// take next unused one
	control = &XFormControl.TaskControl[x];
	if (x > XFormControl.Highest)
		XFormControl.Highest = x;

	// make a copy of the current map
	LockMutex (maplock);
	map = colormaps[index];
	if (!map)
	{
		UnlockMutex (maplock);
		UnlockMutex (XFormControl.Lock);
		log_add (log_Warning, "BUG: XFormPLUT(): no current map");
		return (0);
	}
	GetColorMapColors (control->OldCMap, map);
	UnlockMutex (maplock);

	control->CMapIndex = index;
	control->CMapPtr = ColorMapPtr;
	control->Ticks = TimeInterval;
	if (control->Ticks < 0)
		control->Ticks = 0; /* prevent negative fade */
	control->StartTime = Now;
	control->EndTime = EndTime = Now + control->Ticks;

	UnlockMutex (XFormControl.Lock);

	return (EndTime);
}

DWORD
XFormColorMap (COLORMAPPTR ColorMapPtr, SIZE TimeInterval)
{
	if (!ColorMapPtr)
		return (0);

	// Don't make users wait for transforms
	if (QuitPosted)
		TimeInterval = 0;

	return XFormPLUT (ColorMapPtr, TimeInterval);
}

void
FlushColorXForms (void)
{
	FlushFadeXForms ();
	FlushPLUTXForms ();
}

// The type conversions are implicit and will generate errors
// or warnings if types change imcompatibly
COLORMAPPTR
GetColorMapAddress (COLORMAP colormap)
{
	return GetStringAddress (colormap);
}
