import { clamp, Vec2d } from '@tldraw/primitives'
import {
	AssetRecordType,
	PageRecordType,
	TLAlignType,
	TLArrowheadType,
	TLArrowShape,
	TLArrowTerminal,
	TLAsset,
	TLAssetId,
	TLColorType,
	TLDashType,
	TLDrawShape,
	TLFontType,
	TLGeoShape,
	TLImageShape,
	TLNoteShape,
	TLPageId,
	TLShapeId,
	TLShapePartial,
	TLSizeType,
	TLTextShape,
	TLVideoShape,
	Vec2dModel,
} from '@tldraw/tlschema'
import { transact } from 'signia'
import { App } from '../app/App'
import { TLArrowUtil } from '../app/shapeutils/TLArrowUtil/TLArrowUtil'
import { MAX_SHAPES_PER_PAGE } from '../constants'

const TLDRAW_V1_VERSION = 15.5

/** @internal */
export function buildFromV1Document(app: App, document: LegacyTldrawDocument) {
	transact(() => {
		document = migrate(document, TLDRAW_V1_VERSION)
		// Cancel any interactions / states
		app.cancel().cancel().cancel().cancel()

		const firstPageId = app.pages[0].id

		// Set the current page to the first page
		app.setCurrentPageId(firstPageId)

		// Delete all pages except first page
		for (const page of app.pages.slice(1)) {
			app.deletePage(page.id)
		}

		// Delete all of the shapes on the current page
		app.selectAll()
		app.deleteShapes()

		// Create assets
		const v1AssetIdsToV2AssetIds = new Map<string, TLAssetId>()

		Object.values(document.assets ?? {}).forEach((v1Asset) => {
			switch (v1Asset.type) {
				case TDAssetType.Image: {
					const assetId: TLAssetId = AssetRecordType.createId()
					v1AssetIdsToV2AssetIds.set(v1Asset.id, assetId)
					const placeholderAsset: TLAsset = {
						id: assetId,
						typeName: 'asset',
						type: 'image',
						props: {
							w: coerceDimension(v1Asset.size[0]),
							h: coerceDimension(v1Asset.size[1]),
							name: v1Asset.fileName ?? 'Untitled',
							isAnimated: false,
							mimeType: null,
							src: v1Asset.src,
						},
					}
					app.createAssets([placeholderAsset])
					tryMigrateAsset(app, placeholderAsset)
					break
				}
				case TDAssetType.Video:
					{
						const assetId: TLAssetId = AssetRecordType.createId()
						v1AssetIdsToV2AssetIds.set(v1Asset.id, assetId)
						app.createAssets([
							{
								id: assetId,
								typeName: 'asset',
								type: 'video',
								props: {
									w: coerceDimension(v1Asset.size[0]),
									h: coerceDimension(v1Asset.size[1]),
									name: v1Asset.fileName ?? 'Untitled',
									isAnimated: true,
									mimeType: null,
									src: v1Asset.src,
								},
							},
						])
					}
					break
			}
		})

		// Create pages

		const v1PageIdsToV2PageIds = new Map<string, TLPageId>()

		Object.values(document.pages ?? {})
			.sort((a, b) => ((a.childIndex ?? 1) < (b.childIndex ?? 1) ? -1 : 1))
			.forEach((v1Page, i) => {
				if (i === 0) {
					v1PageIdsToV2PageIds.set(v1Page.id, app.currentPageId)
				} else {
					const pageId = PageRecordType.createId()
					v1PageIdsToV2PageIds.set(v1Page.id, pageId)
					app.createPage(v1Page.name ?? 'Page', pageId)
				}
			})

		Object.values(document.pages ?? {})
			.sort((a, b) => ((a.childIndex ?? 1) < (b.childIndex ?? 1) ? -1 : 1))
			.forEach((v1Page) => {
				// Set the current page id to the current page
				app.setCurrentPageId(v1PageIdsToV2PageIds.get(v1Page.id)!)

				const v1ShapeIdsToV2ShapeIds = new Map<string, TLShapeId>()
				const v1GroupShapeIdsToV1ChildIds = new Map<string, string[]>()

				const v1Shapes = Object.values(v1Page.shapes ?? {})
					.sort((a, b) => (a.childIndex < b.childIndex ? -1 : 1))
					.slice(0, MAX_SHAPES_PER_PAGE)

				// Groups only
				v1Shapes.forEach((v1Shape) => {
					if (v1Shape.type !== TDShapeType.Group) return

					const shapeId = app.createShapeId()
					v1ShapeIdsToV2ShapeIds.set(v1Shape.id, shapeId)
					v1GroupShapeIdsToV1ChildIds.set(v1Shape.id, [])
				})

				function decideNotToCreateShape(v1Shape: TDShape) {
					v1ShapeIdsToV2ShapeIds.delete(v1Shape.id)
					const v1GroupParent = v1GroupShapeIdsToV1ChildIds.has(v1Shape.parentId)
					if (v1GroupParent) {
						const ids = v1GroupShapeIdsToV1ChildIds
							.get(v1Shape.parentId)!
							.filter((id) => id !== v1Shape.id)
						v1GroupShapeIdsToV1ChildIds.set(v1Shape.parentId, ids)
					}
				}

				// Non-groups only
				v1Shapes.forEach((v1Shape) => {
					// Skip groups for now, we'll create groups via the app's API
					if (v1Shape.type === TDShapeType.Group) {
						return
					}

					const shapeId = app.createShapeId()
					v1ShapeIdsToV2ShapeIds.set(v1Shape.id, shapeId)

					if (v1Shape.parentId !== v1Page.id) {
						// If the parent is a group, then add the shape to the group's children
						if (v1GroupShapeIdsToV1ChildIds.has(v1Shape.parentId)) {
							v1GroupShapeIdsToV1ChildIds.get(v1Shape.parentId)!.push(v1Shape.id)
						} else {
							console.warn('parent does not exist', v1Shape)
						}
					}

					// First, try to find the shape's parent among the existing groups
					const parentId = v1PageIdsToV2PageIds.get(v1Page.id)!

					const inCommon = {
						id: shapeId,
						parentId,
						x: coerceNumber(v1Shape.point[0]),
						y: coerceNumber(v1Shape.point[1]),
						rotation: 0,
						isLocked: !!v1Shape.isLocked,
					}

					switch (v1Shape.type) {
						case TDShapeType.Sticky: {
							const partial: TLShapePartial<TLNoteShape> = {
								...inCommon,
								type: 'note',
								props: {
									text: v1Shape.text ?? '',
									color: getV2Color(v1Shape.style.color),
									size: getV2Size(v1Shape.style.size),
									font: getV2Font(v1Shape.style.font),
									align: getV2Align(v1Shape.style.textAlign),
								},
							}

							app.createShapes([partial])
							break
						}
						case TDShapeType.Rectangle: {
							const partial: TLShapePartial<TLGeoShape> = {
								...inCommon,
								type: 'geo',
								props: {
									geo: 'rectangle',
									w: coerceDimension(v1Shape.size[0]),
									h: coerceDimension(v1Shape.size[1]),
									text: v1Shape.label ?? '',
									fill: getV2Fill(v1Shape.style.isFilled, v1Shape.style.color),
									labelColor: getV2Color(v1Shape.style.color),
									color: getV2Color(v1Shape.style.color),
									size: getV2Size(v1Shape.style.size),
									font: getV2Font(v1Shape.style.font),
									dash: getV2Dash(v1Shape.style.dash),
									align: 'middle',
								},
							}

							app.createShapes([partial])

							const pageBoundsBeforeLabel = app.getPageBoundsById(inCommon.id)!

							app.updateShapes([
								{
									id: inCommon.id,
									type: 'geo',
									props: {
										text: v1Shape.label ?? '',
									},
								},
							])

							if (pageBoundsBeforeLabel.width === pageBoundsBeforeLabel.height) {
								const shape = app.getShapeById<TLGeoShape>(inCommon.id)!
								const { growY } = shape.props
								const w = coerceDimension(shape.props.w)
								const h = coerceDimension(shape.props.h)
								const newW = w + growY / 2
								const newH = h + growY / 2

								app.updateShapes([
									{
										id: inCommon.id,
										type: 'geo',
										x: coerceNumber(shape.x) - (newW - w) / 2,
										y: coerceNumber(shape.y) - (newH - h) / 2,
										props: {
											w: newW,
											h: newH,
										},
									},
								])
							}
							break
						}
						case TDShapeType.Triangle: {
							const partial: TLShapePartial<TLGeoShape> = {
								...inCommon,
								type: 'geo',
								props: {
									geo: 'triangle',
									w: coerceDimension(v1Shape.size[0]),
									h: coerceDimension(v1Shape.size[1]),
									fill: getV2Fill(v1Shape.style.isFilled, v1Shape.style.color),
									labelColor: getV2Color(v1Shape.style.color),
									color: getV2Color(v1Shape.style.color),
									size: getV2Size(v1Shape.style.size),
									font: getV2Font(v1Shape.style.font),
									dash: getV2Dash(v1Shape.style.dash),
									align: 'middle',
								},
							}

							app.createShapes([partial])

							const pageBoundsBeforeLabel = app.getPageBoundsById(inCommon.id)!

							app.updateShapes([
								{
									id: inCommon.id,
									type: 'geo',
									props: {
										text: v1Shape.label ?? '',
									},
								},
							])

							if (pageBoundsBeforeLabel.width === pageBoundsBeforeLabel.height) {
								const shape = app.getShapeById<TLGeoShape>(inCommon.id)!
								const { growY } = shape.props
								const w = coerceDimension(shape.props.w)
								const h = coerceDimension(shape.props.h)
								const newW = w + growY / 2
								const newH = h + growY / 2

								app.updateShapes([
									{
										id: inCommon.id,
										type: 'geo',
										x: coerceNumber(shape.x) - (newW - w) / 2,
										y: coerceNumber(shape.y) - (newH - h) / 2,
										props: {
											w: newW,
											h: newH,
										},
									},
								])
							}
							break
						}
						case TDShapeType.Ellipse: {
							const partial: TLShapePartial<TLGeoShape> = {
								...inCommon,
								type: 'geo',
								props: {
									geo: 'ellipse',
									w: coerceDimension(v1Shape.radius[0]) * 2,
									h: coerceDimension(v1Shape.radius[1]) * 2,
									fill: getV2Fill(v1Shape.style.isFilled, v1Shape.style.color),
									labelColor: getV2Color(v1Shape.style.color),
									color: getV2Color(v1Shape.style.color),
									size: getV2Size(v1Shape.style.size),
									font: getV2Font(v1Shape.style.font),
									dash: getV2Dash(v1Shape.style.dash),
									align: 'middle',
								},
							}

							app.createShapes([partial])

							const pageBoundsBeforeLabel = app.getPageBoundsById(inCommon.id)!

							app.updateShapes([
								{
									id: inCommon.id,
									type: 'geo',
									props: {
										text: v1Shape.label ?? '',
									},
								},
							])

							if (pageBoundsBeforeLabel.width === pageBoundsBeforeLabel.height) {
								const shape = app.getShapeById<TLGeoShape>(inCommon.id)!
								const { growY } = shape.props
								const w = coerceDimension(shape.props.w)
								const h = coerceDimension(shape.props.h)
								const newW = w + growY / 2
								const newH = h + growY / 2

								app.updateShapes([
									{
										id: inCommon.id,
										type: 'geo',
										x: coerceNumber(shape.x) - (newW - w) / 2,
										y: coerceNumber(shape.y) - (newH - h) / 2,
										props: {
											w: newW,
											h: newH,
										},
									},
								])
							}

							break
						}
						case TDShapeType.Draw: {
							if (v1Shape.points.length === 0) {
								decideNotToCreateShape(v1Shape)
								break
							}

							const partial: TLShapePartial<TLDrawShape> = {
								...inCommon,
								type: 'draw',
								props: {
									fill: getV2Fill(v1Shape.style.isFilled, v1Shape.style.color),
									color: getV2Color(v1Shape.style.color),
									size: getV2Size(v1Shape.style.size),
									dash: getV2Dash(v1Shape.style.dash),
									isPen: false,
									isComplete: v1Shape.isComplete,
									segments: [{ type: 'free', points: v1Shape.points.map(getV2Point) }],
								},
							}

							app.createShapes([partial])
							break
						}
						case TDShapeType.Arrow: {
							const v1Bend = coerceNumber(v1Shape.bend)
							const v1Start = getV2Point(v1Shape.handles.start.point)
							const v1End = getV2Point(v1Shape.handles.end.point)
							const dist = Vec2d.Dist(v1Start, v1End)
							const v2Bend = (dist * -v1Bend) / 2

							// Could also be a line... but we'll use it as an arrow anyway
							const partial: TLShapePartial<TLArrowShape> = {
								...inCommon,
								type: 'arrow',
								props: {
									text: v1Shape.label ?? '',
									color: getV2Color(v1Shape.style.color),
									labelColor: getV2Color(v1Shape.style.color),
									size: getV2Size(v1Shape.style.size),
									font: getV2Font(v1Shape.style.font),
									dash: getV2Dash(v1Shape.style.dash),
									arrowheadStart: getV2Arrowhead(v1Shape.decorations?.start),
									arrowheadEnd: getV2Arrowhead(v1Shape.decorations?.end),
									start: {
										type: 'point',
										x: coerceNumber(v1Shape.handles.start.point[0]),
										y: coerceNumber(v1Shape.handles.start.point[1]),
									},
									end: {
										type: 'point',
										x: coerceNumber(v1Shape.handles.end.point[0]),
										y: coerceNumber(v1Shape.handles.end.point[1]),
									},
									bend: v2Bend,
								},
							}

							app.createShapes([partial])

							break
						}
						case TDShapeType.Text: {
							const partial: TLShapePartial<TLTextShape> = {
								...inCommon,
								type: 'text',
								props: {
									text: v1Shape.text ?? ' ',
									color: getV2Color(v1Shape.style.color),
									size: getV2TextSize(v1Shape.style.size),
									font: getV2Font(v1Shape.style.font),
									align: getV2Align(v1Shape.style.textAlign),
									scale: v1Shape.style.scale ?? 1,
								},
							}

							app.createShapes([partial])
							break
						}
						case TDShapeType.Image: {
							const assetId = v1AssetIdsToV2AssetIds.get(v1Shape.assetId)

							if (!assetId) {
								console.warn('Could not find asset id', v1Shape.assetId)
								return
							}

							const partial: TLShapePartial<TLImageShape> = {
								...inCommon,
								type: 'image',
								props: {
									w: coerceDimension(v1Shape.size[0]),
									h: coerceDimension(v1Shape.size[1]),
									assetId,
								},
							}

							app.createShapes([partial])
							break
						}
						case TDShapeType.Video: {
							const assetId = v1AssetIdsToV2AssetIds.get(v1Shape.assetId)

							if (!assetId) {
								console.warn('Could not find asset id', v1Shape.assetId)
								return
							}

							const partial: TLShapePartial<TLVideoShape> = {
								...inCommon,
								type: 'video',
								props: {
									w: coerceDimension(v1Shape.size[0]),
									h: coerceDimension(v1Shape.size[1]),
									assetId,
								},
							}

							app.createShapes([partial])
							break
						}
					}

					const rotation = coerceNumber(v1Shape.rotation)

					if (rotation !== 0) {
						app.select(shapeId)
						app.rotateShapesBy([shapeId], rotation)
					}
				})

				// Create groups
				v1GroupShapeIdsToV1ChildIds.forEach((v1ChildIds, v1GroupId) => {
					const v2ChildShapeIds = v1ChildIds.map((id) => v1ShapeIdsToV2ShapeIds.get(id)!)
					const v2GroupId = v1ShapeIdsToV2ShapeIds.get(v1GroupId)!
					app.groupShapes(v2ChildShapeIds, v2GroupId)

					const v1Group = v1Page.shapes[v1GroupId]
					const rotation = coerceNumber(v1Group.rotation)

					if (rotation !== 0) {
						app.select(v2GroupId)
						app.rotateShapesBy([v2GroupId], rotation)
					}
				})

				// Bind arrows to shapes

				v1Shapes.forEach((v1Shape) => {
					if (v1Shape.type !== TDShapeType.Arrow) {
						return
					}

					const v2ShapeId = v1ShapeIdsToV2ShapeIds.get(v1Shape.id)!
					const util = app.getShapeUtil(TLArrowUtil)

					// dumb but necessary
					app.inputs.ctrlKey = false

					for (const handleId of ['start', 'end'] as const) {
						const bindingId = v1Shape.handles[handleId].bindingId
						if (bindingId) {
							const binding = v1Page.bindings[bindingId]
							if (!binding) {
								// arrow has a reference to a binding that no longer exists
								continue
							}

							const targetId = v1ShapeIdsToV2ShapeIds.get(binding.toId)!

							const targetShape = app.getShapeById(targetId)!

							// (unexpected) We didn't create the target shape
							if (!targetShape) continue

							if (targetId) {
								const bounds = app.getPageBoundsById(targetId)!

								const v2ShapeFresh = app.getShapeById<TLArrowShape>(v2ShapeId)!

								const nx = clamp((coerceNumber(binding.point[0]) + 0.5) / 2, 0.2, 0.8)
								const ny = clamp((coerceNumber(binding.point[1]) + 0.5) / 2, 0.2, 0.8)

								const point = app.getPointInShapeSpace(v2ShapeFresh, {
									x: bounds.minX + bounds.width * nx,
									y: bounds.minY + bounds.height * ny,
								})

								const handles = util.handles(v2ShapeFresh)

								const change = util.onHandleChange!(v2ShapeFresh, {
									handle: {
										...handles.find((h) => h.id === handleId)!,
										x: point.x,
										y: point.y,
									},
									isPrecise: point.x !== 0.5 || point.y !== 0.5,
								})

								if (change) {
									if (change.props?.[handleId]) {
										const terminal = change.props?.[handleId] as TLArrowTerminal
										if (terminal.type === 'binding') {
											terminal.isExact = binding.distance === 0

											if (terminal.boundShapeId !== targetId) {
												console.warn('Hit the wrong shape!')
												terminal.boundShapeId = targetId
												terminal.normalizedAnchor = { x: 0.5, y: 0.5 }
											}
										}
									}
									app.updateShapes([change])
								}
							}
						}
					}
				})
			})

		// Set the current page to the first page again
		app.setCurrentPageId(firstPageId)

		app.history.clear()
		app.selectNone()
		app.updateViewportScreenBounds()

		const bounds = app.allShapesCommonBounds
		if (bounds) {
			app.zoomToBounds(bounds.minX, bounds.minY, bounds.width, bounds.height, 1)
		}
	})
}

