'use strict';
export * from './config';

import {
	ConfigurationChangeEvent,
	ConfigurationTarget,
	Event,
	EventEmitter,
	ExtensionContext,
	Uri,
	workspace
} from 'vscode';
import { Config } from './config';
import { extensionId } from './constants';
import { Objects } from './system';

const emptyConfig: Config = new Proxy<Config>({} as Config, {
	get: function() {
		return emptyConfig;
	}
});

type ConfigInspection<T> = {
	key: string;
	defaultValue?: T;
	globalValue?: T;
	workspaceValue?: T;
	workspaceFolderValue?: T;
};

export interface ConfigurationWillChangeEvent {
	change: ConfigurationChangeEvent;
	transform?(e: ConfigurationChangeEvent): ConfigurationChangeEvent;
}

export class Configuration {
	static configure(context: ExtensionContext) {
		context.subscriptions.push(
			workspace.onDidChangeConfiguration(configuration.onConfigurationChanged, configuration)
		);
	}

	private _onDidChange = new EventEmitter<ConfigurationChangeEvent>();
	get onDidChange(): Event<ConfigurationChangeEvent> {
		return this._onDidChange.event;
	}

	private _onDidChangeAny = new EventEmitter<ConfigurationChangeEvent>();
	get onDidChangeAny(): Event<ConfigurationChangeEvent> {
		return this._onDidChangeAny.event;
	}

	private _onWillChange = new EventEmitter<ConfigurationWillChangeEvent>();
	get onWillChange(): Event<ConfigurationWillChangeEvent> {
		return this._onWillChange.event;
	}

	private onConfigurationChanged(e: ConfigurationChangeEvent) {
		if (!e.affectsConfiguration(extensionId, null!)) {
			this._onDidChangeAny.fire(e);

			return;
		}

		const evt: ConfigurationWillChangeEvent = {
			change: e
		};
		this._onWillChange.fire(evt);

		if (evt.transform !== undefined) {
			e = evt.transform(e);
		}

		this._onDidChangeAny.fire(e);
		this._onDidChange.fire(e);
	}

	readonly initializingChangeEvent: ConfigurationChangeEvent = {
		affectsConfiguration: (section: string, resource?: Uri) => true
	};

	get(): Config;
	get<S1 extends keyof Config>(s1: S1, resource?: Uri | null, defaultValue?: Config[S1]): Config[S1];
	get<S1 extends keyof Config, S2 extends keyof Config[S1]>(
		s1: S1,
		s2: S2,
		resource?: Uri | null,
		defaultValue?: Config[S1][S2]
	): Config[S1][S2];
	get<S1 extends keyof Config, S2 extends keyof Config[S1], S3 extends keyof Config[S1][S2]>(
		s1: S1,
		s2: S2,
		s3: S3,
		resource?: Uri | null,
		defaultValue?: Config[S1][S2][S3]
	): Config[S1][S2][S3];
	get<
		S1 extends keyof Config,
		S2 extends keyof Config[S1],
		S3 extends keyof Config[S1][S2],
		S4 extends keyof Config[S1][S2][S3]
	>(
		s1: S1,
		s2: S2,
		s3: S3,
		s4: S4,
		resource?: Uri | null,
		defaultValue?: Config[S1][S2][S3][S4]
	): Config[S1][S2][S3][S4];
	get<T>(...args: any[]): T {
		let section: string | undefined;
		let resource: Uri | null | undefined;
		let defaultValue: T | undefined;
		if (args.length > 0) {
			section = args[0];
			if (typeof args[1] === 'string') {
				section += `.${args[1]}`;
				if (typeof args[2] === 'string') {
					section += `.${args[2]}`;
					if (typeof args[3] === 'string') {
						section += `.${args[3]}`;
						resource = args[4];
						defaultValue = args[5];
					} else {
						resource = args[3];
						defaultValue = args[4];
					}
				} else {
					resource = args[2];
					defaultValue = args[3];
				}
			} else {
				resource = args[1];
				defaultValue = args[2];
			}
		}

		return defaultValue === undefined
			? workspace
					.getConfiguration(section === undefined ? undefined : extensionId, resource)
					.get<T>(section === undefined ? extensionId : section)!
			: workspace
					.getConfiguration(section === undefined ? undefined : extensionId, resource)
					.get<T>(section === undefined ? extensionId : section, defaultValue)!;
	}

