import { createShapeId, PI, TLShapeId } from '@tldraw/editor'
import { vi } from 'vitest'
import { TestEditor } from '../TestEditor'

vi.useFakeTimers()

let editor: TestEditor

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

beforeEach(() => {
	editor = new TestEditor()
})

describe('distributeShapes command', () => {
	beforeEach(() => {
		editor.selectAll()
		editor.deleteShapes(editor.getSelectedShapeIds())
		editor.createShapes([
			{
				id: ids.boxA,
				type: 'geo',
				x: 0,
				y: 0,
			},
			{
				id: ids.boxB,
				type: 'geo',
				x: 100,
				y: 100,
			},
			{
				id: ids.boxC,
				type: 'geo',
				x: 400,
				y: 400,
			},
		])
	})

	describe('when less than three shapes are selected', () => {
		it('does nothing', () => {
			editor.setSelectedShapes([ids.boxA, ids.boxB])
			const fn = vi.fn()
			editor.store.listen(fn)
			editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
			vi.advanceTimersByTime(1000)
			expect(fn).not.toHaveBeenCalled()
		})
	})

	describe('When distributing...', () => {
		it('distributeShapes horizontally', () => {
			editor.selectAll()
			editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
			vi.advanceTimersByTime(1000)
			editor.expectShapeToMatch(
				{ id: ids.boxA, x: 0 },
				{ id: ids.boxB, x: 200 },
				{ id: ids.boxC, x: 400 }
			)
		})

		it('distributeShapes horizontally when shapes are clustered', () => {
			editor.updateShapes([{ id: ids.boxC, type: 'geo', x: 25 }])
			editor.selectAll()
			editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
			vi.advanceTimersByTime(1000)
			editor.expectShapeToMatch(
				{ id: ids.boxA, x: 0 },
				{ id: ids.boxB, x: 100 },
				{ id: ids.boxC, x: 50 }
			)
		})

		it('distributeShapes vertically', () => {
			editor.selectAll()
			editor.distributeShapes(editor.getSelectedShapeIds(), 'vertical')
			vi.advanceTimersByTime(1000)
			editor.expectShapeToMatch(
				{ id: ids.boxA, y: 0 },
				{ id: ids.boxB, y: 200 },
				{ id: ids.boxC, y: 400 }
			)
		})

		it('distributeShapes vertically when shapes are clustered', () => {
			editor.updateShapes([{ id: ids.boxC, type: 'geo', y: 25 }])
			editor.selectAll()
			editor.distributeShapes(editor.getSelectedShapeIds(), 'vertical')
			vi.advanceTimersByTime(1000)
			editor.expectShapeToMatch(
				{ id: ids.boxA, y: 0 },
				{ id: ids.boxB, y: 100 },
				{ id: ids.boxC, y: 50 }
			)
		})
	})

	it('distributeShapes shapes that are the child of another shape.', () => {
		editor.selectAll()
		editor.deleteShapes(editor.getSelectedShapeIds())
		editor.createShapes([
			{
				id: ids.boxA,
				type: 'geo',
				x: 0,
				y: 0,
			},
			{
				id: ids.boxB,
				type: 'geo',
				parentId: ids.boxA,
				x: 100,
				y: 100,
			},
			{
				id: ids.boxC,
				type: 'geo',
				x: 200,
				y: 100,
			},
			{
				id: ids.boxD,
				type: 'geo',
				x: 400,
				y: 100,
			},
		])
		editor.setSelectedShapes([ids.boxB, ids.boxC, ids.boxD])

		editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
		vi.advanceTimersByTime(1000)

		editor.expectShapeToMatch(
			{ id: ids.boxB, x: 100 },
			{ id: ids.boxC, x: 250 },
			{ id: ids.boxD, x: 400 }
		)
	})

	it('distributeShapes shapes that are the child of another shape when clustered.', () => {
		editor.selectAll()
		editor.deleteShapes(editor.getSelectedShapeIds())
		editor.createShapes([
			{
				id: ids.boxA,
				type: 'geo',
				x: 0,
				y: 0,
			},
			{
				id: ids.boxB,
				type: 'geo',
				parentId: ids.boxA,
				x: 100,
				y: 100,
			},
			{
				id: ids.boxC,
				type: 'geo',
				x: 200,
				y: 100,
			},
			{
				id: ids.boxD,
				type: 'geo',
				x: 175,
				y: 200,
			},
		])
		editor.setSelectedShapes([ids.boxB, ids.boxC, ids.boxD])

		editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
		vi.advanceTimersByTime(1000)

		editor.expectShapeToMatch(
			{ id: ids.boxB, x: 100 },
			{ id: ids.boxC, x: 200 },
			{ id: ids.boxD, x: 150 }
		)
	})

	it('distributeShapes shapes that are the child of another shape when a parent is rotated.', () => {
		editor = new TestEditor()
		editor.selectAll()
		editor.deleteShapes(editor.getSelectedShapeIds())
		editor.createShapes([
			{
				id: ids.boxA,
				type: 'geo',
				x: 0,
				y: 0,
				rotation: PI,
			},
			{
				id: ids.boxB,
				type: 'geo',
				parentId: ids.boxA,
				x: 100,
				y: 100,
			},
			{
				id: ids.boxC,
				type: 'geo',
				x: 200,
				y: 0,
			},
			{
				id: ids.boxD,
				type: 'geo',
				x: 300,
				y: 0,
			},
		])

		editor.setSelectedShapes([ids.boxB, ids.boxC, ids.boxD])

		editor.distributeShapes(editor.getSelectedShapeIds(), 'horizontal')
		vi.advanceTimersByTime(1000)

		editor.expectShapeToMatch(
			{ id: ids.boxB, x: 100 },
			{ id: ids.boxC, x: 50 },
			{ id: ids.boxD, x: 300 }
		)
	})
})