function coerceNumber(n: unknown): number {
	if (typeof n !== 'number') return 0
	if (Number.isNaN(n)) return 0
	if (!Number.isFinite(n)) return 0
	return n
}

function coerceDimension(d: unknown): number {
	const n = coerceNumber(d)
	if (n <= 0) return 1
	return n
}

/**
 * We want to move assets over to our new S3 bucket & extract any relevant metadata. That process is
 * async though, where the rest of our migration is synchronous.
 *
 * We'll write placeholder assets to the app using the old asset URLs, then kick off a process async
 * to try and download the real assets, extract the metadata, and upload them to our new bucket.
 * It's not a big deal if this fails though.
 */
async function tryMigrateAsset(app: App, placeholderAsset: TLAsset) {
	try {
		if (placeholderAsset.type === 'bookmark' || !placeholderAsset.props.src) return

		const response = await fetch(placeholderAsset.props.src)
		if (!response.ok) return

		const file = new File([await response.blob()], placeholderAsset.props.name, {
			type: response.headers.get('content-type') ?? placeholderAsset.props.mimeType ?? undefined,
		})

		const newAsset = await app.onCreateAssetFromFile(file)
		if (newAsset.type === 'bookmark') return

		app.updateAssets([
			{
				id: placeholderAsset.id,
				type: placeholderAsset.type,
				props: {
					...newAsset.props,
					name: placeholderAsset.props.name,
				},
			},
		])
	} catch (err) {
		// not a big deal, we'll just keep the placeholder asset
	}
}

