//TODO FIND OUT WHY I HAVE TO RESIZE A TEXTBOX AND THEN START USING IT TO AVOID THE 1px WHITE LINE ON LEFT EDGES DURING IMG2IMG
//...lmao did setting min width 200 on info div fix that accidentally?  once the canvas is infinite and the menu bar is hideable it'll probably be a problem again

/**
 * Workaround for Firefox bug #733698
 *
 * https://bugzilla.mozilla.org/show_bug.cgi?id=733698
 *
 * Workaround by https://github.com/subzey on https://gist.github.com/subzey/2030480
 *
 * Replaces and handles NS_ERROR_FAILURE errors triggered by 733698.
 */
(function () {
	var FakeTextMetrics,
		proto,
		fontSetterNative,
		measureTextNative,
		fillTextNative,
		strokeTextNative;

	if (
		!window.CanvasRenderingContext2D ||
		!window.TextMetrics ||
		!(proto = window.CanvasRenderingContext2D.prototype) ||
		!proto.hasOwnProperty("font") ||
		!proto.hasOwnProperty("mozTextStyle") ||
		typeof proto.__lookupSetter__ !== "function" ||
		!(fontSetterNative = proto.__lookupSetter__("font"))
	) {
		return;
	}

	proto.__defineSetter__("font", function (value) {
		try {
			return fontSetterNative.call(this, value);
		} catch (e) {
			if (e.name !== "NS_ERROR_FAILURE") {
				throw e;
			}
		}
	});

	measureTextNative = proto.measureText;
	FakeTextMetrics = function () {
		this.width = 0;
		this.isFake = true;
		this.__proto__ = window.TextMetrics.prototype;
	};
	proto.measureText = function ($0) {
		try {
			return measureTextNative.apply(this, arguments);
		} catch (e) {
			if (e.name !== "NS_ERROR_FAILURE") {
				throw e;
			} else {
				return new FakeTextMetrics();
			}
		}
	};

	fillTextNative = proto.fillText;
	proto.fillText = function ($0, $1, $2, $3) {
		try {
			fillTextNative.apply(this, arguments);
		} catch (e) {
			if (e.name !== "NS_ERROR_FAILURE") {
				throw e;
			}
		}
	};

	strokeTextNative = proto.strokeText;
	proto.strokeText = function ($0, $1, $2, $3) {
		try {
			strokeTextNative.apply(this, arguments);
		} catch (e) {
			if (e.name !== "NS_ERROR_FAILURE") {
				throw e;
			}
		}
	};
})();

// Parse url parameters
const urlParams = new URLSearchParams(window.location.search);

window.onload = startup;

var stableDiffusionData = {
	//includes img2img data but works for txt2img just fine
	prompt: "",
	negative_prompt: "",
	seed: -1,
	cfg_scale: null,
	sampler_index: "DDIM",
	steps: null,
	denoising_strength: 1,
	mask_blur: 0,
	batch_size: null,
	width: 512,
	height: 512,
	n_iter: null, // batch count
	mask: "",
	init_images: [],
	inpaint_full_res: false,
	inpainting_fill: 1,
	outpainting_fill: 2,
	enable_hr: false,
	restore_faces: false,
	//firstphase_width: 0,
	//firstphase_height: 0, //20230102 welp looks like the entire way HRfix is implemented has become bonkersly different
	hr_scale: 2.0,
	hr_upscaler: "None",
	hr_second_pass_steps: 0,
	hr_resize_x: 0,
	hr_resize_y: 0,
	hr_square_aspect: false,
	styles: [],
	// here's some more fields that might be useful

	// ---txt2img specific:
	// "enable_hr": false,   // hires fix
	// "denoising_strength": 0, // ok this is in both txt and img2img but txt2img only applies it if enable_hr == true
	// "firstphase_width": 0, // hires fp w
	// "firstphase_height": 0, // see above s/w/h/

	// ---img2img specific
	// "init_images": [ // imageS!??!? wtf maybe for batch img2img?? i just dump one base64 in here
	//     "string"
	// ],
	// "resize_mode": 0,
	// "denoising_strength": 0.75, // yeah see
	// "mask": "string", // string is just a base64 image
	// "mask_blur": 4,
	// "inpainting_fill": 0, // 0- fill, 1- orig, 2- latent noise, 3- latent nothing
	// "inpaint_full_res": true,
	// "inpaint_full_res_padding": 0, // px
	// "inpainting_mask_invert": 0, // bool??????? wtf
	// "include_init_images": false // ??????
};

// stuff things use
var host = "";
var url = "/sdapi/v1/";
const basePixelCount = 64; //64 px - ALWAYS 64 PX
var focused = true;

function startup() {
	testHostConfiguration();
	loadSettings();

	const hostEl = document.getElementById("host");
	testHostConnection().then((checkConnection) => {
		hostEl.onchange = () => {
			host = hostEl.value.endsWith("/")
				? hostEl.value.substring(0, hostEl.value.length - 1)
				: hostEl.value;
			hostEl.value = host;
			localStorage.setItem("openoutpaint/host", host);
			checkConnection();
		};
	});

	drawBackground();
	changeMaskBlur();
	changeSmoothRendering();
	changeSeed();
	changeHiResFix();
	changeHiResSquare();
	changeRestoreFaces();
	changeSyncCursorSize();
	checkFocus();
}

