#include "sys_undo.h"
#include "system.h"
#include "sys_log.h"
#include "sys_tilestore.h"
#include "platform.h"
#include <string.h>

#define UNDO_SIZE (256)

static int yrUndo_init(void* loadtoken);
static int yrUndo_tick(void) {return 0;}
static int yrUndo_save(void* savetoken, void* saveevent, int* savefailed) { yrEvent_set(saveevent, 1); return 0; };
static int yrUndo_canfreeze(void) {return 1;}
static int yrUndo_freeze(void) {return 0;}
static int yrUndo_unfreeze(void) {return 0;}
static int yrUndo_shutdown(void);

void yrUndo_reg(void)
{
	yrSystem_register(sysUndo,
					  yrUndo_init,
					  yrUndo_tick,
					  yrUndo_save,
					  yrUndo_canfreeze,
					  yrUndo_freeze,
					  yrUndo_unfreeze,
					  yrUndo_shutdown,
					  (1<<sysLog)|(1<<sysTileStore)|(1<<sysQuadStore));
}

/******
* Data
******/
static struct {
	enum undo_type type;
	uint32_t quadid;
	yrQuad quaddata[2];
	uint32_t tiledata[2][64];
}
history[UNDO_SIZE];
yrQuad* quadptr[UNDO_SIZE];
unsigned quadref[UNDO_SIZE];
static yrQuad* unassigned = (yrQuad*)0xFFFFFFFFFFFFFFFFull;

size_t history_lo = 0;
size_t history_hi = 0;
size_t history_now = 0;

/******************
* Undo/Redo perform
*******************/
static void undo_quad(size_t idx)
{
	yrQuad* q = quadptr[history[idx].quadid];
	YR_ASSERT(q != NULL && q != unassigned);
	yrQuad_ext* saveext = q->ext;
	*q = history[idx].quaddata[0];
	q->ext = saveext;
	yrQuadStore_update(q);
}

static void redo_quad(size_t idx)
{
	yrQuad* q = quadptr[history[idx].quadid];
	YR_ASSERT(q != NULL && q != unassigned);
	yrQuad_ext* saveext = q->ext;
	*q = history[idx].quaddata[1];
	q->ext = saveext;
	yrQuadStore_update(q);
}

static void undo_tile(size_t idx)
{
	yrQuad* q = quadptr[history[idx].quadid];
	YR_ASSERT(q != NULL && q != unassigned);
	memcpy(q->ext->tiles, history[idx].tiledata[0], 64 * sizeof(uint32_t));
	yrTileStore_important(q->ext->tile_max_idx, (TileID*)q->ext->tiles);
	q->flags |= QUAD_CLEARCACHE;
}

static void redo_tile(size_t idx)
{
	yrQuad* q = quadptr[history[idx].quadid];
	YR_ASSERT(q != NULL && q != unassigned);
	memcpy(q->ext->tiles, history[idx].tiledata[1], 64 * sizeof(uint32_t));
	yrTileStore_important(q->ext->tile_max_idx, (TileID*)q->ext->tiles);
	q->flags |= QUAD_CLEARCACHE;
}

static void undo_both(size_t idx)
{
	undo_quad(idx);
	undo_tile(idx);
}

static void redo_both(size_t idx)
{
	redo_quad(idx);
	redo_tile(idx);
}

static void undo_add(size_t idx)
{
	yrQuad* q = quadptr[history[idx].quadid];
	YR_ASSERT(q != NULL && q != unassigned);
	yrQuadStore_remove(q);
	quadptr[history[idx].quadid] = NULL;
}

static void redo_add(size_t idx)
{
	yrQuad* q = quadptr[history[idx].quadid];
	YR_ASSERT(q == NULL);
	q = yrQuadStore_add(&history[idx].quaddata[1]);
	if(q) {
		//TODO: silent failure not good
		memcpy(q->ext->tiles, history[idx].tiledata[1], 64 * sizeof(uint32_t));
		yrTileStore_important(q->ext->tile_max_idx, (TileID*)q->ext->tiles);
	}
	quadptr[history[idx].quadid] = q;
}

static void undo_remove(size_t idx)
{
	yrQuad* q = quadptr[history[idx].quadid];
	YR_ASSERT(q == NULL);
	q = yrQuadStore_add(&history[idx].quaddata[0]);
	if(q) {
		//TODO: silent failure not good
		memcpy(q->ext->tiles, history[idx].tiledata[0], 64 * sizeof(uint32_t));
		yrTileStore_important(q->ext->tile_max_idx, (TileID*)q->ext->tiles);
	}
	quadptr[history[idx].quadid] = q;
}