function migrate(document: LegacyTldrawDocument, newVersion: number): LegacyTldrawDocument {
	const { version = 0 } = document

	if (!document.assets) {
		document.assets = {}
	}

	// Remove unused assets when loading a document
	const assetIdsInUse = new Set<string>()

	Object.values(document.pages).forEach((page) =>
		Object.values(page.shapes).forEach((shape) => {
			const { parentId, children, assetId } = shape

			if (assetId) {
				assetIdsInUse.add(assetId)
			}

			// Fix missing parent bug
			if (parentId !== page.id && !page.shapes[parentId]) {
				console.warn('Encountered a shape with a missing parent!')
				shape.parentId = page.id
			}

			if (shape.type === TDShapeType.Group && children) {
				children.forEach((childId) => {
					if (!page.shapes[childId]) {
						console.warn('Encountered a parent with a missing child!', shape.id, childId)
						children?.splice(children.indexOf(childId), 1)
					}
				})

				// TODO: Remove the shape if it has no children
			}
		})
	)

	Object.keys(document.assets).forEach((assetId) => {
		if (!assetIdsInUse.has(assetId)) {
			delete document.assets[assetId]
		}
	})

	if (version !== newVersion) {
		if (version < 14) {
			Object.values(document.pages).forEach((page) => {
				Object.values(page.shapes)
					.filter((shape) => shape.type === TDShapeType.Text)
					.forEach((shape) => {
						if ((shape as TextShape).style.font === undefined) {
							;(shape as TextShape).style.font === FontStyle.Script
						}
					})
			})
		}

		// Lowercase styles, move binding meta to binding
		if (version <= 13) {
			Object.values(document.pages).forEach((page) => {
				Object.values(page.bindings).forEach((binding) => {
					Object.assign(binding, (binding as any).meta)
				})

				Object.values(page.shapes).forEach((shape) => {
					Object.entries(shape.style).forEach(([id, style]) => {
						if (typeof style === 'string') {
							// @ts-ignore
							shape.style[id] = style.toLowerCase()
						}
					})

					if (shape.type === TDShapeType.Arrow) {
						if (shape.decorations) {
							Object.entries(shape.decorations).forEach(([id, decoration]) => {
								if ((decoration as unknown) === 'Arrow') {
									shape.decorations = {
										...shape.decorations,
										[id]: Decoration.Arrow,
									}
								}
							})
						}
					}
				})
			})
		}

		// Add document name and file system handle
		if (version <= 13.1 && document.name == null) {
			document.name = 'New Document'
		}

		if (version < 15 && document.assets == null) {
			document.assets = {}
		}

		Object.values(document.pages).forEach((page) => {
			Object.values(page.shapes).forEach((shape) => {
				if (version < 15.2) {
					if (
						(shape.type === TDShapeType.Image || shape.type === TDShapeType.Video) &&
						shape.style.isFilled == null
					) {
						shape.style.isFilled = true
					}
				}

				if (version < 15.3) {
					if (
						shape.type === TDShapeType.Rectangle ||
						shape.type === TDShapeType.Triangle ||
						shape.type === TDShapeType.Ellipse ||
						shape.type === TDShapeType.Arrow
					) {
						if ('text' in shape && typeof shape.text === 'string') {
							shape.label = shape.text
						}
						if (!shape.label) {
							shape.label = ''
						}
						if (!shape.labelPoint) {
							shape.labelPoint = [0.5, 0.5]
						}
					}
				}
			})
		})
	}

	// Cleanup
	Object.values(document.pageStates).forEach((pageState) => {
		pageState.selectedIds = pageState.selectedIds.filter((id) => {
			return document.pages[pageState.id].shapes[id] !== undefined
		})
		pageState.bindingId = undefined
		pageState.editingId = undefined
		pageState.hoveredId = undefined
		pageState.pointedId = undefined
	})

	document.version = newVersion

	return document
}

