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

import * as dom from '../../../../base/browser/dom.js';
import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js';
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import { URI } from '../../../../base/common/uri.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
import { Location, SymbolKinds } from '../../../../editor/common/languages.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { DefinitionAction } from '../../../../editor/contrib/gotoSymbol/browser/goToCommands.js';
import * as nls from '../../../../nls.js';
import { getFlatContextMenuActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { Action2, IMenuService, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { IResourceStat } from '../../../../platform/dnd/browser/dnd.js';
import { ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js';
import { FileKind, IFileService } from '../../../../platform/files/common/files.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ILabelService } from '../../../../platform/label/common/label.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { FolderThemeIcon, IThemeService } from '../../../../platform/theme/common/themeService.js';
import { fillEditorsDragData } from '../../../browser/dnd.js';
import { ResourceContextKey } from '../../../common/contextkeys.js';
import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
import { INotebookDocumentService } from '../../../services/notebook/common/notebookDocumentService.js';
import { ExplorerFolderContext } from '../../files/common/files.js';
import { IWorkspaceSymbol } from '../../search/common/search.js';
import { IChatContentInlineReference } from '../common/chatService.js';
import { IChatWidgetService } from './chat.js';
import { chatAttachmentResourceContextKey, hookUpSymbolAttachmentDragAndContextMenu } from './chatAttachmentWidgets.js';
import { IChatMarkdownAnchorService } from './chatContentParts/chatMarkdownAnchorService.js';

type ContentRefData =
	| { readonly kind: 'symbol'; readonly symbol: IWorkspaceSymbol }
	| {
		readonly kind?: undefined;
		readonly uri: URI;
		readonly range?: IRange;
	};

export function renderFileWidgets(element: HTMLElement, instantiationService: IInstantiationService, chatMarkdownAnchorService: IChatMarkdownAnchorService, disposables: DisposableStore) {
	// eslint-disable-next-line no-restricted-syntax
	const links = element.querySelectorAll('a');
	links.forEach(a => {
		// Empty link text -> render file widget
		if (!a.textContent?.trim()) {
			const href = a.getAttribute('data-href');
			const uri = href ? URI.parse(href) : undefined;
			if (uri?.scheme) {
				const widget = instantiationService.createInstance(InlineAnchorWidget, a, { kind: 'inlineReference', inlineReference: uri });
				disposables.add(chatMarkdownAnchorService.register(widget));
				disposables.add(widget);
			}
		}
	});
}

export class InlineAnchorWidget extends Disposable {

	public static readonly className = 'chat-inline-anchor-widget';

	private readonly _chatResourceContext: IContextKey<string>;

	readonly data: ContentRefData;

	constructor(
		private readonly element: HTMLAnchorElement | HTMLElement,
		public readonly inlineReference: IChatContentInlineReference,
		@IContextKeyService originalContextKeyService: IContextKeyService,
		@IContextMenuService contextMenuService: IContextMenuService,
		@IFileService fileService: IFileService,
		@IHoverService hoverService: IHoverService,
		@IInstantiationService instantiationService: IInstantiationService,
		@ILabelService labelService: ILabelService,
		@ILanguageService languageService: ILanguageService,
		@IMenuService menuService: IMenuService,
		@IModelService modelService: IModelService,
		@ITelemetryService telemetryService: ITelemetryService,
		@IThemeService themeService: IThemeService,
		@INotebookDocumentService private readonly notebookDocumentService: INotebookDocumentService,
	) {
		super();

		// TODO: Make sure we handle updates from an inlineReference being `resolved` late

		this.data = 'uri' in inlineReference.inlineReference
			? inlineReference.inlineReference
			: 'name' in inlineReference.inlineReference
				? { kind: 'symbol', symbol: inlineReference.inlineReference }
				: { uri: inlineReference.inlineReference };

		const contextKeyService = this._register(originalContextKeyService.createScoped(element));
		this._chatResourceContext = chatAttachmentResourceContextKey.bindTo(contextKeyService);

		element.classList.add(InlineAnchorWidget.className, 'show-file-icons');

		let iconText: string;
		let iconClasses: string[];

		let location: { readonly uri: URI; readonly range?: IRange };

		let updateContextKeys: (() => Promise<void>) | undefined;
		if (this.data.kind === 'symbol') {
			const symbol = this.data.symbol;

			location = this.data.symbol.location;
			iconText = this.data.symbol.name;
			iconClasses = ['codicon', ...getIconClasses(modelService, languageService, undefined, undefined, SymbolKinds.toIcon(symbol.kind))];

			this._store.add(instantiationService.invokeFunction(accessor => hookUpSymbolAttachmentDragAndContextMenu(accessor, element, contextKeyService, { value: symbol.location, name: symbol.name, kind: symbol.kind }, MenuId.ChatInlineSymbolAnchorContext)));
		} else {
			location = this.data;

			const label = labelService.getUriBasenameLabel(location.uri);
			iconText = location.range && this.data.kind !== 'symbol' ?
				`${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` :
				location.uri.scheme === 'vscode-notebook-cell' && this.data.kind !== 'symbol' ?
					`${label} • cell${this.getCellIndex(location.uri)}` :
					label;

			let fileKind = location.uri.path.endsWith('/') ? FileKind.FOLDER : FileKind.FILE;
			const recomputeIconClasses = () => getIconClasses(modelService, languageService, location.uri, fileKind, fileKind === FileKind.FOLDER && !themeService.getFileIconTheme().hasFolderIcons ? FolderThemeIcon : undefined);

			iconClasses = recomputeIconClasses();

			const refreshIconClasses = () => {
				iconEl.classList.remove(...iconClasses);
				iconClasses = recomputeIconClasses();
				iconEl.classList.add(...iconClasses);
			};

			this._register(themeService.onDidFileIconThemeChange(() => {
				refreshIconClasses();
			}));

			const isFolderContext = ExplorerFolderContext.bindTo(contextKeyService);
			fileService.stat(location.uri)
				.then(stat => {
					isFolderContext.set(stat.isDirectory);
					if (stat.isDirectory) {
						fileKind = FileKind.FOLDER;
						refreshIconClasses();
					}
				})
				.catch(() => { });

			// Context menu
			this._register(dom.addDisposableListener(element, dom.EventType.CONTEXT_MENU, async domEvent => {
				const event = new StandardMouseEvent(dom.getWindow(domEvent), domEvent);
				dom.EventHelper.stop(domEvent, true);

				try {
					await updateContextKeys?.();
				} catch (e) {
					console.error(e);
				}

				if (this._store.isDisposed) {
					return;
				}

				contextMenuService.showContextMenu({
					contextKeyService,
					getAnchor: () => event,
					getActions: () => {
						const menu = menuService.getMenuActions(MenuId.ChatInlineResourceAnchorContext, contextKeyService, { arg: location.uri });
						return getFlatContextMenuActions(menu);
					},
				});
			}));
		}

		const resourceContextKey = this._register(new ResourceContextKey(contextKeyService, fileService, languageService, modelService));
		resourceContextKey.set(location.uri);
		this._chatResourceContext.set(location.uri.toString());

		const iconEl = dom.$('span.icon');
		iconEl.classList.add(...iconClasses);
		element.replaceChildren(iconEl, dom.$('span.icon-label', {}, iconText));

		const fragment = location.range ? `${location.range.startLineNumber},${location.range.startColumn}` : '';
		element.setAttribute('data-href', (fragment ? location.uri.with({ fragment }) : location.uri).toString());

		// Hover
		const relativeLabel = labelService.getUriLabel(location.uri, { relative: true });
		this._register(hoverService.setupManagedHover(getDefaultHoverDelegate('element'), element, relativeLabel));

		// Drag and drop
		if (this.data.kind !== 'symbol') {
			element.draggable = true;
			this._register(dom.addDisposableListener(element, 'dragstart', e => {
				const stat: IResourceStat = {
					resource: location.uri,
					selection: location.range,
				};
				instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, [stat], e));


				e.dataTransfer?.setDragImage(element, 0, 0);
			}));
		}
	}

	getHTMLElement(): HTMLElement {
		return this.element;
	}

	private getCellIndex(location: URI) {
		const notebook = this.notebookDocumentService.getNotebook(location);
		const index = notebook?.getCellIndex(location) ?? -1;
		return index >= 0 ? ` ${index + 1}` : '';
	}
}

//#region Resource context menu

registerAction2(class AddFileToChatAction extends Action2 {

	static readonly id = 'chat.inlineResourceAnchor.addFileToChat';

	constructor() {
		super({
			id: AddFileToChatAction.id,
			title: nls.localize2('actions.attach.label', "Add File to Chat"),
			menu: [{
				id: MenuId.ChatInlineResourceAnchorContext,
				group: 'chat',
				order: 1,
				when: ExplorerFolderContext.negate(),
			}]
		});
	}

	override async run(accessor: ServicesAccessor, resource: URI): Promise<void> {
		const chatWidgetService = accessor.get(IChatWidgetService);

		const widget = chatWidgetService.lastFocusedWidget;
		if (widget) {
			widget.attachmentModel.addFile(resource);

		}
	}
});

//#endregion

//#region Resource keybindings

registerAction2(class CopyResourceAction extends Action2 {

	static readonly id = 'chat.inlineResourceAnchor.copyResource';

	constructor() {
		super({
			id: CopyResourceAction.id,
			title: nls.localize2('actions.copy.label', "Copy"),
			f1: false,
			precondition: chatAttachmentResourceContextKey,
			keybinding: {
				weight: KeybindingWeight.WorkbenchContrib,
				primary: KeyMod.CtrlCmd | KeyCode.KeyC,
			}
		});
	}

	override async run(accessor: ServicesAccessor): Promise<void> {
		const chatWidgetService = accessor.get(IChatMarkdownAnchorService);
		const clipboardService = accessor.get(IClipboardService);

		const anchor = chatWidgetService.lastFocusedAnchor;
		if (!anchor) {
			return;
		}

		// TODO: we should also write out the standard mime types so that external programs can use them
		// like how `fillEditorsDragData` works but without having an event to work with.
		const resource = anchor.data.kind === 'symbol' ? anchor.data.symbol.location.uri : anchor.data.uri;
		clipboardService.writeResources([resource]);
	}
});

registerAction2(class OpenToSideResourceAction extends Action2 {

	static readonly id = 'chat.inlineResourceAnchor.openToSide';

	constructor() {
		super({
			id: OpenToSideResourceAction.id,
			title: nls.localize2('actions.openToSide.label', "Open to the Side"),
			f1: false,
			precondition: chatAttachmentResourceContextKey,
			keybinding: {
				weight: KeybindingWeight.ExternalExtension + 2,
				primary: KeyMod.CtrlCmd | KeyCode.Enter,
				mac: {
					primary: KeyMod.WinCtrl | KeyCode.Enter
				},
			},
			menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
				id: id,
				group: 'navigation',
				order: 1
			}))
		});
	}

	override async run(accessor: ServicesAccessor, arg?: Location | URI): Promise<void> {
		const editorService = accessor.get(IEditorService);

		const target = this.getTarget(accessor, arg);
		if (!target) {
			return;
		}

		const input: ITextResourceEditorInput = URI.isUri(target)
			? { resource: target }
			: {
				resource: target.uri, options: {
					selection: {
						startColumn: target.range.startColumn,
						startLineNumber: target.range.startLineNumber,
					}
				}
			};

		await editorService.openEditors([input], SIDE_GROUP);
	}

	private getTarget(accessor: ServicesAccessor, arg: URI | Location | undefined): Location | URI | undefined {
		const chatWidgetService = accessor.get(IChatMarkdownAnchorService);

		if (arg) {
			return arg;
		}

		const anchor = chatWidgetService.lastFocusedAnchor;
		if (!anchor) {
			return undefined;
		}

		return anchor.data.kind === 'symbol' ? anchor.data.symbol.location : anchor.data.uri;
	}
});

