import { autorun, computed, observable } from "mobx";
import {
	ColorTheme,
	ColorThemeKind,
	commands,
	ConfigurationTarget,
	env,
	Memento,
	Uri,
	window,
	workspace,
} from "vscode";
import { Style, ColorScheme, DrawioLibraryData } from "./DrawioClient";
import { BufferImpl } from "./utils/buffer";
import { mapObject } from "./utils/mapObject";
import { SimpleTemplate } from "./utils/SimpleTemplate";
import {
	serializerWithDefault,
	VsCodeSetting,
} from "./vscode-utils/VsCodeSetting";
import * as packageJson from "../package.json";

const extensionId = "hediet.vscode-drawio";
const experimentalFeaturesEnabled = "vscode-drawio.experimentalFeaturesEnabled";

export async function setContext(
	key: string,
	value: string | boolean
): Promise<void> {
	return (await commands.executeCommand("setContext", key, value)) as any;
}

export class Config {
	public readonly packageJson: {
		version: string;
		versionName?: string;
		name: string;
		feedbackUrl?: string;
	} = packageJson;

	public get feedbackUrl(): Uri | undefined {
		if (this.packageJson.feedbackUrl) {
			return Uri.parse(this.packageJson.feedbackUrl);
		}
		return undefined;
	}

	public get isInsiders() {
		return (
			this.packageJson.name === "vscode-drawio-insiders-build" ||
			process.env.DEV === "1"
		);
	}

	@observable.ref
	private _vscodeTheme: ColorTheme;

	public get vscodeTheme(): ColorTheme {
		return this._vscodeTheme;
	}

	constructor(private readonly globalState: Memento) {
		autorun(() => {
			setContext(
				experimentalFeaturesEnabled,
				this.experimentalFeaturesEnabled
			);
		});

		this._vscodeTheme = window.activeColorTheme;
		window.onDidChangeActiveColorTheme((theme) => {
			this._vscodeTheme = theme;
		});
	}

	public getDiagramConfig(uri: Uri): DiagramConfig {
		return new DiagramConfig(uri, this, this.globalState);
	}

	private readonly _experimentalFeatures = new VsCodeSetting(
		`${extensionId}.enableExperimentalFeatures`,
		{
			serializer: serializerWithDefault<boolean>(false),
		}
	);

	public get experimentalFeaturesEnabled(): boolean {
		return this._experimentalFeatures.get();
	}

	public get canAskForFeedback(): boolean {
		if (
			this.getInternalConfig().versionLastAskedForFeedback ===
			this.packageJson.version
		) {
			return false;
		}
		const secondsIn20Minutes = 60 * 20;
		if (
			this.getInternalConfig().thisVersionUsageTimeInSeconds <
			secondsIn20Minutes
		) {
			return false;
		}
		return true;
	}

	public async markAskedToTest(): Promise<void> {
		await this.updateInternalConfig((config) => ({
			...config,
			versionLastAskedForFeedback: this.packageJson.version,
		}));
	}

	private readonly _knownPlugins = new VsCodeSetting<
		{ pluginId: string; fingerprint: string; allowed: boolean }[]
	>(`${extensionId}.knownPlugins`, {
		serializer: serializerWithDefault<any>([]),
		// Don't use workspace settings here!
		target: ConfigurationTarget.Global,
	});

	public isPluginAllowed(
		pluginId: string,
		fingerprint: string
	): boolean | undefined {
		const data = this._knownPlugins.get();
		const entry = data.find(
			(d) => d.pluginId === pluginId && d.fingerprint === fingerprint
		);
		if (!entry) {
			return undefined;
		}
		return entry.allowed;
	}

	public async addKnownPlugin(
		pluginId: string,
		fingerprint: string,
		allowed: boolean
	): Promise<void> {
		const plugins = [...this._knownPlugins.get()].filter(
			(p) => p.pluginId !== pluginId || p.fingerprint !== fingerprint
		);

		plugins.push({ pluginId, fingerprint, allowed });
		await this._knownPlugins.set(plugins);
	}

	public getUsageTimeInSeconds(): number {
		return this.getInternalConfig().totalUsageTimeInSeconds;
	}

	public getUsageTimeOfThisVersionInSeconds(): number {
		return this.getInternalConfig().thisVersionUsageTimeInSeconds;
	}