function setFixedHost(h, changePromptMessage) {
	console.info(`[index] Fixed host to '${h}'`);
	const hostInput = document.getElementById("host");
	host = h;
	hostInput.value = h;
	hostInput.readOnly = true;
	hostInput.style.cursor = "default";
	hostInput.style.backgroundColor = "#ddd";
	hostInput.addEventListener("dblclick", () => {
		if (confirm(changePromptMessage)) {
			hostInput.style.backgroundColor = null;
			hostInput.style.cursor = null;
			hostInput.readOnly = false;
			hostInput.focus();
		}
	});
}

/**
 * Initial connection checks
 */
function testHostConfiguration() {
	/**
	 * Check host configuration
	 */
	const hostEl = document.getElementById("host");
	hostEl.value = localStorage.getItem("openoutpaint/host");

	const requestHost = (prompt, def = "http://127.0.0.1:7860") => {
		let value = null;

		if (!urlParams.has("noprompt")) value = window.prompt(prompt, def);
		if (value === null) value = def;

		value = value.endsWith("/") ? value.substring(0, value.length - 1) : value;
		host = value;
		hostEl.value = host;
		localStorage.setItem("openoutpaint/host", host);
	};

	const current = localStorage.getItem("openoutpaint/host");
	if (current) {
		if (!current.match(/^https?:\/\/[a-z0-9][a-z0-9.]+[a-z0-9](:[0-9]+)?$/i))
			requestHost(
				"Host seems to be invalid! Please fix your host here:",
				current
			);
		else
			host = current.endsWith("/")
				? current.substring(0, current.length - 1)
				: current;
	} else {
		requestHost(
			"This seems to be the first time you are using openOutpaint! Please set your host here:"
		);
	}
}

