import {
	TableType,
} from "qrc:/js/lib/generated/enum";
import {
	isAnyTable,
	isStringIndexedInterface,
} from "qrc:/js/lib/generated/typeguard";
import {
	createCheckBoxRow,
	createDropDownRow,
	createLineEditRow,
} from "qrc:/js/lib/gui_form_widget";
import {
	getDirectoryPath,
	getOpenFilePath,
	getOpenFilePaths,
	getSaveFilePath,
	showError,
	showFormWidget,
	showWarning,
} from "qrc:/js/lib/gui_utils";
import {
	getTable,
	TableTypeMap,
} from "qrc:/js/lib/table_utils";
import {
	getKeysOfObject,
	isArray,
	isBoolean,
	isString,
	parseCsv,
	parseJson,
	readSetting,
	toCsvString,
	writeSetting,
} from "qrc:/js/lib/utils";
import {
	getSettingOrDefault,
} from "qrc:/js/lib/settings_table";

import {
	extractFileName,
	removeFileExtention,
} from "./gui_io_filesystem";
import {
	convertToAnyTable,
	isPotentialAnyTable,
	PotentialAnyTable,
} from "./table_import";

const settingsKeyFilePath = "table_io_file_path";
const settingsKeyFilesDirPath = "table_io_files_dir_path";
const settingsKeyServerUrl = "table_io_server_url";
const settingsKeyTableExport = "table_io_export_dir_path";

export function exportTablesToFile(): void {
	const path = (() => {
		const p = getSaveFilePath(wsi4.util.translate("table_export_path"), readSetting(settingsKeyFilePath, "", isString));
		return p === undefined ? p : p.endsWith(".dat") ? p : p + ".dat";
	})();
	if (path === undefined) {
		// User canceled
		return;
	}
	writeSetting(settingsKeyFilePath, path);
	// Note:  `wsi4.tables.isInternalTable(someType) === true` does not imply `!wsi4.tables.isExternalTable(someType)` as of now.
	const filteredTableTypes = Array.from(TableType)
		.filter(tableType => !wsi4.tables.isExternalTable(tableType));
	wsi4.io.fs.writeFile(path, wsi4.tables.serialize(filteredTableTypes));
}

interface TableImportDecision {
	type: TableType;
	confirmed: boolean;
}

function showImportConfirmationDialog(types: TableType[]): TableImportDecision[] | undefined {
	const dialogResult = showFormWidget(types.map(type => createCheckBoxRow(type, type, true)));
	return dialogResult === undefined ? undefined : types.map(type => {
		const confirmed = dialogResult.values[type];
		return !isBoolean(confirmed) ? wsi4.throwError("Result invalid") : {
			type: type,
			confirmed: confirmed,
		};
	});
}

export function importTablesFromFile(): void {
	const path = getOpenFilePath(wsi4.util.translate("table_import_path"), readSetting(settingsKeyFilePath, "", isString));
	if (path === undefined) {
		// User canceled
		return;
	}
	const ba = wsi4.io.fs.readFile(path);
	if (ba === undefined) {
		showError(wsi4.util.translate("FileOpenErrorTitle"), wsi4.util.translate("FileOpenErrorMessage") + "\n\n" + path);
		return;
	}

	const anyTables = wsi4.tables.deserialize(ba);
	if (anyTables === undefined) {
		showError(wsi4.util.translate("deserialization_error"), wsi4.util.translate("deserialization_error_message."));
		return;
	}
	anyTables.forEach(anyTable => wsi4.tables.setInternalTable(anyTable));
	wsi4.tables.commit();

	if (wsi4.tables.findErrors().length > 0) {
		showWarning(wsi4.util.translate("tables_inconsistent_title"), wsi4.util.translate("tables_inconsistent_text."));
	}
}

interface TypeAndArrayBuffer {
	type: TableType;
	data: ArrayBuffer;
}

function parseSiis(ab: ArrayBuffer): StringIndexedInterface[] | undefined {
	return parseJson(wsi4.util.arrayBufferToString(ab), (arg: unknown): arg is StringIndexedInterface[] => isArray(arg, isStringIndexedInterface));
}

function showInternalOrExternalSelectionDialog(): boolean | undefined {
	const key = "external";
	const dialogResult = showFormWidget([
		createCheckBoxRow(
			key,
			wsi4.util.translate("set_as_external_table(s)?"),
			true,
		),
	]);
	if (dialogResult === undefined) {
		// User canceled
		return undefined;
	}
	const value = dialogResult.values[key];
	return isBoolean(value) ? value : wsi4.throwError("Value invalid");
}