	getAny<T>(section: string, resource?: Uri | null, defaultValue?: T) {
		return defaultValue === undefined
			? workspace.getConfiguration(undefined, resource).get<T>(section)!
			: workspace.getConfiguration(undefined, resource).get<T>(section, defaultValue)!;
	}

	changed<S1 extends keyof Config>(e: ConfigurationChangeEvent, s1: S1, resource?: Uri | null): boolean;
	changed<S1 extends keyof Config, S2 extends keyof Config[S1]>(
		e: ConfigurationChangeEvent,
		s1: S1,
		s2: S2,
		resource?: Uri | null
	): boolean;
	changed<S1 extends keyof Config, S2 extends keyof Config[S1], S3 extends keyof Config[S1][S2]>(
		e: ConfigurationChangeEvent,
		s1: S1,
		s2: S2,
		s3: S3,
		resource?: Uri | null
	): boolean;
	changed<
		S1 extends keyof Config,
		S2 extends keyof Config[S1],
		S3 extends keyof Config[S1][S2],
		S4 extends keyof Config[S1][S2][S3]
	>(e: ConfigurationChangeEvent, s1: S1, s2: S2, s3: S3, s4: S4, resource?: Uri | null): boolean;
	changed(e: ConfigurationChangeEvent, ...args: any[]) {
		let section: string = args[0];
		let resource: Uri | null | undefined;
		if (typeof args[1] === 'string') {
			section += `.${args[1]}`;
			if (typeof args[2] === 'string') {
				section += `.${args[2]}`;
				if (typeof args[3] === 'string') {
					section += args[3];
					resource = args[4];
				} else {
					resource = args[3];
				}
			} else {
				resource = args[2];
			}
		} else {
			resource = args[1];
		}

		// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
		return e.affectsConfiguration(`${extensionId}.${section}`, resource!);
	}

	initializing(e: ConfigurationChangeEvent) {
		return e === this.initializingChangeEvent;
	}

	inspect<S1 extends keyof Config>(s1: S1, resource?: Uri | null): ConfigInspection<Config[S1]> | undefined;
	inspect<S1 extends keyof Config, S2 extends keyof Config[S1]>(
		s1: S1,
		s2: S2,
		resource?: Uri | null
	): ConfigInspection<Config[S1][S2]> | undefined;
	inspect<S1 extends keyof Config, S2 extends keyof Config[S1], S3 extends keyof Config[S1][S2]>(
		s1: S1,
		s2: S2,
		s3: S3,
		resource?: Uri | null
	): ConfigInspection<Config[S1][S2][S3]> | undefined;
	inspect<
		S1 extends keyof Config,
		S2 extends keyof Config[S1],
		S3 extends keyof Config[S1][S2],
		S4 extends keyof Config[S1][S2][S3]
	>(s1: S1, s2: S2, s3: S3, s4: S4, resource?: Uri | null): ConfigInspection<Config[S1][S2][S3][S4]> | undefined;
	inspect(...args: any[]) {
		let section: string = args[0];
		let resource: Uri | null | undefined;
		if (typeof args[1] === 'string') {
			section += `.${args[1]}`;
			if (typeof args[2] === 'string') {
				section += `.${args[2]}`;
				if (typeof args[3] === 'string') {
					section += args[3];
					resource = args[4];
				} else {
					resource = args[3];
				}
			} else {
				resource = args[2];
			}
		} else {
			resource = args[1];
		}

		return workspace
			.getConfiguration(section === undefined ? undefined : extensionId, resource)
			.inspect(section === undefined ? extensionId : section);
	}

	inspectAny(section: string, resource?: Uri | null) {
		return workspace.getConfiguration(undefined, resource).inspect(section);
	}

