import {
	BaseBoxShapeUtil,
	DefaultFontFamilies,
	Editor,
	Ellipse2d,
	Geometry2d,
	Group2d,
	HALF_PI,
	HTMLContainer,
	HandleSnapGeometry,
	PI2,
	Polygon2d,
	Polyline2d,
	Rectangle2d,
	SVGContainer,
	Stadium2d,
	SvgExportContext,
	TLDefaultDashStyle,
	TLGeoShape,
	TLOnEditEndHandler,
	TLOnResizeHandler,
	TLShapeUtilCanvasSvgDef,
	Vec,
	VecLike,
	exhaustiveSwitchError,
	geoShapeMigrations,
	geoShapeProps,
	getDefaultColorTheme,
	getPolygonVertices,
} from '@tldraw/editor'

import { HyperlinkButton } from '../shared/HyperlinkButton'
import { TextLabel } from '../shared/TextLabel'
import {
	FONT_FAMILIES,
	LABEL_FONT_SIZES,
	STROKE_SIZES,
	TEXT_PROPS,
} from '../shared/default-shape-constants'
import {
	getFillDefForCanvas,
	getFillDefForExport,
	getFontDefForExport,
} from '../shared/defaultStyleDefs'
import { getTextLabelSvgElement } from '../shared/getTextLabelSvgElement'
import { getRoundedInkyPolygonPath, getRoundedPolygonPoints } from '../shared/polygon-helpers'
import { cloudOutline, cloudSvgPath } from './cloudOutline'
import { DashStyleCloud, DashStyleCloudSvg } from './components/DashStyleCloud'
import { DashStyleEllipse, DashStyleEllipseSvg } from './components/DashStyleEllipse'
import { DashStyleOval, DashStyleOvalSvg } from './components/DashStyleOval'
import { DashStylePolygon, DashStylePolygonSvg } from './components/DashStylePolygon'
import { DrawStyleCloud, DrawStyleCloudSvg } from './components/DrawStyleCloud'
import { DrawStyleEllipseSvg, getEllipseIndicatorPath } from './components/DrawStyleEllipse'
import { DrawStylePolygon, DrawStylePolygonSvg } from './components/DrawStylePolygon'
import { SolidStyleCloud, SolidStyleCloudSvg } from './components/SolidStyleCloud'
import { SolidStyleEllipse, SolidStyleEllipseSvg } from './components/SolidStyleEllipse'
import {
	SolidStyleOval,
	SolidStyleOvalSvg,
	getOvalIndicatorPath,
} from './components/SolidStyleOval'
import { SolidStylePolygon, SolidStylePolygonSvg } from './components/SolidStylePolygon'

const LABEL_PADDING = 16
const MIN_SIZE_WITH_LABEL = 17 * 3

/** @public */
export class GeoShapeUtil extends BaseBoxShapeUtil<TLGeoShape> {
	static override type = 'geo' as const
	static override props = geoShapeProps
	static override migrations = geoShapeMigrations

	override canEdit = () => true

	override getDefaultProps(): TLGeoShape['props'] {
		return {
			w: 100,
			h: 100,
			geo: 'rectangle',
			color: 'black',
			labelColor: 'black',
			fill: 'none',
			dash: 'draw',
			size: 'm',
			font: 'draw',
			text: '',
			align: 'middle',
			verticalAlign: 'middle',
			growY: 0,
			url: '',
		}
	}

