/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the Source EULA. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import * as nls from 'vscode-nls';
import * as azdata from 'azdata';
import * as vscode from 'vscode';
import * as os from 'os';
import * as path from 'path';
import * as mssql from '../../mssql';
import { SchemaCompareOptionsDialog } from './dialogs/schemaCompareOptionsDialog';
import { Telemetry } from './telemetry';
import { getTelemetryErrorType, getEndpointName, verifyConnectionAndGetOwnerUri, getRootPath } from './utils';
import { SchemaCompareDialog } from './dialogs/schemaCompareDialog';
import { isNullOrUndefined } from 'util';
const localize = nls.loadMessageBundle();
const diffEditorTitle = localize('schemaCompare.CompareDetailsTitle', 'Compare Details');
const applyConfirmation = localize('schemaCompare.ApplyConfirmation', 'Are you sure you want to update the target?');
const reCompareToRefeshMessage = localize('schemaCompare.RecompareToRefresh', 'Press Compare to refresh the comparison.');
const generateScriptEnabledMessage = localize('schemaCompare.generateScriptEnabledButton', 'Generate script to deploy changes to target');
const generateScriptNoChangesMessage = localize('schemaCompare.generateScriptNoChanges', 'No changes to script');
const applyEnabledMessage = localize('schemaCompare.applyButtonEnabledTitle', 'Apply changes to target');
const applyNoChangesMessage = localize('schemaCompare.applyNoChanges', 'No changes to apply');
// Do not localize this, this is used to decide the icon for the editor.
// TODO : In future icon should be decided based on language id (scmp) and not resource name
const schemaCompareResourceName = 'Schema Compare';

enum ResetButtonState {
	noSourceTarget,
	beforeCompareStart,
	comparing,
	afterCompareComplete
}

export class SchemaCompareMainWindow {
	private differencesTable: azdata.TableComponent;
	private loader: azdata.LoadingComponent;
	private startText: azdata.TextComponent;
	private waitText: azdata.TextComponent;
	private editor: azdata.workspace.ModelViewEditor;
	private diffEditor: azdata.DiffEditorComponent;
	private splitView: azdata.SplitViewContainer;
	private flexModel: azdata.FlexContainer;
	private noDifferencesLabel: azdata.TextComponent;
	private sourceTargetFlexLayout: azdata.FlexContainer;
	private switchButton: azdata.ButtonComponent;
	private compareButton: azdata.ButtonComponent;
	private cancelCompareButton: azdata.ButtonComponent;
	private optionsButton: azdata.ButtonComponent;
	private generateScriptButton: azdata.ButtonComponent;
	private applyButton: azdata.ButtonComponent;
	private openScmpButton: azdata.ButtonComponent;
	private selectSourceButton: azdata.ButtonComponent;
	private selectTargetButton: azdata.ButtonComponent;
	private saveScmpButton: azdata.ButtonComponent;
	private SchemaCompareActionMap: Map<Number, string>;
	private operationId: string;
	private comparisonResult: mssql.SchemaCompareResult;
	private sourceNameComponent: azdata.TableComponent;
	private targetNameComponent: azdata.TableComponent;
	private deploymentOptions: mssql.DeploymentOptions;
	private schemaCompareOptionDialog: SchemaCompareOptionsDialog;
	private tablelistenersToDispose: vscode.Disposable[] = [];
	private originalSourceExcludes = new Map<string, mssql.DiffEntry>();
	private originalTargetExcludes = new Map<string, mssql.DiffEntry>();
	private sourceTargetSwitched = false;
	private sourceName: string;
	private targetName: string;
	private scmpSourceExcludes: mssql.SchemaCompareObjectId[];
	private scmpTargetExcludes: mssql.SchemaCompareObjectId[];

	public sourceEndpointInfo: mssql.SchemaCompareEndpointInfo;
	public targetEndpointInfo: mssql.SchemaCompareEndpointInfo;