async function testHostConnection() {
	class CheckInProgressError extends Error {}

	const connectionIndicator = document.getElementById(
		"connection-status-indicator"
	);

	let connectionStatus = false;
	let firstTimeOnline = true;

	const setConnectionStatus = (status) => {
		const connectionIndicatorText = document.getElementById(
			"connection-status-indicator-text"
		);

		const statuses = {
			online: () => {
				connectionIndicator.classList.add("online");
				connectionIndicator.classList.remove(
					"webui-issue",
					"offline",
					"before",
					"server-error"
				);
				connectionIndicatorText.textContent = connectionIndicator.title =
					"Connected";
				connectionStatus = true;
			},
			error: () => {
				connectionIndicator.classList.add("server-error");
				connectionIndicator.classList.remove(
					"online",
					"offline",
					"before",
					"webui-issue"
				);
				connectionIndicatorText.textContent = "Error";
				connectionIndicator.title =
					"Server is online, but is returning an error response";
				connectionStatus = false;
			},
			corsissue: () => {
				connectionIndicator.classList.add("webui-issue");
				connectionIndicator.classList.remove(
					"online",
					"offline",
					"before",
					"server-error"
				);
				connectionIndicatorText.textContent = "CORS";
				connectionIndicator.title =
					"Server is online, but CORS is blocking our requests";
				connectionStatus = false;
			},
			apiissue: () => {
				connectionIndicator.classList.add("webui-issue");
				connectionIndicator.classList.remove(
					"online",
					"offline",
					"before",
					"server-error"
				);
				connectionIndicatorText.textContent = "API";
				connectionIndicator.title =
					"Server is online, but the API seems to be disabled";
				connectionStatus = false;
			},
			offline: () => {
				connectionIndicator.classList.add("offline");
				connectionIndicator.classList.remove(
					"webui-issue",
					"online",
					"before",
					"server-error"
				);
				connectionIndicatorText.textContent = "Offline";
				connectionIndicator.title =
					"Server seems to be offline. Please check the console for more information.";
				connectionStatus = false;
			},
			before: () => {
				connectionIndicator.classList.add("before");
				connectionIndicator.classList.remove(
					"webui-issue",
					"online",
					"offline",
					"server-error"
				);
				connectionIndicatorText.textContent = "Waiting";
				connectionIndicator.title = "Waiting for check to complete.";
				connectionStatus = false;
			},
		};

		statuses[status] &&
			(() => {
				statuses[status]();
				global.connection = status;
			})();
	};

	setConnectionStatus("before");

	let checkInProgress = false;

	const checkConnection = async (
		notify = false,
		simpleProgressStatus = false
	) => {
		const apiIssueResult = () => {
			setConnectionStatus("apiissue");
			const message = `The host is online, but the API seems to be disabled.\nHave you run the webui with the flag '--api', or is the flag '--gradio-debug' currently active?`;
			console.error(message);
			if (notify) alert(message);
		};

		const offlineResult = () => {
			setConnectionStatus("offline");
			const message = `The connection with the host returned an error: ${response.status} - ${response.statusText}`;
			console.error(message);
			if (notify) alert(message);
		};
		if (checkInProgress)
			throw new CheckInProgressError(
				"Check is currently in progress, please try again"
			);
		checkInProgress = true;
		var url = document.getElementById("host").value + "/startup-events";
		// Attempt normal request
		try {
			if (simpleProgressStatus) {
				const response = await fetch(
					document.getElementById("host").value + "/sdapi/v1/progress" // seems to be the "lightest" endpoint?
				);
				switch (response.status) {
					case 200: {
						setConnectionStatus("online");
						break;
					}
					case 404: {
						apiIssueResult();
						break;
					}
					default: {
						offlineResult();
					}
				}
			} else {
				// Check if API is available
				const response = await fetch(
					document.getElementById("host").value + "/sdapi/v1/options"
				);
				const optionsdata = await response.json();
				if (optionsdata["use_scale_latent_for_hires_fix"]) {
					const message = `You are using an outdated version of A1111 webUI.\nThe HRfix options will not work until you update to at least commit ef27a18 or newer.\n(https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/ef27a18b6b7cb1a8eebdc9b2e88d25baf2c2414d)\nHRfix will fallback to half-resolution only.`;
					console.warn(message);
					if (notify) alert(message);
					// Hide all new hrfix options
					document
						.querySelectorAll(".hrfix")
						.forEach((el) => (el.style.display = "none"));

					// We are using old HRFix
					global.isOldHRFix = true;
					stableDiffusionData.enable_hr = false;
				}
				switch (response.status) {
					case 200: {
						setConnectionStatus("online");
						// Load data as soon as connection is first stablished
						if (firstTimeOnline) {
							getConfig();
							getStyles();
							getSamplers();
							getUpscalers();
							getModels();
							firstTimeOnline = false;
						}
						break;
					}
					case 404: {
						apiIssueResult();
						break;
					}
					default: {
						offlineResult();
					}
				}
			}
		} catch (e) {
			try {
				if (e instanceof DOMException) throw "offline";
				// Tests if problem is CORS
				await fetch(url, {mode: "no-cors"});

				setConnectionStatus("corsissue");
				const message = `CORS is blocking our requests. Try running the webui with the flag '--cors-allow-origins=${window.location.protocol}//${window.location.host}/'`;
				console.error(message);
				if (notify) alert(message);
			} catch (e) {
				setConnectionStatus("offline");
				const message = `The server seems to be offline. Is host '${
					document.getElementById("host").value
				}' correct?`;
				console.error(message);
				if (notify) alert(message);
			}
		}
		checkInProgress = false;
		return status;
	};

	if (focused || firstTimeOnline) {
		await checkConnection(!urlParams.has("noprompt"));
	}

	// On click, attempt to refresh
	connectionIndicator.onclick = async () => {
		try {
			await checkConnection(true);
			checked = true;
		} catch (e) {
			console.debug("Already refreshing");
		}
	};

	// Checks every 5 seconds if offline, 60 seconds if online
	const checkAgain = () => {
		checkFocus();
		if (focused || firstTimeOnline) {
			setTimeout(
				async () => {
					let simple = !firstTimeOnline;
					await checkConnection(false, simple);
					checkAgain();
				},
				connectionStatus ? 60000 : 5000
			);
		} else {
			setTimeout(() => {
				checkAgain();
			}, 60000);
		}
	};

	checkAgain();

	return () => {
		checkConnection().catch(() => {});
	};
}

function newImage(evt) {
	clearPaintedMask();
	uil.layers.forEach(({layer}) => {
		commands.runCommand(
			"eraseImage",
			"Clear Canvas",
			{
				...layer.bb,
				ctx: layer.ctx,
			},
			{
				extra: {
					log: `Cleared Canvas`,
				},
			}
		);
	});
}

function clearPaintedMask() {
	maskPaintLayer.clear();
}

function march(bb, options = {}) {
	defaultOpt(options, {
		title: null,
		titleStyle: "#FFF5",
		style: "#FFFF",
		width: "2px",
		filter: null,
	});

	const expanded = {...bb};
	expanded.x--;
	expanded.y--;
	expanded.w += 2;
	expanded.h += 2;

	// Get temporary layer to draw marching ants
	const layer = imageCollection.registerLayer(null, {
		bb: expanded,
		category: "display",
	});
	layer.canvas.style.imageRendering = "pixelated";
	let offset = 0;

	const interval = setInterval(() => {
		drawMarchingAnts(layer.ctx, bb, offset++, options);
		offset %= 12;
	}, 20);

	return () => {
		clearInterval(interval);
		imageCollection.deleteLayer(layer);
	};
}

function drawMarchingAnts(ctx, bb, offset, options) {
	ctx.save();

	ctx.clearRect(0, 0, bb.w + 2, bb.h + 2);

	// Draw Tool Name
	if (bb.h > 40 && options.title) {
		ctx.font = `bold 20px Open Sans`;

		ctx.textAlign = "left";
		ctx.fillStyle = options.titleStyle;
		ctx.fillText(options.title, 10, 30, bb.w);
	}

	ctx.strokeStyle = options.style;
	ctx.strokeWidth = options.width;
	ctx.filter = options.filter;
	ctx.setLineDash([4, 2]);
	ctx.lineDashOffset = -offset;
	ctx.strokeRect(1, 1, bb.w, bb.h);

	ctx.restore();
}

