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

'use strict';

import { TPromise } from 'vs/base/common/winjs.base';
import { sequence } from 'vs/base/common/async';
import * as strings from 'vs/base/common/strings';
import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService';
import { IThreadService } from 'vs/workbench/services/thread/common/threadService';
import { ISaveParticipant, ITextFileEditorModel, SaveReason } from 'vs/workbench/services/textfile/common/textfiles';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IPosition, IModel, ICommonCodeEditor, ISingleEditOperation, IIdentifiedSingleEditOperation } from 'vs/editor/common/editorCommon';
import { Range } from 'vs/editor/common/core/range';
import { Selection } from 'vs/editor/common/core/selection';
import { trimTrailingWhitespace } from 'vs/editor/common/commands/trimTrailingWhitespaceCommand';
import { getDocumentFormattingEdits } from 'vs/editor/contrib/format/common/format';
import { EditOperationsCommand } from 'vs/editor/contrib/format/common/formatCommand';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel';
import { ExtHostContext, ExtHostDocumentSaveParticipantShape } from './extHost.protocol';
import { EditOperation } from 'vs/editor/common/core/editOperation';

export interface INamedSaveParticpant extends ISaveParticipant {
	readonly name: string;
}

class TrimWhitespaceParticipant implements INamedSaveParticpant {

	readonly name = 'TrimWhitespaceParticipant';

	constructor(
		@IConfigurationService private configurationService: IConfigurationService,
		@ICodeEditorService private codeEditorService: ICodeEditorService
	) {
		// Nothing
	}

	public participate(model: ITextFileEditorModel, env: { reason: SaveReason }): any {
		if (this.configurationService.lookup('files.trimTrailingWhitespace').value) {
			this.doTrimTrailingWhitespace(model.textEditorModel, env.reason === SaveReason.AUTO);
		}
	}

	private doTrimTrailingWhitespace(model: IModel, isAutoSaved: boolean): void {
		let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)];
		const cursors: IPosition[] = [];

		let editor = findEditor(model, this.codeEditorService);
		if (editor) {
			// Find `prevSelection` in any case do ensure a good undo stack when pushing the edit
			// Collect active cursors in `cursors` only if `isAutoSaved` to avoid having the cursors jump
			prevSelection = editor.getSelections();
			if (isAutoSaved) {
				cursors.push(...prevSelection.map(s => {
					return {
						lineNumber: s.positionLineNumber,
						column: s.positionColumn
					};
				}));
			}
		}

		const ops = trimTrailingWhitespace(model, cursors);
		if (!ops.length) {
			return; // Nothing to do
		}

		model.pushEditOperations(prevSelection, ops, (edits) => prevSelection);
	}
}

function findEditor(model: IModel, codeEditorService: ICodeEditorService): ICommonCodeEditor {
	if (model.isAttachedToEditor()) {
		const allEditors = codeEditorService.listCodeEditors();
		for (let i = 0, len = allEditors.length; i < len; i++) {
			const editor = allEditors[i];
			const editorModel = editor.getModel();

			if (!editorModel) {
				continue; // empty editor
			}

			if (model === editorModel) {
				return editor;
			}
		}
	}

	return null;
}

export class FinalNewLineParticipant implements INamedSaveParticpant {

	readonly name = 'FinalNewLineParticipant';

	constructor(
		@IConfigurationService private configurationService: IConfigurationService,
		@ICodeEditorService private codeEditorService: ICodeEditorService
	) {
		// Nothing
	}

	public participate(model: ITextFileEditorModel, env: { reason: SaveReason }): any {
		if (this.configurationService.lookup('files.insertFinalNewline').value) {
			this.doInsertFinalNewLine(model.textEditorModel);
		}
	}

	private doInsertFinalNewLine(model: IModel): void {
		const lineCount = model.getLineCount();
		const lastLine = model.getLineContent(lineCount);
		const lastLineIsEmptyOrWhitespace = strings.lastNonWhitespaceIndex(lastLine) === -1;

		if (!lineCount || lastLineIsEmptyOrWhitespace) {
			return;
		}

		let prevSelection: Selection[] = [new Selection(1, 1, 1, 1)];
		const editor = findEditor(model, this.codeEditorService);
		if (editor) {
			prevSelection = editor.getSelections();
		}

		model.pushEditOperations(prevSelection, [EditOperation.insert({ lineNumber: lineCount + 1, column: 0 }, model.getEOL())], (edits) => prevSelection);
	}
}

class FormatOnSaveParticipant implements INamedSaveParticpant {

