import { SelectionHandle, Vec2d } from '@tldraw/primitives'
import {
	TLBaseShape,
	TLImageCrop,
	TLImageShapeProps,
	TLShape,
	TLShapePartial,
} from '@tldraw/tlschema'
import { deepCopy } from '@tldraw/utils'
import { MIN_CROP_SIZE } from '../../../../constants'
import { TLImageUtil } from '../../../shapeutils/TLImageUtil/TLImageUtil'
import { TLEventHandlers, TLPointerEventInfo, UiEnterHandler } from '../../../types/event-types'
import { StateNode } from '../../StateNode'
import { CursorTypeMap } from './PointingResizeHandle'

type Snapshot = ReturnType<Cropping['createSnapshot']>

export class Cropping extends StateNode {
	static override id = 'cropping'

	info = {} as TLPointerEventInfo & {
		target: 'selection'
		handle: SelectionHandle
		onInteractionEnd?: string
	}

	markId = ''

	private snapshot = {} as any as Snapshot

	onEnter: UiEnterHandler = (
		info: TLPointerEventInfo & {
			target: 'selection'
			handle: SelectionHandle
			onInteractionEnd?: string
		}
	) => {
		this.info = info
		this.markId = this.app.mark('cropping')
		this.snapshot = this.createSnapshot()
		this.updateShapes()
	}

	onPointerMove: TLEventHandlers['onPointerMove'] = () => {
		this.updateShapes()
	}

	onPointerUp: TLEventHandlers['onPointerUp'] = () => {
		this.complete()
	}

	onComplete: TLEventHandlers['onComplete'] = () => {
		this.complete()
	}

	onCancel: TLEventHandlers['onCancel'] = () => {
		this.cancel()
	}

	private updateCursor() {
		const selectedShape = this.app.selectedShapes[0]
		if (!selectedShape) return

		const cursorType = CursorTypeMap[this.info.handle!]
		this.app.setCursor({
			type: cursorType,
			rotation: selectedShape.rotation,
		})
	}

	private getDefaultCrop = (): TLImageCrop => ({
		topLeft: { x: 0, y: 0 },
		bottomRight: { x: 1, y: 1 },
	})

