import {
	AssetRecordType,
	Editor,
	MediaHelpers,
	TLAsset,
	TLAssetId,
	TLBookmarkShape,
	TLEmbedShape,
	TLShapeId,
	TLShapePartial,
	TLTextShape,
	TLTextShapeProps,
	Vec,
	VecLike,
	assert,
	compact,
	createShapeId,
	getHashForBuffer,
	getHashForString,
} from '@tldraw/editor'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from './shapes/shared/default-shape-constants'
import { TLUiToastsContextType } from './ui/context/toasts'
import { useTranslation } from './ui/hooks/useTranslation/useTranslation'
import { containBoxSize, downsizeImage, isGifAnimated } from './utils/assets/assets'
import { getEmbedInfo } from './utils/embeds/embeds'
import { cleanupText, isRightToLeftLanguage, truncateStringWithEllipsis } from './utils/text/text'

/** @public */
export type TLExternalContentProps = {
	// The maximum dimension (width or height) of an image. Images larger than this will be rescaled to fit. Defaults to infinity.
	maxImageDimension: number
	// The maximum size (in bytes) of an asset. Assets larger than this will be rejected. Defaults to 10mb (10 * 1024 * 1024).
	maxAssetSize: number
	// The mime types of images that are allowed to be handled. Defaults to ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'].
	acceptedImageMimeTypes: readonly string[]
	// The mime types of videos that are allowed to be handled. Defaults to ['video/mp4', 'video/webm', 'video/quicktime'].
	acceptedVideoMimeTypes: readonly string[]
}