	migrate<S1 extends keyof Config>(
		from: string,
		to1: S1,
		options: { fallbackValue?: Config[S1]; migrationFn?(value: any): Config[S1] }
	): Promise<boolean>;
	migrate<S1 extends keyof Config, S2 extends keyof Config[S1]>(
		from: string,
		to1: S1,
		to2: S2,
		options: { fallbackValue?: Config[S1][S2]; migrationFn?(value: any): Config[S1][S2] }
	): Promise<boolean>;
	migrate<S1 extends keyof Config, S2 extends keyof Config[S1], S3 extends keyof Config[S1][S2]>(
		from: string,
		to1: S1,
		to2: S2,
		to3: S3,
		options: { fallbackValue?: Config[S1][S2][S3]; migrationFn?(value: any): Config[S1][S2][S3] }
	): Promise<boolean>;
	migrate<
		S1 extends keyof Config,
		S2 extends keyof Config[S1],
		S3 extends keyof Config[S1][S2],
		S4 extends keyof Config[S1][S2][S3]
	>(
		from: string,
		to1: S1,
		to2: S2,
		to3: S3,
		to4: S4,
		options: { fallbackValue?: Config[S1][S2][S3][S4]; migrationFn?(value: any): Config[S1][S2][S3][S4] }
	): Promise<boolean>;
	async migrate(from: string, ...args: any[]): Promise<boolean> {
		let to: string = args[0];
		let options: { fallbackValue?: any; migrationFn?(value: any): any } | undefined;
		if (typeof args[1] === 'string' && args.length > 3) {
			to += `.${args[1]}`;
			if (typeof args[2] === 'string' && args.length > 4) {
				to += `.${args[2]}`;
				if (typeof args[3] === 'string' && args.length > 5) {
					to += `.${args[3]}`;
					options = args[4];
				} else {
					options = args[3];
				}
			} else {
				options = args[2];
			}
		} else {
			options = args[1];
		}

		if (options === undefined) {
			options = {};
		}

		const inspection = configuration.inspect(from as any);
		if (inspection === undefined) return false;

		let migrated = false;
		if (inspection.globalValue !== undefined) {
			await this.update(
				to as any,
				options.migrationFn ? options.migrationFn(inspection.globalValue) : inspection.globalValue,
				ConfigurationTarget.Global
			);
			migrated = true;
			// Can't delete the old setting currently because it errors with `Unable to write to User Settings because <setting name> is not a registered configuration`
			// if (from !== to) {
			//     try {
			//         await this.update(from, undefined, ConfigurationTarget.Global);
			//     }
			//     catch { }
			// }
		}

		if (inspection.workspaceValue !== undefined) {
			await this.update(
				to as any,
				options.migrationFn ? options.migrationFn(inspection.workspaceValue) : inspection.workspaceValue,
				ConfigurationTarget.Workspace
			);
			migrated = true;
			// Can't delete the old setting currently because it errors with `Unable to write to User Settings because <setting name> is not a registered configuration`
			// if (from !== to) {
			//     try {
			//         await this.update(from, undefined, ConfigurationTarget.Workspace);
			//     }
			//     catch { }
			// }
		}

		if (inspection.workspaceFolderValue !== undefined) {
			await this.update(
				to as any,
				options.migrationFn
					? options.migrationFn(inspection.workspaceFolderValue)
					: inspection.workspaceFolderValue,
				ConfigurationTarget.WorkspaceFolder
			);
			migrated = true;
			// Can't delete the old setting currently because it errors with `Unable to write to User Settings because <setting name> is not a registered configuration`
			// if (from !== to) {
			//     try {
			//         await this.update(from, undefined, ConfigurationTarget.WorkspaceFolder);
			//     }
			//     catch { }
			// }
		}

		if (!migrated && options.fallbackValue !== undefined) {
			await this.update(to as any, options.fallbackValue, ConfigurationTarget.Global);
			migrated = true;
		}

		return migrated;
	}