function importTablesFromPotentialAnyTables(input: PotentialAnyTable[]): void {
	const unfilteredAnyTables =
		input.map(potentialAnyTable => convertToAnyTable(potentialAnyTable, true))
			.filter((anyTableOpt: AnyTable | undefined): anyTableOpt is AnyTable => isAnyTable(anyTableOpt));

	if (unfilteredAnyTables.length === 0) {
		showError(wsi4.util.translate("table_import_error"), wsi4.util.translate("no_valid_table_found"));
		return;
	}

	const tableImportConfirmations = showImportConfirmationDialog(unfilteredAnyTables.map(anyTable => anyTable.type));
	if (tableImportConfirmations === undefined || tableImportConfirmations.every(tic => tic.confirmed === false)) {
		// User canceled
		return;
	}
	const filteredAnyTables = unfilteredAnyTables.filter(anyTable => tableImportConfirmations.some(tic => tic.type === anyTable.type && tic.confirmed));

	const setAsExternal = showInternalOrExternalSelectionDialog();
	if (setAsExternal === undefined) {
		// User canceled
		return;
	} else if (setAsExternal) {
		filteredAnyTables.forEach(anyTable => wsi4.tables.setExternalTable(anyTable));
	} else {
		filteredAnyTables.forEach(anyTable => wsi4.tables.setInternalTable(anyTable));
		wsi4.tables.commit();
	}

	if (wsi4.tables.findErrors().length > 0) {
		showWarning(wsi4.util.translate("tables_inconsistent_title"), wsi4.util.translate("tables_inconsistent_text."));
	}
}

function queryTableTypeInteractively(filePath?: string): TableType | undefined {
	const tableTypes = Array.from(TableType)
		.filter(type => type !== TableType.setting);
	const key = "type";
	const name = filePath === undefined ? wsi4.util.translate("table") : wsi4.util.translate("table_type_of_file:") + "\n\"" + filePath + "\"";
	const dialogResult = showFormWidget([
		createDropDownRow({
			name: name,
			key: key,
			values: tableTypes,
			toId: (tableType: string) => tableType,
			toName: (tableType: string) => wsi4.tables.name(tableType as TableType),
			computeInitialValueIndex: () => 0,
		}),
	]);
	return dialogResult === undefined ? undefined : tableTypes.find(tt => tt === dialogResult.values[key]);
}

type AllTableNameAliases = { [index in keyof TableTypeMap]: string[] };
const tableNameAliasMap: Partial<AllTableNameAliases> = {
	sheetMaterial: [ "globalMaterial" ],
	sheetMaterialDensity: [ "materialDensity" ],
	sheetCuttingMaterialMapping: [ "laserSheetCuttingMaterial" ],
	sheetBendingMaterialMapping: [ "bendMaterial" ],
};

function tableNames(tableType: TableType): string[] {
	const aliases: readonly string[] = tableNameAliasMap[tableType] ?? [];
	return [
		tableType,
		...aliases,
	];
}

function extractTypeInteractively(path: string): TableType | undefined {
	const stem = removeFileExtention(extractFileName(path));
	const typeOpt = Array.from(TableType)
		.find(tableType => tableNames(tableType)
			.some(tableName => tableName === stem));
	return typeOpt !== undefined ? typeOpt : queryTableTypeInteractively(path);
}

function getFilePaths(defaultPath: string, filter = ""): { type: TableType, path: string }[] {
	return getOpenFilePaths(wsi4.util.translate("table_import_file_selection"), defaultPath, filter)
		.map(path => ({
			type: extractTypeInteractively(path),
			path: path,
		}))
		.filter((arg): arg is { type: TableType, path: string } => arg.type !== undefined);
}

function writeFileDirectoryToSettings(settingsKey: string, filePath: string) {
	writeSetting(settingsKey, filePath.slice(0, filePath.lastIndexOf("/")));
}

export function importJsonTables(): void {
	const typesWithPaths = getFilePaths(readSetting(settingsKeyFilesDirPath, "", isString), "*.json *.JSON");
	if (typesWithPaths.length === 0) {
		return;
	}
	writeFileDirectoryToSettings(settingsKeyFilesDirPath, typesWithPaths[0]!.path);
	const anyTables = typesWithPaths.map(twp => ({
		type: twp.type,
		data: wsi4.io.fs.readFile(twp.path),
	}))
		.filter((arg): arg is TypeAndArrayBuffer => arg.data !== undefined)
		// First case: version JSON de-serialization
		// Second case: manual JSON import in case serialization_version is unused
		.map(arg => wsi4.tables.parseJson(arg.type, wsi4.util.arrayBufferToString(arg.data)) ?? {
			type: arg.type,
			content: parseSiis(arg.data),
		})
		.filter((arg): arg is PotentialAnyTable => isPotentialAnyTable(arg))
		.filter(arg => arg.content.length > 0);

	// Type cast is safe since an AnyTable's content is still an array of `StringIndexedInterface`s.
	importTablesFromPotentialAnyTables(anyTables as unknown as PotentialAnyTable[]);
}

