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

import 'vs/css!./media/review';
import * as dom from 'vs/base/browser/dom';
import { Emitter } from 'vs/base/common/event';
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import * as languages from 'vs/editor/common/languages';
import { IMarkdownRendererOptions } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { CommentMenus } from 'vs/workbench/contrib/comments/browser/commentMenus';
import { CommentReply } from 'vs/workbench/contrib/comments/browser/commentReply';
import { ICommentService } from 'vs/workbench/contrib/comments/browser/commentService';
import { CommentThreadBody } from 'vs/workbench/contrib/comments/browser/commentThreadBody';
import { CommentThreadHeader } from 'vs/workbench/contrib/comments/browser/commentThreadHeader';
import { CommentThreadAdditionalActions } from 'vs/workbench/contrib/comments/browser/commentThreadAdditionalActions';
import { CommentContextKeys } from 'vs/workbench/contrib/comments/common/commentContextKeys';
import { ICommentThreadWidget } from 'vs/workbench/contrib/comments/common/commentThreadWidget';
import { IColorTheme } from 'vs/platform/theme/common/themeService';
import { contrastBorder, focusBorder, inputValidationErrorBackground, inputValidationErrorBorder, inputValidationErrorForeground, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground } from 'vs/platform/theme/common/colorRegistry';
import { PANEL_BORDER } from 'vs/workbench/common/theme';
import { IRange } from 'vs/editor/common/core/range';
import { commentThreadStateBackgroundColorVar, commentThreadStateColorVar } from 'vs/workbench/contrib/comments/browser/commentColors';
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';

export const COMMENTEDITOR_DECORATION_KEY = 'commenteditordecoration';


export class CommentThreadWidget<T extends IRange | ICellRange = IRange> extends Disposable implements ICommentThreadWidget {
	private _header!: CommentThreadHeader<T>;
	private _body: CommentThreadBody<T>;
	private _commentReply?: CommentReply<T>;
	private _additionalActions?: CommentThreadAdditionalActions<T>;
	private _commentMenus: CommentMenus;
	private _commentThreadDisposables: IDisposable[] = [];
	private _threadIsEmpty: IContextKey<boolean>;
	private _styleElement: HTMLStyleElement;
	private _commentThreadContextValue: IContextKey<string | undefined>;
	private _onDidResize = new Emitter<dom.Dimension>();
	onDidResize = this._onDidResize.event;

	get commentThread() {
		return this._commentThread;
	}

	constructor(
		readonly container: HTMLElement,
		private _owner: string,
		private _parentResourceUri: URI,
		private _contextKeyService: IContextKeyService,
		private _scopedInstatiationService: IInstantiationService,
		private _commentThread: languages.CommentThread<T>,
		private _pendingComment: string | undefined,
		private _pendingEdits: { [key: number]: string } | undefined,
		private _markdownOptions: IMarkdownRendererOptions,
		private _commentOptions: languages.CommentOptions | undefined,
		private _containerDelegate: {
			actionRunner: (() => void) | null;
			collapse: () => void;
		},
		@ICommentService private commentService: ICommentService,
		@IContextMenuService contextMenuService: IContextMenuService
	) {
		super();

		this._threadIsEmpty = CommentContextKeys.commentThreadIsEmpty.bindTo(this._contextKeyService);
		this._threadIsEmpty.set(!_commentThread.comments || !_commentThread.comments.length);

		this._commentMenus = this.commentService.getCommentMenus(this._owner);

		this._header = new CommentThreadHeader<T>(
			container,
			{
				collapse: this.collapse.bind(this)
			},
			this._commentMenus,
			this._commentThread,
			this._contextKeyService,
			this._scopedInstatiationService,
			contextMenuService
		);

		this._header.updateCommentThread(this._commentThread);

		const bodyElement = <HTMLDivElement>dom.$('.body');
		container.appendChild(bodyElement);

		this._body = this._scopedInstatiationService.createInstance(
			CommentThreadBody,
			this._owner,
			this._parentResourceUri,
			bodyElement,
			this._markdownOptions,
			this._commentThread,
			this._pendingEdits,
			this._scopedInstatiationService,
			this
		) as unknown as CommentThreadBody<T>;
		this._register(this._body);

		this._styleElement = dom.createStyleSheet(this.container);


		this._commentThreadContextValue = CommentContextKeys.commentThreadContext.bindTo(this._contextKeyService);
		this._commentThreadContextValue.set(_commentThread.contextValue);

		const commentControllerKey = CommentContextKeys.commentControllerContext.bindTo(this._contextKeyService);
		const controller = this.commentService.getCommentController(this._owner);

		if (controller?.contextValue) {
			commentControllerKey.set(controller.contextValue);
		}

		this.currentThreadListeners();
	}