	migrateIfMissing<S1 extends keyof Config>(
		from: string,
		to1: S1,
		options: { migrationFn?(value: any): Config[S1] }
	): Promise<void>;
	migrateIfMissing<S1 extends keyof Config, S2 extends keyof Config[S1]>(
		from: string,
		to1: S1,
		to2: S2,
		options: { migrationFn?(value: any): Config[S1][S2] }
	): Promise<void>;
	migrateIfMissing<S1 extends keyof Config, S2 extends keyof Config[S1], S3 extends keyof Config[S1][S2]>(
		from: string,
		to1: S1,
		to2: S2,
		to3: S3,
		options: { migrationFn?(value: any): Config[S1][S2][S3] }
	): Promise<void>;
	migrateIfMissing<
		S1 extends keyof Config,
		S2 extends keyof Config[S1],
		S3 extends keyof Config[S1][S2],
		S4 extends keyof Config[S1][S2][S3]
	>(
		from: string,
		to1: S1,
		to2: S2,
		to3: S3,
		to4: S4,
		options: { migrationFn?(value: any): Config[S1][S2][S3][S4] }
	): Promise<void>;
	async migrateIfMissing(from: string, ...args: any[]): Promise<void> {
		let to: string = args[0];
		let options: { migrationFn?(value: any): any } | undefined;
		if (typeof args[1] === 'string' && args.length > 3) {
			to += `.${args[1]}`;
			if (typeof args[2] === 'string' && args.length > 4) {
				to += `.${args[2]}`;
				if (typeof args[3] === 'string' && args.length > 5) {
					to += `.${args[3]}`;
					options = args[4];
				} else {
					options = args[3];
				}
			} else {
				options = args[2];
			}
		} else {
			options = args[1];
		}

		if (options === undefined) {
			options = {};
		}

		// async migrateIfMissing<TFrom, TTo>(from: string, to: string, options: { migrationFn?(value: TFrom): TTo } = {}) {
		const fromInspection = configuration.inspect(from as any);
		if (fromInspection === undefined) return;

		const toInspection = configuration.inspect(to as any);
		if (fromInspection.globalValue !== undefined) {
			if (toInspection === undefined || toInspection.globalValue === undefined) {
				await this.update(
					to as any,
					options.migrationFn ? options.migrationFn(fromInspection.globalValue) : fromInspection.globalValue,
					ConfigurationTarget.Global
				);
				// Can't delete the old setting currently because it errors with `Unable to write to User Settings because <setting name> is not a registered configuration`
				// if (from !== to) {
				//     try {
				//         await this.update(from, undefined, ConfigurationTarget.Global);
				//     }
				//     catch { }
				// }
			}
		}

		if (fromInspection.workspaceValue !== undefined) {
			if (toInspection === undefined || toInspection.workspaceValue === undefined) {
				await this.update(
					to as any,
					options.migrationFn
						? options.migrationFn(fromInspection.workspaceValue)
						: fromInspection.workspaceValue,
					ConfigurationTarget.Workspace
				);
				// Can't delete the old setting currently because it errors with `Unable to write to User Settings because <setting name> is not a registered configuration`
				// if (from !== to) {
				//     try {
				//         await this.update(from, undefined, ConfigurationTarget.Workspace);
				//     }
				//     catch { }
				// }
			}
		}

		if (fromInspection.workspaceFolderValue !== undefined) {
			if (toInspection === undefined || toInspection.workspaceFolderValue === undefined) {
				await this.update(
					to as any,
					options.migrationFn
						? options.migrationFn(fromInspection.workspaceFolderValue)
						: fromInspection.workspaceFolderValue,
					ConfigurationTarget.WorkspaceFolder
				);
				// Can't delete the old setting currently because it errors with `Unable to write to User Settings because <setting name> is not a registered configuration`
				// if (from !== to) {
				//     try {
				//         await this.update(from, undefined, ConfigurationTarget.WorkspaceFolder);
				//     }
				//     catch { }
				// }
			}
		}
	}

	name<S1 extends keyof Config>(s1: S1): string;
	name<S1 extends keyof Config, S2 extends keyof Config[S1]>(s1: S1, s2: S2): string;
	name<S1 extends keyof Config, S2 extends keyof Config[S1], S3 extends keyof Config[S1][S2]>(
		s1: S1,
		s2: S2,
		s3: S3
	): string;
	name<
		S1 extends keyof Config,
		S2 extends keyof Config[S1],
		S3 extends keyof Config[S1][S2],
		S4 extends keyof Config[S1][S2][S3]
	>(s1: S1, s2: S2, s3: S3, s4: S4): string;
	name(...args: string[]) {
		return args.join('.');
	}