/* -------------------- TLV1 Types -------------------- */

interface TLV1Handle {
	id: string
	index: number
	point: number[]
}

interface TLV1Binding {
	id: string
	toId: string
	fromId: string
}

interface TLV1Shape {
	id: string
	type: string
	parentId: string
	childIndex: number
	name: string
	point: number[]
	assetId?: string
	rotation?: number
	children?: string[]
	handles?: Record<string, TLV1Handle>
	isGhost?: boolean
	isHidden?: boolean
	isLocked?: boolean
	isGenerated?: boolean
	isAspectRatioLocked?: boolean
}

enum TDShapeType {
	Sticky = 'sticky',
	Ellipse = 'ellipse',
	Rectangle = 'rectangle',
	Triangle = 'triangle',
	Draw = 'draw',
	Arrow = 'arrow',
	Text = 'text',
	Group = 'group',
	Image = 'image',
	Video = 'video',
}

enum ColorStyle {
	White = 'white',
	LightGray = 'lightGray',
	Gray = 'gray',
	Black = 'black',
	Green = 'green',
	Cyan = 'cyan',
	Blue = 'blue',
	Indigo = 'indigo',
	Violet = 'violet',
	Red = 'red',
	Orange = 'orange',
	Yellow = 'yellow',
}

enum SizeStyle {
	Small = 'small',
	Medium = 'medium',
	Large = 'large',
}

