import { ExternalEditorPlugin } from '@microsoft/stacks-editor';
import { MarkdownSerializerState } from 'prosemirror-markdown';
import { Node as ProsemirrorNode } from 'prosemirror-model';
import { EditorView, NodeView } from 'prosemirror-view';
import { getImageFromGitHubAndSetSrc } from '../api/github';
import { updateImagesMap } from '../imageHandlers/updateImageMap';
import { AltTextValidationResult, getPosParam, images } from '../models/editor-model';
import { escapeHTML } from '../utils/escapeHtml';

// keep track of all images so we can assign unique IDs for stacks popovers
let imageId = 0;
export class DocsImagesView implements NodeView {
	dom?: Node | null;
	img: HTMLImageElement;
	dataUrl: string;
	popover: HTMLElement;
	form: HTMLFormElement;
	id: number;
	selectionActive: boolean;

	constructor(node: ProsemirrorNode, view: EditorView, getPos: getPosParam) {
		this.id = imageId++;

		this.img = this.createImage(node, view, getPos);
		this.form = this.createForm();
		this.form.addEventListener('submit', event =>
			this.handleChangedImageAttributes(event, getPos, view)
		);

		this.popover = this.createPopover();

		this.dom = document.createElement('div');
		this.dom.appendChild(this.img);
		this.dom.appendChild(this.popover);
		this.dom.addEventListener('s-popover:hide', (event: Event) => this.preventClose(event));
	}
	/**
	 * We want to trigger Stacks' showing and hiding of popovers whenever an image is considered
	 * "selected" by prosemirror. This can happen by mouse-clicking or arrowing through the editor
	 */
	selectNode(): void {
		this.img.classList.add('bs-ring');
		this.selectionActive = true;
		// tell Stacks to hide the popover
		this.img.dispatchEvent(new Event('image-popover-show'));

		this.setFocus();
	}

	deselectNode(): void {
		this.img.classList.remove('bs-ring');
		this.selectionActive = false;
		// tell Stacks to hide the popover
		this.img.dispatchEvent(new Event('image-popover-hide'));
	}

	stopEvent(event: Event): boolean {
		// prevent elements from within the tooltip to bubble up to the outer editor
		// as this would make the editor view close the tooltip view right away
		return this.popover.contains(event.target as Element);
	}

	ignoreMutation(): boolean {
		return true;
	}

	destroy(): void {
		this.img.remove();
		this.popover.remove();
		this.dom = null;
		this.form.remove();
	}

	private setFocus() {
		const inputFields = this.form.querySelectorAll('input');
		if (inputFields.length > 0) {
			const srcInputField = inputFields[0] as HTMLInputElement;
			if (!srcInputField.disabled) {
				srcInputField.focus({
					preventScroll: true
				});
			} else {
				const altInputField = inputFields[1] as HTMLInputElement;
				altInputField.focus({
					preventScroll: true
				});
			}
		}
	}

	private showValidationError(errorMessage: string, level = 'warning'): void {
		const validationElement = (this.form as HTMLElement).querySelector('.js-validation-message');

		if (level === 'warning') {
			validationElement.classList.remove('s-notice__danger');
			validationElement.classList.add('s-notice__warning');
		} else {
			validationElement.classList.remove('s-notice__warning');
			validationElement.classList.add('s-notice__danger');
		}

		validationElement.classList.remove('d-none');
		validationElement.textContent = errorMessage;
	}

	private hideValidationError(): void {
		const validationElement = (this.form as HTMLElement).querySelector('.js-validation-message');
		validationElement.classList.add('d-none');
		validationElement.classList.remove('s-notice__warning');
		validationElement.classList.remove('s-notice__danger');
		validationElement.innerHTML = '';
	}

	private updateNode(
		view: EditorView,
		getPos: getPosParam,
		attributes: { src: string; alt: string }
	): void {
		if (typeof getPos !== 'function') return;
		const { src, alt } = attributes;
		view.dispatch(
			view.state.tr.setNodeMarkup(getPos(), null, {
				src,
				alt
			})
		);

		view.focus();
	}

	private shouldUpdateImageStore(src: string) {
		const found = images.get(src);
		if (!found) {
			images.forEach(({ data, file }, key) => {
				const imageElement = document.querySelector(
					`img[aria-controls="img-popover-${this.id}"]`
				) as HTMLImageElement;
				if (imageElement.src === data && key !== src) {
					updateImagesMap(images, src, { id: this.id.toString(), data, file }, key);
				}
			});
		}
	}