static void redo_remove(size_t idx)
{
	yrQuad* q = quadptr[history[idx].quadid];
	YR_ASSERT(q != NULL && q != unassigned);
	yrQuadStore_remove(q);
	quadptr[history[idx].quadid] = NULL;
}


/*****************
* Push/Pop actions
******************/
static uint32_t find_quad_id(yrQuad* q)
{
	for(uint32_t i = 0; i < UNDO_SIZE; ++i)
		if(quadptr[i] == q)
			return i;
	return UNDO_SIZE;
}

static uint32_t ref_quad(yrQuad* q)
{
	uint32_t id = find_quad_id(q);
	if(id == UNDO_SIZE) {
		id = find_quad_id(unassigned);
		quadptr[id] = q;
	}
	YR_ASSERT(id != UNDO_SIZE);
	quadref[id] += 1;
	return id;
}

static void release_quadid(uint32_t id)
{
	YR_ASSERT(id < UNDO_SIZE);
	YR_ASSERT(quadptr[id] != unassigned);
	YR_ASSERT(quadref[id] > 0);
	quadref[id] -= 1;
	if(quadref[id] == 0)
		quadptr[id] = unassigned;
}

static void push_quad(size_t idx, yrQuad* old, yrQuad* now)
{
	YR_ASSERT(old);
	YR_ASSERT(now);
	history[idx].type = utQuad;
	history[idx].quadid = ref_quad(now);
	history[idx].quaddata[0] = *old;
	history[idx].quaddata[1] = *now;
	history[idx].quaddata[0].flags &= QUAD_BG_AR11 | QUAD_BG_PIN | QUAD_BG_TILE | QUAD_VALID;
	history[idx].quaddata[1].flags &= QUAD_BG_AR11 | QUAD_BG_PIN | QUAD_BG_TILE | QUAD_VALID;
}

static void push_tile(size_t idx, yrQuad* q, uint32_t* old, uint32_t* now)
{
	YR_ASSERT(q);
	YR_ASSERT(old);
	YR_ASSERT(now);
	history[idx].type = utTile;
	history[idx].quadid = ref_quad(q);
	memcpy(history[idx].tiledata[0], old, 64 * sizeof(uint32_t));
	memcpy(history[idx].tiledata[1], now, 64 * sizeof(uint32_t));
}

static void push_both(size_t idx, yrQuad* oldq, yrQuad* nowq, uint32_t* oldt, uint32_t* nowt)
{
	YR_ASSERT(oldq);
	YR_ASSERT(nowq);
	YR_ASSERT(nowt);
	YR_ASSERT(nowt);
	history[idx].type = utBoth;
	history[idx].quadid = ref_quad(nowq);
	history[idx].quaddata[0] = *oldq;
	history[idx].quaddata[1] = *nowq;
	history[idx].quaddata[0].flags &= QUAD_BG_AR11 | QUAD_BG_PIN | QUAD_BG_TILE | QUAD_VALID;
	history[idx].quaddata[1].flags &= QUAD_BG_AR11 | QUAD_BG_PIN | QUAD_BG_TILE | QUAD_VALID;
	memcpy(history[idx].tiledata[0], oldt, 64 * sizeof(uint32_t));
	memcpy(history[idx].tiledata[1], nowt, 64 * sizeof(uint32_t));
}

static void push_add(size_t idx, yrQuad* q, uint32_t* t)
{
	YR_ASSERT(q);
	YR_ASSERT(t);
	history[idx].type = utAdd;
	history[idx].quadid = ref_quad(q);
	history[idx].quaddata[1] = *q;
	history[idx].quaddata[1].flags &= QUAD_BG_AR11 | QUAD_BG_PIN | QUAD_BG_TILE | QUAD_VALID;
	memcpy(history[idx].tiledata[1], t, 64 * sizeof(uint32_t));
}

static void push_remove(size_t idx, yrQuad* q, uint32_t* t)
{
	YR_ASSERT(q);
	YR_ASSERT(t);
	history[idx].type = utRemove;
	history[idx].quadid = ref_quad(q);
	history[idx].quaddata[0] = *q;
	history[idx].quaddata[0].flags &= QUAD_BG_AR11 | QUAD_BG_PIN | QUAD_BG_TILE | QUAD_VALID;
	memcpy(history[idx].tiledata[0], t, 64 * sizeof(uint32_t));
	quadptr[history[idx].quadid] = NULL;
}