const makeSlider = (
	label,
	el,
	lsKey,
	min,
	max,
	step,
	defaultValue,
	textStep = null,
	valuecb = null
) => {
	const local = lsKey && localStorage.getItem(`openoutpaint/${lsKey}`);
	const def = parseFloat(local === null ? defaultValue : local);
	let cb = (v) => {
		stableDiffusionData[lsKey] = v;
		if (lsKey) localStorage.setItem(`openoutpaint/${lsKey}`, v);
	};
	if (valuecb) {
		cb = (v) => {
			valuecb(v);
			localStorage.setItem(`openoutpaint/${lsKey}`, v);
		};
	}
	return createSlider(label, el, {
		valuecb: cb,
		min,
		max,
		step,
		defaultValue: def,
		textStep,
	});
};

let modelAutoComplete = createAutoComplete(
	"Model",
	document.getElementById("models-ac-select"),
	{},
	document.getElementById("refreshModelsBtn"),
	"refreshable"
);
modelAutoComplete.onchange.on(({value}) => {
	if (value.toLowerCase().includes("inpainting"))
		document.querySelector(
			"#models-ac-select input.autocomplete-text"
		).style.backgroundColor = "#cfc";
	else
		document.querySelector(
			"#models-ac-select input.autocomplete-text"
		).style.backgroundColor = "#fcc";
});

const samplerAutoComplete = createAutoComplete(
	"Sampler",
	document.getElementById("sampler-ac-select")
);

const upscalerAutoComplete = createAutoComplete(
	"Upscaler",
	document.getElementById("upscaler-ac-select")
);

const hrFixUpscalerAutoComplete = createAutoComplete(
	"HRfix Upscaler",
	document.getElementById("hrFixUpscaler")
);

hrFixUpscalerAutoComplete.onchange.on(({value}) => {
	stableDiffusionData.hr_upscaler = value;
	localStorage.setItem(`openoutpaint/hr_upscaler`, value);
});

const resSlider = makeSlider(
	"Resolution",
	document.getElementById("resolution"),
	"resolution",
	128,
	2048,
	128,
	512,
	2,
	(v) => {
		stableDiffusionData.width = stableDiffusionData.height = v;

		toolbar.currentTool &&
			toolbar.currentTool.redraw &&
			toolbar.currentTool.redraw();
	}
);
makeSlider(
	"CFG Scale",
	document.getElementById("cfgScale"),
	"cfg_scale",
	localStorage.getItem("openoutpaint/settings.min-cfg") || 1,
	localStorage.getItem("openoutpaint/settings.max-cfg") || 25,
	0.5,
	7.0,
	0.1
);
makeSlider(
	"Batch Size",
	document.getElementById("batchSize"),
	"batch_size",
	1,
	8,
	1,
	2
);
makeSlider(
	"Iterations",
	document.getElementById("batchCount"),
	"n_iter",
	1,
	8,
	1,
	2
);
makeSlider(
	"Upscale X",
	document.getElementById("upscaleX"),
	"upscale_x",
	1.0,
	4.0,
	0.1,
	2.0,
	0.1
);

makeSlider(
	"Steps",
	document.getElementById("steps"),
	"steps",
	1,
	localStorage.getItem("openoutpaint/settings.max-steps") || 70,
	5,
	30,
	1
);

// 20230102 grumble grumble
const hrFixScaleSlider = makeSlider(
	"HRfix Scale",
	document.getElementById("hrFixScale"),
	"hr_scale",
	1.0,
	4.0,
	0.1,
	2.0,
	0.1
);

makeSlider(
	"HRfix Denoising",
	document.getElementById("hrDenoising"),
	"hr_denoising_strength",
	0.0,
	1.0,
	0.05,
	0.7,
	0.01
);

const lockPxSlider = makeSlider(
	"HRfix Autoscale Lock Px.",
	document.getElementById("hrFixLockPx"),
	"hr_fix_lock_px",
	0,
	1024,
	256,
	0,
	1
);

const hrStepsSlider = makeSlider(
	"HRfix Steps",
	document.getElementById("hrFixSteps"),
	"hr_second_pass_steps",
	0,
	localStorage.getItem("openoutpaint/settings.max-steps") || 70,
	5,
	0,
	1
);

function changeMaskBlur() {
	stableDiffusionData.mask_blur = parseInt(
		document.getElementById("maskBlur").value
	);
	localStorage.setItem("openoutpaint/mask_blur", stableDiffusionData.mask_blur);
}

function changeSeed() {
	stableDiffusionData.seed = document.getElementById("seed").value;
	localStorage.setItem("openoutpaint/seed", stableDiffusionData.seed);
}

function changeHRFX() {
	stableDiffusionData.hr_resize_x =
		document.getElementById("hr_resize_x").value;
}

function changeHRFY() {
	stableDiffusionData.hr_resize_y =
		document.getElementById("hr_resize_y").value;
}