	private createImage(
		node: ProsemirrorNode,
		view: EditorView,
		getPos: getPosParam
	): HTMLImageElement {
		let src = node.attrs.src as string;

		const img = document.createElement('img');
		img.setAttribute('aria-controls', `img-popover-${this.id}`);
		img.setAttribute('data-controller', 's-popover');
		img.setAttribute(
			'data-action',
			'image-popover-show->s-popover#show image-popover-hide->s-popover#hide'
		);
		// is this a relative path?
		// pull that image from github
		if (src && !images.get(src)) {
			if (!/^http(s)?:\/\//.test(src) && !/^data:image\/(gif|png|jpeg|webp);/.test(src)) {
				const updateNode = this.updateNode;
				const id = this.id;
				getImageFromGitHubAndSetSrc(view, getPos, node, updateNode, id);
			}
		} else {
			const found = images.get(src);
			src = found.data;
		}
		if (!img.src) img.src = src;
		if (node.attrs.alt) img.alt = node.attrs.alt as string;
		if (node.attrs.title) img.title = node.attrs.title as string;

		return img;
	}

	private createForm(): HTMLFormElement {
		let src = this.img.src;
		let disableInput = false;
		images.forEach(({ data, file }, key) => {
			if (this.img.src === data) {
				src = key;
				if (!file) {
					disableInput = true;
				}
			}
		});
		const form = document.createElement('form');
		form.className = 'd-flex fd-column';
		form.innerHTML = escapeHTML`
            <label class="flex--item s-label mb4" for="img-src-${this.id}">Image src</label>
            <div class="d-flex ps-relative mb12">
                <input class="flex--item s-input" type="text" name="src" ${
									disableInput ? 'disabled' : ''
								} id="img-src-${
			this.id
		}" value="${src}" placeholder="https://example.com/image.png"/>
            </div>
            <label class="flex--item s-label mb4" for="img-alt-${this.id}">Image description</label>
            <div class="d-flex ps-relative mb12">
                <input class="flex--item s-input" type="text" name="alt" id="img-alt-${
									this.id
								}" value="${this.img.alt}" placeholder="A description for the image"/>
            </div>
            <div class="d-flex gs4 gsy fd-column">
            <aside class="s-notice s-notice__warning d-none m8 js-validation-message" role="status" aria-hidden="true"></aside>
            <button class="s-btn s-btn__primary" type="submit" aria-pressed="false">Apply</button>
        `;
		return form;
	}

	private createPopover(): HTMLDivElement {
		const popover = document.createElement('div');
		popover.className = 's-popover ws-normal wb-normal js-img-popover';
		popover.id = `img-popover-${this.id}`;

		// TODO added `ws-normal` to fix FF only bug. Will file bug against Stacks and revisit
		popover.innerHTML = `<div class="s-popover--arrow ws-normal"></div>`;
		popover.append(this.form);

		return popover;
	}

	private handleChangedImageAttributes(event: Event, getPos: getPosParam, view: EditorView) {
		event.preventDefault();

		if (typeof getPos !== 'function') return;

		this.hideValidationError();

		const findInput = (selector: string): HTMLInputElement => this.form.querySelector(selector);
		const alt = findInput(`#img-alt-${this.id}`);
		const src = findInput(`#img-src-${this.id}`);
		const validationResult = this.validateImageAltText(alt.value);
		switch (validationResult) {
			case AltTextValidationResult.AltTextDoesntEndWithPeriod:
				this.showValidationError('Alt text should end with a period.');
				return;
			case AltTextValidationResult.AltTextStartsWithLowerCase:
				this.showValidationError('Alt text should start with an uppercase letter.');
				return;
			case AltTextValidationResult.AltTextTooLong:
				this.showValidationError('Alt text too long. The max is 150 characters, including spaces.');
				return;
		}
		this.shouldUpdateImageStore(src.value);
		this.updateNode(view, getPos, { src: src.value, alt: alt.value });
	}

	private validateImageAltText(alt: string): AltTextValidationResult {
		if (!alt.endsWith('.')) {
			return AltTextValidationResult.AltTextDoesntEndWithPeriod;
		}
		if (alt.charAt(0) === alt.charAt(0).toLowerCase()) {
			return AltTextValidationResult.AltTextStartsWithLowerCase;
		}
		if (alt.length > 150) {
			return AltTextValidationResult.AltTextTooLong;
		}

		return AltTextValidationResult.Ok;
	}

	// don't let Stacks close the popover if the image
	// element is still selected in the editor
	private preventClose(event: Event) {
		if (this.selectionActive) {
			event.preventDefault();
		}
	}
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export const DocsImagesPlugin: ExternalEditorPlugin = {
	markdownParser: {
		tokens: {},
		plugins: []
	},
	markdownSerializers: {
		image(state: MarkdownSerializerState, node: ProsemirrorNode): void {
			let src = node.attrs.src as string;
			images.forEach(({ data }, key) => {
				if (src === data) {
					src = key;
				}
			});
			state.write(
				'![' +
					state.esc(node.attrs.alt || '') +
					'](' +
					state.esc(src) +
					(node.attrs.title ? ' ' + state.quote(node.attrs.title) : '') +
					')'
			);
		}
	},
	menuEntries: [],
	nodeViews: {
		image(node, view, getPos): NodeView {
			return new DocsImagesView(node, view, getPos);
		}
	},
	plugins: [],
	schema: null
};