export function registerDefaultExternalContentHandlers(
	editor: Editor,
	{
		maxImageDimension,
		maxAssetSize,
		acceptedImageMimeTypes,
		acceptedVideoMimeTypes,
	}: TLExternalContentProps,
	{ toasts, msg }: { toasts: TLUiToastsContextType; msg: ReturnType<typeof useTranslation> }
) {
	// files -> asset
	editor.registerExternalAssetHandler('file', async ({ file: _file }) => {
		const name = _file.name
		let file: Blob = _file
		const isImageType = acceptedImageMimeTypes.includes(file.type)
		const isVideoType = acceptedVideoMimeTypes.includes(file.type)

		assert(isImageType || isVideoType, `File type not allowed: ${file.type}`)
		assert(
			file.size <= maxAssetSize,
			`File size too big: ${(file.size / 1024).toFixed()}kb > ${(maxAssetSize / 1024).toFixed()}kb`
		)

		if (file.type === 'video/quicktime') {
			// hack to make .mov videos work
			file = new Blob([file], { type: 'video/mp4' })
		}

		let size = isImageType
			? await MediaHelpers.getImageSize(file)
			: await MediaHelpers.getVideoSize(file)

		const isAnimated = file.type === 'image/gif' ? await isGifAnimated(file) : isVideoType

		const hash = await getHashForBuffer(await file.arrayBuffer())

		if (isFinite(maxImageDimension)) {
			const resizedSize = containBoxSize(size, { w: maxImageDimension, h: maxImageDimension })
			if (size !== resizedSize && (file.type === 'image/jpeg' || file.type === 'image/png')) {
				size = resizedSize
			}
		}

		// Always rescale the image
		if (file.type === 'image/jpeg' || file.type === 'image/png') {
			file = await downsizeImage(file, size.w, size.h, {
				type: file.type,
				quality: 0.92,
			})
		}

		const assetId: TLAssetId = AssetRecordType.createId(hash)

		const asset = AssetRecordType.create({
			id: assetId,
			type: isImageType ? 'image' : 'video',
			typeName: 'asset',
			props: {
				name,
				src: await MediaHelpers.blobToDataUrl(file),
				w: size.w,
				h: size.h,
				mimeType: file.type,
				isAnimated,
			},
		})

		return asset
	})

	// urls -> bookmark asset
	editor.registerExternalAssetHandler('url', async ({ url }) => {
		let meta: { image: string; title: string; description: string }

		try {
			const resp = await fetch(url, { method: 'GET', mode: 'no-cors' })
			const html = await resp.text()
			const doc = new DOMParser().parseFromString(html, 'text/html')
			meta = {
				image: doc.head.querySelector('meta[property="og:image"]')?.getAttribute('content') ?? '',
				title:
					doc.head.querySelector('meta[property="og:title"]')?.getAttribute('content') ??
					truncateStringWithEllipsis(url, 32),
				description:
					doc.head.querySelector('meta[property="og:description"]')?.getAttribute('content') ?? '',
			}
		} catch (error) {
			console.error(error)
			toasts.addToast({
				title: msg('assets.url.failed'),
			})
			meta = { image: '', title: truncateStringWithEllipsis(url, 32), description: '' }
		}

		// Create the bookmark asset from the meta
		return {
			id: AssetRecordType.createId(getHashForString(url)),
			typeName: 'asset',
			type: 'bookmark',
			props: {
				src: url,
				description: meta.description,
				image: meta.image,
				title: meta.title,
			},
			meta: {},
		}
	})

	// svg text
	editor.registerExternalContentHandler('svg-text', async ({ point, text }) => {
		const position =
			point ??
			(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())

		const svg = new DOMParser().parseFromString(text, 'image/svg+xml').querySelector('svg')
		if (!svg) {
			throw new Error('No <svg/> element present')
		}

		let width = parseFloat(svg.getAttribute('width') || '0')
		let height = parseFloat(svg.getAttribute('height') || '0')

		if (!(width && height)) {
			document.body.appendChild(svg)
			const box = svg.getBoundingClientRect()
			document.body.removeChild(svg)

			width = box.width
			height = box.height
		}

		const asset = await editor.getAssetForExternalContent({
			type: 'file',
			file: new File([text], 'asset.svg', { type: 'image/svg+xml' }),
		})

		if (!asset) throw Error('Could not create an asset')

		createShapesForAssets(editor, [asset], position)
	})

	// embeds
	editor.registerExternalContentHandler('embed', ({ point, url, embed }) => {
		const position =
			point ??
			(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())

		const { width, height } = embed

		const id = createShapeId()

		const shapePartial: TLShapePartial<TLEmbedShape> = {
			id,
			type: 'embed',
			x: position.x - (width || 450) / 2,
			y: position.y - (height || 450) / 2,
			props: {
				w: width,
				h: height,
				url,
			},
		}

		editor.createShapes([shapePartial]).select(id)
	})

	// files
	editor.registerExternalContentHandler('files', async ({ point, files }) => {
		const position =
			point ??
			(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())

		const pagePoint = new Vec(position.x, position.y)

		const assets: TLAsset[] = []

		await Promise.all(
			files.map(async (file, i) => {
				if (file.size > maxAssetSize) {
					console.warn(
						`File size too big: ${(file.size / 1024).toFixed()}kb > ${(
							maxAssetSize / 1024
						).toFixed()}kb`
					)
					return null
				}

				// Use mime type instead of file ext, this is because
				// window.navigator.clipboard does not preserve file names
				// of copied files.
				if (!file.type) {
					throw new Error('No mime type')
				}

				// We can only accept certain extensions (either images or a videos)
				if (!acceptedImageMimeTypes.concat(acceptedVideoMimeTypes).includes(file.type)) {
					console.warn(`${file.name} not loaded - Extension not allowed.`)
					return null
				}

				try {
					const asset = await editor.getAssetForExternalContent({ type: 'file', file })

					if (!asset) {
						throw Error('Could not create an asset')
					}

					assets[i] = asset
				} catch (error) {
					toasts.addToast({
						title: msg('assets.files.upload-failed'),
					})
					console.error(error)
					return null
				}
			})
		)

		createShapesForAssets(editor, compact(assets), pagePoint)
	})

	// text
	editor.registerExternalContentHandler('text', async ({ point, text }) => {
		const p =
			point ??
			(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())

		const defaultProps = editor.getShapeUtil<TLTextShape>('text').getDefaultProps()

		const textToPaste = cleanupText(text)

		// Measure the text with default values
		let w: number
		let h: number
		let autoSize: boolean
		let align = 'middle' as TLTextShapeProps['align']

		const isMultiLine = textToPaste.split('\n').length > 1

		// check whether the text contains the most common characters in RTL languages
		const isRtl = isRightToLeftLanguage(textToPaste)

		if (isMultiLine) {
			align = isMultiLine ? (isRtl ? 'end' : 'start') : 'middle'
		}

		const rawSize = editor.textMeasure.measureText(textToPaste, {
			...TEXT_PROPS,
			fontFamily: FONT_FAMILIES[defaultProps.font],
			fontSize: FONT_SIZES[defaultProps.size],
			maxWidth: null,
		})

		const minWidth = Math.min(
			isMultiLine ? editor.getViewportPageBounds().width * 0.9 : 920,
			Math.max(200, editor.getViewportPageBounds().width * 0.9)
		)

		if (rawSize.w > minWidth) {
			const shrunkSize = editor.textMeasure.measureText(textToPaste, {
				...TEXT_PROPS,
				fontFamily: FONT_FAMILIES[defaultProps.font],
				fontSize: FONT_SIZES[defaultProps.size],
				maxWidth: minWidth,
			})
			w = shrunkSize.w
			h = shrunkSize.h
			autoSize = false
			align = isRtl ? 'end' : 'start'
		} else {
			// autosize is fine
			w = rawSize.w
			h = rawSize.h
			autoSize = true
		}

		if (p.y - h / 2 < editor.getViewportPageBounds().minY + 40) {
			p.y = editor.getViewportPageBounds().minY + 40 + h / 2
		}

		editor.createShapes<TLTextShape>([
			{
				id: createShapeId(),
				type: 'text',
				x: p.x - w / 2,
				y: p.y - h / 2,
				props: {
					text: textToPaste,
					// if the text has more than one line, align it to the left
					align,
					autoSize,
					w,
				},
			},
		])
	})

	// url
	editor.registerExternalContentHandler('url', async ({ point, url }) => {
		// try to paste as an embed first
		const embedInfo = getEmbedInfo(url)

		if (embedInfo) {
			return editor.putExternalContent({
				type: 'embed',
				url: embedInfo.url,
				point,
				embed: embedInfo.definition,
			})
		}

		const position =
			point ??
			(editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.getViewportPageCenter())

		const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
		const shape = createEmptyBookmarkShape(editor, url, position)

		// Use an existing asset if we have one, or else else create a new one
		let asset = editor.getAsset(assetId) as TLAsset
		let shouldAlsoCreateAsset = false
		if (!asset) {
			shouldAlsoCreateAsset = true
			try {
				const bookmarkAsset = await editor.getAssetForExternalContent({ type: 'url', url })
				if (!bookmarkAsset) throw Error('Could not create an asset')
				asset = bookmarkAsset
			} catch (e) {
				toasts.addToast({
					title: msg('assets.url.failed'),
				})
				return
			}
		}

		editor.batch(() => {
			if (shouldAlsoCreateAsset) {
				editor.createAssets([asset])
			}

			editor.updateShapes([
				{
					id: shape.id,
					type: shape.type,
					props: {
						assetId: asset.id,
					},
				},
			])
		})
	})
}