//#endregion

//#region Symbol context menu

registerAction2(class GoToDefinitionAction extends Action2 {

	static readonly id = 'chat.inlineSymbolAnchor.goToDefinition';

	constructor() {
		super({
			id: GoToDefinitionAction.id,
			title: {
				...nls.localize2('actions.goToDecl.label', "Go to Definition"),
				mnemonicTitle: nls.localize({ key: 'miGotoDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Definition"),
			},
			menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
				id,
				group: '4_symbol_nav',
				order: 1.1,
				when: EditorContextKeys.hasDefinitionProvider,
			}))
		});
	}

	override async run(accessor: ServicesAccessor, location: Location): Promise<unknown> {
		const editorService = accessor.get(ICodeEditorService);
		const instantiationService = accessor.get(IInstantiationService);

		await openEditorWithSelection(editorService, location);

		const action = new DefinitionAction({ openToSide: false, openInPeek: false, muteMessage: true }, { title: { value: '', original: '' }, id: '', precondition: undefined });
		return instantiationService.invokeFunction(accessor => action.run(accessor));
	}
});

async function openEditorWithSelection(editorService: ICodeEditorService, location: Location) {
	await editorService.openCodeEditor({
		resource: location.uri, options: {
			selection: {
				startColumn: location.range.startColumn,
				startLineNumber: location.range.startLineNumber,
			}
		}
	}, null);
}

