import {
	Mat,
	PI,
	TLArrowShape,
	TLArrowShapeProps,
	TLBindingCreate,
	TLImageShape,
	TLShapeId,
	TLShapePartial,
	createBindingId,
	createShapeId,
} from '@tldraw/editor'
import { getArrowBindings } from '../lib/shapes/arrow/shared'
import { TestEditor } from './TestEditor'

let editor: TestEditor

jest.useFakeTimers()

const ids = {
	boxA: createShapeId('boxA'),
	boxB: createShapeId('boxB'),
	boxC: createShapeId('boxC'),
	boxD: createShapeId('boxD'),
}

beforeEach(() => {
	editor = new TestEditor()
	editor.selectAll()
	editor.deleteShapes(editor.getSelectedShapeIds())
	editor.createShapes([
		{
			id: ids.boxA,
			type: 'geo',
			x: 0,
			y: 0,
			props: {
				w: 100,
				h: 100,
			},
		},
		{
			id: ids.boxB,
			type: 'geo',
			x: 150,
			y: 150,
			props: {
				w: 50,
				h: 50,
			},
		},
		{
			id: ids.boxC,
			type: 'geo',
			x: 300,
			y: 300,
			props: {
				w: 100,
				h: 100,
			},
		},
	])
})

describe('When flipping horizontally', () => {
	it('Flips the selected shapes', () => {
		editor.select(ids.boxA, ids.boxB, ids.boxC)
		editor.markHistoryStoppingPoint('flipped')
		editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')

		editor.expectShapeToMatch(
			{
				id: ids.boxA,
				type: 'geo',
				x: 300,
			},
			{
				id: ids.boxB,
				type: 'geo',
				x: 200,
			},
			{
				id: ids.boxC,
				type: 'geo',
				x: 0,
			}
		)
	})

	it('Flips the provided shapes', () => {
		editor.markHistoryStoppingPoint('flipped')
		editor.flipShapes([ids.boxA, ids.boxB], 'horizontal')

		editor.expectShapeToMatch(
			{
				id: ids.boxA,
				type: 'geo',
				x: 100,
			},
			{
				id: ids.boxB,
				type: 'geo',
				x: 0,
			}
		)
	})

	it('Flips rotated shapes', () => {
		editor.updateShapes([{ id: ids.boxA, type: 'geo', rotation: PI }])
		editor.select(ids.boxA, ids.boxB)
		const a = editor.getSelectionPageBounds()
		editor.markHistoryStoppingPoint('flipped')
		editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')

		const b = editor.getSelectionPageBounds()
		expect(a!).toCloselyMatchObject(b!)

		editor.expectShapeToMatch(
			{
				id: ids.boxA,
				type: 'geo',
				x: 200,
			},
			{
				id: ids.boxB,
				type: 'geo',
				x: -100,
			}
		)
	})

	it('Flips the children of rotated shapes', () => {
		editor.reparentShapes([ids.boxB], ids.boxA)
		editor.updateShapes([{ id: ids.boxA, type: 'geo', rotation: PI }])
		editor.select(ids.boxB, ids.boxC)
		const a = editor.getSelectionPageBounds()
		editor.markHistoryStoppingPoint('flipped')
		editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')

		const b = editor.getSelectionPageBounds()
		expect(a).toCloselyMatchObject(b!)
	})
})