	override getGeometry(shape: TLGeoShape) {
		const w = Math.max(1, shape.props.w)
		const h = Math.max(1, shape.props.h + shape.props.growY)
		const cx = w / 2
		const cy = h / 2

		const strokeWidth = STROKE_SIZES[shape.props.size]
		const isFilled = shape.props.fill !== 'none' // || shape.props.text.trim().length > 0

		let body: Geometry2d

		switch (shape.props.geo) {
			case 'cloud': {
				body = new Polygon2d({
					points: cloudOutline(w, h, shape.id, shape.props.size),
					isFilled,
				})
				break
			}
			case 'triangle': {
				body = new Polygon2d({
					points: [new Vec(cx, 0), new Vec(w, h), new Vec(0, h)],
					isFilled,
				})
				break
			}
			case 'diamond': {
				body = new Polygon2d({
					points: [new Vec(cx, 0), new Vec(w, cy), new Vec(cx, h), new Vec(0, cy)],
					isFilled,
				})
				break
			}
			case 'pentagon': {
				body = new Polygon2d({
					points: getPolygonVertices(w, h, 5),
					isFilled,
				})
				break
			}
			case 'hexagon': {
				body = new Polygon2d({
					points: getPolygonVertices(w, h, 6),
					isFilled,
				})
				break
			}
			case 'octagon': {
				body = new Polygon2d({
					points: getPolygonVertices(w, h, 8),
					isFilled,
				})
				break
			}
			case 'ellipse': {
				body = new Ellipse2d({
					width: w,
					height: h,
					isFilled,
				})
				break
			}
			case 'oval': {
				body = new Stadium2d({
					width: w,
					height: h,
					isFilled,
				})
				break
			}
			case 'star': {
				// Most of this code is to offset the center, a 5 point star
				// will need to be moved downward because from its center [0,0]
				// it will have a bigger minY than maxY. This is because it'll
				// have 2 points at the bottom.
				const sides = 5
				const step = PI2 / sides / 2
				const rightMostIndex = Math.floor(sides / 4) * 2
				const leftMostIndex = sides * 2 - rightMostIndex
				const topMostIndex = 0
				const bottomMostIndex = Math.floor(sides / 2) * 2
				const maxX = (Math.cos(-HALF_PI + rightMostIndex * step) * w) / 2
				const minX = (Math.cos(-HALF_PI + leftMostIndex * step) * w) / 2

				const minY = (Math.sin(-HALF_PI + topMostIndex * step) * h) / 2
				const maxY = (Math.sin(-HALF_PI + bottomMostIndex * step) * h) / 2
				const diffX = w - Math.abs(maxX - minX)
				const diffY = h - Math.abs(maxY - minY)
				const offsetX = w / 2 + minX - (w / 2 - maxX)
				const offsetY = h / 2 + minY - (h / 2 - maxY)

				const ratio = 1
				const cx = (w - offsetX) / 2
				const cy = (h - offsetY) / 2
				const ox = (w + diffX) / 2
				const oy = (h + diffY) / 2
				const ix = (ox * ratio) / 2
				const iy = (oy * ratio) / 2

				body = new Polygon2d({
					points: Array.from(Array(sides * 2)).map((_, i) => {
						const theta = -HALF_PI + i * step
						return new Vec(
							cx + (i % 2 ? ix : ox) * Math.cos(theta),
							cy + (i % 2 ? iy : oy) * Math.sin(theta)
						)
					}),
					isFilled,
				})
				break
			}
			case 'rhombus': {
				const offset = Math.min(w * 0.38, h * 0.38)
				body = new Polygon2d({
					points: [new Vec(offset, 0), new Vec(w, 0), new Vec(w - offset, h), new Vec(0, h)],
					isFilled,
				})
				break
			}
			case 'rhombus-2': {
				const offset = Math.min(w * 0.38, h * 0.38)
				body = new Polygon2d({
					points: [new Vec(0, 0), new Vec(w - offset, 0), new Vec(w, h), new Vec(offset, h)],
					isFilled,
				})
				break
			}
			case 'trapezoid': {
				const offset = Math.min(w * 0.38, h * 0.38)
				body = new Polygon2d({
					points: [new Vec(offset, 0), new Vec(w - offset, 0), new Vec(w, h), new Vec(0, h)],
					isFilled,
				})
				break
			}
			case 'arrow-right': {
				const ox = Math.min(w, h) * 0.38
				const oy = h * 0.16
				body = new Polygon2d({
					points: [
						new Vec(0, oy),
						new Vec(w - ox, oy),
						new Vec(w - ox, 0),
						new Vec(w, h / 2),
						new Vec(w - ox, h),
						new Vec(w - ox, h - oy),
						new Vec(0, h - oy),
					],
					isFilled,
				})
				break
			}
			case 'arrow-left': {
				const ox = Math.min(w, h) * 0.38
				const oy = h * 0.16
				body = new Polygon2d({
					points: [
						new Vec(ox, 0),
						new Vec(ox, oy),
						new Vec(w, oy),
						new Vec(w, h - oy),
						new Vec(ox, h - oy),
						new Vec(ox, h),
						new Vec(0, h / 2),
					],
					isFilled,
				})
				break
			}
			case 'arrow-up': {
				const ox = w * 0.16
				const oy = Math.min(w, h) * 0.38
				body = new Polygon2d({
					points: [
						new Vec(w / 2, 0),
						new Vec(w, oy),
						new Vec(w - ox, oy),
						new Vec(w - ox, h),
						new Vec(ox, h),
						new Vec(ox, oy),
						new Vec(0, oy),
					],
					isFilled,
				})
				break
			}
			case 'arrow-down': {
				const ox = w * 0.16
				const oy = Math.min(w, h) * 0.38
				body = new Polygon2d({
					points: [
						new Vec(ox, 0),
						new Vec(w - ox, 0),
						new Vec(w - ox, h - oy),
						new Vec(w, h - oy),
						new Vec(w / 2, h),
						new Vec(0, h - oy),
						new Vec(ox, h - oy),
					],
					isFilled,
				})
				break
			}
			case 'check-box':
			case 'x-box':
			case 'rectangle': {
				body = new Rectangle2d({
					width: w,
					height: h,
					isFilled,
				})
				break
			}
		}

		const labelSize = getLabelSize(this.editor, shape)
		const labelWidth = Math.min(w, Math.max(labelSize.w, Math.min(32, Math.max(1, w - 8))))
		const labelHeight = Math.min(h, Math.max(labelSize.h, Math.min(32, Math.max(1, w - 8)))) // not sure if bug

		const lines = getLines(shape.props, strokeWidth)
		const edges = lines ? lines.map((line) => new Polyline2d({ points: line })) : []

		// todo: use centroid for label position

		return new Group2d({
			children: [
				body,
				new Rectangle2d({
					x:
						shape.props.align === 'start'
							? 0
							: shape.props.align === 'end'
								? w - labelWidth
								: (w - labelWidth) / 2,
					y:
						shape.props.verticalAlign === 'start'
							? 0
							: shape.props.verticalAlign === 'end'
								? h - labelHeight
								: (h - labelHeight) / 2,
					width: labelWidth,
					height: labelHeight,
					isFilled: true,
					isLabel: true,
				}),
				...edges,
			],
		})
	}

