import {
	RecordId,
	UnknownRecord,
	createMigrationIds,
	createRecordMigrationSequence,
	createRecordType,
} from '@tldraw/store'
import { mapObjectMapValues, uniqueId } from '@tldraw/utils'
import { T } from '@tldraw/validate'
import { SchemaPropsInfo } from '../createTLSchema'
import { TLPropsMigrations } from '../recordsWithProps'
import { TLArrowShape } from '../shapes/TLArrowShape'
import { TLBaseShape, createShapeValidator } from '../shapes/TLBaseShape'
import { TLBookmarkShape } from '../shapes/TLBookmarkShape'
import { TLDrawShape } from '../shapes/TLDrawShape'
import { TLEmbedShape } from '../shapes/TLEmbedShape'
import { TLFrameShape } from '../shapes/TLFrameShape'
import { TLGeoShape } from '../shapes/TLGeoShape'
import { TLGroupShape } from '../shapes/TLGroupShape'
import { TLHighlightShape } from '../shapes/TLHighlightShape'
import { TLImageShape } from '../shapes/TLImageShape'
import { TLLineShape } from '../shapes/TLLineShape'
import { TLNoteShape } from '../shapes/TLNoteShape'
import { TLTextShape } from '../shapes/TLTextShape'
import { TLVideoShape } from '../shapes/TLVideoShape'
import { StyleProp } from '../styles/StyleProp'
import { TLPageId } from './TLPage'

/**
 * The default set of shapes that are available in the editor.
 *
 * This union type represents all the built-in shape types supported by tldraw,
 * including arrows, bookmarks, drawings, embeds, frames, geometry shapes,
 * groups, images, lines, notes, text, videos, and highlights.
 *
 * @example
 * ```ts
 * // Check if a shape is a default shape type
 * function isDefaultShape(shape: TLShape): shape is TLDefaultShape {
 *   const defaultTypes = ['arrow', 'bookmark', 'draw', 'embed', 'frame', 'geo', 'group', 'image', 'line', 'note', 'text', 'video', 'highlight']
 *   return defaultTypes.includes(shape.type)
 * }
 * ```
 *
 * @public
 */
export type TLDefaultShape =
	| TLArrowShape
	| TLBookmarkShape
	| TLDrawShape
	| TLEmbedShape
	| TLFrameShape
	| TLGeoShape
	| TLGroupShape
	| TLImageShape
	| TLLineShape
	| TLNoteShape
	| TLTextShape
	| TLVideoShape
	| TLHighlightShape

/**
 * A type for a shape that is available in the editor but whose type is
 * unknown—either one of the editor's default shapes or else a custom shape.
 *
 * This is useful when working with shapes generically without knowing their specific type.
 * The shape type is a string and props are a generic object.
 *
 * @example
 * ```ts
 * // Handle any shape regardless of its specific type
 * function processUnknownShape(shape: TLUnknownShape) {
 *   console.log(`Processing shape of type: ${shape.type}`)
 *   console.log(`Position: (${shape.x}, ${shape.y})`)
 * }
 * ```
 *
 * @public
 */
export type TLUnknownShape = TLBaseShape<string, object>

/** @public */
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface TLGlobalShapePropsMap {}

/** @public */
// prettier-ignore
export type TLIndexedShapes = {
	// We iterate over a union of augmented keys and default shape types.
	// This allows us to include (or conditionally exclude or override) the default shapes in one go.
	//
	// In the `as` clause we are filtering out disabled shapes.
	[K in keyof TLGlobalShapePropsMap | TLDefaultShape['type'] as K extends TLDefaultShape['type']
		? // core shapes are always available and cannot be overridden so we just include them
			K extends 'group'
			? K
			: K extends keyof TLGlobalShapePropsMap
				? // if it extends a nullish value the user has disabled this shape type so we filter it out with never
					TLGlobalShapePropsMap[K] extends null | undefined
					? never
					: K
				: K
		: K]: K extends 'group'
		? // core shapes are always available and cannot be overridden so we just include them
			Extract<TLDefaultShape, { type: K }>
		: K extends TLDefaultShape['type']
			? // if it's a default shape type we need to check if it's been overridden
				K extends keyof TLGlobalShapePropsMap
				? // if it has been overriden then use the custom shape definition
					TLBaseShape<K, TLGlobalShapePropsMap[K]>
				: // if it has not been overriden then reuse existing type aliases for better type display
					Extract<TLDefaultShape, { type: K }>
			: // use the custom shape definition
				TLBaseShape<K, TLGlobalShapePropsMap[K & keyof TLGlobalShapePropsMap]>
}