	public addUsageTime10Seconds(): void {
		this.updateInternalConfig((config) => {
			if (config.currentVersion !== this.packageJson.version) {
				config.currentVersion = this.packageJson.version;
				config.thisVersionUsageTimeInSeconds = 0;
			}

			return {
				...config,
				totalUsageTimeInSeconds: config.totalUsageTimeInSeconds + 10,
				thisVersionUsageTimeInSeconds:
					config.thisVersionUsageTimeInSeconds + 10,
			};
		});
	}

	public markAskedForSponsorship(): void {
		this.updateInternalConfig((c) => ({
			...c,
			dateTimeLastAskedForSponsorship: new Date().toDateString(),
			totalUsageTimeLastAskedForSponsorshipInSeconds:
				c.totalUsageTimeInSeconds,
		}));
	}

	public get canAskForSponsorship(): boolean {
		const c = this.getInternalConfig();
		if (c.dateTimeLastAskedForSponsorship) {
			const d = new Date(c.dateTimeLastAskedForSponsorship);
			const msPerDay = 1000 * 60 * 60 * 24;
			const minTimeBetweenAskingMs = 120 * msPerDay;
			if (new Date().getTime() - d.getTime() < minTimeBetweenAskingMs) {
				return false;
			}
		}
		let usageTimeSinceLastAskedForSponsorship = c.totalUsageTimeInSeconds;
		if (c.totalUsageTimeLastAskedForSponsorshipInSeconds !== undefined) {
			usageTimeSinceLastAskedForSponsorship -=
				c.totalUsageTimeLastAskedForSponsorshipInSeconds;
		}
		const secondsIn1Hr = 60 * 60;
		const minUsageTime = secondsIn1Hr;
		if (usageTimeSinceLastAskedForSponsorship < minUsageTime) {
			return false;
		}

		return true;
	}

	private getInternalConfig(): InternalConfig {
		return (
			this.globalState.get<InternalConfig>("config") || {
				totalUsageTimeInSeconds: 0,
				thisVersionUsageTimeInSeconds: 0,
				versionLastAskedForFeedback: undefined,
				dateTimeLastAskedForSponsorship: undefined,
				currentVersion: this.packageJson.version,
				totalUsageTimeLastAskedForSponsorshipInSeconds: 0,
			}
		);
	}

	private async setInternalConfig(config: InternalConfig): Promise<void> {
		await this.globalState.update("config", config);
	}

	private async updateInternalConfig(
		update: (oldConfig: InternalConfig) => InternalConfig
	): Promise<void> {
		const config = this.getInternalConfig();
		const updated = update(config);
		await this.setInternalConfig(updated);
	}
}

interface InternalConfig {
	totalUsageTimeInSeconds: number;
	thisVersionUsageTimeInSeconds: number;
	currentVersion: string;
	versionLastAskedForFeedback: string | undefined;
	dateTimeLastAskedForSponsorship: string | undefined;
	totalUsageTimeLastAskedForSponsorshipInSeconds: number | undefined;
}

export class DiagramConfig {
	//#region Styles

	private readonly _styles = new VsCodeSetting(`${extensionId}.styles`, {
		scope: this.uri,
		serializer: serializerWithDefault<Style[]>([]),
	});

	@computed
	public get styles(): Style[] {
		return this._styles.get();
	}

	//#endregion

	//#region Custom Color Schemes

	private readonly _customColorSchemes = new VsCodeSetting(
		`${extensionId}.customColorSchemes`,
		{
			scope: this.uri,
			serializer: serializerWithDefault<ColorScheme[][]>([]),
		}
	);

	@computed
	public get customColorSchemes(): ColorScheme[][] {
		return this._customColorSchemes.get();
	}

	//#endregion

	//#region Default Vertex Style

	private readonly _defaultVertexStyle = new VsCodeSetting(
		`${extensionId}.defaultVertexStyle`,
		{
			scope: this.uri,
			serializer: serializerWithDefault<Record<string, string>>({}),
		}
	);

	@computed
	public get defaultVertexStyle(): Record<string, string> {
		return this._defaultVertexStyle.get();
	}

	//#endregion

	//#region Default Edge Style

	private readonly _defaultEdgeStyle = new VsCodeSetting(
		`${extensionId}.defaultEdgeStyle`,
		{
			scope: this.uri,
			serializer: serializerWithDefault<Record<string, string>>({}),
		}
	);

	@computed
	public get defaultEdgeStyle(): Record<string, string> {
		return this._defaultEdgeStyle.get();
	}