function changeHiResFix() {
	stableDiffusionData.enable_hr = Boolean(
		document.getElementById("cbxHRFix").checked
	);
	localStorage.setItem("openoutpaint/enable_hr", stableDiffusionData.enable_hr);
	// var hrfSlider = document.getElementById("hrFixScale");
	// var hrfOpotions = document.getElementById("hrFixUpscaler");
	// var hrfLabel = document.getElementById("hrFixLabel");
	// var hrfDenoiseSlider = document.getElementById("hrDenoising");
	// var hrfLockPxSlider = document.getElementById("hrFixLockPx");
	if (stableDiffusionData.enable_hr) {
		document
			.querySelectorAll(".hrfix")
			.forEach((el) => el.classList.remove("invisible"));
	} else {
		document
			.querySelectorAll(".hrfix")
			.forEach((el) => el.classList.add("invisible"));
	}
}

function changeHiResSquare() {
	stableDiffusionData.hr_square_aspect = Boolean(
		document.getElementById("cbxHRFSquare").checked
	);
}

function changeRestoreFaces() {
	stableDiffusionData.restore_faces = Boolean(
		document.getElementById("cbxRestoreFaces").checked
	);
	localStorage.setItem(
		"openoutpaint/restore_faces",
		stableDiffusionData.restore_faces
	);
}

function changeSyncCursorSize() {
	global.syncCursorSize = Boolean(
		document.getElementById("cbxSyncCursorSize").checked
	);
	localStorage.setItem("openoutpaint/sync_cursor_size", global.syncCursorSize);
}

function changeSmoothRendering() {
	const layers = document.getElementById("layer-render");
	if (document.getElementById("cbxSmooth").checked) {
		layers.classList.remove("pixelated");
	} else {
		layers.classList.add("pixelated");
	}
}

function isCanvasBlank(x, y, w, h, canvas) {
	return !canvas
		.getContext("2d")
		.getImageData(x, y, w, h)
		.data.some((channel) => channel !== 0);
}

function drawBackground() {
	{
		// Existing Canvas BG
		const canvas = document.createElement("canvas");
		canvas.width = config.gridSize * 2;
		canvas.height = config.gridSize * 2;

		const ctx = canvas.getContext("2d");
		ctx.fillStyle = theme.grid.dark;
		ctx.fillRect(0, 0, canvas.width, canvas.height);
		ctx.fillStyle = theme.grid.light;
		ctx.fillRect(0, 0, config.gridSize, config.gridSize);
		ctx.fillRect(
			config.gridSize,
			config.gridSize,
			config.gridSize,
			config.gridSize
		);

		canvas.toBlob((blob) => {
			const url = window.URL.createObjectURL(blob);
			console.debug(url);
			bgLayer.canvas.style.backgroundImage = `url(${url})`;
		});
	}
}

async function exportWorkspaceState() {
	return {
		defaultLayer: {
			id: uil.layerIndex.default.id,
			name: uil.layerIndex.default.name,
		},
		bb: {
			x: imageCollection.bb.x,
			y: imageCollection.bb.y,
			w: imageCollection.bb.w,
			h: imageCollection.bb.h,
		},
		history: await commands.export(),
	};
}

async function importWorkspaceState(state) {
	// Start from zero, effectively
	await commands.clear();

	// Setup initial layer
	const layer = uil.layerIndex.default;
	layer.deletable = true;

	await commands.runCommand(
		"addLayer",
		"Temporary Layer",
		{name: "Temporary Layer", key: "tmp"},
		{recordHistory: false}
	);

	await commands.runCommand(
		"deleteLayer",
		"Deleted Layer",
		{
			layer,
		},
		{recordHistory: false}
	);

	await commands.runCommand(
		"addLayer",
		"Initial Layer Creation",
		{
			id: state.defaultLayer.id,
			name: state.defaultLayer.name,
			key: "default",
			deletable: false,
		},
		{recordHistory: false}
	);

	await commands.runCommand(
		"deleteLayer",
		"Deleted Layer",
		{
			layer: uil.layerIndex.tmp,
		},
		{recordHistory: false}
	);

	// Resize canvas to match original size
	const sbb = new BoundingBox(state.bb);

	const bb = imageCollection.bb;
	let eleft = 0;
	if (bb.x > sbb.x) eleft = bb.x - sbb.x;
	let etop = 0;
	if (bb.y > sbb.y) etop = bb.y - sbb.y;

	let eright = 0;
	if (bb.tr.x < sbb.tr.x) eright = sbb.tr.x - bb.tr.x;
	let ebottom = 0;
	if (bb.br.y < sbb.br.y) ebottom = sbb.br.y - bb.br.y;

	imageCollection.expand(eleft, etop, eright, ebottom);

	// Run commands in order
	for (const command of state.history) {
		await commands.import(command);
	}
}

async function saveWorkspaceToFile() {
	const workspace = await exportWorkspaceState();

	const blob = new Blob([JSON.stringify(workspace)], {
		type: "application/json",
	});

	const url = URL.createObjectURL(blob);
	var link = document.createElement("a"); // Or maybe get it from the current document
	link.href = url;
	link.download = `${new Date().toISOString()}_openOutpaint_workspace.json`;
	link.click();
}