	readonly name = 'FormatOnSaveParticipant';

	constructor(
		@ICodeEditorService private _editorService: ICodeEditorService,
		@IConfigurationService private _configurationService: IConfigurationService
	) {
		// Nothing
	}

	participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason }): TPromise<any> {

		if (env.reason === SaveReason.AUTO
			|| !this._configurationService.lookup('editor.formatOnSave').value) {

			return;
		}

		const model = editorModel.textEditorModel;
		const versionNow = model.getVersionId();
		const {tabSize, insertSpaces} = model.getOptions();

		return new TPromise<ISingleEditOperation[]>((resolve, reject) => {
			setTimeout(reject, 750);
			getDocumentFormattingEdits(model, { tabSize, insertSpaces }).then(resolve, reject);

		}).then(edits => {
			if (edits && versionNow === model.getVersionId()) {
				const editor = this._findEditor(model);
				if (editor) {
					this._editsWithEditor(editor, edits);
				} else {
					this._editWithModel(model, edits);
				}
			}
		});
	}

	private _editsWithEditor(editor: ICommonCodeEditor, edits: ISingleEditOperation[]): void {
		editor.executeCommand('files.formatOnSave', new EditOperationsCommand(edits, editor.getSelection()));
	}

	private _editWithModel(model: IModel, edits: ISingleEditOperation[]): void {

		const [{range}] = edits;
		const initialSelection = new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn);

		model.pushEditOperations([initialSelection], edits.map(FormatOnSaveParticipant._asIdentEdit), undoEdits => {
			for (const {range} of undoEdits) {
				if (Range.areIntersectingOrTouching(range, initialSelection)) {
					return [new Selection(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn)];
				}
			}
		});
	}

	private static _asIdentEdit({text, range}: ISingleEditOperation): IIdentifiedSingleEditOperation {
		return {
			text,
			range: Range.lift(range),
			identifier: undefined,
			forceMoveMarkers: true
		};
	}

	private _findEditor(model: IModel) {
		if (!model.isAttachedToEditor()) {
			return;
		}

		let candidate: ICommonCodeEditor;
		for (const editor of this._editorService.listCodeEditors()) {
			if (editor.getModel() === model) {
				if (editor.isFocused()) {
					return editor;
				} else {
					candidate = editor;
				}
			}
		}
		return candidate;
	}
}

class ExtHostSaveParticipant implements INamedSaveParticpant {

	private _proxy: ExtHostDocumentSaveParticipantShape;

	readonly name = 'ExtHostSaveParticipant';

	constructor( @IThreadService threadService: IThreadService) {
		this._proxy = threadService.get(ExtHostContext.ExtHostDocumentSaveParticipant);
	}

	participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason }): TPromise<any> {
		return new TPromise<any>((resolve, reject) => {
			setTimeout(reject, 1750);
			this._proxy.$participateInSave(editorModel.getResource(), env.reason).then(values => {
				for (const success of values) {
					if (!success) {
						return TPromise.wrapError('listener failed');
					}
				}
			}).then(resolve, reject);
		});
	}
}

// The save participant can change a model before its saved to support various scenarios like trimming trailing whitespace
export class SaveParticipant implements ISaveParticipant {

	private _saveParticipants: INamedSaveParticpant[];

	constructor(
		@ITelemetryService private _telemetryService: ITelemetryService,
		@IInstantiationService instantiationService: IInstantiationService,
		@IThreadService threadService: IThreadService
	) {

		this._saveParticipants = [
			instantiationService.createInstance(TrimWhitespaceParticipant),
			instantiationService.createInstance(FinalNewLineParticipant),
			instantiationService.createInstance(FormatOnSaveParticipant),
			instantiationService.createInstance(ExtHostSaveParticipant)
		];

		// Hook into model
		TextFileEditorModel.setSaveParticipant(this);
	}
	participate(model: ITextFileEditorModel, env: { reason: SaveReason }): TPromise<any> {

		const stats: { [name: string]: number } = Object.create(null);

		const promiseFactory = this._saveParticipants.map(p => () => {

			const {name} = p;
			const t1 = Date.now();

			return TPromise.as(p.participate(model, env)).then(() => {
				stats[`Success-${name}`] = Date.now() - t1;
			}, err => {
				stats[`Failure-${name}`] = Date.now() - t1;
				// console.error(err);
			});
		});

		return sequence(promiseFactory).then(() => {
			this._telemetryService.publicLog('saveParticipantStats', stats);
		});
	}
}
