/*---------------------------------------------------------------------------------------------
 *  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 'vs/css!./media/git.contribution';
import nls = require('vs/nls');
import async = require('vs/base/common/async');
import errors = require('vs/base/common/errors');
import paths = require('vs/base/common/paths');
import lifecycle = require('vs/base/common/lifecycle');
import winjs = require('vs/base/common/winjs.base');
import ext = require('vs/workbench/common/contributions');
import git = require('vs/workbench/parts/git/common/git');
import common = require('vs/editor/common/editorCommon');
import widget = require('vs/editor/browser/codeEditor');
import viewlet = require('vs/workbench/browser/viewlet');
import statusbar = require('vs/workbench/browser/parts/statusbar/statusbar');
import platform = require('vs/platform/platform');
import widgets = require('vs/workbench/parts/git/browser/gitWidgets');
import wbar = require('vs/workbench/common/actionRegistry');
import gitoutput = require('vs/workbench/parts/git/browser/gitOutput');
import output = require('vs/workbench/parts/output/common/output');
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import confregistry = require('vs/platform/configuration/common/configurationRegistry');
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import quickopen = require('vs/workbench/browser/quickopen');
import 'vs/workbench/parts/git/browser/gitEditorContributions';
import { IActivityService, ProgressBadge, NumberBadge } from 'vs/workbench/services/activity/common/activityService';
import { IEventService } from 'vs/platform/event/common/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IMessageService } from 'vs/platform/message/common/message';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { IModelService } from 'vs/editor/common/services/modelService';
import { RawText } from 'vs/editor/common/model/textModel';
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorkerService';
import URI from 'vs/base/common/uri';
import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService';
import { Schemas } from 'vs/base/common/network';

import IGitService = git.IGitService;

export class StatusUpdater implements ext.IWorkbenchContribution {
	static ID = 'vs.git.statusUpdater';

	private gitService: IGitService;
	private eventService: IEventService;
	private activityService: IActivityService;
	private messageService: IMessageService;
	private configurationService: IConfigurationService;
	private progressBadgeDelayer: async.Delayer<void>;
	private toDispose: lifecycle.IDisposable[];

	constructor(
		@IGitService gitService: IGitService,
		@IEventService eventService: IEventService,
		@IActivityService activityService: IActivityService,
		@IMessageService messageService: IMessageService,
		@IConfigurationService configurationService: IConfigurationService
	) {
		this.gitService = gitService;
		this.eventService = eventService;
		this.activityService = activityService;
		this.messageService = messageService;
		this.configurationService = configurationService;

		this.progressBadgeDelayer = new async.Delayer<void>(200);

		this.toDispose = [];
		this.toDispose.push(this.configurationService.onDidUpdateConfiguration(e => this.onGitServiceChange()));
		this.toDispose.push(this.gitService.addBulkListener2(e => this.onGitServiceChange()));
	}

	private onGitServiceChange(): void {
		if (this.gitService.getState() !== git.ServiceState.OK) {
			this.progressBadgeDelayer.cancel();
			this.activityService.showActivity('workbench.view.git', null, 'git-viewlet-label');
		} else if (this.gitService.isIdle()) {
			this.showChangesBadge();
		} else {
			this.progressBadgeDelayer.trigger(() => {
				this.activityService.showActivity('workbench.view.git', new ProgressBadge(() => nls.localize('gitProgressBadge', 'Running git status')), 'git-viewlet-label-progress');
			});
		}
	}

	private showChangesBadge(): void {
		this.progressBadgeDelayer.cancel();

		const { countBadge } = this.configurationService.getConfiguration<git.IGitConfiguration>('git');

		if (countBadge === 'off') {
			return;
		}

		const filter = countBadge === 'tracked'
			? s => s.getStatus() !== git.Status.UNTRACKED
			: () => true;

		const statuses = this.gitService.getModel().getStatus().getGroups()
			.map(g => g.all())
			.reduce((r, g) => r.concat(g), [])
			.filter(filter);

		const badge = new NumberBadge(statuses.length, num => nls.localize('gitPendingChangesBadge', '{0} pending changes', num));
		this.activityService.showActivity('workbench.view.git', badge, 'git-viewlet-label');
	}

	public getId(): string {
		return StatusUpdater.ID;
	}

	public dispose(): void {
		this.toDispose = lifecycle.dispose(this.toDispose);
	}
}

class DirtyDiffModelDecorator {
	static ID = 'vs.git.editor.dirtyDiffDecorator';
	static MODIFIED_DECORATION_OPTIONS: common.IModelDecorationOptions = {
		linesDecorationsClassName: 'git-dirty-modified-diff-glyph',
		isWholeLine: true,
		overviewRuler: {
			color: 'rgba(0, 122, 204, 0.6)',
			darkColor: 'rgba(0, 122, 204, 0.6)',
			position: common.OverviewRulerLane.Left
		}
	};
	static ADDED_DECORATION_OPTIONS: common.IModelDecorationOptions = {
		linesDecorationsClassName: 'git-dirty-added-diff-glyph',
		isWholeLine: true,
		overviewRuler: {
			color: 'rgba(0, 122, 204, 0.6)',
			darkColor: 'rgba(0, 122, 204, 0.6)',
			position: common.OverviewRulerLane.Left
		}
	};
	static DELETED_DECORATION_OPTIONS: common.IModelDecorationOptions = {
		linesDecorationsClassName: 'git-dirty-deleted-diff-glyph',
		isWholeLine: true,
		overviewRuler: {
			color: 'rgba(0, 122, 204, 0.6)',
			darkColor: 'rgba(0, 122, 204, 0.6)',
			position: common.OverviewRulerLane.Left
		}
	};

	private modelService: IModelService;
	private editorWorkerService: IEditorWorkerService;
	private editorService: IWorkbenchEditorService;
	private contextService: IWorkspaceContextService;
	private gitService: IGitService;

	private model: common.IModel;
	private _originalContentsURI: URI;
	private path: string;
	private decorations: string[];

	private delayer: async.ThrottledDelayer<void>;
	private diffDelayer: async.ThrottledDelayer<void>;
	private toDispose: lifecycle.IDisposable[];

	constructor(model: common.IModel, path: string,
		@IModelService modelService: IModelService,
		@IEditorWorkerService editorWorkerService: IEditorWorkerService,
		@IWorkbenchEditorService editorService: IWorkbenchEditorService,
		@IWorkspaceContextService contextService: IWorkspaceContextService,
		@IGitService gitService: IGitService
	) {
		this.modelService = modelService;
		this.editorWorkerService = editorWorkerService;
		this.editorService = editorService;
		this.contextService = contextService;
		this.gitService = gitService;

		this.model = model;
		this._originalContentsURI = model.uri.with({ scheme: Schemas.internal });
		this.path = path;
		this.decorations = [];

		this.delayer = new async.ThrottledDelayer<void>(500);
		this.diffDelayer = new async.ThrottledDelayer<void>(200);

		this.toDispose = [];
		this.toDispose.push(model.onDidChangeContent(() => this.triggerDiff()));
		this.toDispose.push(this.gitService.addListener2(git.ServiceEvents.STATE_CHANGED, () => this.onChanges()));
		this.toDispose.push(this.gitService.addListener2(git.ServiceEvents.OPERATION_END, e => {
			if (e.operation.id !== git.ServiceOperations.BACKGROUND_FETCH) {
				this.onChanges();
			}
		}));

		this.onChanges();
	}

	private onChanges(): void {
		if (!this.gitService) {
			return;
		}

		if (this.gitService.getState() !== git.ServiceState.OK) {
			return;
		}

		// go through all interesting models
		this.trigger();
	}

	private trigger(): void {
		this.delayer
			.trigger(() => this.diffOriginalContents())
			.done(null, errors.onUnexpectedError);
	}

	private diffOriginalContents(): winjs.TPromise<void> {
		return this.getOriginalContents()
			.then(contents => {
				if (!this.model || this.model.isDisposed()) {
					return; // disposed
				}

				if (!contents) {
					// untracked file
					this.modelService.destroyModel(this._originalContentsURI);
					return this.triggerDiff();
				}

				let originalModel = this.modelService.getModel(this._originalContentsURI);
				if (originalModel) {
					let contentsRawText = RawText.fromStringWithModelOptions(contents, originalModel);

					// return early if nothing has changed
					if (originalModel.equals(contentsRawText)) {
						return winjs.TPromise.as(null);
					}

					// we already have the original contents
					originalModel.setValueFromRawText(contentsRawText);
				} else {
					// this is the first time we load the original contents
					this.modelService.createModel(contents, null, this._originalContentsURI);
				}

				return this.triggerDiff();
			});
	}

	private getOriginalContents(): winjs.TPromise<string> {
		var gitModel = this.gitService.getModel();
		var treeish = gitModel.getStatus().find(this.path, git.StatusType.INDEX) ? '~' : 'HEAD';

		return this.gitService.buffer(this.path, treeish);
	}

	private triggerDiff(): winjs.Promise {
		if (!this.diffDelayer) {
			return winjs.TPromise.as(null);
		}

		return this.diffDelayer.trigger(() => {
			if (!this.model || this.model.isDisposed()) {
				return winjs.TPromise.as<any>([]); // disposed
			}

			return this.editorWorkerService.computeDirtyDiff(this._originalContentsURI, this.model.uri, true);
		}).then((diff: common.IChange[]) => {
			if (!this.model || this.model.isDisposed()) {
				return; // disposed
			}

			return this.decorations = this.model.deltaDecorations(this.decorations, DirtyDiffModelDecorator.changesToDecorations(diff || []));
		});
	}

	private static changesToDecorations(diff: common.IChange[]): common.IModelDeltaDecoration[] {
		return diff.map((change) => {
			var startLineNumber = change.modifiedStartLineNumber;
			var endLineNumber = change.modifiedEndLineNumber || startLineNumber;

			// Added
			if (change.originalEndLineNumber === 0) {
				return {
					range: {
						startLineNumber: startLineNumber, startColumn: 1,
						endLineNumber: endLineNumber, endColumn: 1
					},
					options: DirtyDiffModelDecorator.ADDED_DECORATION_OPTIONS
				};
			}

			// Removed
			if (change.modifiedEndLineNumber === 0) {
				return {
					range: {
						startLineNumber: startLineNumber, startColumn: 1,
						endLineNumber: startLineNumber, endColumn: 1
					},
					options: DirtyDiffModelDecorator.DELETED_DECORATION_OPTIONS
				};
			}

			// Modified
			return {
				range: {
					startLineNumber: startLineNumber, startColumn: 1,
					endLineNumber: endLineNumber, endColumn: 1
				},
				options: DirtyDiffModelDecorator.MODIFIED_DECORATION_OPTIONS
			};
		});
	}

	public dispose(): void {
		this.modelService.destroyModel(this._originalContentsURI);
		this.toDispose = lifecycle.dispose(this.toDispose);
		if (this.model && !this.model.isDisposed()) {
			this.model.deltaDecorations(this.decorations, []);
		}
		this.model = null;
		this.decorations = null;
		if (this.delayer) {
			this.delayer.cancel();
			this.delayer = null;
		}
		if (this.diffDelayer) {
			this.diffDelayer.cancel();
			this.diffDelayer = null;
		}
	}
}

export class DirtyDiffDecorator implements ext.IWorkbenchContribution {

	private gitService: IGitService;
	private messageService: IMessageService;
	private editorService: IWorkbenchEditorService;
	private eventService: IEventService;
	private contextService: IWorkspaceContextService;
	private instantiationService: IInstantiationService;
	private models: common.IModel[];
	private decorators: { [modelId: string]: DirtyDiffModelDecorator };
	private toDispose: lifecycle.IDisposable[];

	constructor(
		@IGitService gitService: IGitService,
		@IMessageService messageService: IMessageService,
		@IWorkbenchEditorService editorService: IWorkbenchEditorService,
		@IEditorGroupService editorGroupService: IEditorGroupService,
		@IEventService eventService: IEventService,
		@IWorkspaceContextService contextService: IWorkspaceContextService,
		@IInstantiationService instantiationService: IInstantiationService
	) {
		this.gitService = gitService;
		this.messageService = messageService;
		this.editorService = editorService;
		this.eventService = eventService;
		this.contextService = contextService;
		this.instantiationService = instantiationService;

		this.models = [];
		this.decorators = Object.create(null);
		this.toDispose = [];
		this.toDispose.push(editorGroupService.onEditorsChanged(() => this.onEditorsChanged()));
		this.toDispose.push(gitService.addListener2(git.ServiceEvents.DISPOSE, () => this.dispose()));
	}

	public getId(): string {
		return 'git.DirtyDiffModelDecorator';
	}

	private onEditorsChanged(): void {
		// HACK: This is the best current way of figuring out whether to draw these decorations
		// or not. Needs context from the editor, to know whether it is a diff editor, in place editor
		// etc.

		const repositoryRoot = this.gitService.getModel().getRepositoryRoot();

		// If there is no repository root, just wait until that changes
		if (typeof repositoryRoot !== 'string') {
			this.gitService.addOneTimeDisposableListener(git.ServiceEvents.STATE_CHANGED, () => this.onEditorsChanged());

			this.models.forEach(m => this.onModelInvisible(m));
			this.models = [];
			return;
		}

		const models = this.editorService.getVisibleEditors()

			// map to the editor controls
			.map(e => e.getControl())

			// only interested in code editor widgets
			.filter(c => c instanceof widget.CodeEditor)

			// map to models
			.map(e => (<widget.CodeEditor>e).getModel())

			// remove nulls and duplicates
			.filter((m, i, a) => !!m && a.indexOf(m, i + 1) === -1)

			// get the associated resource
			.map(m => ({ model: m, resource: m.uri }))

			// remove nulls
			.filter(p => !!p.resource &&
				// and invalid resources
				(p.resource.scheme === 'file' && paths.isEqualOrParent(p.resource.fsPath, repositoryRoot))
			)

			// get paths
			.map(p => ({ model: p.model, path: paths.normalize(paths.relative(repositoryRoot, p.resource.fsPath)) }))

			// remove nulls and inside .git files
			.filter(p => !!p.path && p.path.indexOf('.git/') === -1);

		var newModels = models.filter(p => this.models.every(m => p.model !== m));
		var oldModels = this.models.filter(m => models.every(p => p.model !== m));

		newModels.forEach(p => this.onModelVisible(p.model, p.path));
		oldModels.forEach(m => this.onModelInvisible(m));

		this.models = models.map(p => p.model);
	}

	private onModelVisible(model: common.IModel, path: string): void {
		this.decorators[model.id] = this.instantiationService.createInstance(DirtyDiffModelDecorator, model, path);
	}

	private onModelInvisible(model: common.IModel): void {
		this.decorators[model.id].dispose();
		delete this.decorators[model.id];
	}

	public dispose(): void {
		this.toDispose = lifecycle.dispose(this.toDispose);
		this.models.forEach(m => this.decorators[m.id].dispose());
		this.models = null;
		this.decorators = null;
	}
}

export const VIEWLET_ID = 'workbench.view.git';

class OpenGitViewletAction extends viewlet.ToggleViewletAction {
	public static ID = VIEWLET_ID;
	public static LABEL = nls.localize('toggleGitViewlet', "Show Git");

	constructor(id: string, label: string, @IViewletService viewletService: IViewletService, @IWorkbenchEditorService editorService: IWorkbenchEditorService) {
		super(id, label, VIEWLET_ID, viewletService, editorService);
	}
}

export function registerContributions(): void {

	// Register Statusbar item
	(<statusbar.IStatusbarRegistry>platform.Registry.as(statusbar.Extensions.Statusbar)).registerStatusbarItem(new statusbar.StatusbarItemDescriptor(
		widgets.GitStatusbarItem,
		statusbar.StatusbarAlignment.LEFT,
		100 /* High Priority */
	));

	// Register Output Channel
	var outputChannelRegistry = <output.IOutputChannelRegistry>platform.Registry.as(output.Extensions.OutputChannels);
	outputChannelRegistry.registerChannel('Git', nls.localize('git', "Git"));

	// Register Git Output
	(<ext.IWorkbenchContributionsRegistry>platform.Registry.as(ext.Extensions.Workbench)).registerWorkbenchContribution(
		gitoutput.GitOutput
	);

	// Register Viewlet
	(<viewlet.ViewletRegistry>platform.Registry.as(viewlet.Extensions.Viewlets)).registerViewlet(new viewlet.ViewletDescriptor(
		'vs/workbench/parts/git/browser/gitViewlet',
		'GitViewlet',
		VIEWLET_ID,
		nls.localize('git', "Git"),
		'git',
		35
	));

	// Register Action to Open Viewlet
	(<wbar.IWorkbenchActionRegistry>platform.Registry.as(wbar.Extensions.WorkbenchActions)).registerWorkbenchAction(
		new SyncActionDescriptor(OpenGitViewletAction, OpenGitViewletAction.ID, OpenGitViewletAction.LABEL, {
			primary: null,
			win: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G },
			linux: { primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_G },
			mac: { primary: KeyMod.WinCtrl | KeyMod.Shift | KeyCode.KEY_G }
		}),
		'View: Show Git',
		nls.localize('view', "View")
	);

	// Register StatusUpdater
	(<ext.IWorkbenchContributionsRegistry>platform.Registry.as(ext.Extensions.Workbench)).registerWorkbenchContribution(
		StatusUpdater
	);

	// Register DirtyDiffDecorator
	(<ext.IWorkbenchContributionsRegistry>platform.Registry.as(ext.Extensions.Workbench)).registerWorkbenchContribution(
		DirtyDiffDecorator
	);

	// Register Quick Open for git
	(<quickopen.IQuickOpenRegistry>platform.Registry.as(quickopen.Extensions.Quickopen)).registerQuickOpenHandler(
		new quickopen.QuickOpenHandlerDescriptor(
			'vs/workbench/parts/git/browser/gitQuickOpen',
			'GitCommandQuickOpenHandler',
			'git ',
			nls.localize('gitCommands', "Git Commands")
		)
	);

	// Register configuration
	var configurationRegistry = <confregistry.IConfigurationRegistry>platform.Registry.as(confregistry.Extensions.Configuration);
	configurationRegistry.registerConfiguration({
		id: 'git',
		order: 15,
		title: nls.localize('gitConfigurationTitle', "Git"),
		type: 'object',
		properties: {
			'git.enabled': {
				type: 'boolean',
				description: nls.localize('gitEnabled', "Is git enabled"),
				default: true
			},
			'git.path': {
				type: ['string', 'null'],
				description: nls.localize('gitPath', "Path to the git executable"),
				default: null
			},
			'git.autorefresh': {
				type: 'boolean',
				description: nls.localize('gitAutoRefresh', "Whether auto refreshing is enabled"),
				default: true
			},
			'git.autofetch': {
				type: 'boolean',
				description: nls.localize('gitAutoFetch', "Whether auto fetching is enabled."),
				default: true
			},
			'git.enableLongCommitWarning': {
				type: 'boolean',
				description: nls.localize('gitLongCommit', "Whether long commit messages should be warned about."),
				default: true
			},
			'git.allowLargeRepositories': {
				type: 'boolean',
				description: nls.localize('gitLargeRepos', "Always allow large repositories to be managed by Code."),
				default: false
			},
			'git.confirmSync': {
				type: 'boolean',
				description: nls.localize('confirmSync', "Confirm before synchronizing git repositories."),
				default: true
			},
			'git.countBadge': {
				type: 'string',
				enum: ['all', 'tracked', 'off'],
				default: 'all',
				description: nls.localize('countBadge', "Controls the git badge counter."),
			}
		}
	});
}