describe('When flipping vertically', () => {
	it('Flips the selected shapes', () => {
		editor.select(ids.boxA, ids.boxB, ids.boxC)
		editor.markHistoryStoppingPoint('flipped')
		editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')

		editor.expectShapeToMatch(
			{
				id: ids.boxA,
				type: 'geo',
				y: 300,
			},
			{
				id: ids.boxB,
				type: 'geo',
				y: 200,
			},
			{
				id: ids.boxC,
				type: 'geo',
				y: 0,
			}
		)
	})

	it('Flips the provided shapes', () => {
		editor.markHistoryStoppingPoint('flipped')
		editor.flipShapes([ids.boxA, ids.boxB], 'vertical')

		editor.expectShapeToMatch(
			{
				id: ids.boxA,
				type: 'geo',
				y: 100,
			},
			{
				id: ids.boxB,
				type: 'geo',
				y: 0,
			}
		)
	})

	it('Flips rotated shapes', () => {
		editor.updateShapes([{ id: ids.boxA, type: 'geo', rotation: PI }])
		editor.select(ids.boxA, ids.boxB)
		const a = editor.getSelectionPageBounds()
		editor.markHistoryStoppingPoint('flipped')
		editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')

		const b = editor.getSelectionPageBounds()
		expect(a).toCloselyMatchObject(b!)
		editor.expectShapeToMatch(
			{
				id: ids.boxA,
				type: 'geo',
				y: 200,
			},
			{
				id: ids.boxB,
				type: 'geo',
				y: -100,
			}
		)
	})

	it('Flips the children of rotated shapes', () => {
		editor.reparentShapes([ids.boxB], ids.boxA)
		editor.updateShapes([{ id: ids.boxA, type: 'geo', rotation: PI }])
		editor.select(ids.boxB, ids.boxC)
		const a = editor.getSelectionPageBounds()
		editor.markHistoryStoppingPoint('flipped')
		editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')

		const b = editor.getSelectionPageBounds()
		expect(a).toCloselyMatchObject(b!)
	})
})

it('Preserves the selection bounds.', () => {
	editor.selectAll()
	const a = editor.getSelectionPageBounds()
	editor.markHistoryStoppingPoint('flipped')
	editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')

	const b = editor.getSelectionPageBounds()
	expect(a).toMatchObject(b!)
	editor.markHistoryStoppingPoint('flipped')
	editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')

	const c = editor.getSelectionPageBounds()
	expect(a).toMatchObject(c!)
})

it('Does, undoes and redoes', () => {
	editor.markHistoryStoppingPoint('flip vertical')
	editor.flipShapes([ids.boxA, ids.boxB], 'vertical')

	editor.expectShapeToMatch(
		{
			id: ids.boxA,
			type: 'geo',
			y: 100,
		},
		{
			id: ids.boxB,
			type: 'geo',
			y: 0,
		}
	)
	editor.undo()
	editor.expectShapeToMatch(
		{
			id: ids.boxA,
			type: 'geo',
			y: 0,
		},
		{
			id: ids.boxB,
			type: 'geo',
			y: 150,
		}
	)
	editor.redo()
	editor.expectShapeToMatch(
		{
			id: ids.boxA,
			type: 'geo',
			y: 100,
		},
		{
			id: ids.boxB,
			type: 'geo',
			y: 0,
		}
	)
})

describe('When multiple shapes are selected', () => {
	it.todo('Flips the shape positions according to the selection rotation')
	it.todo('Flips using the selection rotation when the shapes have a common selection rotation')
	it.todo('Flips using the main axis when shapes do not have a common selection rotation')
	it.todo('Flips when shapes have different parents')
})

describe('When one shape is selected', () => {
	it('Does nothing if the shape is not a group', () => {
		const before = editor.getShape(ids.boxA)!
		editor.select(ids.boxA)
		editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')

		expect(editor.getShape(ids.boxA)).toMatchObject(before)
	})

	it('Flips the direct child shape positions if the shape is a group', async () => {
		const fn = jest.fn()

		editor.selectAll()
		editor.groupShapes(editor.getSelectedShapeIds()) // this will also select the new group
		const groupBefore = editor.getSelectedShapes()[0]
		editor.on('change', fn)
		editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')

		// The change event should have been called
		jest.runOnlyPendingTimers()
		expect(fn).toHaveBeenCalled()

		editor.expectShapeToMatch(
			{
				...groupBefore, // group should not have changed
			},
			{
				id: ids.boxA, // group's children shapes should have been flipped
				type: 'geo',
				parentId: groupBefore.id,
				x: 300,
				y: 0,
			},
			{
				id: ids.boxB,
				type: 'geo',
				parentId: groupBefore.id,
				x: 200,
				y: 150,
			},
			{
				id: ids.boxC,
				type: 'geo',
				parentId: groupBefore.id,
				x: 0,
				y: 300,
			}
		)
	})

	it.todo('Flips line and arrow shapes when their parent group is flipped')
})