	//#endregion

	//#region Color Names

	private readonly _colorNames = new VsCodeSetting(
		`${extensionId}.colorNames`,
		{
			scope: this.uri,
			serializer: serializerWithDefault<Record<string, string>>({}),
		}
	);

	@computed
	public get colorNames(): Record<string, string> {
		return this._colorNames.get();
	}

	//#endregion

	//#region Preset Colors

	private readonly _presetColors = new VsCodeSetting(
		`${extensionId}.presetColors`,
		{
			scope: this.uri,
			serializer: serializerWithDefault<string[]>([]),
		}
	);

	@computed
	public get presetColors(): string[] {
		return this._presetColors.get();
	}

	//#endregion

	// #region Theme

	private readonly _theme = new VsCodeSetting(`${extensionId}.theme`, {
		scope: this.uri,
		serializer: serializerWithDefault("automatic"),
	});

	@computed
	public get theme(): string {
		const theme = this._theme.get();

		if (theme !== "automatic") {
			return theme;
		}

		return {
			[ColorThemeKind.Light]: "Kennedy",
			[ColorThemeKind.Dark]: "dark",
			[ColorThemeKind.HighContrast]: "Kennedy",
		}[this.config.vscodeTheme.kind];
	}

	public async setTheme(value: string): Promise<void> {
		await this._theme.set(value);
	}

	// #endregion

	// #region Mode

	private readonly _useOfflineMode = new VsCodeSetting(
		`${extensionId}.offline`,
		{
			scope: this.uri,
			serializer: serializerWithDefault(true),
		}
	);

	private readonly _onlineUrl = new VsCodeSetting(
		`${extensionId}.online-url`,
		{
			scope: this.uri,
			serializer: serializerWithDefault("https://embed.diagrams.net/"),
		}
	);

	@computed
	public get mode(): { kind: "offline" } | { kind: "online"; url: string } {
		if (this._useOfflineMode.get()) {
			return { kind: "offline" };
		} else {
			return { kind: "online", url: this._onlineUrl.get() };
		}
	}

	// #endregion

	// #region Code Link Activated

	private readonly _codeLinkActivated = new VsCodeSetting(
		`${extensionId}.codeLinkActivated`,
		{
			scope: this.uri,
			serializer: serializerWithDefault(false),
		}
	);

	public get codeLinkActivated(): boolean {
		return this._codeLinkActivated.get();
	}

	public setCodeLinkActivated(value: boolean): Promise<void> {
		return this._codeLinkActivated.set(value);
	}

	// #endregion

	// #region Local Storage

	private readonly _localStorage = new VsCodeSetting<
		Record<string, string> | undefined
	>(`${extensionId}.local-storage`, {
		scope: this.uri,
		serializer: {
			deserialize: (value) => {
				if (value == undefined) {
					return undefined;
				}
				if (typeof value === "object") {
					// stringify setting
					// https://github.com/microsoft/vscode/issues/98001
					mapObject(value, (item) =>
						typeof item === "string" ? item : JSON.stringify(item)
					);
					return mapObject(value, (item) =>
						typeof item === "string" ? item : JSON.stringify(item)
					);
				} else {
					const str = BufferImpl.from(value || "", "base64").toString(
						"utf-8"
					);
					return JSON.parse(str);
				}
			},
			serializer: (val) => {
				if (val == undefined) {
					return undefined;
				}
				function tryJsonParse(val: string): string | any {
					try {
						return JSON.parse(val);
					} catch (e) {
						return val;
					}
				}

				if (process.env.DEV === "1") {
					// jsonify obj
					const val2 = mapObject(val, (item) => tryJsonParse(item));
					return val2;
				}

				return BufferImpl.from(JSON.stringify(val), "utf-8").toString(
					"base64"
				);
			},
		},
	});

	private migrateLocalStorageSettingsToMemento() {
		const saved = this._localStorage.get();
		if (!saved || Object.keys(saved).length === 0) {
			return;
		}
		this.setLocalStorage(saved);
		this._localStorage.set(undefined);
	}

	public get localStorage(): Record<string, string> {
		return this.memento.get<Record<string, string>>(
			`${extensionId}.local-storage`,
			{}
		);
	}

	public setLocalStorage(value: Record<string, string>): void {
		this.memento.update(`${extensionId}.local-storage`, value);
	}

	//#endregion