	update<S1 extends keyof Config>(s1: S1, value: Config[S1] | undefined, target: ConfigurationTarget): Thenable<void>;
	update<S1 extends keyof Config, S2 extends keyof Config[S1]>(
		s1: S1,
		s2: S2,
		value: Config[S1][S2] | undefined,
		target: ConfigurationTarget
	): Thenable<void>;

	update<S1 extends keyof Config, S2 extends keyof Config[S1], S3 extends keyof Config[S1][S2]>(
		s1: S1,
		s2: S2,
		s3: S3,
		value: Config[S1][S2][S3] | undefined,
		target: ConfigurationTarget
	): Thenable<void>;
	update<
		S1 extends keyof Config,
		S2 extends keyof Config[S1],
		S3 extends keyof Config[S1][S2],
		S4 extends keyof Config[S1][S2][S3]
	>(
		s1: S1,
		s2: S2,
		s3: S3,
		s4: S4,
		value: Config[S1][S2][S3][S4] | undefined,
		target: ConfigurationTarget
	): Thenable<void>;
	update(...args: any[]) {
		let section: string = args[0];
		let value;
		let target: ConfigurationTarget;
		if (typeof args[1] === 'string' && args.length > 3) {
			section += `.${args[1]}`;
			if (typeof args[2] === 'string' && args.length > 4) {
				section += `.${args[2]}`;
				if (typeof args[3] === 'string' && args.length > 5) {
					section += `.${args[3]}`;
					value = args[4];
					target = args[5];
				} else {
					value = args[3];
					target = args[4];
				}
			} else {
				value = args[2];
				target = args[3];
			}
		} else {
			value = args[1];
			target = args[2];
		}

		return workspace.getConfiguration(extensionId).update(section, value, target);
	}

	updateAny(section: string, value: any, target: ConfigurationTarget, resource?: Uri | null) {
		return workspace
			.getConfiguration(undefined, target === ConfigurationTarget.Global ? undefined : resource!)
			.update(section, value, target);
	}

	updateEffective<S1 extends keyof Config>(s1: S1, value: Config[S1]): Thenable<void>;
	updateEffective<S1 extends keyof Config, S2 extends keyof Config[S1]>(
		s1: S1,
		s2: S2,
		value: Config[S1][S2]
	): Thenable<void>;
	updateEffective<S1 extends keyof Config, S2 extends keyof Config[S1], S3 extends keyof Config[S1][S2]>(
		s1: S1,
		s2: S2,
		s3: S3,
		value: Config[S1][S2][S3]
	): Thenable<void>;
	updateEffective<
		S1 extends keyof Config,
		S2 extends keyof Config[S1],
		S3 extends keyof Config[S1][S2],
		S4 extends keyof Config[S1][S2][S3]
	>(s1: S1, s2: S2, s3: S3, s4: S4, value: Config[S1][S2][S3][S4]): Thenable<void>;
	updateEffective(...args: any[]) {
		let section: string = args[0];
		let value;
		if (typeof args[1] === 'string' && args.length > 2) {
			section += `.${args[1]}`;
			if (typeof args[2] === 'string' && args.length > 3) {
				section += `.${args[2]}`;
				if (typeof args[3] === 'string' && args.length > 4) {
					section += `.${args[3]}`;
					value = args[4];
				} else {
					value = args[3];
				}
			} else {
				value = args[2];
			}
		} else {
			value = args[1];
		}

		const inspect = configuration.inspect(section as any)!;
		if (inspect.workspaceFolderValue !== undefined) {
			if (value === inspect.workspaceFolderValue) return Promise.resolve(undefined);

			return configuration.update(section as any, value, ConfigurationTarget.WorkspaceFolder);
		}

		if (inspect.workspaceValue !== undefined) {
			if (value === inspect.workspaceValue) return Promise.resolve(undefined);

			return configuration.update(section as any, value, ConfigurationTarget.Workspace);
		}

		if (inspect.globalValue === value || (inspect.globalValue === undefined && value === inspect.defaultValue)) {
			return Promise.resolve(undefined);
		}

		return configuration.update(
			section as any,
			Objects.areEquivalent(value, inspect.defaultValue) ? undefined : value,
			ConfigurationTarget.Global
		);
	}
}

export const configuration = new Configuration();