	constructor(private schemaCompareService?: mssql.ISchemaCompareService, private extensionContext?: vscode.ExtensionContext) {
		this.SchemaCompareActionMap = new Map<Number, string>();
		this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Delete] = localize('schemaCompare.deleteAction', 'Delete');
		this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Change] = localize('schemaCompare.changeAction', 'Change');
		this.SchemaCompareActionMap[mssql.SchemaUpdateAction.Add] = localize('schemaCompare.addAction', 'Add');

		this.editor = azdata.workspace.createModelViewEditor(localize('schemaCompare.Title', 'Schema Compare'), { retainContextWhenHidden: true, supportsSave: true, resourceName: schemaCompareResourceName });
	}

	public async start(context: any) {
		// if schema compare was launched from a db, set that as the source
		let profile = context ? <azdata.IConnectionProfile>context.connectionProfile : undefined;
		if (profile) {
			let ownerUri = await azdata.connection.getUriForConnection((profile.id));
			this.sourceEndpointInfo = {
				endpointType: mssql.SchemaCompareEndpointType.Database,
				serverDisplayName: `${profile.serverName} ${profile.userName}`,
				serverName: profile.serverName,
				databaseName: profile.databaseName,
				ownerUri: ownerUri,
				packageFilePath: '',
				connectionDetails: undefined
			};
		}

		this.editor.registerContent(async view => {
			this.differencesTable = view.modelBuilder.table().withProperties({
				data: [],
				height: 300,
				title: localize('schemaCompare.differencesTableTitle', "Comparison between Source and Target")
			}).component();

			this.diffEditor = view.modelBuilder.diffeditor().withProperties({
				contentLeft: os.EOL,
				contentRight: os.EOL,
				height: 500,
				title: diffEditorTitle
			}).component();

			this.splitView = view.modelBuilder.splitViewContainer().component();

			let sourceTargetLabels = view.modelBuilder.flexContainer()
				.withProperties({
					alignItems: 'stretch',
					horizontal: true
				}).component();

			this.sourceTargetFlexLayout = view.modelBuilder.flexContainer()
				.withProperties({
					alignItems: 'stretch',
					horizontal: true
				}).component();

			this.createSwitchButton(view);
			this.createCompareButton(view);
			this.createCancelButton(view);
			this.createGenerateScriptButton(view);
			this.createApplyButton(view);
			this.createOptionsButton(view);
			this.createOpenScmpButton(view);
			this.createSaveScmpButton(view);
			this.createSourceAndTargetButtons(view);
			this.resetButtons(ResetButtonState.noSourceTarget);

			let toolBar = view.modelBuilder.toolbarContainer();
			toolBar.addToolbarItems([{
				component: this.compareButton
			}, {
				component: this.cancelCompareButton
			}, {
				component: this.generateScriptButton
			}, {
				component: this.applyButton
			}, {
				component: this.optionsButton,
				toolbarSeparatorAfter: true
			}, {
				component: this.switchButton,
				toolbarSeparatorAfter: true
			}, {
				component: this.openScmpButton
			}, {
				component: this.saveScmpButton
			}]);

			let sourceLabel = view.modelBuilder.text().withProperties({
				value: localize('schemaCompare.sourceLabel', 'Source'),
				CSSStyles: { 'margin-bottom': '0px' }
			}).component();

			let targetLabel = view.modelBuilder.text().withProperties({
				value: localize('schemaCompare.targetLabel', 'Target'),
				CSSStyles: { 'margin-bottom': '0px' }
			}).component();

			let arrowLabel = view.modelBuilder.text().withProperties({
				value: localize('schemaCompare.switchLabel', '➔')
			}).component();

			this.sourceName = getEndpointName(this.sourceEndpointInfo);
			this.targetName = ' ';
			this.sourceNameComponent = view.modelBuilder.table().withProperties({
				columns: [
					{
						value: this.sourceName,
						headerCssClass: 'no-borders',
						toolTip: this.sourceName
					},
				]
			}).component();

			this.targetNameComponent = view.modelBuilder.table().withProperties({
				columns: [
					{
						value: this.targetName,
						headerCssClass: 'no-borders',
						toolTip: this.targetName
					},
				]
			}).component();

			sourceTargetLabels.addItem(sourceLabel, { CSSStyles: { 'width': '55%', 'margin-left': '15px', 'font-size': 'larger', 'font-weight': 'bold' } });
			sourceTargetLabels.addItem(targetLabel, { CSSStyles: { 'width': '45%', 'font-size': 'larger', 'font-weight': 'bold' } });
			this.sourceTargetFlexLayout.addItem(this.sourceNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } });
			this.sourceTargetFlexLayout.addItem(this.selectSourceButton, { CSSStyles: { 'margin-top': '10px' } });
			this.sourceTargetFlexLayout.addItem(arrowLabel, { CSSStyles: { 'width': '10%', 'font-size': 'larger', 'text-align-last': 'center' } });
			this.sourceTargetFlexLayout.addItem(this.targetNameComponent, { CSSStyles: { 'width': '40%', 'height': '25px', 'margin-top': '10px', 'margin-left': '15px' } });
			this.sourceTargetFlexLayout.addItem(this.selectTargetButton, { CSSStyles: { 'margin-top': '10px' } });

			this.loader = view.modelBuilder.loadingComponent().component();
			this.waitText = view.modelBuilder.text().withProperties({
				value: localize('schemaCompare.waitText', 'Initializing Comparison. This might take a moment.')
			}).component();

			this.startText = view.modelBuilder.text().withProperties({
				value: localize('schemaCompare.startText', 'To compare two schemas, first select a source schema and target schema, then press Compare.')
			}).component();

			this.noDifferencesLabel = view.modelBuilder.text().withProperties({
				value: localize('schemaCompare.noDifferences', 'No schema differences were found.')
			}).component();

			this.flexModel = view.modelBuilder.flexContainer().component();
			this.flexModel.addItem(toolBar.component(), { flex: 'none' });
			this.flexModel.addItem(sourceTargetLabels, { flex: 'none' });
			this.flexModel.addItem(this.sourceTargetFlexLayout, { flex: 'none' });
			this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } });

			this.flexModel.setLayout({
				flexFlow: 'column',
				height: '100%'
			});

			await view.initializeModel(this.flexModel);
		});

		await this.GetDefaultDeploymentOptions();
		this.editor.openEditor();
	}

	// update source and target name to display
	public updateSourceAndTarget() {
		this.sourceName = getEndpointName(this.sourceEndpointInfo);
		this.targetName = getEndpointName(this.targetEndpointInfo);

		this.sourceNameComponent.updateProperty('columns', [
			{
				value: this.sourceName,
				headerCssClass: 'no-borders',
				toolTip: this.sourceName
			},
		]);
		this.targetNameComponent.updateProperty('columns', [
			{
				value: this.targetName,
				headerCssClass: 'no-borders',
				toolTip: this.targetName
			},
		]);

		// reset buttons to before comparison state
		this.resetButtons(ResetButtonState.beforeCompareStart);
	}

	// only for test
	public getComparisonResult(): mssql.SchemaCompareResult {
		return this.comparisonResult;
	}

	// only for test
	public getDeploymentOptions(): mssql.DeploymentOptions {
		return this.deploymentOptions;
	}

	public setDeploymentOptions(deploymentOptions: mssql.DeploymentOptions): void {
		this.deploymentOptions = deploymentOptions;
	}

	public async execute(): Promise<void> {
		Telemetry.sendTelemetryEvent('SchemaComparisonStarted');
		const service = await this.getService();
		if (!this.operationId) {
			// create once per page
			this.operationId = generateGuid();
		}
		this.comparisonResult = await service.schemaCompare(this.operationId, this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions);
		if (!this.comparisonResult || !this.comparisonResult.success) {
			Telemetry.sendTelemetryEventForError('SchemaComparisonFailed', {
				'errorType': getTelemetryErrorType(this.comparisonResult.errorMessage),
				'operationId': this.comparisonResult.operationId
			});
			vscode.window.showErrorMessage(localize('schemaCompare.compareErrorMessage', "Schema Compare failed: {0}", this.comparisonResult.errorMessage ? this.comparisonResult.errorMessage : 'Unknown'));
			return;
		}
		Telemetry.sendTelemetryEvent('SchemaComparisonFinished', {
			'endTime': Date.now().toString(),
			'operationId': this.comparisonResult.operationId
		});

		let data = this.getAllDifferences(this.comparisonResult.differences);

		this.differencesTable.updateProperties({
			data: data,
			columns: [
				{
					value: localize('schemaCompare.typeColumn', 'Type'),
					cssClass: 'align-with-header',
					width: 50
				},
				{
					value: localize('schemaCompare.sourceNameColumn', 'Source Name'),
					cssClass: 'align-with-header',
					width: 90
				},
				{
					value: localize('schemaCompare.includeColumnName', 'Include'),
					cssClass: 'align-with-header',
					width: 60,
					type: azdata.ColumnType.checkBox,
					options: { actionOnCheckbox: azdata.ActionOnCellCheckboxCheck.customAction }
				},
				{
					value: localize('schemaCompare.actionColumn', 'Action'),
					cssClass: 'align-with-header',
					width: 30
				},
				{
					value: localize('schemaCompare.targetNameColumn', 'Target Name'),
					cssClass: 'align-with-header',
					width: 150
				}
			],
			CSSStyles: { 'left': '15px' },
			width: '98%'
		});

		this.splitView.addItem(this.differencesTable);
		this.splitView.addItem(this.diffEditor);
		this.splitView.setLayout({
			orientation: 'vertical',
			splitViewHeight: 800
		});

		this.flexModel.removeItem(this.loader);
		this.flexModel.removeItem(this.waitText);
		this.resetButtons(ResetButtonState.afterCompareComplete);

		if (this.comparisonResult.differences.length > 0) {
			this.flexModel.addItem(this.splitView, { CSSStyles: { 'overflow': 'hidden' } });

			// only enable generate script button if the target is a db
			if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
				this.generateScriptButton.enabled = true;
				this.applyButton.enabled = true;
			} else {
				this.generateScriptButton.title = localize('schemaCompare.generateScriptButtonDisabledTitle', 'Generate script is enabled when the target is a database');
				this.applyButton.title = localize('schemaCompare.applyButtonDisabledTitle', 'Apply is enabled when the target is a database');
			}
		} else {
			this.flexModel.addItem(this.noDifferencesLabel, { CSSStyles: { 'margin': 'auto' } });
		}

		// explicitly exclude things that were excluded in previous compare
		const thingsToExclude = this.sourceTargetSwitched ? this.originalTargetExcludes : this.originalSourceExcludes;
		if (thingsToExclude) {
			thingsToExclude.forEach(item => {
				service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, item, false, azdata.TaskExecutionMode.execute);
			});

			// disable apply and generate script buttons if no changes are included
			if (thingsToExclude.size === this.comparisonResult.differences.length) {
				this.setButtonStatesForNoChanges(false);
			}
		}

		let sourceText = '';
		let targetText = '';
		this.tablelistenersToDispose.push(this.differencesTable.onRowSelected(() => {
			let difference = this.comparisonResult.differences[this.differencesTable.selectedRows[0]];
			if (difference !== undefined) {
				sourceText = this.getFormattedScript(difference, true);
				targetText = this.getFormattedScript(difference, false);

				this.diffEditor.updateProperties({
					contentLeft: sourceText,
					contentRight: targetText,
					title: diffEditorTitle
				});
			}
		}));
		this.tablelistenersToDispose.push(this.differencesTable.onCellAction(async (rowState) => {
			let checkboxState = <azdata.ICheckboxCellActionEventArgs>rowState;
			if (checkboxState) {
				let diff = this.comparisonResult.differences[checkboxState.row];
				await service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, diff, checkboxState.checked, azdata.TaskExecutionMode.execute);
				this.saveExcludeState(checkboxState);
			}
		}));
	}

	// save state based on source name if present otherwise target name (parity with SSDT)
	private saveExcludeState(rowState: azdata.ICheckboxCellActionEventArgs) {
		if (rowState) {
			let diff = this.comparisonResult.differences[rowState.row];
			let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue);
			if (key) {
				if (!this.sourceTargetSwitched) {
					this.originalSourceExcludes.delete(key);
					this.removeExcludeEntry(this.scmpSourceExcludes, key);

					if (!rowState.checked) {
						this.originalSourceExcludes.set(key, diff);
						if (this.originalSourceExcludes.size === this.comparisonResult.differences.length) {
							this.setButtonStatesForNoChanges(false);
						}
					}
					else {
						this.setButtonStatesForNoChanges(true);
					}
				}
				else {
					this.originalTargetExcludes.delete(key);
					this.removeExcludeEntry(this.scmpTargetExcludes, key);

					if (!rowState.checked) {
						this.originalTargetExcludes.set(key, diff);
						if (this.originalTargetExcludes.size === this.comparisonResult.differences.length) {
							this.setButtonStatesForNoChanges(false);
						}
						else {
							this.setButtonStatesForNoChanges(true);
						}
					}
				}
			}
		}
	}

	private shouldDiffBeIncluded(diff: mssql.DiffEntry): boolean {
		let key = (diff.sourceValue && diff.sourceValue.length > 0) ? this.createName(diff.sourceValue) : this.createName(diff.targetValue);
		if (key) {
			if (this.sourceTargetSwitched === true
				&& (this.originalTargetExcludes.has(key) || this.hasExcludeEntry(this.scmpTargetExcludes, key))) {
				this.originalTargetExcludes.set(key, diff);
				return false;
			}
			if (this.sourceTargetSwitched === false
				&& (this.originalSourceExcludes.has(key) || this.hasExcludeEntry(this.scmpSourceExcludes, key))) {
				this.originalSourceExcludes.set(key, diff);
				return false;
			}
			return true;
		}
		return true;
	}

	private hasExcludeEntry(collection: mssql.SchemaCompareObjectId[], entryName: string): boolean {
		let found = false;
		if (collection) {
			const index = collection.findIndex(e => this.createName(e.nameParts) === entryName);
			found = index !== -1;
		}
		return found;
	}

	private removeExcludeEntry(collection: mssql.SchemaCompareObjectId[], entryName: string) {
		if (collection) {
			console.error('removing ' + entryName);
			const index = collection.findIndex(e => this.createName(e.nameParts) === entryName);
			collection.splice(index, 1);
		}
	}

	private getAllDifferences(differences: mssql.DiffEntry[]): string[][] {
		let data = [];
		let finalDifferences: mssql.DiffEntry[] = [];
		if (differences) {
			differences.forEach(difference => {
				if (difference.differenceType === mssql.SchemaDifferenceType.Object) {
					if ((difference.sourceValue !== null && difference.sourceValue.length > 0) || (difference.targetValue !== null && difference.targetValue.length > 0)) {
						finalDifferences.push(difference); // Add only non-null changes to ensure index does not mismatch between dictionay and UI - #6234
						let state: boolean = this.shouldDiffBeIncluded(difference);
						data.push([difference.name, this.createName(difference.sourceValue), state, this.SchemaCompareActionMap[difference.updateAction], this.createName(difference.targetValue)]);
					}
				}
			});
		}
		this.comparisonResult.differences = finalDifferences;
		return data;
	}

	private createName(nameParts: string[]): string {
		if (isNullOrUndefined(nameParts) || nameParts.length === 0) {
			return '';
		}
		return nameParts.join('.');
	}

	private getFormattedScript(diffEntry: mssql.DiffEntry, getSourceScript: boolean): string {
		// if there is no entry, the script has to be \n because an empty string shows up as a difference but \n doesn't
		if ((getSourceScript && diffEntry.sourceScript === null)
			|| (!getSourceScript && diffEntry.targetScript === null)) {
			return '\n';
		}

		let script = this.getAggregatedScript(diffEntry, getSourceScript);
		return script;
	}

	private getAggregatedScript(diffEntry: mssql.DiffEntry, getSourceScript: boolean): string {
		let script = '';
		if (diffEntry !== null) {
			let diffEntryScript = getSourceScript ? diffEntry.sourceScript : diffEntry.targetScript;
			if (diffEntryScript) {
				// add a blank line between each statement
				script += diffEntryScript + '\n\n';
			}

			diffEntry.children.forEach(child => {
				let childScript = this.getAggregatedScript(child, getSourceScript);
				script += childScript;
			});
		}
		return script;
	}

	public startCompare(): void {
		this.flexModel.removeItem(this.splitView);
		this.flexModel.removeItem(this.noDifferencesLabel);
		this.flexModel.removeItem(this.startText);
		this.flexModel.addItem(this.loader, { CSSStyles: { 'margin-top': '30px' } });
		this.flexModel.addItem(this.waitText, { CSSStyles: { 'margin-top': '30px', 'align-self': 'center' } });
		this.diffEditor.updateProperties({
			contentLeft: os.EOL,
			contentRight: os.EOL,
			title: diffEditorTitle
		});

		this.differencesTable.selectedRows = null;
		if (this.tablelistenersToDispose) {
			this.tablelistenersToDispose.forEach(x => x.dispose());
		}
		this.resetButtons(ResetButtonState.comparing);
		this.execute();
	}

	private createCompareButton(view: azdata.ModelView): void {
		this.compareButton = view.modelBuilder.button().withProperties({
			label: localize('schemaCompare.compareButton', 'Compare'),
			iconPath: {
				light: path.join(this.extensionContext.extensionPath, 'media', 'compare.svg'),
				dark: path.join(this.extensionContext.extensionPath, 'media', 'compare-inverse.svg')
			},
			title: localize('schemaCompare.compareButtonTitle', 'Compare')
		}).component();

		this.compareButton.onDidClick(async (click) => {
			this.startCompare();
		});
	}

	private createCancelButton(view: azdata.ModelView): void {
		this.cancelCompareButton = view.modelBuilder.button().withProperties({
			label: localize('schemaCompare.cancelCompareButton', 'Stop'),
			iconPath: {
				light: path.join(this.extensionContext.extensionPath, 'media', 'stop.svg'),
				dark: path.join(this.extensionContext.extensionPath, 'media', 'stop-inverse.svg')
			},
			title: localize('schemaCompare.cancelCompareButtonTitle', 'Stop')
		}).component();

		this.cancelCompareButton.onDidClick(async (click) => {
			await this.cancelCompare();
		});
	}

	private async cancelCompare() {

		Telemetry.sendTelemetryEvent('SchemaCompareCancelStarted', {
			'startTime:': Date.now().toString(),
			'operationId': this.operationId
		});

		// clean the pane
		this.flexModel.removeItem(this.loader);
		this.flexModel.removeItem(this.waitText);
		this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } });
		this.resetButtons(ResetButtonState.beforeCompareStart);

		// cancel compare
		if (this.operationId) {
			const service = await this.getService();
			const result = await service.schemaCompareCancel(this.operationId);

			if (!result || !result.success) {
				Telemetry.sendTelemetryEvent('SchemaCompareCancelFailed', {
					'errorType': getTelemetryErrorType(result.errorMessage),
					'operationId': this.operationId
				});
				vscode.window.showErrorMessage(
					localize('schemaCompare.cancelErrorMessage', "Cancel schema compare failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown'));
			}
			Telemetry.sendTelemetryEvent('SchemaCompareCancelEnded', {
				'endTime:': Date.now().toString(),
				'operationId': this.operationId
			});
		}
	}

	private createGenerateScriptButton(view: azdata.ModelView): void {
		this.generateScriptButton = view.modelBuilder.button().withProperties({
			label: localize('schemaCompare.generateScriptButton', 'Generate script'),
			iconPath: {
				light: path.join(this.extensionContext.extensionPath, 'media', 'generate-script.svg'),
				dark: path.join(this.extensionContext.extensionPath, 'media', 'generate-script-inverse.svg')
			},
		}).component();

		this.generateScriptButton.onDidClick(async (click) => {
			Telemetry.sendTelemetryEvent('SchemaCompareGenerateScriptStarted', {
				'startTime:': Date.now().toString(),
				'operationId': this.comparisonResult.operationId
			});
			const service = await this.getService();
			const result = await service.schemaCompareGenerateScript(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.script);
			if (!result || !result.success) {
				Telemetry.sendTelemetryEvent('SchemaCompareGenerateScriptFailed', {
					'errorType': getTelemetryErrorType(result.errorMessage),
					'operationId': this.comparisonResult.operationId
				});
				vscode.window.showErrorMessage(
					localize('schemaCompare.generateScriptErrorMessage', "Generate script failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown'));
			}
			Telemetry.sendTelemetryEvent('SchemaCompareGenerateScriptEnded', {
				'endTime:': Date.now().toString(),
				'operationId': this.comparisonResult.operationId
			});
		});
	}

	private createOptionsButton(view: azdata.ModelView) {
		this.optionsButton = view.modelBuilder.button().withProperties({
			label: localize('schemaCompare.optionsButton', 'Options'),
			iconPath: {
				light: path.join(this.extensionContext.extensionPath, 'media', 'options.svg'),
				dark: path.join(this.extensionContext.extensionPath, 'media', 'options-inverse.svg')
			},
			title: localize('schemaCompare.optionsButtonTitle', 'Options')
		}).component();

		this.optionsButton.onDidClick(async (click) => {
			Telemetry.sendTelemetryEvent('SchemaCompareOptionsOpened');

			// create fresh every time
			this.schemaCompareOptionDialog = new SchemaCompareOptionsDialog(this.deploymentOptions, this);
			await this.schemaCompareOptionDialog.openDialog();
		});
	}

	private createApplyButton(view: azdata.ModelView) {

		this.applyButton = view.modelBuilder.button().withProperties({
			label: localize('schemaCompare.updateButton', 'Apply'),
			iconPath: {
				light: path.join(this.extensionContext.extensionPath, 'media', 'start.svg'),
				dark: path.join(this.extensionContext.extensionPath, 'media', 'start-inverse.svg')
			},
		}).component();

		// need only yes button - since the modal dialog has a default cancel
		const yesString = localize('schemaCompare.ApplyYes', 'Yes');
		this.applyButton.onDidClick(async (click) => {

			vscode.window.showWarningMessage(applyConfirmation, { modal: true }, yesString).then(async (result) => {
				if (result === yesString) {
					Telemetry.sendTelemetryEvent('SchemaCompareApplyStarted', {
						'startTime': Date.now().toString(),
						'operationId': this.comparisonResult.operationId
					});

					// disable apply and generate script buttons because the results are no longer valid after applying the changes
					this.setButtonsForRecompare();

					const service = await this.getService();
					const result = await service.schemaComparePublishChanges(this.comparisonResult.operationId, this.targetEndpointInfo.serverName, this.targetEndpointInfo.databaseName, azdata.TaskExecutionMode.execute);
					if (!result || !result.success) {
						Telemetry.sendTelemetryEvent('SchemaCompareApplyFailed', {
							'errorType': getTelemetryErrorType(result.errorMessage),
							'operationId': this.comparisonResult.operationId
						});
						vscode.window.showErrorMessage(
							localize('schemaCompare.updateErrorMessage', "Schema Compare Apply failed '{0}'", result.errorMessage ? result.errorMessage : 'Unknown'));

						// reenable generate script and apply buttons if apply failed
						this.generateScriptButton.enabled = true;
						this.generateScriptButton.title = generateScriptEnabledMessage;
						this.applyButton.enabled = true;
						this.applyButton.title = applyEnabledMessage;
					}
					Telemetry.sendTelemetryEvent('SchemaCompareApplyEnded', {
						'endTime': Date.now().toString(),
						'operationId': this.comparisonResult.operationId
					});
				}
			});
		});
	}

	private resetButtons(resetButtonState: ResetButtonState): void {
		switch (resetButtonState) {
			case (ResetButtonState.noSourceTarget): {
				this.compareButton.enabled = false;
				this.optionsButton.enabled = false;
				this.switchButton.enabled = this.sourceEndpointInfo ? true : false; // allows switching if the source is set
				this.openScmpButton.enabled = true;
				this.cancelCompareButton.enabled = false;
				this.selectSourceButton.enabled = true;
				this.selectTargetButton.enabled = true;
				break;
			}
			// Before start and after complete are same functionally. Adding two enum values for clarity.
			case (ResetButtonState.beforeCompareStart):
			case (ResetButtonState.afterCompareComplete): {
				this.compareButton.enabled = true;
				this.optionsButton.enabled = true;
				this.switchButton.enabled = true;
				this.openScmpButton.enabled = true;
				this.saveScmpButton.enabled = true;
				this.cancelCompareButton.enabled = false;
				this.selectSourceButton.enabled = true;
				this.selectTargetButton.enabled = true;
				break;
			}
			case (ResetButtonState.comparing): {
				this.compareButton.enabled = false;
				this.optionsButton.enabled = false;
				this.switchButton.enabled = false;
				this.openScmpButton.enabled = false;
				this.cancelCompareButton.enabled = true;
				this.selectSourceButton.enabled = false;
				this.selectTargetButton.enabled = false;
				break;
			}
		}

		// Set generate script and apply to false because specific values depend on result and are set separately
		this.generateScriptButton.enabled = false;
		this.applyButton.enabled = false;
		this.generateScriptButton.title = generateScriptEnabledMessage;
		this.applyButton.title = applyEnabledMessage;
	}

	public setButtonsForRecompare(): void {
		this.generateScriptButton.enabled = false;
		this.applyButton.enabled = false;
		this.generateScriptButton.title = reCompareToRefeshMessage;
		this.applyButton.title = reCompareToRefeshMessage;
	}

	// reset state afer loading an scmp
	private resetForNewCompare(): void {
		this.resetButtons(ResetButtonState.beforeCompareStart);
		this.flexModel.removeItem(this.splitView);
		this.flexModel.removeItem(this.noDifferencesLabel);
		this.flexModel.addItem(this.startText, { CSSStyles: { 'margin': 'auto' } });
	}

	private createSwitchButton(view: azdata.ModelView): void {
		this.switchButton = view.modelBuilder.button().withProperties({
			label: localize('schemaCompare.switchDirectionButton', 'Switch direction'),
			iconPath: {
				light: path.join(this.extensionContext.extensionPath, 'media', 'switch-directions.svg'),
				dark: path.join(this.extensionContext.extensionPath, 'media', 'switch-directions-inverse.svg')
			},
			title: localize('schemaCompare.switchButtonTitle', 'Switch source and target')
		}).component();

		this.switchButton.onDidClick(async (click) => {
			Telemetry.sendTelemetryEvent('SchemaCompareSwitch');

			// switch source and target
			[this.sourceEndpointInfo, this.targetEndpointInfo] = [this.targetEndpointInfo, this.sourceEndpointInfo];
			[this.sourceName, this.targetName] = [this.targetName, this.sourceName];

			this.sourceNameComponent.updateProperties({
				columns: [
					{
						value: this.sourceName,
						headerCssClass: 'no-borders',
						toolTip: this.sourceName
					},
				]
			});

			this.targetNameComponent.updateProperties({
				columns: [
					{
						value: this.targetName,
						headerCssClass: 'no-borders',
						toolTip: this.targetName
					},
				]
			});

			// remember that source target have been toggled
			this.sourceTargetSwitched = this.sourceTargetSwitched ? false : true;

			// only compare if both source and target are set
			if (this.sourceEndpointInfo && this.targetEndpointInfo) {
				this.startCompare();
			}
		});
	}

	private createSourceAndTargetButtons(view: azdata.ModelView): void {
		this.selectSourceButton = view.modelBuilder.button().withProperties({
			label: '•••',
			title: localize('schemaCompare.sourceButtonTitle', "Select Source"),
			ariaLabel: localize('schemaCompare.sourceButtonTitle', "Select Source")
		}).component();

		this.selectSourceButton.onDidClick(() => {
			Telemetry.sendTelemetryEvent('SchemaCompareSelectSource');
			let dialog = new SchemaCompareDialog(this);
			dialog.openDialog();
		});

		this.selectTargetButton = view.modelBuilder.button().withProperties({
			label: '•••',
			title: localize('schemaCompare.targetButtonTitle', "Select Target"),
			ariaLabel: localize('schemaCompare.targetButtonTitle', "Select Target")
		}).component();

		this.selectTargetButton.onDidClick(() => {
			Telemetry.sendTelemetryEvent('SchemaCompareSelectTarget');
			let dialog = new SchemaCompareDialog(this);
			dialog.openDialog();
		});
	}

	private createOpenScmpButton(view: azdata.ModelView) {
		this.openScmpButton = view.modelBuilder.button().withProperties({
			label: localize('schemaCompare.openScmpButton', 'Open .scmp file'),
			iconPath: {
				light: path.join(this.extensionContext.extensionPath, 'media', 'open-scmp.svg'),
				dark: path.join(this.extensionContext.extensionPath, 'media', 'open-scmp-inverse.svg')
			},
			title: localize('schemaCompare.openScmpButtonTitle', 'Load source, target, and options saved in an .scmp file')
		}).component();

		this.openScmpButton.onDidClick(async (click) => {
			Telemetry.sendTelemetryEvent('SchemaCompareOpenScmpStarted');
			const rootPath = getRootPath();
			let fileUris = await vscode.window.showOpenDialog(
				{
					canSelectFiles: true,
					canSelectFolders: false,
					canSelectMany: false,
					defaultUri: vscode.Uri.file(rootPath),
					openLabel: localize('schemaCompare.openFile', 'Open'),
					filters: {
						'scmp Files': ['scmp'],
					}
				}
			);

			if (!fileUris || fileUris.length === 0) {
				return;
			}

			let fileUri = fileUris[0];
			const service = await this.getService();
			let startTime = Date.now();
			const result = await service.schemaCompareOpenScmp(fileUri.fsPath);
			if (!result || !result.success) {
				Telemetry.sendTelemetryEvent('SchemaCompareOpenScmpFailed', {
					'errorType': getTelemetryErrorType(result.errorMessage)
				});
				vscode.window.showErrorMessage(
					localize('schemaCompare.openScmpErrorMessage', "Open scmp failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown'));
				return;
			}

			if (result.sourceEndpointInfo && result.sourceEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
				// only set endpoint info if able to connect to the database
				const ownerUri = await verifyConnectionAndGetOwnerUri(result.sourceEndpointInfo);
				if (ownerUri) {
					this.sourceEndpointInfo = result.sourceEndpointInfo;
					this.sourceEndpointInfo.ownerUri = ownerUri;
				}
			} else {
				// need to do this instead of just setting it to the result.sourceEndpointInfo because some fields are null which will cause an error when sending the compare request
				this.sourceEndpointInfo = {
					endpointType: mssql.SchemaCompareEndpointType.Dacpac,
					serverDisplayName: '',
					serverName: '',
					databaseName: '',
					ownerUri: '',
					packageFilePath: result.sourceEndpointInfo.packageFilePath,
					connectionDetails: undefined
				};
			}

			if (result.targetEndpointInfo && result.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
				const ownerUri = await verifyConnectionAndGetOwnerUri(result.targetEndpointInfo);
				if (ownerUri) {
					this.targetEndpointInfo = result.targetEndpointInfo;
					this.targetEndpointInfo.ownerUri = ownerUri;
				}
			} else {
				// need to do this instead of just setting it to the result.targetEndpointInfo because some fields are null which will cause an error when sending the compare request
				this.targetEndpointInfo = {
					endpointType: mssql.SchemaCompareEndpointType.Dacpac,
					serverDisplayName: '',
					serverName: '',
					databaseName: '',
					ownerUri: '',
					packageFilePath: result.targetEndpointInfo.packageFilePath,
					connectionDetails: undefined
				};
			}

			this.updateSourceAndTarget();
			this.setDeploymentOptions(result.deploymentOptions);
			this.scmpSourceExcludes = result.excludedSourceElements;
			this.scmpTargetExcludes = result.excludedTargetElements;
			this.sourceTargetSwitched = result.originalTargetName !== this.targetEndpointInfo.databaseName;

			// clear out any old results
			this.resetForNewCompare();

			Telemetry.sendTelemetryEvent('SchemaCompareOpenScmpEnded', {
				'elapsedTime:': (Date.now() - startTime).toString()
			});
		});
	}

	private createSaveScmpButton(view: azdata.ModelView): void {
		this.saveScmpButton = view.modelBuilder.button().withProperties({
			label: localize('schemaCompare.saveScmpButton', 'Save .scmp file'),
			iconPath: {
				light: path.join(this.extensionContext.extensionPath, 'media', 'save-scmp.svg'),
				dark: path.join(this.extensionContext.extensionPath, 'media', 'save-scmp-inverse.svg')
			},
			title: localize('schemaCompare.saveScmpButtonTitle', 'Save source and target, options, and excluded elements'),
			enabled: false
		}).component();

		this.saveScmpButton.onDidClick(async (click) => {
			const rootPath = getRootPath();
			const filePath = await vscode.window.showSaveDialog(
				{
					defaultUri: vscode.Uri.file(rootPath),
					saveLabel: localize('schemaCompare.saveFile', 'Save'),
					filters: {
						'scmp Files': ['scmp'],
					}
				}
			);

			if (!filePath) {
				return;
			}

			// convert include/exclude maps to arrays of object ids
			let sourceExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalSourceExcludes);
			let targetExcludes: mssql.SchemaCompareObjectId[] = this.convertExcludesToObjectIds(this.originalTargetExcludes);

			let startTime = Date.now();
			Telemetry.sendTelemetryEvent('SchemaCompareSaveScmp');
			const service = await this.getService();
			const result = await service.schemaCompareSaveScmp(this.sourceEndpointInfo, this.targetEndpointInfo, azdata.TaskExecutionMode.execute, this.deploymentOptions, filePath.fsPath, sourceExcludes, targetExcludes);
			if (!result || !result.success) {
				Telemetry.sendTelemetryEvent('SchemaCompareSaveScmpFailed', {
					'errorType': getTelemetryErrorType(result.errorMessage),
					'operationId': this.comparisonResult.operationId
				});
				vscode.window.showErrorMessage(
					localize('schemaCompare.saveScmpErrorMessage', "Save scmp failed: '{0}'", (result && result.errorMessage) ? result.errorMessage : 'Unknown'));
			}

			Telemetry.sendTelemetryEvent('SchemaCompareSaveScmpEnded', {
				'elapsedTime:': (Date.now() - startTime).toString(),
				'operationId': this.comparisonResult.operationId
			});
		});
	}

	/**
	 * Converts excluded diff entries into object ids which are needed to save them in an scmp
	*/
	private convertExcludesToObjectIds(excludedDiffEntries: Map<string, mssql.DiffEntry>): mssql.SchemaCompareObjectId[] {
		let result = [];
		excludedDiffEntries.forEach((value: mssql.DiffEntry) => {
			result.push({
				nameParts: value.sourceValue ? value.sourceValue : value.targetValue,
				sqlObjectType: `Microsoft.Data.Tools.Schema.Sql.SchemaModel.${value.name}`
			});
		});

		return result;
	}

	private setButtonStatesForNoChanges(enableButtons: boolean): void {
		// generate script and apply can only be enabled if the target is a database
		if (this.targetEndpointInfo.endpointType === mssql.SchemaCompareEndpointType.Database) {
			this.applyButton.enabled = enableButtons;
			this.generateScriptButton.enabled = enableButtons;
			this.applyButton.title = enableButtons ? applyEnabledMessage : applyNoChangesMessage;
			this.generateScriptButton.title = enableButtons ? generateScriptEnabledMessage : generateScriptNoChangesMessage;
		}
	}

	private async getService(): Promise<mssql.ISchemaCompareService> {
		if (isNullOrUndefined(this.schemaCompareService)) {
			this.schemaCompareService = (vscode.extensions.getExtension(mssql.extension.name).exports as mssql.IExtension).schemaCompare;
		}
		return this.schemaCompareService;
	}

	private async GetDefaultDeploymentOptions(): Promise<void> {
		// Same as dacfx default options
		const service = await this.getService();
		let result = await service.schemaCompareGetDefaultOptions();
		this.setDeploymentOptions(result.defaultDeploymentOptions);
	}
}

// Borrowed as is from other extensions
// TODO : figure out if any inbuilt alternative is available
export function generateGuid(): string {
	let hexValues: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
	// c.f. rfc4122 (UUID version 4 = xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx)
	let oct: string = '';
	let tmp: number;
	/* tslint:disable:no-bitwise */
	for (let a: number = 0; a < 4; a++) {
		tmp = (4294967296 * Math.random()) | 0;
		oct += hexValues[tmp & 0xF] +
			hexValues[tmp >> 4 & 0xF] +
			hexValues[tmp >> 8 & 0xF] +
			hexValues[tmp >> 12 & 0xF] +
			hexValues[tmp >> 16 & 0xF] +
			hexValues[tmp >> 20 & 0xF] +
			hexValues[tmp >> 24 & 0xF] +
			hexValues[tmp >> 28 & 0xF];
	}

	// 'Set the two most significant bits (bits 6 and 7) of the clock_seq_hi_and_reserved to zero and one, respectively'
	let clockSequenceHi: string = hexValues[8 + (Math.random() * 4) | 0];
	return oct.substr(0, 8) + '-' + oct.substr(9, 4) + '-4' + oct.substr(13, 3) + '-' + clockSequenceHi + oct.substr(16, 3) + '-' + oct.substr(19, 12);
	/* tslint:enable:no-bitwise */
}