enum DashStyle {
	Draw = 'draw',
	Solid = 'solid',
	Dashed = 'dashed',
	Dotted = 'dotted',
}

enum AlignStyle {
	Start = 'start',
	Middle = 'middle',
	End = 'end',
	Justify = 'justify',
}

enum FontStyle {
	Script = 'script',
	Sans = 'sans',
	Serif = 'serif',
	Mono = 'mono',
}

type ShapeStyles = {
	color: ColorStyle
	size: SizeStyle
	dash: DashStyle
	font?: FontStyle
	textAlign?: AlignStyle
	isFilled?: boolean
	scale?: number
}

interface TDBaseShape extends TLV1Shape {
	style: ShapeStyles
	type: TDShapeType
	label?: string
	handles?: Record<string, TDHandle>
}

interface DrawShape extends TDBaseShape {
	type: TDShapeType.Draw
	points: number[][]
	isComplete: boolean
}

// The extended handle (used for arrows)
interface TDHandle extends TLV1Handle {
	canBind?: boolean
	bindingId?: string
}

interface RectangleShape extends TDBaseShape {
	type: TDShapeType.Rectangle
	size: number[]
	label?: string
	labelPoint?: number[]
}

interface EllipseShape extends TDBaseShape {
	type: TDShapeType.Ellipse
	radius: number[]
	label?: string
	labelPoint?: number[]
}