export async function createShapesForAssets(
	editor: Editor,
	assets: TLAsset[],
	position: VecLike
): Promise<TLShapeId[]> {
	if (!assets.length) return []

	const currentPoint = Vec.From(position)
	const partials: TLShapePartial[] = []

	for (const asset of assets) {
		switch (asset.type) {
			case 'bookmark': {
				partials.push({
					id: createShapeId(),
					type: 'bookmark',
					x: currentPoint.x - 150,
					y: currentPoint.y - 160,
					opacity: 1,
					props: {
						assetId: asset.id,
						url: asset.props.src,
					},
				})

				currentPoint.x += 300
				break
			}
			case 'image': {
				partials.push({
					id: createShapeId(),
					type: 'image',
					x: currentPoint.x - asset.props.w / 2,
					y: currentPoint.y - asset.props.h / 2,
					opacity: 1,
					props: {
						assetId: asset.id,
						w: asset.props.w,
						h: asset.props.h,
					},
				})

				currentPoint.x += asset.props.w
				break
			}
			case 'video': {
				partials.push({
					id: createShapeId(),
					type: 'video',
					x: currentPoint.x - asset.props.w / 2,
					y: currentPoint.y - asset.props.h / 2,
					opacity: 1,
					props: {
						assetId: asset.id,
						w: asset.props.w,
						h: asset.props.h,
					},
				})

				currentPoint.x += asset.props.w
			}
		}
	}

	editor.batch(() => {
		// Create any assets
		const assetsToCreate = assets.filter((asset) => !editor.getAsset(asset.id))
		if (assetsToCreate.length) {
			editor.createAssets(assetsToCreate)
		}

		// Create the shapes
		editor.createShapes(partials).select(...partials.map((p) => p.id))

		// Re-position shapes so that the center of the group is at the provided point
		centerSelectionAroundPoint(editor, position)
	})

	return partials.map((p) => p.id)
}

function centerSelectionAroundPoint(editor: Editor, position: VecLike) {
	// Re-position shapes so that the center of the group is at the provided point
	const viewportPageBounds = editor.getViewportPageBounds()
	let selectionPageBounds = editor.getSelectionPageBounds()

	if (selectionPageBounds) {
		const offset = selectionPageBounds!.center.sub(position)

		editor.updateShapes(
			editor.getSelectedShapes().map((shape) => {
				const localRotation = editor.getShapeParentTransform(shape).decompose().rotation
				const localDelta = Vec.Rot(offset, -localRotation)
				return {
					id: shape.id,
					type: shape.type,
					x: shape.x! - localDelta.x,
					y: shape.y! - localDelta.y,
				}
			})
		)
	}

	// Zoom out to fit the shapes, if necessary
	selectionPageBounds = editor.getSelectionPageBounds()
	if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
		editor.zoomToSelection()
	}
}

export function createEmptyBookmarkShape(
	editor: Editor,
	url: string,
	position: VecLike
): TLBookmarkShape {
	const partial: TLShapePartial = {
		id: createShapeId(),
		type: 'bookmark',
		x: position.x - 150,
		y: position.y - 160,
		opacity: 1,
		props: {
			assetId: null,
			url,
		},
	}

	editor.batch(() => {
		editor.createShapes([partial]).select(partial.id)
		centerSelectionAroundPoint(editor, position)
	})

	return editor.getShape(partial.id) as TLBookmarkShape
}