	override getHandleSnapGeometry(shape: TLGeoShape): HandleSnapGeometry {
		const geometry = this.getGeometry(shape)
		// we only want to snap handles to the outline of the shape - not to its label etc.
		const outline = geometry.children[0]
		switch (shape.props.geo) {
			case 'arrow-down':
			case 'arrow-left':
			case 'arrow-right':
			case 'arrow-up':
			case 'check-box':
			case 'diamond':
			case 'hexagon':
			case 'octagon':
			case 'pentagon':
			case 'rectangle':
			case 'rhombus':
			case 'rhombus-2':
			case 'star':
			case 'trapezoid':
			case 'triangle':
			case 'x-box':
				// poly-line type shapes hand snap points for each vertex & the center
				return { outline: outline, points: [...outline.getVertices(), geometry.bounds.center] }
			case 'cloud':
			case 'ellipse':
			case 'oval':
				// blobby shapes only have a snap point in their center
				return { outline: outline, points: [geometry.bounds.center] }
			default:
				exhaustiveSwitchError(shape.props.geo)
		}
	}

	override onEditEnd: TLOnEditEndHandler<TLGeoShape> = (shape) => {
		const {
			id,
			type,
			props: { text },
		} = shape

		if (text.trimEnd() !== shape.props.text) {
			this.editor.updateShapes([
				{
					id,
					type,
					props: {
						text: text.trimEnd(),
					},
				},
			])
		}
	}