	private readonly _plugins = new VsCodeSetting<{ file: string }[]>(
		`${extensionId}.plugins`,
		{
			scope: this.uri,
			serializer: serializerWithDefault<any[]>([]),
		}
	);

	public get plugins(): { file: Uri }[] {
		return this._plugins.get().map((entry) => {
			const fullFilePath = this.evaluateTemplate(entry.file, "plugins");
			return { file: Uri.file(fullFilePath) };
		});
	}

	// #region Custom Libraries

	private readonly _customLibraries = new VsCodeSetting<
		DrawioCustomLibrary[]
	>(`${extensionId}.customLibraries`, {
		scope: this.uri,
		serializer: serializerWithDefault<DrawioCustomLibrary[]>([]),
	});

	@computed
	public get customLibraries(): Promise<DrawioLibraryData[]> {
		const normalizeLib = async (
			lib: DrawioCustomLibrary
		): Promise<DrawioLibraryData> => {
			function parseJson(json: string): unknown {
				return JSON.parse(json);
			}

			function parseXml(xml: string): unknown {
				const parse = require("xml-parser-xo");
				const parsedXml = parse(xml);
				return JSON.parse(parsedXml.root.children[0].content);
			}

			let data: DrawioLibraryData["data"];
			if ("json" in lib) {
				data = { kind: "value", value: parseJson(lib.json) };
			} else if ("xml" in lib) {
				data = {
					kind: "value",
					value: parseXml(lib.xml),
				};
			} else if ("file" in lib) {
				const file = this.evaluateTemplate(
					lib.file,
					"custom libraries"
				);
				const buffer = await workspace.fs.readFile(Uri.file(file));
				const content = BufferImpl.from(buffer).toString("utf-8");
				if (file.endsWith(".json")) {
					data = {
						kind: "value",
						value: parseJson(content),
					};
				} else {
					data = {
						kind: "value",
						value: parseXml(content),
					};
				}
			} else {
				data = { kind: "url", url: lib.url };
			}

			return {
				libName: lib.libName,
				entryId: lib.entryId,
				data,
			};
		};

		return Promise.all(
			this._customLibraries.get().map((lib) => normalizeLib(lib))
		);
	}

	private evaluateTemplate(template: string, context: string): string {
		const tpl = new SimpleTemplate(template);
		return tpl.render({
			workspaceFolder: () => {
				const workspaceFolder = workspace.getWorkspaceFolder(this.uri);
				if (!workspaceFolder) {
					throw new Error(
						`Cannot get workspace folder of opened diagram - '${template}' cannot be evaluated to load ${context}!`
					);
				}
				return workspaceFolder.uri.fsPath;
			},
		});
	}

	// #endregion

	// #region Custom Fonts

	private readonly _customFonts = new VsCodeSetting<string[]>(
		`${extensionId}.customFonts`,
		{
			scope: this.uri,
			serializer: serializerWithDefault<string[]>([]),
		}
	);

	@computed
	public get customFonts(): string[] {
		return this._customFonts.get();
	}

	// #endregion

	// #region Zoom Factor

	private readonly _zoomFactor = new VsCodeSetting<number>(
		`${extensionId}.zoomFactor`,
		{
			scope: this.uri,
			serializer: serializerWithDefault<number>(1.2),
		}
	);

	@computed
	public get zoomFactor(): number {
		return this._zoomFactor.get();
	}

	// #endregion

	// #region Global Variables

	private readonly _globalVars = new VsCodeSetting<object | null>(
		`${extensionId}.globalVars`,
		{
			scope: this.uri,
			serializer: serializerWithDefault<object | null>(null),
		}
	);

	@computed
	public get globalVars(): object | null {
		return this._globalVars.get();
	}

	// #endregion

	constructor(
		public readonly uri: Uri,
		private readonly config: Config,
		private readonly memento: Memento
	) {
		this.migrateLocalStorageSettingsToMemento();
	}

	@computed
	public get drawioLanguage(): string {
		if (env.language.toLowerCase() === "zh-tw") {
			// See https://github.com/hediet/vscode-drawio/issues/231.
			// Seems to be an exception, all other language codes are just the language, not the country.
			return "zh-tw";
		}
		const lang = env.language.split("-")[0].toLowerCase();
		return lang;
	}
}

type DrawioCustomLibrary = (
	| {
			xml: string;
	  }
	| {
			url: string;
	  }
	| {
			json: string;
	  }
	| {
			file: string;
	  }
) & { libName: string; entryId: string };