	private updateShapes() {
		const { shape, cursorHandleOffset } = this.snapshot

		if (!shape) return
		const util = this.app.getShapeUtil(TLImageUtil)
		if (!util) return

		const props = shape.props as TLImageShapeProps

		const currentPagePoint = this.app.inputs.currentPagePoint.clone().sub(cursorHandleOffset)
		const originPagePoint = this.app.inputs.originPagePoint.clone().sub(cursorHandleOffset)

		const change = currentPagePoint.clone().sub(originPagePoint).rot(-shape.rotation)

		const crop = props.crop ?? this.getDefaultCrop()
		const newCrop = deepCopy(crop)

		const newPoint = new Vec2d(shape.x, shape.y)
		const pointDelta = new Vec2d(0, 0)

		// original (uncropped) width and height of shape
		const w = (1 / (crop.bottomRight.x - crop.topLeft.x)) * props.w
		const h = (1 / (crop.bottomRight.y - crop.topLeft.y)) * props.h

		let hasCropChanged = false

		// Set y dimension
		switch (this.info.handle) {
			case 'top':
			case 'top_left':
			case 'top_right': {
				if (h < MIN_CROP_SIZE) break
				hasCropChanged = true
				// top
				newCrop.topLeft.y = newCrop.topLeft.y + change.y / h
				const heightAfterCrop = h * (newCrop.bottomRight.y - newCrop.topLeft.y)

				if (heightAfterCrop < MIN_CROP_SIZE) {
					newCrop.topLeft.y = newCrop.bottomRight.y - MIN_CROP_SIZE / h
					pointDelta.y = (newCrop.topLeft.y - crop.topLeft.y) * h
				} else {
					if (newCrop.topLeft.y <= 0) {
						newCrop.topLeft.y = 0
						pointDelta.y = (newCrop.topLeft.y - crop.topLeft.y) * h
					} else {
						pointDelta.y = change.y
					}
				}
				break
			}
			case 'bottom':
			case 'bottom_left':
			case 'bottom_right': {
				if (h < MIN_CROP_SIZE) break
				hasCropChanged = true
				// bottom
				newCrop.bottomRight.y = Math.min(1, newCrop.bottomRight.y + change.y / h)
				const heightAfterCrop = h * (newCrop.bottomRight.y - newCrop.topLeft.y)

				if (heightAfterCrop < MIN_CROP_SIZE) {
					newCrop.bottomRight.y = newCrop.topLeft.y + MIN_CROP_SIZE / h
				}
				break
			}
		}

		// Set x dimension
		switch (this.info.handle) {
			case 'left':
			case 'top_left':
			case 'bottom_left': {
				if (w < MIN_CROP_SIZE) break
				hasCropChanged = true
				// left
				newCrop.topLeft.x = newCrop.topLeft.x + change.x / w
				const widthAfterCrop = w * (newCrop.bottomRight.x - newCrop.topLeft.x)

				if (widthAfterCrop < MIN_CROP_SIZE) {
					newCrop.topLeft.x = newCrop.bottomRight.x - MIN_CROP_SIZE / w
					pointDelta.x = (newCrop.topLeft.x - crop.topLeft.x) * w
				} else {
					if (newCrop.topLeft.x <= 0) {
						newCrop.topLeft.x = 0
						pointDelta.x = (newCrop.topLeft.x - crop.topLeft.x) * w
					} else {
						pointDelta.x = change.x
					}
				}
				break
			}
			case 'right':
			case 'top_right':
			case 'bottom_right': {
				if (w < MIN_CROP_SIZE) break
				hasCropChanged = true
				// right
				newCrop.bottomRight.x = Math.min(1, newCrop.bottomRight.x + change.x / w)
				const widthAfterCrop = w * (newCrop.bottomRight.x - newCrop.topLeft.x)

				if (widthAfterCrop < MIN_CROP_SIZE) {
					newCrop.bottomRight.x = newCrop.topLeft.x + MIN_CROP_SIZE / w
				}
				break
			}
		}
		if (!hasCropChanged) return

		newPoint.add(pointDelta.rot(shape.rotation))

		const partial: TLShapePartial<
			TLBaseShape<string, { w: number; h: number; crop: TLImageCrop }>
		> = {
			id: shape.id,
			type: shape.type,
			x: newPoint.x,
			y: newPoint.y,
			props: {
				crop: newCrop,
				w: (newCrop.bottomRight.x - newCrop.topLeft.x) * w,
				h: (newCrop.bottomRight.y - newCrop.topLeft.y) * h,
			},
		}

		this.app.updateShapes([partial], true)
		this.updateCursor()
	}

	private complete() {
		if (this.info.onInteractionEnd) {
			this.app.setSelectedTool(this.info.onInteractionEnd, this.info)
		} else {
			this.app.setCroppingId(null)
			this.parent.transition('idle', {})
		}
	}

	private cancel() {
		this.app.bailToMark(this.markId)
		if (this.info.onInteractionEnd) {
			this.app.setSelectedTool(this.info.onInteractionEnd, this.info)
		} else {
			this.app.setCroppingId(null)
			this.parent.transition('idle', {})
		}
	}

	private createSnapshot() {
		const {
			selectionRotation,
			inputs: { originPagePoint },
		} = this.app

		const shape = this.app.onlySelectedShape as TLShape

		const selectionBounds = this.app.selectionBounds!

		const dragHandlePoint = Vec2d.RotWith(
			selectionBounds.getHandlePoint(this.info.handle!),
			selectionBounds.point,
			selectionRotation
		)

		const cursorHandleOffset = Vec2d.Sub(originPagePoint, dragHandlePoint)

		return {
			shape,
			cursorHandleOffset,
		}
	}
}