	component(shape: TLGeoShape) {
		const { id, type, props } = shape

		const strokeWidth = STROKE_SIZES[props.size]

		const { w, color, labelColor, fill, dash, growY, font, align, verticalAlign, size, text } =
			props

		const getShape = () => {
			const h = props.h + growY

			switch (props.geo) {
				case 'cloud': {
					if (dash === 'solid') {
						return (
							<SolidStyleCloud
								color={color}
								fill={fill}
								strokeWidth={strokeWidth}
								w={w}
								h={h}
								id={id}
								size={size}
							/>
						)
					} else if (dash === 'dashed' || dash === 'dotted') {
						return (
							<DashStyleCloud
								color={color}
								fill={fill}
								strokeWidth={strokeWidth}
								w={w}
								h={h}
								id={id}
								size={size}
								dash={dash}
							/>
						)
					} else if (dash === 'draw') {
						return (
							<DrawStyleCloud
								color={color}
								fill={fill}
								strokeWidth={strokeWidth}
								w={w}
								h={h}
								id={id}
								size={size}
							/>
						)
					}

					break
				}
				case 'ellipse': {
					if (dash === 'solid') {
						return (
							<SolidStyleEllipse strokeWidth={strokeWidth} w={w} h={h} color={color} fill={fill} />
						)
					} else if (dash === 'dashed' || dash === 'dotted') {
						return (
							<DashStyleEllipse
								id={id}
								strokeWidth={strokeWidth}
								w={w}
								h={h}
								dash={dash}
								color={color}
								fill={fill}
							/>
						)
					} else if (dash === 'draw') {
						return (
							<SolidStyleEllipse strokeWidth={strokeWidth} w={w} h={h} color={color} fill={fill} />
						)
					}
					break
				}
				case 'oval': {
					if (dash === 'solid') {
						return (
							<SolidStyleOval strokeWidth={strokeWidth} w={w} h={h} color={color} fill={fill} />
						)
					} else if (dash === 'dashed' || dash === 'dotted') {
						return (
							<DashStyleOval
								id={id}
								strokeWidth={strokeWidth}
								w={w}
								h={h}
								dash={dash}
								color={color}
								fill={fill}
							/>
						)
					} else if (dash === 'draw') {
						return (
							<SolidStyleOval strokeWidth={strokeWidth} w={w} h={h} color={color} fill={fill} />
						)
					}
					break
				}
				default: {
					const geometry = this.editor.getShapeGeometry(shape)
					const outline =
						geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
					const lines = getLines(shape.props, strokeWidth)

					if (dash === 'solid') {
						return (
							<SolidStylePolygon
								fill={fill}
								color={color}
								strokeWidth={strokeWidth}
								outline={outline}
								lines={lines}
							/>
						)
					} else if (dash === 'dashed' || dash === 'dotted') {
						return (
							<DashStylePolygon
								dash={dash}
								fill={fill}
								color={color}
								strokeWidth={strokeWidth}
								outline={outline}
								lines={lines}
							/>
						)
					} else if (dash === 'draw') {
						return (
							<DrawStylePolygon
								id={id}
								fill={fill}
								color={color}
								strokeWidth={strokeWidth}
								outline={outline}
								lines={lines}
							/>
						)
					}
				}
			}
		}

		return (
			<>
				<SVGContainer id={id}>{getShape()}</SVGContainer>
				<HTMLContainer
					id={shape.id}
					style={{ overflow: 'hidden', width: shape.props.w, height: shape.props.h + props.growY }}
				>
					<TextLabel
						id={id}
						type={type}
						font={font}
						fill={fill}
						size={size}
						align={align}
						verticalAlign={verticalAlign}
						text={text}
						labelColor={labelColor}
						wrap
						bounds={props.geo === 'cloud' ? this.getGeometry(shape).bounds : undefined}
					/>
					{shape.props.url && (
						<HyperlinkButton url={shape.props.url} zoomLevel={this.editor.getZoomLevel()} />
					)}
				</HTMLContainer>
			</>
		)
	}