static void delete_tilediff_past(size_t idx)
{
	uint32_t* oldt = history[idx].tiledata[0];
	uint32_t* newt = history[idx].tiledata[1];
	//delete the old tiles that aren't in the new tiles
	for(size_t oi = 0; oi < 64; ++oi) {
		if(oldt[oi]==0xFFFFFFFFul) continue;
		int found = 0;
		for(size_t ni = 0; !found && ni < 64; ++ni)
			if(oldt[oi] == newt[ni]) found = 1;
		if(!found) {
			TileID tid = {oldt[oi]};
			yrTileStore_remove(tid);
		}
	}
}

static void delete_tilediff_future(size_t idx)
{
	uint32_t* oldt = history[idx].tiledata[0];
	uint32_t* newt = history[idx].tiledata[1];
	//delete the new tiles that aren't in the old tiles
	for(size_t ni = 0; ni < 64; ++ni) {
		if(newt[ni]==0xFFFFFFFFul) continue;
		int found = 0;
		for(size_t oi = 0; !found && oi < 64; ++oi)
			if(oldt[oi] == newt[ni]) found = 1;
		if(!found) {
			TileID tid = {newt[ni]};
			yrTileStore_remove(tid);
		}
	}
}

static void pop_quad_past(size_t idx)
{
	YR_ASSERT(history[idx].type == utQuad);
	release_quadid(history[idx].quadid);
}

static void pop_tile_past(size_t idx)
{
	YR_ASSERT(history[idx].type == utTile);
	release_quadid(history[idx].quadid);
	//we can't undo the change to the new tiles so all tiles that have been overwritten are lost
	delete_tilediff_past(idx);
}

static void pop_both_past(size_t idx)
{
	YR_ASSERT(history[idx].type == utBoth);
	release_quadid(history[idx].quadid);
	delete_tilediff_past(idx);
}

static void pop_add_past(size_t idx)
{
	YR_ASSERT(history[idx].type == utAdd);
	release_quadid(history[idx].quadid);
}

static void pop_remove_past(size_t idx)
{
	YR_ASSERT(history[idx].type == utRemove);
	release_quadid(history[idx].quadid);
	//and now we can't undo the removal so the tiles are lost to us
	for(size_t i = 0; i < 64; ++i) {
		if(history[idx].tiledata[0][i] != 0xFFFFFFFFul) {
			TileID tid = {history[idx].tiledata[0][i]};
			yrTileStore_remove(tid);
		}
	}
}

static void pop_quad_future(size_t idx)
{
	YR_ASSERT(history[idx].type == utQuad);
	release_quadid(history[idx].quadid);
	//this future is lost to us now, as are all the new tiles in it...
	delete_tilediff_future(idx);
}

static void pop_tile_future(size_t idx)
{
	YR_ASSERT(history[idx].type == utTile);
	release_quadid(history[idx].quadid);
}

static void pop_both_future(size_t idx)
{
	YR_ASSERT(history[idx].type == utBoth);
	release_quadid(history[idx].quadid);
	delete_tilediff_future(idx);
}

static void pop_add_future(size_t idx)
{
	YR_ASSERT(history[idx].type == utAdd);
	release_quadid(history[idx].quadid);
	//this future is lost to us now, as are all the tiles that came with it...
	for(size_t i = 0; i < 64; ++i) {
		if(history[idx].tiledata[1][i] != 0xFFFFFFFFul) {
			TileID tid = {history[idx].tiledata[1][i]};
			yrTileStore_remove(tid);
		}
	}
}

static void pop_remove_future(size_t idx)
{
	YR_ASSERT(history[idx].type == utRemove);
	release_quadid(history[idx].quadid);
}

/*****************
* Public functions
******************/
void yrUndo_undo(void)
{
	if(history_now == history_lo) return;
	if(yrQuadStore_editlocked()) return;

	if(!history_now) history_now = UNDO_SIZE;
	history_now -= 1;

	switch(history[history_now].type) {
		case utQuad:	undo_quad(history_now); break;
		case utTile:	undo_tile(history_now); break;
		case utBoth:	undo_both(history_now); break;
		case utAdd:		undo_add(history_now); break;
		case utRemove:	undo_remove(history_now); break;
		default:
			yrLog(0, "Invalid undo action: %u", (unsigned)history[history_now].type);
			return;
	}
}