interface TriangleShape extends TDBaseShape {
	type: TDShapeType.Triangle
	size: number[]
	label?: string
	labelPoint?: number[]
}

enum Decoration {
	Arrow = 'arrow',
}

// The shape created with the arrow tool
interface ArrowShape extends TDBaseShape {
	type: TDShapeType.Arrow
	bend: number
	handles: {
		start: TDHandle
		bend: TDHandle
		end: TDHandle
	}
	decorations?: {
		start?: Decoration
		end?: Decoration
		middle?: Decoration
	}
	label?: string
	labelPoint?: number[]
}

interface ArrowBinding extends TLV1Binding {
	handleId: keyof ArrowShape['handles']
	distance: number
	point: number[]
}

type TDBinding = ArrowBinding

interface ImageShape extends TDBaseShape {
	type: TDShapeType.Image
	size: number[]
	assetId: string
}

interface VideoShape extends TDBaseShape {
	type: TDShapeType.Video
	size: number[]
	assetId: string
	isPlaying: boolean
	currentTime: number
}

// The shape created by the text tool
interface TextShape extends TDBaseShape {
	type: TDShapeType.Text
	text: string
}

// The shape created by the sticky tool
interface StickyShape extends TDBaseShape {
	type: TDShapeType.Sticky
	size: number[]
	text: string
}