/**
 * The set of all shapes that are available in the editor.
 *
 * This is the primary shape type used throughout tldraw. It includes both the
 * built-in default shapes and any custom shapes that might be added.
 *
 * You can use this type without a type argument to work with any shape, or pass
 * a specific shape type string (e.g., `'geo'`, `'arrow'`, `'text'`) to narrow
 * down to that specific shape type.
 *
 * @example
 * ```ts
 * // Work with any shape in the editor
 * function moveShape(shape: TLShape, deltaX: number, deltaY: number): TLShape {
 *   return {
 *     ...shape,
 *     x: shape.x + deltaX,
 *     y: shape.y + deltaY
 *   }
 * }
 *
 * // Narrow to a specific shape type by passing the type as a generic argument
 * function getArrowLabel(shape: TLShape<'arrow'>): string {
 *   return shape.props.text // TypeScript knows this is a TLArrowShape
 * }
 * ```
 *
 * @public
 */
export type TLShape<K extends keyof TLIndexedShapes = keyof TLIndexedShapes> = TLIndexedShapes[K]

/**
 * A partial version of a shape, useful for updates and patches.
 *
 * This type represents a shape where all properties except `id` and `type` are optional.
 * It's commonly used when updating existing shapes or creating shape patches.
 *
 * @example
 * ```ts
 * // Update a shape's position
 * const shapeUpdate: TLShapePartial = {
 *   id: 'shape:123',
 *   type: 'geo',
 *   x: 100,
 *   y: 200
 * }
 *
 * // Update shape properties
 * const propsUpdate: TLShapePartial<TLGeoShape> = {
 *   id: 'shape:123',
 *   type: 'geo',
 *   props: {
 *     w: 150,
 *     h: 100
 *   }
 * }
 * ```
 *
 * @public
 */
export type TLShapePartial<T extends TLShape = TLShape> = T extends T
	? {
			id: TLShapeId
			type: T['type']
			props?: Partial<T['props']>
			meta?: Partial<T['meta']>
		} & Partial<Omit<T, 'type' | 'id' | 'props' | 'meta'>>
	: never

/**
 * A partial version of a shape, useful for creating shapes.
 *
 * This type represents a shape where all properties except `type` are optional.
 * It's commonly used when creating shapes.
 *
 * @example
 * ```ts
 * // Create a shape
 * const shapeCreate: TLCreateShapePartial = {
 *   type: 'geo',
 *   x: 100,
 *   y: 200
 * }
 *
 * // Create shape properties
 * const propsCreate: TLCreateShapePartial<TLGeoShape> = {
 *   type: 'geo',
 *   props: {
 *     w: 150,
 *     h: 100
 *   }
 * }
 * ```
 *
 * @public
 */
export type TLCreateShapePartial<T extends TLShape = TLShape> = T extends T
	? {
			type: T['type']
			props?: Partial<T['props']>
			meta?: Partial<T['meta']>
		} & Partial<Omit<T, 'type' | 'props' | 'meta'>>
	: never

/**
 * Extract a shape type by its props.
 *
 * This utility type takes a props object type and returns the corresponding shape type
 * from the TLShape union whose props match the given type.
 *
 * @example
 * ```ts
 * type MyShape = ExtractShapeByProps<{ w: number; h: number }>
 * // MyShape is now the type of shape(s) that have props with w and h as numbers
 * ```
 *
 * @public
 */
export type ExtractShapeByProps<P> = Extract<TLShape, { props: P }>

/**
 * A unique identifier for a shape record.
 *
 * Shape IDs are branded strings that start with "shape:" followed by a unique identifier.
 * This type-safe approach prevents mixing up different types of record IDs.
 *
 * @example
 * ```ts
 * const shapeId: TLShapeId = createShapeId() // "shape:abc123"
 * const customId: TLShapeId = createShapeId('my-custom-id') // "shape:my-custom-id"
 * ```
 *
 * @public
 */
export type TLShapeId = RecordId<TLShape>

/**
 * The ID of a shape's parent, which can be either a page or another shape.
 *
 * Shapes can be parented to pages (for top-level shapes) or to other shapes
 * (for shapes inside frames or groups).
 *
 * @example
 * ```ts
 * // Shape parented to a page
 * const pageParentId: TLParentId = 'page:main'
 *
 * // Shape parented to another shape (e.g., inside a frame)
 * const shapeParentId: TLParentId = 'shape:frame123'
 * ```
 *
 * @public
 */