	indicator(shape: TLGeoShape) {
		const { id, props } = shape
		const { w, size } = props
		const h = props.h + props.growY

		const strokeWidth = STROKE_SIZES[size]

		switch (props.geo) {
			case 'ellipse': {
				if (props.dash === 'draw') {
					return <path d={getEllipseIndicatorPath(id, w, h, strokeWidth)} />
				}

				return <ellipse cx={w / 2} cy={h / 2} rx={w / 2} ry={h / 2} />
			}
			case 'oval': {
				return <path d={getOvalIndicatorPath(w, h)} />
			}
			case 'cloud': {
				return <path d={cloudSvgPath(w, h, id, size)} />
			}

			default: {
				const geometry = this.editor.getShapeGeometry(shape)
				const outline =
					geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
				let path: string

				if (props.dash === 'draw') {
					const polygonPoints = getRoundedPolygonPoints(id, outline, 0, strokeWidth * 2, 1)
					path = getRoundedInkyPolygonPath(polygonPoints)
				} else {
					path = 'M' + outline[0] + 'L' + outline.slice(1) + 'Z'
				}

				const lines = getLines(shape.props, strokeWidth)

				if (lines) {
					for (const [A, B] of lines) {
						path += `M${A.x},${A.y}L${B.x},${B.y}`
					}
				}

				return <path d={path} />
			}
		}
	}

