import { InkRunner, Utility, Processor } from "../inkrunner.js";

// in order to use this, you need to add a global list called "saveslots" to your story
// i.e. LIST saveslots = slot1, slot2, slot3
// use as many or as few slots as you like

let saveDisabled = false;
let slots = {};

// add the callback storage object during the StoryActive event
// which means that processors can add their callbacks during the StoryLoaded event
addEventListener(
	"StoryActive",
	(event) => {
		InkRunner.instance.saveloadcallbacks = {};
		InkRunner.instance.AddSaveLoadCallback = async (key, value) => (InkRunner.instance.saveloadcallbacks[key] = value);
		InkRunner.instance.AddProcessor(saveTag, loadTag, deleteTag);
	},
	{ once: true }
);

addEventListener(
	"StoryLoaded",
	() => {
		SetInkSlots();
	},
	{ once: true }
);

const SetInkSlots = () => {
	const slotcount = InkRunner.instance.story.variablesState["saveslots"].all.size;
	let inklist = InkRunner.instance.story.variablesState["saveslots"];
	localStorage.setItem("slotcount", slotcount);
	slots = [...inklist.origins[0]._items.keys()];
	for (let i = 1; i < slotcount + 1; i++) {
		if (localStorage.getItem(`slot${i}`) && !inklist.ContainsItemNamed(`slot${i}`)) inklist.AddItem(`slot${i}`);
	}
};

const SaveState = async (stateIndex) => {
	if (stateIndex === undefined) {
		InkRunner.Warn(`Cannot save: no saveslot index provided`);
		return;
	}
	if (InkRunner.instance.currentStatus !== InkRunner.Status.Idle && InkRunner.instance.currentStatus !== InkRunner.Status.Waiting) {
		InkRunner.Warn(`Cannot save: the story is not idle`);
		return;
	}
	let serializedDOM = new XMLSerializer().serializeToString(document.body);
	let serializedStoryState = await InkRunner.instance.story.state.toJson();
	let serializedProcessorStates = {};
	Object.keys(InkRunner.instance.saveloadcallbacks).forEach((processorName) => {
		if (InkRunner.instance.saveloadcallbacks[processorName].save && InkRunner.instance.saveloadcallbacks[processorName].save instanceof Function) {
			serializedProcessorStates[processorName] = JSON.stringify(InkRunner.instance.saveloadcallbacks[processorName].save());
		}
	});
	serializedProcessorStates = JSON.stringify(serializedProcessorStates);

	// the XMLSerializer escapes html characters in <script> tags
	serializedDOM = serializedDOM.replaceAll(/&amp;/g, "&");
	serializedDOM = serializedDOM.replaceAll(/&lt;/g, "<");
	serializedDOM = serializedDOM.replaceAll(/&gt;/g, ">");
	serializedDOM = serializedDOM.replaceAll(/&quot;/g, '"');
	serializedDOM = serializedDOM.replaceAll(/&apos;/g, "'");

	let saveState = {
		timestamp: Date.now(),
		dom: serializedDOM,
		storyState: serializedStoryState,
		currentContainerId: InkRunner.instance.currentContainer.id,
		processorStates: serializedProcessorStates,
	};

	localStorage.setItem(`slot${stateIndex}`, JSON.stringify(saveState));
	let inklist = InkRunner.instance.story.variablesState["saveslots"];
	if (!inklist.ContainsItemNamed(`slot${stateIndex}`)) inklist.AddItem(`slot${stateIndex}`);
};

