import { Emitter } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { localize } from 'vs/nls';
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { ILocalizationsService } from 'vs/platform/localizations/common/localizations';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { Registry } from 'vs/platform/registry/common/platform';
import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { INodeProxyService, NodeProxyChannelClient } from 'vs/server/common/nodeProxy';
import { TelemetryChannelClient } from 'vs/server/common/telemetry';
import 'vs/workbench/contrib/localizations/browser/localizations.contribution';
import { LocalizationsService } from 'vs/workbench/services/localizations/electron-browser/localizationsService';
import { IRemoteAgentService } from 'vs/workbench/services/remote/common/remoteAgentService';
import { Options } from 'vs/server/ipc.d';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ILogService } from 'vs/platform/log/common/log';
import * as path from 'vs/base/common/path';

class TelemetryService extends TelemetryChannelClient {
	public constructor(
		@IRemoteAgentService remoteAgentService: IRemoteAgentService,
	) {
		super(remoteAgentService.getConnection()!.getChannel('telemetry'));
	}
}

/**
 * Remove extra slashes in a URL.
 */
export const normalize = (url: string, keepTrailing = false): string => {
	return url.replace(/\/\/+/g, '/').replace(/\/+$/, keepTrailing ? '/' : '');
};

/**
 * Get options embedded in the HTML.
 */
export const getOptions = <T extends Options>(): T => {
	try {
		return JSON.parse(document.getElementById('coder-options')!.getAttribute('data-settings')!);
	} catch (error) {
		return {} as T;
	}
};

const options = getOptions();

const TELEMETRY_SECTION_ID = 'telemetry';
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
	'id': TELEMETRY_SECTION_ID,
	'order': 110,
	'type': 'object',
	'title': localize('telemetryConfigurationTitle', 'Telemetry'),
	'properties': {
		'telemetry.enableTelemetry': {
			'type': 'boolean',
			'description': localize('telemetry.enableTelemetry', 'Enable usage data and errors to be sent to a Microsoft online service.'),
			'default': !options.disableTelemetry,
			'tags': ['usesOnlineServices']
		}
	}
});

class NodeProxyService extends NodeProxyChannelClient implements INodeProxyService {
	private readonly _onClose = new Emitter<void>();
	public readonly onClose = this._onClose.event;
	private readonly _onDown = new Emitter<void>();
	public readonly onDown = this._onDown.event;
	private readonly _onUp = new Emitter<void>();
	public readonly onUp = this._onUp.event;

	public constructor(
		@IRemoteAgentService remoteAgentService: IRemoteAgentService,
	) {
		super(remoteAgentService.getConnection()!.getChannel('nodeProxy'));
		remoteAgentService.getConnection()!.onDidStateChange((state) => {
			switch (state.type) {
				case PersistentConnectionEventType.ConnectionGain:
					return this._onUp.fire();
				case PersistentConnectionEventType.ConnectionLost:
					return this._onDown.fire();
				case PersistentConnectionEventType.ReconnectionPermanentFailure:
					return this._onClose.fire();
			}
		});
	}
}

registerSingleton(ILocalizationsService, LocalizationsService);
registerSingleton(INodeProxyService, NodeProxyService);
registerSingleton(ITelemetryService, TelemetryService);

/**
 * This is called by vs/workbench/browser/web.main.ts after the workbench has
 * been initialized so we can initialize our own client-side code.
 */