	override toSvg(shape: TLGeoShape, ctx: SvgExportContext) {
		const { id, props } = shape
		const strokeWidth = STROKE_SIZES[props.size]
		const theme = getDefaultColorTheme({ isDarkMode: ctx.isDarkMode })
		ctx.addExportDef(getFillDefForExport(shape.props.fill, theme))

		let svgElm: SVGElement

		switch (props.geo) {
			case 'ellipse': {
				switch (props.dash) {
					case 'draw':
						svgElm = DrawStyleEllipseSvg({
							id,
							w: props.w,
							h: props.h,
							color: props.color,
							fill: props.fill,
							strokeWidth,
							theme,
						})
						break

					case 'solid':
						svgElm = SolidStyleEllipseSvg({
							strokeWidth,
							w: props.w,
							h: props.h,
							color: props.color,
							fill: props.fill,
							theme,
						})
						break

					default:
						svgElm = DashStyleEllipseSvg({
							id,
							strokeWidth,
							w: props.w,
							h: props.h,
							dash: props.dash,
							color: props.color,
							fill: props.fill,
							theme,
						})
						break
				}
				break
			}

			case 'oval': {
				switch (props.dash) {
					case 'draw':
						svgElm = DashStyleOvalSvg({
							id,
							strokeWidth,
							w: props.w,
							h: props.h,
							dash: props.dash,
							color: props.color,
							fill: props.fill,
							theme,
						})
						break

					case 'solid':
						svgElm = SolidStyleOvalSvg({
							strokeWidth,
							w: props.w,
							h: props.h,
							color: props.color,
							fill: props.fill,
							theme,
						})
						break

					default:
						svgElm = DashStyleOvalSvg({
							id,
							strokeWidth,
							w: props.w,
							h: props.h,
							dash: props.dash,
							color: props.color,
							fill: props.fill,
							theme,
						})
				}
				break
			}

			case 'cloud': {
				switch (props.dash) {
					case 'draw':
						svgElm = DrawStyleCloudSvg({
							id,
							strokeWidth,
							w: props.w,
							h: props.h,
							color: props.color,
							fill: props.fill,
							size: props.size,
							theme,
						})
						break

					case 'solid':
						svgElm = SolidStyleCloudSvg({
							strokeWidth,
							w: props.w,
							h: props.h,
							color: props.color,
							fill: props.fill,
							size: props.size,
							id,
							theme,
						})
						break

					default:
						svgElm = DashStyleCloudSvg({
							id,
							strokeWidth,
							w: props.w,
							h: props.h,
							dash: props.dash,
							color: props.color,
							fill: props.fill,
							theme,
							size: props.size,
						})
				}
				break
			}
			default: {
				const geometry = this.editor.getShapeGeometry(shape)
				const outline =
					geometry instanceof Group2d ? geometry.children[0].vertices : geometry.vertices
				const lines = getLines(shape.props, strokeWidth)

				switch (props.dash) {
					case 'draw':
						svgElm = DrawStylePolygonSvg({
							id,
							fill: props.fill,
							color: props.color,
							strokeWidth,
							outline,
							lines,
							theme,
						})
						break

					case 'solid':
						svgElm = SolidStylePolygonSvg({
							fill: props.fill,
							color: props.color,
							strokeWidth,
							outline,
							lines,
							theme,
						})
						break

					default:
						svgElm = DashStylePolygonSvg({
							dash: props.dash,
							fill: props.fill,
							color: props.color,
							strokeWidth,
							outline,
							lines,
							theme,
						})
						break
				}
				break
			}
		}

		if (props.text) {
			const bounds = this.editor.getShapeGeometry(shape).bounds

			ctx.addExportDef(getFontDefForExport(shape.props.font))

			const rootTextElm = getTextLabelSvgElement({
				editor: this.editor,
				shape,
				font: DefaultFontFamilies[shape.props.font],
				bounds,
			})

			const textElm = rootTextElm.cloneNode(true) as SVGTextElement
			textElm.setAttribute('fill', theme[shape.props.labelColor].solid)
			textElm.setAttribute('stroke', 'none')

			const textBgEl = rootTextElm.cloneNode(true) as SVGTextElement
			textBgEl.setAttribute('stroke-width', '2')
			textBgEl.setAttribute('fill', theme.background)
			textBgEl.setAttribute('stroke', theme.background)

			const groupEl = document.createElementNS('http://www.w3.org/2000/svg', 'g')
			groupEl.append(textBgEl)
			groupEl.append(textElm)

			if (svgElm.nodeName === 'g') {
				svgElm.appendChild(groupEl)
				return svgElm
			} else {
				const g = document.createElementNS('http://www.w3.org/2000/svg', 'g')
				g.appendChild(svgElm)
				g.appendChild(groupEl)
				return g
			}
		}

		return svgElm
	}

	override getCanvasSvgDefs(): TLShapeUtilCanvasSvgDef[] {
		return [getFillDefForCanvas()]
	}

	override onResize: TLOnResizeHandler<TLGeoShape> = (
		shape,
		{ handle, newPoint, scaleX, scaleY, initialShape }
	) => {
		// use the w/h from props here instead of the initialBounds here,
		// since cloud shapes calculated bounds can differ from the props w/h.
		let w = initialShape.props.w * scaleX
		let h = (initialShape.props.h + initialShape.props.growY) * scaleY
		let overShrinkX = 0
		let overShrinkY = 0

		if (shape.props.text.trim()) {
			let newW = Math.max(Math.abs(w), MIN_SIZE_WITH_LABEL)
			let newH = Math.max(Math.abs(h), MIN_SIZE_WITH_LABEL)

			if (newW < MIN_SIZE_WITH_LABEL && newH === MIN_SIZE_WITH_LABEL) {
				newW = MIN_SIZE_WITH_LABEL
			}

			if (newW === MIN_SIZE_WITH_LABEL && newH < MIN_SIZE_WITH_LABEL) {
				newH = MIN_SIZE_WITH_LABEL
			}

			const labelSize = getLabelSize(this.editor, {
				...shape,
				props: {
					...shape.props,
					w: newW,
					h: newH,
				},
			})

			const nextW = Math.max(Math.abs(w), labelSize.w) * Math.sign(w)
			const nextH = Math.max(Math.abs(h), labelSize.h) * Math.sign(h)
			overShrinkX = Math.abs(nextW) - Math.abs(w)
			overShrinkY = Math.abs(nextH) - Math.abs(h)

			w = nextW
			h = nextH
		}

		const offset = new Vec(0, 0)

		// x offsets

		if (scaleX < 0) {
			offset.x += w
		}

		if (handle === 'left' || handle === 'top_left' || handle === 'bottom_left') {
			offset.x += scaleX < 0 ? overShrinkX : -overShrinkX
		}

		// y offsets

		if (scaleY < 0) {
			offset.y += h
		}

		if (handle === 'top' || handle === 'top_left' || handle === 'top_right') {
			offset.y += scaleY < 0 ? overShrinkY : -overShrinkY
		}

		const { x, y } = offset.rot(shape.rotation).add(newPoint)

		return {
			x,
			y,
			props: {
				w: Math.max(Math.abs(w), 1),
				h: Math.max(Math.abs(h), 1),
				growY: 0,
			},
		}
	}