describe('when shapes are overlapping', () => {
	let ids: Record<string, TLShapeId> = {}

	//     AAAA      DDDDDDDD
	//        BB
	//				 CC
	beforeEach(() => {
		editor = new TestEditor()
		ids = {
			boxA: createShapeId('boxA'),
			boxB: createShapeId('boxB'),
			boxC: createShapeId('boxC'),
			boxD: createShapeId('boxD'),
		}
		editor.createShapes([
			{ id: ids.boxA, type: 'geo', x: 100, y: 100, props: { w: 100, h: 100 } },
			{ id: ids.boxB, type: 'geo', x: 175, y: 175, props: { w: 50, h: 50 } },
			{ id: ids.boxC, type: 'geo', x: 200, y: 200, props: { w: 50, h: 50 } },
			{ id: ids.boxD, type: 'geo', x: 350, y: 350, props: { w: 200, h: 200 } },
		])

		editor.selectAll()
	})

	it('distributes horizontally', () => {
		editor.selectAll().distributeShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD], 'horizontal')
		// total range is 150 (boxA.maxX = 200, boxD.minX = 350)
		// spaced used by inner shapes is 100 (50 + 50)
		// gap should be ((150 - 100) / 3) = 16.666666666666668

		// does not move the first or last shape
		expect(editor.getShape(ids.boxA)!.x).toBe(100)
		expect(editor.getShape(ids.boxD)!.x).toBe(350)

		expect(editor.getShape(ids.boxB)!.x).toBeCloseTo(200 + 16.67, 1)
		expect(editor.getShape(ids.boxC)!.x).toBeCloseTo(200 + 50 + 16.7 + 16.67, 1)
	})

	it('aligns horizontally', () => {
		editor.selectAll().distributeShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD], 'vertical')
		// total range is 150 (boxA.maxX = 200, boxD.minX = 350)
		// spaced used by inner shapes is 100 (50 + 50)
		// gap should be ((150 - 100) / 3) = 16.666666666666668

		// does not move the first or last shape
		expect(editor.getShape(ids.boxA)!.y).toBe(100)
		expect(editor.getShape(ids.boxD)!.y).toBe(350)

		expect(editor.getShape(ids.boxB)!.y).toBeCloseTo(200 + 16.67, 1)
		expect(editor.getShape(ids.boxC)!.y).toBeCloseTo(200 + 50 + 16.7 + 16.67, 1)
	})
})

it('preserves common bounds when distributing shapes with a lot of overlap', () => {
	editor = new TestEditor()
	// AAAABBCC EEE
	//     DDDDDD
	const ids = {
		boxA: createShapeId('boxA'),
		boxB: createShapeId('boxB'),
		boxC: createShapeId('boxC'),
		boxD: createShapeId('boxD'),
		boxE: createShapeId('boxE'),
	}
	editor.createShapes([
		{ id: ids.boxA, type: 'geo', x: 0, y: 0, props: { w: 100, h: 100 } },
		{ id: ids.boxB, type: 'geo', x: 20, y: 0, props: { w: 15, h: 100 } },
		{ id: ids.boxC, type: 'geo', x: 30, y: 0, props: { w: 10, h: 100 } },
		{ id: ids.boxD, type: 'geo', x: 10, y: 0, props: { w: 380, h: 100 } },
		{ id: ids.boxE, type: 'geo', x: 300, y: 0, props: { w: 100, h: 100 } },
	])

	editor.selectAll()

	const prevBounds = editor.getSelectionPageBounds()!

	editor.distributeShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD, ids.boxE], 'horizontal')

	// If we didn't clamp this, then the right side of boxD would be to the right of boxE's right side
	expect(editor.getShapePageBounds(ids.boxD)!.maxX).toEqual(
		editor.getShapePageBounds(ids.boxE)!.maxX - 1
	)

	// The bounds should be the same as when we started
	expect(editor.getSelectionPageBounds()!).toCloselyMatchObject(prevBounds)

	// this is the best possible handling of an impossible distribution.
	// It's not worth trying to do anything more clever since this would almost certainly never come up.
	// We just need to be sure it's idempotent.

	// fails, but this is what we want:

	// const xsBefore = objectMapFromEntries(
	// 	Object.entries(ids).map(([id, shapeId]) => [id, editor.getShapePageBounds(shapeId)!.x])
	// )
	// editor.distributeShapes([ids.boxA, ids.boxB, ids.boxC, ids.boxD], 'horizontal')
	// expect(editor.getSelectionPageBounds()!).toCloselyMatchObject(prevBounds)

	// const xsAfter = objectMapFromEntries(
	// 	Object.entries(ids).map(([id, shapeId]) => [id, editor.getShapePageBounds(shapeId)!.x])
	// )
	// expect(xsBefore).toCloselyMatchObject(xsAfter)
})