async function getUpscalers() {
	var url = document.getElementById("host").value + "/sdapi/v1/upscalers";
	let upscalers = [];

	try {
		const response = await fetch(url, {
			method: "GET",
			headers: {
				Accept: "application/json",
				"Content-Type": "application/json",
			},
		});
		const data = await response.json();
		for (var i = 0; i < data.length; i++) {
			if (data[i].name.includes("None")) {
				continue;
			}
			upscalers.push(data[i].name);
		}
	} catch (e) {
		console.warn("[index] Failed to fetch upscalers:");
		console.warn(e);
		upscalers = [
			"Lanczos",
			"Nearest",
			"LDSR",
			"SwinIR",
			"R-ESRGAN General 4xV3",
			"R-ESRGAN General WDN 4xV3",
			"R-ESRGAN AnimeVideo",
			"R-ESRGAN 4x+",
			"R-ESRGAN 4x+ Anime6B",
			"R-ESRGAN 2x+",
		];
	}
	const upscalersPlusNone = [...upscalers];
	upscalersPlusNone.unshift("None");
	upscalersPlusNone.push("Latent");
	upscalersPlusNone.push("Latent (antialiased)");
	upscalersPlusNone.push("Latent (bicubic)");
	upscalersPlusNone.push("Latent (bicubic, antialiased)");
	upscalersPlusNone.push("Latent (nearest)");

	upscalerAutoComplete.options = upscalers.map((u) => {
		return {name: u, value: u};
	});
	hrFixUpscalerAutoComplete.options = upscalersPlusNone.map((u) => {
		return {name: u, value: u};
	});

	upscalerAutoComplete.value = upscalers[0];
	hrFixUpscalerAutoComplete.value =
		localStorage.getItem("openoutpaint/hr_upscaler") === null
			? "None"
			: localStorage.getItem("openoutpaint/hr_upscaler");
}

async function getModels(refresh = false) {
	const url = document.getElementById("host").value + "/sdapi/v1/sd-models";
	let opt = null;

	try {
		const response = await fetch(url);
		const data = await response.json();

		opt = data.map((option) => ({
			name: option.title,
			value: option.title,
			optionelcb: (el) => {
				if (option.title.toLowerCase().includes("inpainting"))
					el.classList.add("inpainting");
			},
		}));

		modelAutoComplete.options = opt;

		try {
			const optResponse = await fetch(
				document.getElementById("host").value + "/sdapi/v1/options"
			);
			const optData = await optResponse.json();

			const model = optData.sd_model_checkpoint;
			console.log("Current model: " + model);
			if (modelAutoComplete.value !== model) modelAutoComplete.value = model;
		} catch (e) {
			console.warn("[index] Failed to fetch current model:");
			console.warn(e);
		}
	} catch (e) {
		console.warn("[index] Failed to fetch models:");
		console.warn(e);
	}

	if (!refresh)
		modelAutoComplete.onchange.on(async ({value}) => {
			console.log(`[index] Changing model to [${value}]`);
			const payload = {
				sd_model_checkpoint: value,
			};
			const url = document.getElementById("host").value + "/sdapi/v1/options/";
			try {
				await fetch(url, {
					method: "POST",
					headers: {
						"Content-Type": "application/json",
					},
					body: JSON.stringify(payload),
				});

				alert(`Model changed to [${value}]`);
			} catch (e) {
				console.warn("[index] Error changing model");
				console.warn(e);

				alert(
					"Error changing model, please check console for additional information"
				);
			}
		});

	// If first time running, ask if user wants to switch to an inpainting model
	if (global.firstRun && !modelAutoComplete.value.includes("inpainting")) {
		const inpainting = opt.find(({name}) => name.includes("inpainting"));

		let message =
			"It seems this is your first time using openOutpaint. It is highly recommended that you switch to an inpainting model. \
			These are highlighted as green in the model selector.";

		if (inpainting) {
			message += `\n\nWe have found the inpainting model\n\n - ${inpainting.name}\n\navailable in the webui. Do you want to switch to it?`;
			if (confirm(message)) {
				modelAutoComplete.value = inpainting.value;
			}
		} else {
			message += `\n\nNo inpainting model seems to be available in the webui. It is recommended that you download an inpainting model, or outpainting results may not be optimal.`;
			alert(message);
		}
	}
}