	override onBeforeCreate = (shape: TLGeoShape) => {
		if (!shape.props.text) {
			if (shape.props.growY) {
				// No text / some growY, set growY to 0
				return {
					...shape,
					props: {
						...shape.props,
						growY: 0,
					},
				}
			} else {
				// No text / no growY, nothing to change
				return
			}
		}

		const prevHeight = shape.props.h
		const nextHeight = getLabelSize(this.editor, shape).h

		let growY: number | null = null

		if (nextHeight > prevHeight) {
			growY = nextHeight - prevHeight
		} else {
			if (shape.props.growY) {
				growY = 0
			}
		}

		if (growY !== null) {
			return {
				...shape,
				props: {
					...shape.props,
					growY,
				},
			}
		}
	}

	override onBeforeUpdate = (prev: TLGeoShape, next: TLGeoShape) => {
		const prevText = prev.props.text
		const nextText = next.props.text

		if (
			prevText === nextText &&
			prev.props.font === next.props.font &&
			prev.props.size === next.props.size
		) {
			return
		}

		if (prevText && !nextText) {
			return {
				...next,
				props: {
					...next.props,
					growY: 0,
				},
			}
		}

		const prevWidth = prev.props.w
		const prevHeight = prev.props.h
		const nextSize = getLabelSize(this.editor, next)
		const nextWidth = nextSize.w
		const nextHeight = nextSize.h

		// When entering the first character in a label (not pasting in multiple characters...)
		if (!prevText && nextText && nextText.length === 1) {
			let w = Math.max(prevWidth, nextWidth)
			let h = Math.max(prevHeight, nextHeight)

			// If both the width and height were less than the minimum size, make the shape square
			if (prev.props.w < MIN_SIZE_WITH_LABEL && prev.props.h < MIN_SIZE_WITH_LABEL) {
				w = Math.max(w, MIN_SIZE_WITH_LABEL)
				h = Math.max(h, MIN_SIZE_WITH_LABEL)
				w = Math.max(w, h)
				h = Math.max(w, h)
			}

			// Don't set a growY—at least, not until we've implemented a growX property
			return {
				...next,
				props: {
					...next.props,
					w,
					h,
					growY: 0,
				},
			}
		}

		let growY: number | null = null

		if (nextHeight > prevHeight) {
			growY = nextHeight - prevHeight
		} else {
			if (prev.props.growY) {
				growY = 0
			}
		}

		if (growY !== null) {
			return {
				...next,
				props: {
					...next.props,
					growY,
					w: Math.max(next.props.w, nextWidth),
				},
			}
		}

		if (nextWidth > prev.props.w) {
			return {
				...next,
				props: {
					...next.props,
					w: nextWidth,
				},
			}
		}
	}

	override onDoubleClick = (shape: TLGeoShape) => {
		// Little easter egg: double-clicking a rectangle / checkbox while
		// holding alt will toggle between check-box and rectangle
		if (this.editor.inputs.altKey) {
			switch (shape.props.geo) {
				case 'rectangle': {
					return {
						...shape,
						props: {
							geo: 'check-box' as const,
						},
					}
				}
				case 'check-box': {
					return {
						...shape,
						props: {
							geo: 'rectangle' as const,
						},
					}
				}
			}
		}

		return
	}
}

