import { defaultWindowId } from '@joplin/lib/reducer';
import shim from '@joplin/lib/shim';
import * as React from 'react';
import { useState, useEffect, useRef, createContext } from 'react';
import { createPortal } from 'react-dom';
import { SecondaryWindowApi } from '../utils/window/types';

// This component uses react-dom's Portals to render its children in a different HTML
// document. As children are rendered in a different Window/Document, they should avoid
// referencing the `window` and `document` globals. Instead, HTMLElement.ownerDocument
// and refs can be used to access the child component's DOM.

export const WindowIdContext = createContext(defaultWindowId);

type OnCloseCallback = ()=> void;
type OnFocusCallback = ()=> void;

export enum WindowMode {
	Iframe, NewWindow,
}

interface Props {
	// Note: children will be rendered in a different DOM from this node. Avoid using document.* methods
	// in child components.
	children: React.ReactNode[]|React.ReactNode;
	title: string;
	mode: WindowMode;
	windowId: string;
	onClose: OnCloseCallback;
	onFocus?: OnFocusCallback;
}

const useDocument = (
	mode: WindowMode,
	iframeElement: HTMLIFrameElement|null,
	onClose: OnCloseCallback,
) => {
	const [doc, setDoc] = useState<Document>(null);

	const onCloseRef = useRef(onClose);
	onCloseRef.current = onClose;

	useEffect(() => {
		let openedWindow: Window|null = null;
		const unmounted = false;
		if (iframeElement) {
			setDoc(iframeElement?.contentWindow?.document);
		} else if (mode === WindowMode.NewWindow) {
			openedWindow = window.open('about:blank');
			setDoc(openedWindow.document);

			// .onbeforeunload and .onclose events don't seem to fire when closed by a user -- rely on polling
			// instead:
			void (async () => {
				while (!unmounted) {
					await new Promise<void>(resolve => {
						shim.setTimeout(() => resolve(), 2000);
					});

					if (openedWindow?.closed) {
						onCloseRef.current?.();
						openedWindow = null;
						break;
					}
				}
			})();
		}

		return () => {
			// Delay: Closing immediately causes Electron to crash
			setTimeout(() => {
				if (!openedWindow?.closed) {
					openedWindow?.close();
					onCloseRef.current?.();
					openedWindow = null;
				}
			}, 200);

			if (iframeElement && !openedWindow) {
				onCloseRef.current?.();
			}
		};
	}, [iframeElement, mode]);

	return doc;
};

type OnSetLoaded = (loaded: boolean)=> void;
const useDocumentSetup = (doc: Document|null, setLoaded: OnSetLoaded, onFocus?: OnFocusCallback) => {
	const onFocusRef = useRef(onFocus);
	onFocusRef.current = onFocus;

	useEffect(() => {
		if (!doc) return;

		doc.open();
		doc.write('<!DOCTYPE html><html><head></head><body></body></html>');
		doc.close();

		const cssUrls = [
			'style.min.css',
		];

		for (const url of cssUrls) {
			const style = doc.createElement('link');
			style.rel = 'stylesheet';
			style.href = url;
			doc.head.appendChild(style);
		}

		const jsUrls = [
			'vendor/lib/smalltalk/dist/smalltalk.min.js',
			'./utils/window/eventHandlerOverrides.js',
		];
		for (const url of jsUrls) {
			const script = doc.createElement('script');
			script.src = url;
			doc.head.appendChild(script);
		}

		doc.body.style.height = '100vh';

		const containerWindow = doc.defaultView;
		containerWindow.addEventListener('focus', () => {
			onFocusRef.current?.();
		});
		if (doc.hasFocus()) {
			onFocusRef.current?.();
		}

		setLoaded(true);
	}, [doc, setLoaded]);
};

const NewWindowOrIFrame: React.FC<Props> = props => {
	const [iframeRef, setIframeRef] = useState<HTMLIFrameElement|null>(null);
	const [loaded, setLoaded] = useState(false);

	const doc = useDocument(props.mode, iframeRef, props.onClose);
	useDocumentSetup(doc, setLoaded, props.onFocus);

	useEffect(() => {
		if (!doc) return;
		doc.title = props.title;
	}, [doc, props.title]);

	useEffect(() => {
		const win = doc?.defaultView;
		if (win && 'electronWindow' in win && typeof win.electronWindow === 'object') {
			const electronWindow = win.electronWindow as SecondaryWindowApi;
			electronWindow.onSetWindowId(props.windowId);
		}
	}, [doc, props.windowId]);

	const parentNode = loaded ? doc?.body : null;
	const wrappedChildren = <WindowIdContext.Provider value={props.windowId}>{props.children}</WindowIdContext.Provider>;

	// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Needed to allow adding the portal to the DOM
	const contentPortal = parentNode && createPortal(wrappedChildren, parentNode) as any;
	if (props.mode === WindowMode.NewWindow) {
		return <div style={{ display: 'none' }}>{contentPortal}</div>;
	} else {
		return <iframe
			ref={setIframeRef}
			style={{ flexGrow: 1, width: '100%', height: '100%', border: 'none' }}
		>
			{contentPortal}
		</iframe>;
	}
};

export default NewWindowOrIFrame;