describe('flipping rotated shapes', () => {
	const arrowLength = 100
	const diamondRadius = Math.cos(Math.PI / 4) * arrowLength

	const topPoint = { x: 0, y: 0 }
	const rightPoint = { x: diamondRadius, y: diamondRadius }
	const bottomPoint = { x: 0, y: 2 * diamondRadius }
	const leftPoint = { x: -diamondRadius, y: diamondRadius }

	const ids = {
		arrowA: createShapeId('arrowA'),
		arrowB: createShapeId('arrowB'),
		arrowC: createShapeId('arrowC'),
		arrowD: createShapeId('arrowD'),
	}
	beforeEach(() => {
		editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
		const props: Partial<TLArrowShapeProps> = {
			start: {
				x: 0,
				y: 0,
			},
			end: {
				x: 100,
				y: 0,
			},
		}
		// create a diamond of rotated arrows, pointing clockwise, with the top point at 0,0
		editor.createShapes([
			{
				// top to right
				type: 'arrow',
				id: ids.arrowA,
				...topPoint,
				rotation: Math.PI / 4,
				props,
			},
			{
				// right to bottom
				type: 'arrow',
				id: ids.arrowB,
				...rightPoint,
				rotation: (Math.PI * 3) / 4,
				props,
			},
			{
				// bottom to left
				type: 'arrow',
				id: ids.arrowC,
				...bottomPoint,
				rotation: (Math.PI * 5) / 4,
				props,
			},
			{
				// left to top
				type: 'arrow',
				id: ids.arrowD,
				...leftPoint,
				rotation: (Math.PI * 7) / 4,
				props,
			},
		])

		editor.select(ids.arrowA, ids.arrowB, ids.arrowC, ids.arrowD)
	})

	const getStartAndEndPoints = (id: TLShapeId) => {
		const transform = editor.getShapePageTransform(id)
		if (!transform) throw new Error('no transform')
		const arrow = editor.getShape<TLArrowShape>(id)!
		const bindings = getArrowBindings(editor, arrow)
		if (bindings.start || bindings.end) throw new Error('not a point')
		const start = Mat.applyToPoint(transform, arrow.props.start)
		const end = Mat.applyToPoint(transform, arrow.props.end)
		return { start, end }
	}

	test('flipping horizontally', () => {
		editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
		// now arrow A should be pointing from top to left
		let { start, end } = getStartAndEndPoints(ids.arrowA)
		expect(start).toCloselyMatchObject(topPoint)
		expect(end).toCloselyMatchObject(leftPoint)

		// now arrow B should be pointing from left to bottom
		;({ start, end } = getStartAndEndPoints(ids.arrowB))
		expect(start).toCloselyMatchObject(leftPoint)
		expect(end).toCloselyMatchObject(bottomPoint)

		// now arrow C should be pointing from bottom to right
		;({ start, end } = getStartAndEndPoints(ids.arrowC))
		expect(start).toCloselyMatchObject(bottomPoint)
		expect(end).toCloselyMatchObject(rightPoint)

		// now arrow D should be pointing from right to top
		;({ start, end } = getStartAndEndPoints(ids.arrowD))
		expect(start).toCloselyMatchObject(rightPoint)
		expect(end).toCloselyMatchObject(topPoint)
	})

	test('flipping vertically', () => {
		editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
		// arrows that have height 0 get nudged by a pixel when flipped vertically
		// so we need to use a fairly loose tolerance
		// now arrow A should be pointing from bottom to right
		let { start, end } = getStartAndEndPoints(ids.arrowA)
		expect(start).toCloselyMatchObject(bottomPoint, 5)
		expect(end).toCloselyMatchObject(rightPoint, 5)

		// now arrow B should be pointing from right to top
		;({ start, end } = getStartAndEndPoints(ids.arrowB))
		expect(start).toCloselyMatchObject(rightPoint, 5)
		expect(end).toCloselyMatchObject(topPoint, 5)

		// now arrow C should be pointing from top to left
		;({ start, end } = getStartAndEndPoints(ids.arrowC))
		expect(start).toCloselyMatchObject(topPoint, 5)
		expect(end).toCloselyMatchObject(leftPoint, 5)

		// now arrow D should be pointing from left to bottom
		;({ start, end } = getStartAndEndPoints(ids.arrowD))
		expect(start).toCloselyMatchObject(leftPoint, 5)
		expect(end).toCloselyMatchObject(bottomPoint, 5)
	})
})