function getLabelSize(editor: Editor, shape: TLGeoShape) {
	const text = shape.props.text

	if (!text) {
		return { w: 0, h: 0 }
	}

	const minSize = editor.textMeasure.measureText('w', {
		...TEXT_PROPS,
		fontFamily: FONT_FAMILIES[shape.props.font],
		fontSize: LABEL_FONT_SIZES[shape.props.size],
		maxWidth: 100,
	})

	// TODO: Can I get these from somewhere?
	const sizes = {
		s: 2,
		m: 3.5,
		l: 5,
		xl: 10,
	}

	const size = editor.textMeasure.measureText(text, {
		...TEXT_PROPS,
		fontFamily: FONT_FAMILIES[shape.props.font],
		fontSize: LABEL_FONT_SIZES[shape.props.size],
		minWidth: minSize.w + 'px',
		maxWidth: Math.max(
			// Guard because a DOM nodes can't be less 0
			0,
			// A 'w' width that we're setting as the min-width
			Math.ceil(minSize.w + sizes[shape.props.size]),
			// The actual text size
			Math.ceil(shape.props.w - LABEL_PADDING * 2)
		),
	})

	return {
		w: size.w + LABEL_PADDING * 2,
		h: size.h + LABEL_PADDING * 2,
	}
}

function getLines(props: TLGeoShape['props'], sw: number) {
	switch (props.geo) {
		case 'x-box': {
			return getXBoxLines(props.w, props.h, sw, props.dash)
		}
		case 'check-box': {
			return getCheckBoxLines(props.w, props.h)
		}
		default: {
			return undefined
		}
	}
}

function getXBoxLines(w: number, h: number, sw: number, dash: TLDefaultDashStyle) {
	const inset = dash === 'draw' ? 0.62 : 0

	if (dash === 'dashed') {
		return [
			[new Vec(0, 0), new Vec(w / 2, h / 2)],
			[new Vec(w, h), new Vec(w / 2, h / 2)],
			[new Vec(0, h), new Vec(w / 2, h / 2)],
			[new Vec(w, 0), new Vec(w / 2, h / 2)],
		]
	}

	const clampX = (x: number) => Math.max(0, Math.min(w, x))
	const clampY = (y: number) => Math.max(0, Math.min(h, y))

	return [
		[
			new Vec(clampX(sw * inset), clampY(sw * inset)),
			new Vec(clampX(w - sw * inset), clampY(h - sw * inset)),
		],
		[
			new Vec(clampX(sw * inset), clampY(h - sw * inset)),
			new Vec(clampX(w - sw * inset), clampY(sw * inset)),
		],
	]
}

function getCheckBoxLines(w: number, h: number) {
	const size = Math.min(w, h) * 0.82
	const ox = (w - size) / 2
	const oy = (h - size) / 2

	const clampX = (x: number) => Math.max(0, Math.min(w, x))
	const clampY = (y: number) => Math.max(0, Math.min(h, y))

	return [
		[
			new Vec(clampX(ox + size * 0.25), clampY(oy + size * 0.52)),
			new Vec(clampX(ox + size * 0.45), clampY(oy + size * 0.82)),
		],
		[
			new Vec(clampX(ox + size * 0.45), clampY(oy + size * 0.82)),
			new Vec(clampX(ox + size * 0.82), clampY(oy + size * 0.22)),
		],
	]
}

/**
 * Get the centroid of a regular polygon.
 * @param points - The points that make up the polygon.
 * @internal
 */
export function getCentroidOfRegularPolygon(points: VecLike[]) {
	const len = points.length
	let x = 0
	let y = 0
	for (let i = 0; i < len; i++) {
		x += points[i].x
		y += points[i].y
	}
	return new Vec(x / len, y / len)
}