	private updateCurrentThread(hasMouse: boolean, hasFocus: boolean) {
		if (hasMouse || hasFocus) {
			this.commentService.setCurrentCommentThread(this.commentThread);
		} else {
			this.commentService.setCurrentCommentThread(undefined);
		}
	}

	private currentThreadListeners() {
		let hasMouse = false;
		let hasFocus = false;
		this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_ENTER, (e) => {
			if ((<any>e).toElement === this.container) {
				hasMouse = true;
				this.updateCurrentThread(hasMouse, hasFocus);
			}
		}, true));
		this._register(dom.addDisposableListener(this.container, dom.EventType.MOUSE_LEAVE, (e) => {
			if ((<any>e).fromElement === this.container) {
				hasMouse = false;
				this.updateCurrentThread(hasMouse, hasFocus);
			}
		}, true));
		this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_IN, () => {
			hasFocus = true;
			this.updateCurrentThread(hasMouse, hasFocus);
		}, true));
		this._register(dom.addDisposableListener(this.container, dom.EventType.FOCUS_OUT, () => {
			hasFocus = false;
			this.updateCurrentThread(hasMouse, hasFocus);
		}, true));
	}

	updateCommentThread(commentThread: languages.CommentThread<T>) {
		this._commentThread = commentThread;
		dispose(this._commentThreadDisposables);
		this._commentThreadDisposables = [];
		this._bindCommentThreadListeners();

		this._body.updateCommentThread(commentThread);
		this._threadIsEmpty.set(!this._body.length);
		this._header.updateCommentThread(commentThread);
		this._commentReply?.updateCommentThread(commentThread);

		if (this._commentThread.contextValue) {
			this._commentThreadContextValue.set(this._commentThread.contextValue);
		} else {
			this._commentThreadContextValue.reset();
		}
	}

	display(lineHeight: number) {
		const headHeight = Math.ceil(lineHeight * 1.2);
		this._header.updateHeight(headHeight);

		this._body.display();

		// create comment thread only when it supports reply
		if (this._commentThread.canReply) {
			this._createCommentForm();
		}
		this._createAdditionalActions();

		this._register(this._body.onDidResize(dimension => {
			this._refresh(dimension);
		}));

		// If there are no existing comments, place focus on the text area. This must be done after show, which also moves focus.
		// if this._commentThread.comments is undefined, it doesn't finish initialization yet, so we don't focus the editor immediately.
		if (this._commentThread.canReply && this._commentReply) {
			this._commentReply.focusIfNeeded();
		}

		this._bindCommentThreadListeners();
	}

	private _refresh(dimension: dom.Dimension) {
		this._body.layout();
		this._onDidResize.fire(dimension);
	}

	override dispose() {
		super.dispose();
		this.updateCurrentThread(false, false);
	}

	private _bindCommentThreadListeners() {
		this._commentThreadDisposables.push(this._commentThread.onDidChangeCanReply(() => {
			if (this._commentReply) {
				this._commentReply.updateCanReply();
			} else {
				if (this._commentThread.canReply) {
					this._createCommentForm();
				}
			}
		}));

		this._commentThreadDisposables.push(this._commentThread.onDidChangeComments(async _ => {
			await this.updateCommentThread(this._commentThread);
		}));

		this._commentThreadDisposables.push(this._commentThread.onDidChangeLabel(_ => {
			this._header.createThreadLabel();
		}));
	}

	private _createCommentForm() {
		this._commentReply = this._scopedInstatiationService.createInstance(
			CommentReply,
			this._owner,
			this._body.container,
			this._commentThread,
			this._scopedInstatiationService,
			this._contextKeyService,
			this._commentMenus,
			this._commentOptions,
			this._pendingComment,
			this,
			this._containerDelegate.actionRunner
		);

		this._register(this._commentReply);
	}

	private _createAdditionalActions() {
		this._additionalActions = this._scopedInstatiationService.createInstance(
			CommentThreadAdditionalActions,
			this._body.container,
			this._commentThread,
			this._contextKeyService,
			this._commentMenus,
			this._containerDelegate.actionRunner,
		);

		this._register(this._additionalActions);
	}

	getCommentCoords(commentUniqueId: number) {
		return this._body.getCommentCoords(commentUniqueId);
	}

	getPendingEdits(): { [key: number]: string } {
		return this._body.getPendingEdits();
	}

	getPendingComment(): string | undefined {
		if (this._commentReply) {
			return this._commentReply.getPendingComment();
		}

		return undefined;
	}

	getDimensions() {
		return this._body.getDimensions();
	}

	layout(widthInPixel?: number) {
		this._body.layout();

		if (widthInPixel !== undefined) {
			this._commentReply?.layout(widthInPixel);
		}
	}

	focusCommentEditor() {
		this._commentReply?.focusCommentEditor();
	}

	focus() {
		this._body.focus();
	}

	async submitComment() {
		const activeComment = this._body.activeComment;
		if (activeComment) {
			activeComment.submitComment();
		} else if ((this._commentReply?.getPendingComment()?.length ?? 0) > 0) {
			this._commentReply?.submitComment();
		}
	}

	collapse() {
		this._containerDelegate.collapse();
	}

	applyTheme(theme: IColorTheme, fontInfo: FontInfo) {
		const content: string[] = [];

		content.push(`.monaco-editor .review-widget > .body { border-top: 1px solid var(${commentThreadStateColorVar}) }`);
		content.push(`.monaco-editor .review-widget > .head { background-color: var(${commentThreadStateBackgroundColorVar}) }`);

		const linkColor = theme.getColor(textLinkForeground);
		if (linkColor) {
			content.push(`.review-widget .body .comment-body a { color: ${linkColor} }`);
		}

		const linkActiveColor = theme.getColor(textLinkActiveForeground);
		if (linkActiveColor) {
			content.push(`.review-widget .body .comment-body a:hover, a:active { color: ${linkActiveColor} }`);
		}

		const focusColor = theme.getColor(focusBorder);
		if (focusColor) {
			content.push(`.review-widget .body .comment-body a:focus { outline: 1px solid ${focusColor}; }`);
			content.push(`.review-widget .body .monaco-editor.focused { outline: 1px solid ${focusColor}; }`);
		}

		const blockQuoteBackground = theme.getColor(textBlockQuoteBackground);
		if (blockQuoteBackground) {
			content.push(`.review-widget .body .review-comment blockquote { background: ${blockQuoteBackground}; }`);
		}

		const blockQuoteBOrder = theme.getColor(textBlockQuoteBorder);
		if (blockQuoteBOrder) {
			content.push(`.review-widget .body .review-comment blockquote { border-color: ${blockQuoteBOrder}; }`);
		}

		const border = theme.getColor(PANEL_BORDER);
		if (border) {
			content.push(`.review-widget .body .review-comment .review-comment-contents .comment-reactions .action-item a.action-label { border-color: ${border}; }`);
		}

		const hcBorder = theme.getColor(contrastBorder);
		if (hcBorder) {
			content.push(`.review-widget .body .comment-form .review-thread-reply-button { outline-color: ${hcBorder}; }`);
			content.push(`.review-widget .body .monaco-editor { outline: 1px solid ${hcBorder}; }`);
		}

		const errorBorder = theme.getColor(inputValidationErrorBorder);
		if (errorBorder) {
			content.push(`.review-widget .validation-error { border: 1px solid ${errorBorder}; }`);
		}

		const errorBackground = theme.getColor(inputValidationErrorBackground);
		if (errorBackground) {
			content.push(`.review-widget .validation-error { background: ${errorBackground}; }`);
		}

		const errorForeground = theme.getColor(inputValidationErrorForeground);
		if (errorForeground) {
			content.push(`.review-widget .body .comment-form .validation-error { color: ${errorForeground}; }`);
		}

		const fontFamilyVar = '--comment-thread-editor-font-family';
		const fontSizeVar = '--comment-thread-editor-font-size';
		const fontWeightVar = '--comment-thread-editor-font-weight';
		this.container?.style.setProperty(fontFamilyVar, fontInfo.fontFamily);
		this.container?.style.setProperty(fontSizeVar, `${fontInfo.fontSize}px`);
		this.container?.style.setProperty(fontWeightVar, fontInfo.fontWeight);

		content.push(`.review-widget .body code {
			font-family: var(${fontFamilyVar});
			font-weight: var(${fontWeightVar});
		}`);

		this._styleElement.textContent = content.join('\n');
		this._commentReply?.setCommentEditorDecorations();
	}
}