void yrUndo_redo(void)
{
	if(history_now == history_hi) return;
	if(yrQuadStore_editlocked()) return;

	switch(history[history_now].type) {
		case utQuad:	redo_quad(history_now); break;
		case utTile:	redo_tile(history_now); break;
		case utBoth:	redo_both(history_now); break;
		case utAdd:		redo_add(history_now); break;
		case utRemove:	redo_remove(history_now); break;
		default:
			yrLog(0, "Invalid redo action: %u", (unsigned)history[history_now].type);
			return;
	}
	history_now = (history_now + 1) % UNDO_SIZE;
}

void yrUndo_append(enum undo_type t, yrQuad* oldquad, yrQuad* newquad, uint32_t* oldtile, uint32_t* newtile)
{
	//wipe future because we'll rewrite it
	if(history_now != history_hi) {
		size_t i = history_hi;
		do {
			i = (i?(i-1):(UNDO_SIZE-1));
			switch(history[i].type) {
				case utQuad:	pop_quad_future(i); break;
				case utTile:	pop_tile_future(i); break;
				case utBoth:	pop_both_future(i); break;
				case utAdd:		pop_add_future(i); break;
				case utRemove:	pop_remove_future(i); break;
				default: yrLog(0, "Invalid undo action during clear: %u", (unsigned)history[i].type);
			}
		}
		while(i != history_now);
		history_hi = history_now;
	}
	//forget past to make room
	if(((history_hi+1)%UNDO_SIZE) == history_lo) {
		switch(history[history_lo].type) {
			case utQuad:	pop_quad_past(history_lo); break;
			case utTile:	pop_tile_past(history_lo); break;
			case utBoth:	pop_both_past(history_lo); break;
			case utAdd:		pop_add_past(history_lo); break;
			case utRemove:	pop_remove_past(history_lo); break;
			default: yrLog(0, "Invalid undo action during clear: %u", (unsigned)history[history_lo].type);
		}
		history_lo = (history_lo + 1) % UNDO_SIZE;
	}
	//apend the new action
	switch(t) {
		case utQuad:	push_quad(history_now, oldquad, newquad); break;
		case utTile:	push_tile(history_now, newquad, oldtile, newtile); break;
		case utBoth:	push_both(history_now, oldquad, newquad, oldtile, newtile); break;
		case utAdd:		push_add(history_now, newquad, newtile); break;
		case utRemove:	push_remove(history_now, oldquad, oldtile); break;
		default:
			yrLog(0, "Invalid undo action attempted to append: %u", (unsigned)t);
			return;
	}
	//adjust pointers
	history_now = (history_now + 1) % UNDO_SIZE;
	history_hi = history_now;
}

/**************
* Init/Shutdowm
***************/
static int yrUndo_init(void* loadtoken)
{
	history_lo = 0;
	history_hi = 0;
	history_now = 0;
	memset(quadptr, -1, UNDO_SIZE * sizeof(yrQuad*));

	return 0;
}

static int yrUndo_shutdown(void)
{
	//forget the past
	if(history_lo != history_now) {
		size_t i = history_lo;
		do {
			i = (i+1) % UNDO_SIZE;
			switch(history[i].type) {
				case utQuad:	pop_quad_past(i); break;
				case utTile:	pop_tile_past(i); break;
				case utBoth:	pop_both_past(i); break;
				case utAdd:		pop_add_past(i); break;
				case utRemove:	pop_remove_future(i); break;
				default: yrLog(0, "Invalid undo action during final clear: %u", (unsigned)history[i].type);
			}
		}
		while(i != history_now);
	}
	//erase the future
	if(history_now != history_hi) {
		size_t i = history_hi;
		do {
			i = (i?(i-1):(UNDO_SIZE-1));
			switch(history[i].type) {
				case utQuad:	pop_quad_future(i); break;
				case utTile:	pop_tile_future(i); break;
				case utBoth:	pop_both_future(i); break;
				case utAdd:		pop_add_future(i); break;
				case utRemove:	pop_remove_future(i); break;
				default: yrLog(0, "Invalid undo action during final clear: %u", (unsigned)history[i].type);
			}
		}
		while(i != history_now);
	}
	return 0;
}