describe('When flipping shapes that include arrows', () => {
	let shapes: TLShapePartial[]
	let bindings: TLBindingCreate[]

	beforeEach(() => {
		const box1 = createShapeId()
		const box2 = createShapeId()
		const box3 = createShapeId()
		const arrow1 = createShapeId()
		const arrow2 = createShapeId()
		const arrow3 = createShapeId()

		shapes = [
			{
				id: box1,
				type: 'geo',
				x: 0,
				y: 0,
			},
			{
				id: box2,
				type: 'geo',
				x: 300,
				y: 300,
			},
			{
				id: box3,
				type: 'geo',
				x: 300,
				y: 0,
			},
			{
				id: arrow1,
				type: 'arrow',
				x: 50,
				y: 50,
				props: {
					bend: 200,
				},
			},
			{
				id: arrow2,
				type: 'arrow',
				x: 50,
				y: 50,
				props: {
					bend: -200,
				},
			},
			{
				id: arrow3,
				type: 'arrow',
				x: 50,
				y: 50,
				props: {
					bend: -200,
				},
			},
		]
		bindings = [
			{
				id: createBindingId(),
				type: 'arrow',
				fromId: arrow1,
				toId: box1,
				props: {
					terminal: 'start',
					normalizedAnchor: { x: 0.75, y: 0.75 },
					isExact: false,
					isPrecise: true,
				},
			},
			{
				id: createBindingId(),
				type: 'arrow',
				fromId: arrow1,
				toId: box1,
				props: {
					terminal: 'end',
					normalizedAnchor: { x: 0.25, y: 0.25 },
					isExact: false,
					isPrecise: true,
				},
			},
			{
				id: createBindingId(),
				type: 'arrow',
				fromId: arrow2,
				toId: box1,
				props: {
					terminal: 'start',
					normalizedAnchor: { x: 0.75, y: 0.75 },
					isExact: false,
					isPrecise: true,
				},
			},
			{
				id: createBindingId(),
				type: 'arrow',
				fromId: arrow2,
				toId: box1,
				props: {
					terminal: 'end',
					normalizedAnchor: { x: 0.25, y: 0.25 },
					isExact: false,
					isPrecise: true,
				},
			},
			{
				id: createBindingId(),
				type: 'arrow',
				fromId: arrow3,
				toId: box1,
				props: {
					terminal: 'start',
					normalizedAnchor: { x: 0.75, y: 0.75 },
					isExact: false,
					isPrecise: true,
				},
			},
			{
				id: createBindingId(),
				type: 'arrow',
				fromId: arrow3,
				toId: box3,
				props: {
					terminal: 'end',
					normalizedAnchor: { x: 0.25, y: 0.25 },
					isExact: false,
					isPrecise: true,
				},
			},
		]
	})

	it('Flips horizontally', () => {
		editor
			.selectAll()
			.deleteShapes(editor.getSelectedShapeIds())
			.createShapes(shapes)
			.createBindings(bindings)

		const boundsBefore = editor.getSelectionRotatedPageBounds()!
		editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
		expect(editor.getSelectionRotatedPageBounds()).toCloselyMatchObject(boundsBefore)
	})

	it('Flips vertically', () => {
		editor
			.selectAll()
			.deleteShapes(editor.getSelectedShapeIds())
			.createShapes(shapes)
			.createBindings(bindings)

		const boundsBefore = editor.getSelectionRotatedPageBounds()!
		editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
		expect(editor.getSelectionRotatedPageBounds()).toCloselyMatchObject(boundsBefore)
	})
})

it('Updates the image shape flip properties when flipped', () => {
	editor.createShape({
		type: 'image',
	})
	editor.select(editor.getLastCreatedShape())
	editor.flipShapes(editor.getSelectedShapeIds(), 'horizontal')
	expect(editor.getLastCreatedShape<TLImageShape>().props.flipX).toBe(true)
	editor.flipShapes(editor.getSelectedShapeIds(), 'vertical')
	expect(editor.getLastCreatedShape<TLImageShape>().props.flipY).toBe(true)
})