const LoadState = async (stateIndex) => {
	if (stateIndex === undefined) {
		InkRunner.Warn(`Cannot load: no saveslot index provided`);
		return;
	}
	if (InkRunner.instance.currentStatus === InkRunner.Status.Active) {
		InkRunner.Warn(`Cannot load: the story is active`);
		return;
	}

	if (!InkRunner.instance.story.variablesState["saveslots"].ContainsItemNamed(`slot${stateIndex}`)) {
		InkRunner.Warn(`Cannot load: index ${stateIndex} is not occupied`);
		return;
	}

	let saveState = JSON.parse(localStorage.getItem(`slot${stateIndex}`));
	let processorStates = JSON.parse(saveState.processorStates);

	// restore the document body
	document.body.innerHTML = saveState.dom;

	// rebuild choice links
	document.body.querySelectorAll(`[data-ir-role="choice"]`).forEach((choiceElement) => {
		choiceElement.addEventListener(
			"click",
			(e) => {
				e.stopPropagation();
				InkRunner.instance.Choose(choiceElement.dataset.irChoiceIndex);
			},
			{ once: true, capture: true }
		);
	});

	// reset containers
	InkRunner.instance.defaultContainer = document.getElementById(InkRunner.instance.defaultContainer.id);
	InkRunner.instance.SetContainer(document.getElementById(saveState.currentContainerId));

	// handle processors
	Object.keys(processorStates).forEach((processorName) => {
		if (InkRunner.instance.saveloadcallbacks[processorName].load && InkRunner.instance.saveloadcallbacks[processorName].load instanceof Function) {
			InkRunner.instance.saveloadcallbacks[processorName].load(JSON.parse(processorStates[processorName]));
		}
	});

	// load story state
	await InkRunner.instance.story.state.LoadJson(saveState.storyState);
	InkRunner.instance.story.currentChoices.length > 0 ? InkRunner.instance.SetStatus(InkRunner.Status.Waiting) : InkRunner.instance.SetStatus(InkRunner.Status.Idle);
};

const DeleteState = async (stateIndex) => {
	if (stateIndex === undefined) {
		InkRunner.Warn(`Cannot delete: no saveslot index provided`);
		return;
	}
	let inklist = InkRunner.instance.story.variablesState["saveslots"];
	if (!inklist.ContainsItemNamed(`slot${stateIndex}`)) {
		InkRunner.Warn(`Cannot delete: index ${stateIndex} is not occupied`);
		return;
	}
	localStorage.removeItem(`slot${stateIndex}`);
	inklist.delete(slots[stateIndex]);
};

const saveTag = new Processor({
	name: "Save state",
	author: "isyourguy",
	description: "",
	tag: "saveState",
	type: Processor.Type.Tag,
	stage: InkRunner.ProcessingStage.Final,
	priority: -Infinity,
	callback: async (params) => {
		if (!params.tag.value) {
			InkRunner.Warn(`Save: no slot provided (i.e. #save: 1)`);
			return;
		}
		if (saveDisabled) {
			InkRunner.Warn(`Save: save disabled (did you create the "saveslots" list in ink?)`);
			return;
		}
		addEventListener("ContinueComplete", () => SaveState(params.tag.value[0]), { once: true });
	},
});

const loadTag = new Processor({
	name: "Load state",
	author: "isyourguy",
	description: "",
	tag: "loadState",
	type: Processor.Type.Tag,
	stage: InkRunner.ProcessingStage.Final,
	priority: -Infinity,
	callback: async (params) => {
		if (!params.tag.value) {
			InkRunner.Warn(`Load: no slot provided (i.e. #load: 1)`);
			return;
		}
		addEventListener("ContinueComplete", () => LoadState(params.tag.value[0]), { once: true });
	},
});

const deleteTag = new Processor({
	name: "Delete state",
	author: "isyourguy",
	description: "",
	tag: "deleteState",
	type: Processor.Type.Tag,
	stage: InkRunner.ProcessingStage.Final,
	priority: -Infinity,
	callback: async (params) => {
		if (!params.tag.value) {
			InkRunner.Warn(`Delete: no slot provided (i.e. #delete: 1)`);
			return;
		}
		addEventListener("ContinueComplete", () => params.tag.value.forEach((i) => DeleteState(i)), { once: true });
	},
});