async function runGoToCommand(accessor: ServicesAccessor, command: string, location: Location) {
	const editorService = accessor.get(ICodeEditorService);
	const commandService = accessor.get(ICommandService);

	await openEditorWithSelection(editorService, location);

	return commandService.executeCommand(command);
}

registerAction2(class GoToTypeDefinitionsAction extends Action2 {

	static readonly id = 'chat.inlineSymbolAnchor.goToTypeDefinitions';

	constructor() {
		super({
			id: GoToTypeDefinitionsAction.id,
			title: {
				...nls.localize2('goToTypeDefinitions.label', "Go to Type Definitions"),
				mnemonicTitle: nls.localize({ key: 'miGotoTypeDefinition', comment: ['&& denotes a mnemonic'] }, "Go to &&Type Definitions"),
			},
			menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
				id,
				group: '4_symbol_nav',
				order: 1.1,
				when: EditorContextKeys.hasTypeDefinitionProvider,
			})),
		});
	}

	override async run(accessor: ServicesAccessor, location: Location): Promise<void> {
		await runGoToCommand(accessor, 'editor.action.goToTypeDefinition', location);
	}
});

registerAction2(class GoToImplementations extends Action2 {

	static readonly id = 'chat.inlineSymbolAnchor.goToImplementations';

	constructor() {
		super({
			id: GoToImplementations.id,
			title: {
				...nls.localize2('goToImplementations.label', "Go to Implementations"),
				mnemonicTitle: nls.localize({ key: 'miGotoImplementations', comment: ['&& denotes a mnemonic'] }, "Go to &&Implementations"),
			},
			menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
				id,
				group: '4_symbol_nav',
				order: 1.2,
				when: EditorContextKeys.hasImplementationProvider,
			})),
		});
	}

	override async run(accessor: ServicesAccessor, location: Location): Promise<void> {
		await runGoToCommand(accessor, 'editor.action.goToImplementation', location);
	}
});

registerAction2(class GoToReferencesAction extends Action2 {

	static readonly id = 'chat.inlineSymbolAnchor.goToReferences';

	constructor() {
		super({
			id: GoToReferencesAction.id,
			title: {
				...nls.localize2('goToReferences.label', "Go to References"),
				mnemonicTitle: nls.localize({ key: 'miGotoReference', comment: ['&& denotes a mnemonic'] }, "Go to &&References"),
			},
			menu: [MenuId.ChatInlineSymbolAnchorContext, MenuId.ChatInputSymbolAttachmentContext].map(id => ({
				id,
				group: '4_symbol_nav',
				order: 1.3,
				when: EditorContextKeys.hasReferenceProvider,
			})),
		});
	}

	override async run(accessor: ServicesAccessor, location: Location): Promise<void> {
		await runGoToCommand(accessor, 'editor.action.goToReferences', location);
	}
});

//#endregion