// The shape created when multiple shapes are grouped
interface GroupShape extends TDBaseShape {
	type: TDShapeType.Group
	size: number[]
	children: string[]
}

type TDShape =
	| RectangleShape
	| EllipseShape
	| TriangleShape
	| DrawShape
	| ArrowShape
	| TextShape
	| GroupShape
	| StickyShape
	| ImageShape
	| VideoShape

type TDPage = {
	id: string
	name?: string
	childIndex?: number
	shapes: Record<string, TDShape>
	bindings: Record<string, TDBinding>
}

interface TLV1Bounds {
	minX: number
	minY: number
	maxX: number
	maxY: number
	width: number
	height: number
	rotation?: number
}

interface TLV1PageState {
	id: string
	selectedIds: string[]
	camera: {
		point: number[]
		zoom: number
	}
	brush?: TLV1Bounds | null
	pointedId?: string | null
	hoveredId?: string | null
	editingId?: string | null
	bindingId?: string | null
}

enum TDAssetType {
	Image = 'image',
	Video = 'video',
}

interface TDImageAsset extends TLV1Asset {
	type: TDAssetType.Image
	fileName: string
	src: string
	size: number[]
}

interface TDVideoAsset extends TLV1Asset {
	type: TDAssetType.Video
	fileName: string
	src: string
	size: number[]
}