export type TLParentId = TLPageId | TLShapeId

/**
 * Migration version IDs for the root shape schema.
 *
 * These track the evolution of the base shape structure over time, ensuring
 * that shapes created in older versions can be migrated to newer formats.
 *
 * @example
 * ```ts
 * // Check if a migration needs to be applied
 * if (shapeVersion < rootShapeVersions.AddIsLocked) {
 *   // Apply isLocked migration
 * }
 * ```
 *
 * @public
 */
export const rootShapeVersions = createMigrationIds('com.tldraw.shape', {
	AddIsLocked: 1,
	HoistOpacity: 2,
	AddMeta: 3,
	AddWhite: 4,
})

/**
 * Migration sequence for the root shape record type.
 *
 * This sequence defines how shape records should be transformed when migrating
 * between different schema versions. Each migration handles a specific version
 * upgrade, ensuring data compatibility across tldraw versions.
 *
 * @public
 */
export const rootShapeMigrations = createRecordMigrationSequence({
	sequenceId: 'com.tldraw.shape',
	recordType: 'shape',
	sequence: [
		{
			id: rootShapeVersions.AddIsLocked,
			up: (record: any) => {
				record.isLocked = false
			},
			down: (record: any) => {
				delete record.isLocked
			},
		},
		{
			id: rootShapeVersions.HoistOpacity,
			up: (record: any) => {
				record.opacity = Number(record.props.opacity ?? '1')
				delete record.props.opacity
			},
			down: (record: any) => {
				const opacity = record.opacity
				delete record.opacity
				record.props.opacity =
					opacity < 0.175
						? '0.1'
						: opacity < 0.375
							? '0.25'
							: opacity < 0.625
								? '0.5'
								: opacity < 0.875
									? '0.75'
									: '1'
			},
		},
		{
			id: rootShapeVersions.AddMeta,
			up: (record: any) => {
				record.meta = {}
			},
		},
		{
			id: rootShapeVersions.AddWhite,
			up: (_record) => {
				// noop
			},
			down: (record: any) => {
				if (record.props.color === 'white') {
					record.props.color = 'black'
				}
			},
		},
	],
})

/**
 * Type guard to check if a record is a shape.
 *
 * @param record - The record to check
 * @returns True if the record is a shape, false otherwise
 *
 * @example
 * ```ts
 * const record = store.get('shape:abc123')
 * if (isShape(record)) {
 *   console.log(`Shape type: ${record.type}`)
 *   console.log(`Position: (${record.x}, ${record.y})`)
 * }
 * ```
 *
 * @public
 */
export function isShape(record?: UnknownRecord): record is TLShape {
	if (!record) return false
	return record.typeName === 'shape'
}

/**
 * Type guard to check if a string is a valid shape ID.
 *
 * @param id - The string to check
 * @returns True if the string is a valid shape ID, false otherwise
 *
 * @example
 * ```ts
 * const id = 'shape:abc123'
 * if (isShapeId(id)) {
 *   const shape = store.get(id) // TypeScript knows id is TLShapeId
 * }
 *
 * // Check user input
 * function selectShape(id: string) {
 *   if (isShapeId(id)) {
 *     editor.selectShape(id)
 *   } else {
 *     console.error('Invalid shape ID format')
 *   }
 * }
 * ```
 *
 * @public
 */
export function isShapeId(id?: string): id is TLShapeId {
	if (!id) return false
	return id.startsWith('shape:')
}

/**
 * Creates a new shape ID.
 *
 * @param id - Optional custom ID suffix. If not provided, a unique ID will be generated
 * @returns A new shape ID with the "shape:" prefix
 *
 * @example
 * ```ts
 * // Create a shape with auto-generated ID
 * const shapeId = createShapeId() // "shape:abc123"
 *
 * // Create a shape with custom ID
 * const customShapeId = createShapeId('my-rectangle') // "shape:my-rectangle"
 *
 * // Use in shape creation
 * const newShape: TLGeoShape = {
 *   id: createShapeId(),
 *   type: 'geo',
 *   x: 100,
 *   y: 200,
 *   // ... other properties
 * }
 * ```
 *
 * @public
 */
export function createShapeId(id?: string): TLShapeId {
	return `shape:${id ?? uniqueId()}` as TLShapeId
}

