// Copyright (c) 2023 Alex Diener. All rights reserved.

#include "PROJECT_NAME/RoomState.h"
#include "PROJECT_NAME/RoomStateUndoStateDelta.h"
#include "PROJECT_NAME/Utilities.h"
#include "utilities/UndoUtilities.h"

#define stemobject_implementation RoomStateUndoStateDelta

stemobject_vtable_begin();
stemobject_vtable_entry(dispose);
stemobject_vtable_entry(revert);
stemobject_vtable_entry(apply);
stemobject_vtable_entry(copy);
stemobject_vtable_end();

typedef enum UndoStateDelta_changeType {
	CHANGE_TYPE_ADD,
	CHANGE_TYPE_REMOVE,
	CHANGE_TYPE_MODIFY
} UndoStateDelta_changeType;

struct RoomStateUndoStateDelta_entityChange {
	UndoStateDelta_changeType type;
	unsigned int index;
	struct GameEntity_vtableStruct * vtable;
	size_t size;
	char data[1];
};

RoomStateUndoStateDelta * RoomStateUndoStateDelta_create(RoomState * roomState, RoomStateUndoData * undoData) {
	stemobject_create_implementation(init, roomState, undoData)
}

static bool compareEntities(void * oldItem, void * newItem, unsigned int * outCompareResult, void * context) {
	struct RoomState_entity * oldRoomEntity = oldItem;
	struct RoomState_entity * newRoomEntity = newItem;
	GameEntity * oldEntity = oldRoomEntity->entity;
	GameEntity * newEntity = newRoomEntity->entity;
	if (oldEntity->entityID != newEntity->entityID) {
		return false;
	}
	*outCompareResult = !call_virtual(isEqual, oldEntity, newEntity);
	return true;
}

struct encodeChangeContext {
	ChunkArray * array;
	UndoStateDelta_changeType type;
	unsigned int index;
};

struct enumerateChangesContext {
	RoomStateUndoStateDelta * self;
	struct memwriteContext memwriteContext1;
	struct memwriteContext memwriteContext2;
};

static void addEntityChange(RoomStateUndoStateDelta * self, UndoStateDelta_changeType type, unsigned int index, struct GameEntity_vtableStruct * vtable, struct memwriteContext * memwriteContext) {
	struct RoomStateUndoStateDelta_entityChange * change = ChunkArray_append(&self->entityChanges, NULL, sizeof(*change) - sizeof(change->data) + memwriteContext->length);
	change->type = type;
	change->index = index;
	change->vtable = vtable;
	change->size = memwriteContext->length;
	memcpy(change->data, memwriteContext->data, memwriteContext->length);
	memwriteContext->length = memwriteContext->position = 0;
}

static void entityAddedCallback(unsigned int index, void * newItem, void * context) {
	struct enumerateChangesContext * contextStruct = context;
	RoomStateUndoStateDelta * self = contextStruct->self;
	struct RoomState_entity * roomEntity = newItem;
	call_virtual(encodeState, roomEntity->entity, &contextStruct->memwriteContext1, false);
	addEntityChange(self, CHANGE_TYPE_ADD, index, roomEntity->entity->vtable, &contextStruct->memwriteContext1);
}

static void entityRemovedCallback(unsigned int index, void * oldItem, void * context) {
	struct enumerateChangesContext * contextStruct = context;
	RoomStateUndoStateDelta * self = contextStruct->self;
	struct RoomState_entity * roomEntity = oldItem;
	call_virtual(encodeState, roomEntity->entity, &contextStruct->memwriteContext1, false);
	addEntityChange(self, CHANGE_TYPE_REMOVE, index, roomEntity->entity->vtable, &contextStruct->memwriteContext1);
}

static void entityModifiedCallback(unsigned int index, void * oldItem, void * newItem, unsigned int compareResult, void * context) {
	struct enumerateChangesContext * contextStruct = context;
	RoomStateUndoStateDelta * self = contextStruct->self;
	struct RoomState_entity * oldRoomEntity = oldItem;
	struct RoomState_entity * newRoomEntity = newItem;
	call_virtual(encodeState, oldRoomEntity->entity, &contextStruct->memwriteContext1, true);
	call_virtual(encodeState, newRoomEntity->entity, &contextStruct->memwriteContext2, true);
	memxor(contextStruct->memwriteContext1.data, contextStruct->memwriteContext2.data, contextStruct->memwriteContext1.length);
	contextStruct->memwriteContext2.length = contextStruct->memwriteContext2.position = 0;
	addEntityChange(self, CHANGE_TYPE_MODIFY, index, oldRoomEntity->entity->vtable, &contextStruct->memwriteContext1);
}

