import {
	Editor,
	TLAnyBindingUtilConstructor,
	TLAnyShapeUtilConstructor,
	TLAssetStore,
	TLEditorSnapshot,
	TLImageExportOptions,
	TLPageId,
	TLStoreSnapshot,
	TLTextOptions,
	TldrawOptions,
	mergeArraysAndReplaceDefaults,
	useShallowArrayIdentity,
	useTLStore,
} from '@tldraw/editor'
import { memo, useEffect, useLayoutEffect, useMemo, useState } from 'react'
import { defaultBindingUtils } from './defaultBindingUtils'
import { defaultShapeUtils } from './defaultShapeUtils'
import { TLUiAssetUrlOverrides } from './ui/assetUrls'
import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls'
import { defaultAddFontsFromNode, tipTapDefaultExtensions } from './utils/text/richText'

/** @public */
export interface TldrawImageProps extends TLImageExportOptions {
	/**
	 * The snapshot to display.
	 */
	snapshot: Partial<TLEditorSnapshot> | TLStoreSnapshot

	/**
	 * The image format to use. Defaults to 'svg'.
	 */
	format?: 'svg' | 'png'

	/**
	 * The page to display. Defaults to the first page.
	 */
	pageId?: TLPageId

	/**
	 * Additional shape utils to use.
	 */
	shapeUtils?: readonly TLAnyShapeUtilConstructor[]
	/**
	 * Additional binding utils to use.
	 */
	bindingUtils?: readonly TLAnyBindingUtilConstructor[]
	/**
	 * The license key.
	 */
	licenseKey?: string
	/**
	 * Asset URL overrides.
	 */
	assetUrls?: TLUiAssetUrlOverrides
	/**
	 * The asset store to use for resolving the snapshot's assets, matching the `assets` prop on
	 * {@link Tldraw}. Without one, assets that aren't stored inline (as data URLs) can't be
	 * resolved and won't appear in the image.
	 */
	assets?: TLAssetStore
	/**
	 * Options for the editor.
	 */
	options?: Partial<TldrawOptions>
	/**
	 * Text options for the editor.
	 *
	 * @deprecated Use `options.text` instead. This prop will be removed in a future release.
	 */
	textOptions?: TLTextOptions
}

const defaultOptions: Partial<TldrawOptions> = {
	text: {
		tipTapConfig: {
			extensions: tipTapDefaultExtensions,
		},
		addFontsFromNode: defaultAddFontsFromNode,
	},
}

/**
 * A rendered SVG image of a Tldraw snapshot.
 *
 * @example
 * ```tsx
 * <TldrawImage
 * 	snapshot={snapshot}
 * 	pageId={pageId}
 * 	background={false}
 *  darkMode={true}
 *  bounds={new Box(0,0,600,400)}
 *  scale={1}
 * />
 * ```
 *
 * @public
 * @react
 */
export const TldrawImage = memo(function TldrawImage(props: TldrawImageProps) {
	const [url, setUrl] = useState<string | null>(null)
	const [container, setContainer] = useState<HTMLDivElement | null>(null)

	const _shapeUtils = useShallowArrayIdentity(props.shapeUtils ?? [])
	const shapeUtilsWithDefaults = useMemo(
		() => mergeArraysAndReplaceDefaults('type', _shapeUtils, defaultShapeUtils),
		[_shapeUtils]
	)
	const _bindingUtils = useShallowArrayIdentity(props.bindingUtils ?? [])
	const bindingUtilsWithDefaults = useMemo(
		() => mergeArraysAndReplaceDefaults('type', _bindingUtils, defaultBindingUtils),
		[_bindingUtils]
	)
	const store = useTLStore({
		snapshot: props.snapshot,
		shapeUtils: shapeUtilsWithDefaults,
		assets: props.assets,
	})

	const {
		pageId,
		bounds,
		scale,
		pixelRatio,
		background,
		padding,
		darkMode,
		preserveAspectRatio,
		format = 'svg',
		licenseKey,
		assetUrls,
		options: _options,
		// eslint-disable-next-line @typescript-eslint/no-deprecated
		textOptions: _textOptions,
	} = props

	const options = useMemo(
		() => ({
			...defaultOptions,
			..._options,
			text: {
				...defaultOptions.text,
				...(_options?.text ?? _textOptions),
			},
		}),
		[_options, _textOptions]
	)
	const assetUrlsWithOverrides = useDefaultEditorAssetsWithOverrides(assetUrls)

	useLayoutEffect(() => {
		if (!container) return
		if (!store) return

		let isCancelled = false

		const tempElm = container.ownerDocument.createElement('div')
		container.appendChild(tempElm)
		container.classList.add('tl-container', 'tl-theme__light')

		const editor = new Editor({
			store,
			shapeUtils: shapeUtilsWithDefaults,
			bindingUtils: bindingUtilsWithDefaults,
			tools: [],
			getContainer: () => tempElm,
			licenseKey,
			fontAssetUrls: assetUrlsWithOverrides.fonts,
			options,
		})

		if (pageId) editor.setCurrentPage(pageId)

		const shapeIds = editor.getCurrentPageShapeIds()

		async function setSvg() {
			// toImage waits for the required fonts to load (so text is measured correctly) before
			// it renders, so we don't need to do it here.
			const imageResult = await editor.toImage([...shapeIds], {
				bounds,
				scale,
				background,
				padding,
				darkMode,
				preserveAspectRatio,
				format,
			})
			if (!imageResult || isCancelled) return

			const url = URL.createObjectURL(imageResult.blob)
			setUrl(url)

			editor.dispose()
		}

		setSvg()

		return () => {
			isCancelled = true
		}
	}, [
		format,
		container,
		store,
		shapeUtilsWithDefaults,
		bindingUtilsWithDefaults,
		pageId,
		bounds,
		scale,
		background,
		padding,
		darkMode,
		preserveAspectRatio,
		licenseKey,
		pixelRatio,
		assetUrlsWithOverrides,
		options,
	])

	useEffect(() => {
		return () => {
			if (url) URL.revokeObjectURL(url)
		}
	}, [url])

	return (
		<div ref={setContainer} style={{ position: 'relative', width: '100%', height: '100%' }}>
			{url && (
				<img
					src={url}
					referrerPolicy="strict-origin-when-cross-origin"
					style={{ width: '100%', height: '100%' }}
				/>
			)}
		</div>
	)
})