async function getConfig() {
	var url = document.getElementById("host").value + "/sdapi/v1/options";

	let message =
		"The following options for the AUTOMATIC1111's webui are not recommended to use with this software:";

	try {
		const response = await fetch(url);

		const data = await response.json();

		let wrong = false;

		// Check if img2img color correction is disabled and inpainting mask weight is set to one
		// TODO: API Seems bugged for retrieving inpainting mask weight - returning 0 for all values different than 1.0
		if (data.img2img_color_correction) {
			message += "\n - Image to Image Color Correction: false recommended";
			wrong = true;
		}

		if (data.inpainting_mask_weight < 1.0) {
			message += `\n - Inpainting Conditioning Mask Strength: 1.0 recommended`;
			wrong = true;
		}

		message += "\n\nShould these values be changed to the recommended ones?";

		if (!wrong) {
			console.info("[index] WebUI Settings set as recommended.");
			return;
		}

		console.info(
			"[index] WebUI Settings not set as recommended. Prompting for changing settings automatically."
		);

		if (!confirm(message)) return;

		try {
			await fetch(url, {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
				},
				body: JSON.stringify({
					img2img_color_correction: false,
					inpainting_mask_weight: 1.0,
				}),
			});
		} catch (e) {
			console.warn("[index] Failed to fetch WebUI Configuration");
			console.warn(e);
		}
	} catch (e) {
		console.warn("[index] Failed to fetch WebUI Configuration");
		console.warn(e);
	}
}

function changeStyles() {
	/** @type {HTMLSelectElement} */
	const styleSelectEl = document.getElementById("styleSelect");
	const selected = Array.from(styleSelectEl.options).filter(
		(option) => option.selected
	);
	let selectedString = selected.map((option) => option.value);

	if (selectedString.find((selected) => selected === "None")) {
		selectedString = [];
		Array.from(styleSelectEl.options).forEach((option) => {
			if (option.value !== "None") option.selected = false;
		});
	}

	localStorage.setItem(
		"openoutpaint/promptStyle",
		JSON.stringify(selectedString)
	);

	// change the model
	if (selectedString.length > 0)
		console.log(`[index] Changing styles to ${selectedString.join(", ")}`);
	else console.log(`[index] Clearing styles`);
	stableDiffusionData.styles = selectedString;
}

async function getSamplers() {
	var url = document.getElementById("host").value + "/sdapi/v1/samplers";

	try {
		const response = await fetch(url);
		const data = await response.json();

		samplerAutoComplete.onchange.on(({value}) => {
			stableDiffusionData.sampler_index = value;
			localStorage.setItem("openoutpaint/sampler", value);
		});

		samplerAutoComplete.options = data.map((sampler) => ({
			name: sampler.name,
			value: sampler.name,
		}));

		// Initial sampler
		if (localStorage.getItem("openoutpaint/sampler") != null) {
			samplerAutoComplete.value = localStorage.getItem("openoutpaint/sampler");
		} else {
			samplerAutoComplete.value = data[0].name;
			localStorage.setItem("openoutpaint/sampler", samplerAutoComplete.value);
		}
		stableDiffusionData.sampler_index = samplerAutoComplete.value;
	} catch (e) {
		console.warn("[index] Failed to fetch samplers");
		console.warn(e);
	}
}
async function upscaleAndDownload() {
	// Future improvements: some upscalers take a while to upscale, so we should show a loading bar or something, also a slider for the upscale amount

	// get cropped canvas, send it to upscaler, download result
	var upscale_factor = localStorage.getItem("openoutpaint/upscale_x")
		? localStorage.getItem("openoutpaint/upscale_x")
		: 2;
	var upscaler = upscalerAutoComplete.value;
	var croppedCanvas = cropCanvas(
		uil.getVisible({
			x: 0,
			y: 0,
			w: imageCollection.size.w,
			h: imageCollection.size.h,
		})
	);
	if (croppedCanvas != null) {
		var url =
			document.getElementById("host").value + "/sdapi/v1/extra-single-image/";
		var imgdata = croppedCanvas.canvas.toDataURL("image/png");
		var data = {
			"resize-mode": 0, // 0 = just resize, 1 = crop and resize, 2 = resize and fill i assume based on theimg2img tabs options
			upscaling_resize: upscale_factor,
			upscaler_1: upscaler,
			image: imgdata,
		};
		console.log(data);
		await fetch(url, {
			method: "POST",
			headers: {
				Accept: "application/json",
				"Content-Type": "application/json",
			},
			body: JSON.stringify(data),
		})
			.then((response) => response.json())
			.then((data) => {
				console.log(data);
				var link = document.createElement("a");
				link.download =
					new Date()
						.toISOString()
						.slice(0, 19)
						.replace("T", " ")
						.replace(":", " ") +
					" openOutpaint image upscaler_" +
					upscaler +
					"_x" +
					upscale_factor +
					".png";
				link.href = "data:image/png;base64," + data["image"];
				link.click();
			});
	}
}

