import {
	Box,
	StateNode,
	TLPointerEventInfo,
	TLShapeId,
	isAccelKey,
	pointInPolygon,
} from '@tldraw/editor'

export class Erasing extends StateNode {
	static override id = 'erasing'

	private info = {} as TLPointerEventInfo
	private scribbleId = 'id'
	private markId = ''
	private excludedShapeIds = new Set<TLShapeId>()

	_isHoldingAccelKey = false
	_firstErasingShapeId: TLShapeId | null = null
	_erasingShapeIds: TLShapeId[] = []

	override onEnter(info: TLPointerEventInfo) {
		this._isHoldingAccelKey = isAccelKey(this.editor.inputs)
		this._firstErasingShapeId = this.editor.getErasingShapeIds()[0] // the first one should be the first one we hit... is it?
		this._erasingShapeIds = this.editor.getErasingShapeIds()

		this.markId = this.editor.markHistoryStoppingPoint('erase scribble begin')
		this.info = info

		const originPagePoint = this.editor.inputs.getOriginPagePoint()
		this.excludedShapeIds = new Set(
			this.editor
				.getCurrentPageShapes()
				.filter((shape) => {
					//If the shape is locked, we shouldn't erase it
					if (this.editor.isShapeOrAncestorLocked(shape)) return true
					//If the shape is a group or frame, check we're inside it when we start erasing
					if (
						this.editor.isShapeOfType(shape, 'group') ||
						this.editor.isShapeOfType(shape, 'frame')
					) {
						const pointInShapeShape = this.editor.getPointInShapeSpace(shape, originPagePoint)
						const geometry = this.editor.getShapeGeometry(shape)
						return geometry.bounds.containsPoint(pointInShapeShape)
					}

					return false
				})
				.map((shape) => shape.id)
		)

		const scribble = this.editor.scribbles.addScribble({
			color: 'muted-1',
			size: 12,
		})
		this.scribbleId = scribble.id

		this.update()
	}

	private pushPointToScribble() {
		const { x, y } = this.editor.inputs.getCurrentPagePoint()
		this.editor.scribbles.addPoint(this.scribbleId, x, y)
	}

	override onExit() {
		this.editor.setErasingShapes([])
		this.editor.scribbles.stop(this.scribbleId)
	}

	override onPointerMove() {
		this.update()
	}

	override onPointerUp() {
		this.complete()
	}

	override onCancel() {
		this.cancel()
	}

	override onComplete() {
		this.complete()
	}

	override onKeyUp() {
		this._isHoldingAccelKey = isAccelKey(this.editor.inputs)
		this.update()
	}

	override onKeyDown() {
		this._isHoldingAccelKey = isAccelKey(this.editor.inputs)
		this.update()
	}

	update() {
		const { editor, excludedShapeIds } = this
		const erasingShapeIds = editor.getErasingShapeIds()
		const zoomLevel = editor.getZoomLevel()
		const currentPagePoint = editor.inputs.getCurrentPagePoint()
		const previousPagePoint = editor.inputs.getPreviousPagePoint()

		this.pushPointToScribble()

		// Otherwise, erasing shapes are all the shapes that were hit before plus any new shapes that are hit
		const erasing = new Set<TLShapeId>(erasingShapeIds)
		const minDist = this.editor.options.hitTestMargin / zoomLevel

		// Create bounds around line segment with margin
		const lineBounds = Box.FromPoints([previousPagePoint, currentPagePoint]).expandBy(minDist)
		const candidateIds = editor.getShapeIdsInsideBounds(lineBounds)

		// Early return if no candidates - avoid expensive getCurrentPageRenderingShapesSorted()
		if (candidateIds.size === 0) {
			editor.setErasingShapes(Array.from(erasing))
			return
		}

		const allShapes = editor.getCurrentPageRenderingShapesSorted()
		const currentPageShapes = allShapes.filter((shape) => candidateIds.has(shape.id))

		for (const shape of currentPageShapes) {
			if (editor.isShapeOfType(shape, 'group')) continue

			// Avoid testing masked shapes, unless the pointer is inside the mask
			const pageMask = editor.getShapeMask(shape.id)
			if (pageMask && !pointInPolygon(currentPagePoint, pageMask)) {
				continue
			}

			// Hit test the shape using a line segment
			const geometry = editor.getShapeGeometry(shape)
			const pageTransform = editor.getShapePageTransform(shape)
			if (!geometry || !pageTransform) continue
			const pt = pageTransform.clone().invert()
			const A = pt.applyToPoint(previousPagePoint)
			const B = pt.applyToPoint(currentPagePoint)

			// If the line segment is entirely above / below / left / right of the shape's bounding box, skip the hit test
			const { bounds } = geometry
			if (
				bounds.minX - minDist > Math.max(A.x, B.x) ||
				bounds.minY - minDist > Math.max(A.y, B.y) ||
				bounds.maxX + minDist < Math.min(A.x, B.x) ||
				bounds.maxY + minDist < Math.min(A.y, B.y)
			) {
				continue
			}

			if (geometry.hitTestLineSegment(A, B, minDist)) {
				erasing.add(editor.getOutermostSelectableShape(shape).id)
			}

			this._erasingShapeIds = [...erasing]
		}

		// If the user is holding the meta / ctrl key, we should only erase the first shape we hit
		if (this._isHoldingAccelKey && this._firstErasingShapeId) {
			const erasingShapeId = this._firstErasingShapeId
			if (erasingShapeId && this.editor.getShape(erasingShapeId)) {
				editor.setErasingShapes([erasingShapeId])
			}
			return
		}

		// Remove the hit shapes, except if they're in the list of excluded shapes
		// (these excluded shapes will be any frames or groups the pointer was inside of
		// when the user started erasing)
		this.editor.setErasingShapes(this._erasingShapeIds.filter((id) => !excludedShapeIds.has(id)))
	}

	complete() {
		const { editor } = this
		editor.deleteShapes(editor.getCurrentPageState().erasingShapeIds)
		this.parent.transition('idle')
		this._erasingShapeIds = []
		this._firstErasingShapeId = null
	}

	cancel() {
		const { editor } = this
		editor.bailToMark(this.markId)
		this.parent.transition('idle', this.info)
	}
}