export function importCsvTables(): void {
	const locale = getSettingOrDefault("csvLocale");
	const typesWithPaths = getFilePaths(readSetting(settingsKeyFilesDirPath, "", isString), "*.csv *.CSV");
	if (typesWithPaths.length === 0) {
		return;
	}
	writeFileDirectoryToSettings(settingsKeyFilesDirPath, typesWithPaths[0]!.path);
	importTablesFromPotentialAnyTables(typesWithPaths.map(twp => ({
		type: twp.type,
		data: wsi4.io.fs.readFile(twp.path),
	}))
		.filter((arg): arg is TypeAndArrayBuffer => arg.type !== undefined && arg.data !== undefined)
		.map(arg => ({
			type: arg.type,
			content: parseCsv(wsi4.util.arrayBufferToString(arg.data), locale),
		}))
		.filter((arg): arg is PotentialAnyTable => isPotentialAnyTable(arg))
		.filter(arg => arg.content.length > 0));
}

function exportInternalTables(targetTables: readonly TableType[], tableToString: (type: TableType) => string, fileExtention: Readonly<string>): void {
	const dirPath = getDirectoryPath(wsi4.util.translate("table_export"), readSetting(settingsKeyTableExport, "", isString));
	if (dirPath === undefined) {
		// User canceled;
		return;
	}

	writeSetting(settingsKeyTableExport, dirPath);

	targetTables.filter(type => !wsi4.tables.isExternalTable(type)
		// Settings table should not be user-visible
		&& type !== TableType.setting)
		.forEach(type => {
			const content = tableToString(type);
			const path = dirPath + "/" + type + "." + fileExtention;
			wsi4.io.fs.writeFile(path, wsi4.util.stringToArrayBuffer(content));
		});
}

function tableToCsv(type: TableType): string {
	const locale = getSettingOrDefault("csvLocale");
	const table = getTable(type);
	const keys = table.length === 0 ? [] : getKeysOfObject(table[0]!);
	const rows = table.map(row => keys.map(key => row[key])) as (string | number | boolean | undefined)[][];
	return toCsvString([
		keys,
		...rows,
	], locale);
}

export function exportCsvTables(): void {
	exportInternalTables(
		Array.from(TableType),
		tableToCsv,
		"csv",
	);
}

export function exportCsvTable(): void {
	const targetType = queryTableTypeInteractively();
	if (targetType !== undefined) {
		exportInternalTables(
			[ targetType ],
			tableToCsv,
			"csv",
		);
	}
}

function tableToJson(type: TableType): string {
	return wsi4.tables.toJson(type);
}

export function exportJsonTables(): void {
	exportInternalTables(
		Array.from(TableType),
		tableToJson,
		"json",
	);
}

export function exportJsonTable(): void {
	const targetType = queryTableTypeInteractively();
	if (targetType !== undefined) {
		exportInternalTables(
			[ targetType ],
			tableToJson,
			"json",
		);
	}
}

/**
 * Import tables from an HTTP server
 *
 * The user will be asked to enter the server's URL
 * The get requests URLs will be built like this: User-URL + "/" + TableType
 * Missing tables will be ignored.
 */
export function importTablesFromHttpServer(): void {
	const dialogResult = showFormWidget([
		createLineEditRow(
			"url",
			"Server URL",
			readSetting(settingsKeyServerUrl, "http://server", isString),
		),
	]);
	if (dialogResult === undefined) {
		return;
	}
	if (!isString(dialogResult.values["url"])) {
		return wsi4.throwError("Dialog result invalid");
	}
	const url = dialogResult.values["url"].endsWith("/") ? dialogResult.values["url"] : dialogResult.values["url"] + "/";
	writeSetting(settingsKeyServerUrl, url);

	interface TypeAndReply {
		type: TableType,
		reply: HttpReply,
	}

	const typesAndReplies: TypeAndReply[] = [];
	Array.from(TableType)
		.forEach(tableType => {
			const names = tableNames(tableType);
			names.forEach(tableName => typesAndReplies.push({
				type: tableType,
				reply: wsi4.io.net.http.get(url + tableName),
			}));
		});

	const potentialAnyTables = typesAndReplies.filter(typeAndReply => typeAndReply.reply.errorCode === 0)
		// Keep first successful reply for each table type
		.filter((lhs, index, self) => index === self.findIndex(rhs => lhs.type === rhs.type))
		.map(typeAndReply => ({
			type: typeAndReply.type,
			content: parseSiis(typeAndReply.reply.data),
		}))
		.filter((arg): arg is PotentialAnyTable => arg.content !== undefined);

	importTablesFromPotentialAnyTables(potentialAnyTables);
}