function loadSettings() {
	// set default values if not set
	var _mask_blur =
		localStorage.getItem("openoutpaint/mask_blur") == null
			? 0
			: localStorage.getItem("openoutpaint/mask_blur");
	var _seed =
		localStorage.getItem("openoutpaint/seed") == null
			? -1
			: localStorage.getItem("openoutpaint/seed");
	let _enable_hr =
		localStorage.getItem("openoutpaint/enable_hr") === null
			? false
			: localStorage.getItem("openoutpaint/enable_hr") === "true";
	let _restore_faces =
		localStorage.getItem("openoutpaint/restore_faces") === null
			? false
			: localStorage.getItem("openoutpaint/restore_faces") === "true";

	let _sync_cursor_size =
		localStorage.getItem("openoutpaint/sync_cursor_size") === null
			? true
			: localStorage.getItem("openoutpaint/sync_cursor_size") === "true";

	let _hrfix_scale =
		localStorage.getItem("openoutpaint/hr_scale") === null
			? 2.0
			: localStorage.getItem("openoutpaint/hr_scale");

	let _hrfix_denoising =
		localStorage.getItem("openoutpaint/hr_denoising_strength") === null
			? 0.7
			: localStorage.getItem("openoutpaint/hr_denoising_strength");
	let _hrfix_lock_px =
		localStorage.getItem("openoutpaint/hr_fix_lock_px") === null
			? 0
			: localStorage.getItem("openoutpaint/hr_fix_lock_px");

	// set the values into the UI
	document.getElementById("maskBlur").value = Number(_mask_blur);
	document.getElementById("seed").value = Number(_seed);
	document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);
	document.getElementById("cbxRestoreFaces").checked = Boolean(_restore_faces);
	document.getElementById("cbxSyncCursorSize").checked =
		Boolean(_sync_cursor_size);
	document.getElementById("hrFixScale").value = Number(_hrfix_scale);
	document.getElementById("hrDenoising").value = Number(_hrfix_denoising);
	document.getElementById("hrFixLockPx").value = Number(_hrfix_lock_px);
}

imageCollection.element.addEventListener(
	"wheel",
	(evn) => {
		evn.preventDefault();
	},
	{passive: false}
);

imageCollection.element.addEventListener(
	"contextmenu",
	(evn) => {
		evn.preventDefault();
	},
	{passive: false}
);

function resetToDefaults() {
	if (confirm("Are you sure you want to clear your settings?")) {
		localStorage.clear();
	}
}

document.addEventListener("visibilitychange", () => {
	checkFocus();
});

window.addEventListener("blur", () => {
	checkFocus();
});

window.addEventListener("focus", () => {
	checkFocus();
});

function checkFocus() {
	let hasFocus = document.hasFocus();
	if (document.hidden || !hasFocus) {
		focused = false;
	} else {
		focused = true;
	}
}

function changeScript(evt) {
	let enable = () => {
		scriptName.disabled = false;
	};
	let disable = () => {
		scriptName.disabled = true;
	};
	let selected = evt.target.value;
	let scriptName = document.getElementById("script-name-input");
	let scriptArgs = document.getElementById("script-args-input");
	scriptName.value = selected;
	disable();
	switch (selected) {
		case "Loopback": {
			scriptArgs.value = "[8, 0.99]";
			scriptArgs.title =
				"Params:\n" +
				"loops (int) //def: 8\n" +
				"denoising_strength_change_factor (decimal, 0.90-1.10) //def: 0.99";
			break;
		}
		case "Prompt matrix": {
			scriptArgs.value = "[false, false]";
			scriptArgs.title =
				"Params:\n" +
				"put_at_start (bool): expect pipe (|) delimited options at start of prompt //def: false\n" +
				"different_seeds (bool): use different seeds for each picture //def: false";
			break;
		}
		case "X/Y/Z plot": {
			scriptArgs.value =
				'[3, "0.00-0.99 [4]", 4, "5-30 [4]", 6, "2.5-12.5 [4]", false, true, false, false]';
			scriptArgs.title =
				"Params:\n" +
				"x_type (int): index of axis type (see below) //def: 3\n" +
				'x_values (mixed, str) //def: "0.00-0.99 [4]"\n' +
				"y_type (int) //def: 4\n" +
				'y_values (mixed, str) //def: "5-30 [4]"\n' +
				"z_type (int) //def: 5\n" +
				'z_values (mixed, str) //def: "2.5-12.5 [4]"\n' +
				"draw_legend (bool): return grid of all images //def: false\n" +
				"include_lone_images (bool): return individual images //def: true\n" +
				"include_subgrids (bool) //def: false\n" +
				"no_fixed_seeds (bool): use different seeds for each picture //def: false\n\n" +
				"Available axis types:\n" +
				"0: Nothing\n" +
				"1: Seed\n" +
				"2: Var. seed\n" +
				"3: Var. strength\n" +
				"4: Steps\n" +
				"5: Hires steps (txt2img only)\n" +
				"6: CFG Scale\n" +
				"7: Prompt S/R\n" +
				"8: Prompt order\n" +
				"9: Sampler (txt2img only)\n" +
				"10: Sampler (img2img only)\n" +
				"11: Checkpoint Name\n" +
				"12: Sigma Churn\n" +
				"13: Sigma min\n" +
				"14: Sigma max\n" +
				"15: Sigma noise\n" +
				"16: Eta\n" +
				"17: Clip skip\n" +
				"18: Denoising\n" +
				"19: Hires upscaler (txt2img only)\n" +
				"20: Cond. Image Mask Weight (img2img only)\n" +
				"21: VAE\n" +
				"22: Styles";
			break;
		}
		case "custom": {
			scriptName.value = "";
			scriptArgs.value = "";
			scriptArgs.title = "";
			enable();
			break;
		}
		case "":
		default: {
			scriptArgs.value = "";
			scriptArgs.title = "";
		}
	}
}