import Roact from "@rbxts/roact"
import { useEffect, useCallback, useMemo, useMutable } from "@rbxts/roact-hooked"
import { acrylicInstance } from "./acrylicInstance"
import { Lighting, Workspace } from "@rbxts/services"
import Make from "@rbxts/make"

const cylinderAngleOffset = CFrame.Angles(0, math.rad(90), 0)

function viewportPointToWorld(location: Vector2, distance: number): Vector3 {
	const unitRay = Workspace.CurrentCamera!.ScreenPointToRay(location.X, location.Y)
	return unitRay.Origin.add(unitRay.Direction.mul(distance))
}

function map(n: number, min0: number, max0: number, min1: number, max1: number): number {
	return min1 + ((n - min0) * (max1 - min1)) / (max0 - min0)
}

function getOffset(): number {
	return map(Workspace.CurrentCamera!.ViewportSize.Y, 0, 2560, 8, 56)
}

interface acrylicProps extends Roact.JsxInstanceProperties<Frame> {
	radius: number
	distance: number

	Event?: Roact.JsxInstanceEvents<Frame>
	Change?: Roact.JsxInstanceChangeEvents<Frame>
}

Make("DepthOfFieldEffect", {
	FarIntensity: 0,
	InFocusRadius: 0.1,
	NearIntensity: 1,
	Parent: Lighting
})

function acrylic(props: acrylicProps): Roact.Element {
	const { radius, distance } = props

	const spreadableProps = { ...props } as Partial<acrylicProps>
	delete spreadableProps.radius
	delete spreadableProps.distance

	const frameInfo = useMutable({
		topleft2d: new Vector2(),
		topright2d: new Vector2(),
		bottomright2d: new Vector2(),
		topleftradius2d: new Vector2()
	})

	const acrylic = useMemo(() => {
		const clone = acrylicInstance.Clone()
		clone.Parent = Workspace
		return clone
	}, [])

	useEffect(() => {
		return () => acrylic.Destroy()
	}, [])

	const updateFrameInfo = useCallback(
		(size: Vector2, position: Vector2) => {
			const topleftRaw = position.sub(size.div(2))
			const info = frameInfo.current

			info.topleft2d = new Vector2(math.ceil(topleftRaw.X), math.ceil(topleftRaw.Y))
			info.topright2d = info.topleft2d.add(new Vector2(size.X, 0))
			info.bottomright2d = info.topleft2d.add(size)
			info.topleftradius2d = info.topleft2d.add(new Vector2(radius, 0))
		},
		[distance, radius]
	)

	const updateInstance = useCallback(() => {
		const { topleft2d, topright2d, bottomright2d, topleftradius2d } = frameInfo.current

		const topleft = viewportPointToWorld(topleft2d, distance)
		const topright = viewportPointToWorld(topright2d, distance)
		const bottomright = viewportPointToWorld(bottomright2d, distance)
		const topleftradius = viewportPointToWorld(topleftradius2d, distance)

		const cornerRadius = topleftradius.sub(topleft).Magnitude
		const width = topright.sub(topleft).Magnitude
		const height = topright.sub(bottomright).Magnitude

		const center = CFrame.fromMatrix(
			topleft.add(bottomright).div(2),
			Workspace.CurrentCamera!.CFrame.XVector,
			Workspace.CurrentCamera!.CFrame.YVector,
			Workspace.CurrentCamera!.CFrame.ZVector
		)

		if (radius !== undefined && radius > 0) {
			acrylic.Horizontal.CFrame = center
			acrylic.Horizontal.Mesh.Scale = new Vector3(width - cornerRadius * 2, height, 0)
			acrylic.Vertical.CFrame = center
			acrylic.Vertical.Mesh.Scale = new Vector3(width, height - cornerRadius * 2, 0)
		} else {
			acrylic.Horizontal.CFrame = center
			acrylic.Horizontal.Mesh.Scale = new Vector3(width, height, 0)
		}

		if (radius !== undefined && radius > 0) {
			acrylic.TopLeft.CFrame = center
				.mul(new CFrame(-width / 2 + cornerRadius, height / 2 - cornerRadius, 0))
				.mul(cylinderAngleOffset)
			acrylic.TopLeft.Mesh.Scale = new Vector3(0, cornerRadius * 2, cornerRadius * 2)

			acrylic.TopRight.CFrame = center
				.mul(new CFrame(width / 2 - cornerRadius, height / 2 - cornerRadius, 0))
				.mul(cylinderAngleOffset)
			acrylic.TopRight.Mesh.Scale = new Vector3(0, cornerRadius * 2, cornerRadius * 2)

			acrylic.BottomLeft.CFrame = center
				.mul(new CFrame(-width / 2 + cornerRadius, -height / 2 + cornerRadius, 0))
				.mul(cylinderAngleOffset)
			acrylic.BottomLeft.Mesh.Scale = new Vector3(0, cornerRadius * 2, cornerRadius * 2)

			acrylic.BottomRight.CFrame = center
				.mul(new CFrame(width / 2 - cornerRadius, -height / 2 + cornerRadius, 0))
				.mul(cylinderAngleOffset)
			acrylic.BottomRight.Mesh.Scale = new Vector3(0, cornerRadius * 2, cornerRadius * 2)
		}
	}, [radius, distance])

	useEffect(() => {
		updateInstance()

		const posHandle = Workspace.CurrentCamera!.GetPropertyChangedSignal("CFrame").Connect(updateInstance)
		const fovHandle = Workspace.CurrentCamera!.GetPropertyChangedSignal("FieldOfView").Connect(updateInstance)
		const viewportHandle = Workspace.CurrentCamera!.GetPropertyChangedSignal("ViewportSize").Connect(updateInstance)

		return () => {
			posHandle.Disconnect()
			fovHandle.Disconnect()
			viewportHandle.Disconnect()
		}
	}, [updateInstance])

	return (
		<frame
			Change={{
				AbsoluteSize: (rbx): void => {
					const blurOffset = getOffset()
					const size = rbx.AbsoluteSize.sub(new Vector2(blurOffset, blurOffset))
					const position = rbx.AbsolutePosition.add(rbx.AbsoluteSize.div(2))
					updateFrameInfo(size, position)
					task.spawn(updateInstance)
				},
				AbsolutePosition: (rbx): void => {
					const blurOffset = getOffset()
					const size = rbx.AbsoluteSize.sub(new Vector2(blurOffset, blurOffset))
					const position = rbx.AbsolutePosition.add(rbx.AbsoluteSize.div(2))
					updateFrameInfo(size, position)
					task.spawn(updateInstance)
				}
			}}
			Size={new UDim2(1, 0, 1, 0)}
			BackgroundTransparency={1}
		/>
	)
}

export default acrylic