bool RoomStateUndoStateDelta_init(RoomStateUndoStateDelta * self, RoomState * roomState, RoomStateUndoData * undoData) {
	call_super(init, self);
	self->entityChanges = ChunkArray_init();
	struct enumerateChangesContext contextStruct = {self, memwriteContextInit(NULL, 0, 0, true), memwriteContextInit(NULL, 0, 0, true)};
	enumerateIdentifiableObjectChanges(undoData->entities,
	                                   undoData->entityCount,
	                                   roomState->entities,
	                                   roomState->entityCount,
	                                   sizeof(*roomState->entities),
	                                   compareEntities,
	                                   entityAddedCallback,
	                                   entityRemovedCallback,
	                                   entityModifiedCallback,
	                                   &contextStruct);
	free(contextStruct.memwriteContext1.data);
	free(contextStruct.memwriteContext2.data);
	return true;
}

void RoomStateUndoStateDelta_dispose(RoomStateUndoStateDelta * self) {
	ChunkArray_free(&self->entityChanges);
	call_super_virtual(dispose, self);
}

static void applyChange(RoomState * roomState, struct RoomStateUndoStateDelta_entityChange * change, UndoStateDelta_changeType addChangeType, UndoStateDelta_changeType removeChangeType, GameEntity ** createdEntities, unsigned int * ioCreatedEntityCount) {
	if (change->type == addChangeType) {
		struct memreadContext memreadContext = memreadContextInit(change->data, change->size);
		GameEntity * entity = change->vtable->createWithEncodedState(&memreadContext, roomState);
		RoomState_insertEntity(roomState, change->index, entity, true);
		createdEntities[*ioCreatedEntityCount] = entity;
		++*ioCreatedEntityCount;
		
	} else if (change->type == removeChangeType) {
		RoomState_removeEntityAtIndex(roomState, change->index);
		
	} else {
		struct memreadContext memreadContext = memreadContextInit(change->data, change->size);
		call_virtual(xorState, roomState->entities[change->index].entity, &memreadContext);
	}
}

void RoomStateUndoStateDelta_revert(RoomStateUndoStateDelta * self, void * target) {
	GameEntity * createdEntities[self->entityChanges.count];
	unsigned int createdEntityCount = 0;
	for (unsigned int changeIndex = self->entityChanges.count - 1; changeIndex < self->entityChanges.count; changeIndex--) {
		struct RoomStateUndoStateDelta_entityChange * change = ChunkArray_entryAtIndex(&self->entityChanges, changeIndex);
		applyChange(target, change, CHANGE_TYPE_REMOVE, CHANGE_TYPE_ADD, createdEntities, &createdEntityCount);
	}
	for (unsigned int entityIndex = 0; entityIndex < createdEntityCount; entityIndex++) {
		call_virtual(postinit, createdEntities[entityIndex]);
	}
}

void RoomStateUndoStateDelta_apply(RoomStateUndoStateDelta * self, void * target) {
	GameEntity * createdEntities[self->entityChanges.count];
	unsigned int createdEntityCount = 0;
	for (unsigned int changeIndex = 0; changeIndex < self->entityChanges.count; changeIndex++) {
		struct RoomStateUndoStateDelta_entityChange * change = ChunkArray_entryAtIndex(&self->entityChanges, changeIndex);
		applyChange(target, change, CHANGE_TYPE_ADD, CHANGE_TYPE_REMOVE, createdEntities, &createdEntityCount);
	}
	for (unsigned int entityIndex = 0; entityIndex < createdEntityCount; entityIndex++) {
		call_virtual(postinit, createdEntities[entityIndex]);
	}
}

static void RoomStateUndoStateDelta_initCopy(RoomStateUndoStateDelta * self, RoomStateUndoStateDelta * original) {
	self->entityChanges = ChunkArray_copy(&original->entityChanges);
}

RoomStateUndoStateDelta * RoomStateUndoStateDelta_copy(RoomStateUndoStateDelta * self) {
	stemobject_copy_implementation(initCopy)
}