export const initialize = async (services: ServiceCollection): Promise<void> => {
	const event = new CustomEvent('ide-ready');
	window.dispatchEvent(event);

	if (parent) {
		// Tell the parent loading has completed.
		parent.postMessage({ event: 'loaded' }, '*');

		// Proxy or stop proxing events as requested by the parent.
		const listeners = new Map<string, (event: Event) => void>();
		window.addEventListener('message', (parentEvent) => {
			const eventName = parentEvent.data.bind || parentEvent.data.unbind;
			if (eventName) {
				const oldListener = listeners.get(eventName);
				if (oldListener) {
					document.removeEventListener(eventName, oldListener);
				}
			}

			if (parentEvent.data.bind && parentEvent.data.prop) {
				const listener = (event: Event) => {
					parent.postMessage({
						event: parentEvent.data.event,
						[parentEvent.data.prop]: event[parentEvent.data.prop as keyof Event]
					}, window.location.origin);
				};
				listeners.set(parentEvent.data.bind, listener);
				document.addEventListener(parentEvent.data.bind, listener);
			}
		});
	}

	if (!window.isSecureContext) {
		(services.get(INotificationService) as INotificationService).notify({
			severity: Severity.Warning,
			message: 'code-server is being accessed over an insecure domain. Web views, the clipboard, and other functionality will not work as expected.',
			actions: {
				primary: [{
					id: 'understand',
					label: 'I understand',
					tooltip: '',
					class: undefined,
					enabled: true,
					checked: true,
					dispose: () => undefined,
					run: () => {
						return Promise.resolve();
					}
				}],
			}
		});
	}

	const logService = (services.get(ILogService) as ILogService);
	const storageService = (services.get(IStorageService) as IStorageService);
	const updateCheckEndpoint = path.join(options.base, '/update/check');
	const getUpdate = async (): Promise<void> => {
		logService.debug('Checking for update...');

		const response = await fetch(updateCheckEndpoint, {
			headers: { 'Accept': 'application/json' },
		});
		if (!response.ok) {
			throw new Error(response.statusText);
		}
		const json = await response.json();
		if (json.error) {
			throw new Error(json.error);
		}
		if (json.isLatest) {
			return;
		}

		const lastNoti = storageService.getNumber('csLastUpdateNotification', StorageScope.GLOBAL);
		if (lastNoti) {
			// Only remind them again after 1 week.
			const timeout = 1000*60*60*24*7;
			const threshold = lastNoti + timeout;
			if (Date.now() < threshold) {
				return;
			}
		}

		storageService.store('csLastUpdateNotification', Date.now(), StorageScope.GLOBAL);
		(services.get(INotificationService) as INotificationService).notify({
			severity: Severity.Info,
			message: `[code-server v${json.latest}](https://github.com/cdr/code-server/releases/tag/v${json.latest}) has been released!`,
		});
	};

	const updateLoop = (): void => {
		getUpdate().catch((error) => {
			logService.debug(`failed to check for update: ${error}`);
		}).finally(() => {
			// Check again every 6 hours.
			setTimeout(updateLoop, 1000*60*60*6);
		});
	};

	if (!options.disableUpdateCheck) {
		updateLoop();
	}

	// This will be used to set the background color while VS Code loads.
	const theme = storageService.get('colorThemeData', StorageScope.GLOBAL);
	if (theme) {
		localStorage.setItem('colorThemeData', theme);
	}
};

export interface Query {
	[key: string]: string | undefined;
}

/**
 * Split a string up to the delimiter. If the delimiter doesn't exist the first
 * item will have all the text and the second item will be an empty string.
 */
export const split = (str: string, delimiter: string): [string, string] => {
	const index = str.indexOf(delimiter);
	return index !== -1 ? [str.substring(0, index).trim(), str.substring(index + 1)] : [str, ''];
};

/**
 * Return the URL modified with the specified query variables. It's pretty
 * stupid so it probably doesn't cover any edge cases. Undefined values will
 * unset existing values. Doesn't allow duplicates.
 */
export const withQuery = (url: string, replace: Query): string => {
	const uri = URI.parse(url);
	const query = { ...replace };
	uri.query.split('&').forEach((kv) => {
		const [key, value] = split(kv, '=');
		if (!(key in query)) {
			query[key] = value;
		}
	});
	return uri.with({
		query: Object.keys(query)
			.filter((k) => typeof query[k] !== 'undefined')
			.map((k) => `${k}=${query[k]}`).join('&'),
	}).toString(true);
};