/**
 * Extracts style properties from a shape's props definition and maps them to their property keys.
 *
 * This function analyzes shape property validators to identify which ones are style properties
 * and creates a mapping from StyleProp instances to their corresponding property keys.
 * It also validates that each style property is only used once per shape.
 *
 * @param props - Record of property validators for a shape type
 * @returns Map from StyleProp instances to their property keys
 * @throws Error if a style property is used more than once in the same shape
 *
 * @example
 * ```ts
 * const geoShapeProps = {
 *   color: DefaultColorStyle,
 *   fill: DefaultFillStyle,
 *   width: T.number,
 *   height: T.number
 * }
 *
 * const styleMap = getShapePropKeysByStyle(geoShapeProps)
 * // styleMap.get(DefaultColorStyle) === 'color'
 * // styleMap.get(DefaultFillStyle) === 'fill'
 * ```
 *
 * @internal
 */
export function getShapePropKeysByStyle(props: Record<string, T.Validatable<any>>) {
	const propKeysByStyle = new Map<StyleProp<unknown>, string>()
	for (const [key, prop] of Object.entries(props)) {
		if (prop instanceof StyleProp) {
			if (propKeysByStyle.has(prop)) {
				throw new Error(
					`Duplicate style prop ${prop.id}. Each style prop can only be used once within a shape.`
				)
			}
			propKeysByStyle.set(prop, key)
		}
	}
	return propKeysByStyle
}

/**
 * Creates a migration sequence for shape properties.
 *
 * This is a pass-through function that maintains the same structure as the input.
 * It's used for consistency and to provide a clear API for defining shape property migrations.
 *
 * @param migrations - The migration sequence to create
 * @returns The same migration sequence (pass-through)
 *
 * @example
 * ```ts
 * const myShapeMigrations = createShapePropsMigrationSequence({
 *   sequence: [
 *     {
 *       id: 'com.myapp.shape.custom/1.0.0',
 *       up: (props) => ({ ...props, newProperty: 'default' }),
 *       down: ({ newProperty, ...props }) => props
 *     }
 *   ]
 * })
 * ```
 *
 * @public
 */
export function createShapePropsMigrationSequence(
	migrations: TLPropsMigrations
): TLPropsMigrations {
	return migrations
}

/**
 * Creates properly formatted migration IDs for shape properties.
 *
 * Generates standardized migration IDs following the convention:
 * `com.tldraw.shape.{shapeType}/{version}`
 *
 * @param shapeType - The type of shape these migrations apply to
 * @param ids - Record mapping migration names to version numbers
 * @returns Record with the same keys but formatted migration ID values
 *
 * @example
 * ```ts
 * const myShapeVersions = createShapePropsMigrationIds('custom', {
 *   AddColor: 1,
 *   AddSize: 2,
 *   RefactorProps: 3
 * })
 * // Result: {
 * //   AddColor: 'com.tldraw.shape.custom/1',
 * //   AddSize: 'com.tldraw.shape.custom/2',
 * //   RefactorProps: 'com.tldraw.shape.custom/3'
 * // }
 * ```
 *
 * @public
 */
export function createShapePropsMigrationIds<
	const S extends string,
	const T extends Record<string, number>,
>(shapeType: S, ids: T): { [k in keyof T]: `com.tldraw.shape.${S}/${T[k]}` } {
	return mapObjectMapValues(ids, (_k, v) => `com.tldraw.shape.${shapeType}/${v}`) as any
}

/**
 * Creates the record type definition for shapes.
 *
 * This function generates a complete record type for shapes that includes validation
 * for all registered shape types. It combines the base shape properties with
 * type-specific properties and creates a union validator that can handle any
 * registered shape type.
 *
 * @param shapes - Record of shape type names to their schema configuration
 * @returns A complete RecordType for shapes with proper validation and default properties
 *
 * @example
 * ```ts
 * const shapeRecordType = createShapeRecordType({
 *   geo: { props: geoShapeProps, migrations: geoMigrations },
 *   arrow: { props: arrowShapeProps, migrations: arrowMigrations }
 * })
 * ```
 *
 * @internal
 */
export function createShapeRecordType(shapes: Record<string, SchemaPropsInfo>) {
	return createRecordType('shape', {
		scope: 'document',
		validator: T.model(
			'shape',
			T.union(
				'type',
				mapObjectMapValues(shapes, (type, { props, meta }) =>
					createShapeValidator(type, props, meta)
				)
			)
		),
	}).withDefaultProperties(() => ({
		x: 0,
		y: 0,
		rotation: 0,
		isLocked: false,
		opacity: 1,
		meta: {},
	}))
}