interface TLV1Asset {
	id: string
	type: string
}

type TDAsset = TDImageAsset | TDVideoAsset

type TDAssets = Record<string, TDAsset>

/** @internal */
export interface LegacyTldrawDocument {
	id: string
	name: string
	version: number
	pages: Record<string, TDPage>
	pageStates: Record<string, TLV1PageState>
	assets: TDAssets
}

/* ------------------ Translations ------------------ */

const v1ColorsToV2Colors: Record<ColorStyle, TLColorType> = {
	[ColorStyle.White]: 'black',
	[ColorStyle.Black]: 'black',
	[ColorStyle.LightGray]: 'grey',
	[ColorStyle.Gray]: 'grey',
	[ColorStyle.Green]: 'light-green',
	[ColorStyle.Cyan]: 'green',
	[ColorStyle.Blue]: 'light-blue',
	[ColorStyle.Indigo]: 'blue',
	[ColorStyle.Orange]: 'orange',
	[ColorStyle.Yellow]: 'yellow',
	[ColorStyle.Red]: 'red',
	[ColorStyle.Violet]: 'light-violet',
}

const v1FontsToV2Fonts: Record<FontStyle, TLFontType> = {
	[FontStyle.Mono]: 'mono',
	[FontStyle.Sans]: 'sans',
	[FontStyle.Script]: 'draw',
	[FontStyle.Serif]: 'serif',
}

const v1AlignsToV2Aligns: Record<AlignStyle, TLAlignType> = {
	[AlignStyle.Start]: 'start',
	[AlignStyle.Middle]: 'middle',
	[AlignStyle.End]: 'end',
	[AlignStyle.Justify]: 'start',
}

const v1TextSizesToV2TextSizes: Record<SizeStyle, TLSizeType> = {
	[SizeStyle.Small]: 's',
	[SizeStyle.Medium]: 'l',
	[SizeStyle.Large]: 'xl',
}

const v1SizesToV2Sizes: Record<SizeStyle, TLSizeType> = {
	[SizeStyle.Small]: 'm',
	[SizeStyle.Medium]: 'l',
	[SizeStyle.Large]: 'xl',
}

const v1DashesToV2Dashes: Record<DashStyle, TLDashType> = {
	[DashStyle.Solid]: 'solid',
	[DashStyle.Dashed]: 'dashed',
	[DashStyle.Dotted]: 'dotted',
	[DashStyle.Draw]: 'draw',
}

function getV2Color(color: ColorStyle | undefined): TLColorType {
	return color ? v1ColorsToV2Colors[color] ?? 'black' : 'black'
}

function getV2Font(font: FontStyle | undefined): TLFontType {
	return font ? v1FontsToV2Fonts[font] ?? 'draw' : 'draw'
}

function getV2Align(align: AlignStyle | undefined): TLAlignType {
	return align ? v1AlignsToV2Aligns[align] ?? 'middle' : 'middle'
}

function getV2TextSize(size: SizeStyle | undefined): TLSizeType {
	return size ? v1TextSizesToV2TextSizes[size] ?? 'm' : 'm'
}

function getV2Size(size: SizeStyle | undefined): TLSizeType {
	return size ? v1SizesToV2Sizes[size] ?? 'l' : 'l'
}

function getV2Dash(dash: DashStyle | undefined): TLDashType {
	return dash ? v1DashesToV2Dashes[dash] ?? 'draw' : 'draw'
}

function getV2Point(point: number[]): Vec2dModel {
	return {
		x: coerceNumber(point[0]),
		y: coerceNumber(point[1]),
		z: point[2] == null ? 0.5 : coerceNumber(point[2]),
	}
}

function getV2Arrowhead(decoration: Decoration | undefined): TLArrowheadType {
	return decoration === Decoration.Arrow ? 'arrow' : 'none'
}

function getV2Fill(isFilled: boolean | undefined, color: ColorStyle) {
	return isFilled
		? color === ColorStyle.Black || color === ColorStyle.White
			? 'semi'
			: 'solid'
		: 'none'
}
