import { Awaitable } from '@tldraw/utils'
import { ComponentType, Fragment } from 'react'
import { DEFAULT_CAMERA_OPTIONS } from './constants'
import type { Editor } from './editor/Editor'
import { TLContent } from './editor/types/clipboard-types'
import { TLExternalContent } from './editor/types/external-content'
import { TLCameraOptions } from './editor/types/misc-types'
import { VecLike } from './primitives/Vec'
import { TLDeepLinkOptions } from './utils/deepLinks'
import { TLTextOptions } from './utils/richText'

/**
 * Identifies how a clipboard write was triggered (copy vs cut, keyboard vs menu).
 *
 * @public
 */
export interface TLClipboardWriteInfo {
	readonly operation: 'copy' | 'cut'
	readonly source: 'native' | 'menu'
}

/**
 * Raw clipboard paste payload, before tldraw parses clipboard contents into {@link TLExternalContent}.
 *
 * - `native-event`: from the `paste` event — `clipboardData` is available synchronously (unlike async
 *   `navigator.clipboard.read()`).
 * - `clipboard-read`: from an explicit `navigator.clipboard.read()` call — only `ClipboardItem[]`
 *   exists
 *   (no `DataTransfer`).
 *
 * @public
 */
export type TLClipboardPasteRawInfo =
	| {
			readonly editor: Editor
			readonly source: 'native-event'
			readonly event: ClipboardEvent
			readonly clipboardData: DataTransfer | null
			readonly point: VecLike | undefined
	  }
	| {
			readonly editor: Editor
			readonly source: 'clipboard-read'
			readonly clipboardItems: readonly ClipboardItem[]
			readonly point: VecLike | undefined
	  }

/**
 * Options for configuring tldraw. For defaults, see {@link defaultTldrawOptions}.
 *
 * @example
 * ```tsx
 * const options: Partial<TldrawOptions> = {
 *     maxPages: 3,
 *     maxShapesPerPage: 1000,
 * }
 *
 * function MyTldrawComponent() {
 *     return <Tldraw options={options} />
 * }
 * ```
 *
 * @public
 */
export interface TldrawOptions {
	readonly maxShapesPerPage: number
	readonly maxFilesAtOnce: number
	readonly maxPages: number
	readonly animationMediumMs: number
	readonly followChaseViewportSnap: number
	readonly doubleClickDurationMs: number
	readonly multiClickDurationMs: number
	readonly coarseDragDistanceSquared: number
	readonly dragDistanceSquared: number
	readonly uiDragDistanceSquared: number
	readonly uiCoarseDragDistanceSquared: number
	readonly defaultSvgPadding: number
	readonly cameraSlideFriction: number
	readonly gridSteps: readonly {
		readonly min: number
		readonly mid: number
		readonly step: number
	}[]
	readonly collaboratorInactiveTimeoutMs: number
	readonly collaboratorIdleTimeoutMs: number
	readonly collaboratorCheckIntervalMs: number
	readonly cameraMovingTimeoutMs: number
	readonly hitTestMargin: number
	readonly edgeScrollDelay: number
	readonly edgeScrollEaseDuration: number
	readonly edgeScrollSpeed: number
	readonly edgeScrollDistance: number
	readonly coarsePointerWidth: number
	readonly coarseHandleRadius: number
	readonly handleRadius: number
	readonly longPressDurationMs: number
	readonly textShadowLod: number
	readonly adjacentShapeMargin: number
	readonly flattenImageBoundsExpand: number
	readonly flattenImageBoundsPadding: number
	readonly laserDelayMs: number
	/**
	 * How long (in milliseconds) to fade all laser scribbles after the session ends.
	 * The total points across all scribbles will be removed proportionally over this duration.
	 * Defaults to 500ms (0.5 seconds).
	 */
	readonly laserFadeoutMs: number
	readonly maxExportDelayMs: number
	readonly tooltipDelayMs: number
	/**
	 * How long should previews created by {@link Editor.createTemporaryAssetPreview} last before
	 * they expire? Defaults to 3 minutes.
	 */
	readonly temporaryAssetPreviewLifetimeMs: number
	readonly actionShortcutsLocation: 'menu' | 'toolbar' | 'swap'
	readonly createTextOnCanvasDoubleClick: boolean
	/**
	 * The react provider to use when exporting an image. This is useful if your shapes depend on
	 * external context providers. By default, this is `React.Fragment`.
	 */
	readonly exportProvider: ComponentType<{ children: React.ReactNode }>
	/**
	 * By default, the toolbar items are accessible via number shortcuts according to their order. To disable this, set this option to false.
	 */
	readonly enableToolbarKeyboardShortcuts: boolean
	/**
	 * The maximum number of fonts that will be loaded while blocking the main rendering of the
	 * canvas. If there are more than this number of fonts needed, we'll just show the canvas right
	 * away and let the fonts load in in the background.
	 */
	readonly maxFontsToLoadBeforeRender: number
	/**
	 * If you have a CSP policy that blocks inline styles, you can use this prop to provide a
	 * nonce to use in the editor's styles.
	 */
	readonly nonce: string | undefined
	/**
	 * Branding name of the app, currently only used for adding aria-label for the application.
	 */
	readonly branding?: string
	/**
	 * Whether to use debounced zoom level for certain rendering optimizations. When true,
	 * `editor.getEfficientZoomLevel()` returns a cached zoom value while the camera is moving,
	 * reducing re-renders. When false, it always returns the current zoom level.
	 */
	readonly debouncedZoom: boolean
	/**
	 * The number of shapes that must be on the page for the debounced zoom level to be used.
	 * Defaults to 500 shapes.
	 */
	readonly debouncedZoomThreshold: number
	/**
	 * Whether to allow spacebar panning. When true, the spacebar will pan the camera when held down.
	 * When false, the spacebar will not pan the camera.
	 */
	readonly spacebarPanning: boolean
	/**
	 * Whether to allow right-click + drag to pan the camera. When true, right-click + drag pans the
	 * camera and a static right-click opens the context menu at the release position. When false,
	 * right-click opens the context menu on press (no drag-to-pan).
	 */
	readonly rightClickPanning: boolean
	/**
	 * The default padding (in pixels) used when zooming to fit content in the viewport.
	 * This affects methods like `zoomToFit()`, `zoomToSelection()`, and `zoomToBounds()`.
	 * The actual padding used is the minimum of this value and 28% of the viewport width.
	 * Defaults to 128 pixels.
	 */
	readonly zoomToFitPadding: number
	/**
	 * The distance (in screen pixels) at which shapes snap to guides and other shapes.
	 */
	readonly snapThreshold: number
	/**
	 * Options for the editor's camera. These are the initial camera options.
	 * Use {@link Editor.setCameraOptions} to update camera options at runtime.
	 */
	readonly camera: Partial<TLCameraOptions>
	/**
	 * Options for the editor's text rendering. These include TipTap configuration and
	 * font handling. These are the initial text options and cannot be changed at runtime.
	 */
	readonly text: TLTextOptions
	/**
	 * Options for syncing the editor's camera state with the URL. Set to `true` to enable
	 * with default options, or pass an options object to customize behavior.
	 *
	 * @example
	 * ```tsx
	 * // Enable with defaults
	 * <Tldraw options={{ deepLinks: true }} />
	 *
	 * // Enable with custom options
	 * <Tldraw options={{ deepLinks: { param: 'd', debounceMs: 500 } }} />
	 * ```
	 */
	readonly deepLinks: true | TLDeepLinkOptions | undefined
	/**
	 * Whether the quick-zoom brush preserves its screen-pixel size when the user
	 * zooms the overview. When true, zooming in shrinks the target viewport (higher
	 * return zoom); zooming out expands it. When false, the brush keeps the original
	 * viewport's page dimensions regardless of overview zoom changes.
	 */
	readonly quickZoomPreservesScreenBounds: boolean
	/**
	 * Called before content is written to the clipboard during a copy or cut operation.
	 * Receives the serialized content (shapes, bindings, assets) and can filter or transform
	 * it before it reaches the clipboard.
	 *
	 * Return a modified `TLContent` object to change what is copied or cut. Return `false` to
	 * cancel the clipboard write (for cut, the selected shapes are not removed). Return `void`
	 * (or `undefined`) to pass through unchanged. You may return a `Promise` of those values if
	 * the hook is async.
	 *
	 * @example
	 * ```tsx
	 * // Filter out "locked" shapes from copy
	 * onBeforeCopyToClipboard({ content, operation }) {
	 *     return {
	 *         ...content,
	 *         shapes: content.shapes.filter(s => !s.meta.locked),
	 *         rootShapeIds: content.rootShapeIds.filter(id =>
	 *             content.shapes.find(s => s.id === id && !s.meta.locked)
	 *         ),
	 *     }
	 * }
	 * ```
	 */
	onBeforeCopyToClipboard?(
		info: { editor: Editor; content: TLContent } & TLClipboardWriteInfo
	): Awaitable<TLContent | false | void>
	/**
	 * Called before pasted content is processed and shapes are created. Receives the parsed
	 * external content from the clipboard and can filter, transform, or cancel it.
	 *
	 * Return `false` to cancel the paste. Return a modified content object to transform it.
	 * Return `void` (or `undefined`) to pass through unchanged. You may return a `Promise` of
	 * those values if the hook is async.
	 *
	 * This only fires for clipboard paste operations (keyboard shortcuts and menu actions),
	 * not for file drops or programmatic `putExternalContent` calls.
	 *
	 * @example
	 * ```tsx
	 * // Block pasting of image files
	 * onBeforePasteFromClipboard({ content }) {
	 *     if (content.type === 'files') {
	 *         const nonImages = content.files.filter(f => !f.type.startsWith('image/'))
	 *         if (nonImages.length === 0) return false
	 *         return { ...content, files: nonImages }
	 *     }
	 * }
	 * ```
	 */
	onBeforePasteFromClipboard?(info: {
		editor: Editor
		content: TLExternalContent<unknown>
		source: 'native-event' | 'clipboard-read'
		point?: VecLike
	}): Awaitable<TLExternalContent<unknown> | false | void>
	/**
	 * Called first for keyboard and menu paste, **before** tldraw handles or parses clipboard data
	 * (and before {@link TldrawOptions.onBeforePasteFromClipboard}).
	 *
	 * Return `false` to cancel tldraw's default paste handling for this gesture (same convention as
	 * {@link TldrawOptions.onBeforePasteFromClipboard}). Use this when you handle paste yourself from
	 * raw clipboard data, or to block the gesture entirely. Return `void` (or `undefined`) to continue.
	 */
	onClipboardPasteRaw?(info: TLClipboardPasteRawInfo): false | void
	/**
	 * Called when content is dropped on the canvas. Provides the page position
	 * where the drop occurred and the underlying drag event object.
	 * Return true to prevent default drop handling (files, URLs, etc.)
	 */
	experimental__onDropOnCanvas?(options: {
		point: VecLike
		event: React.DragEvent<Element>
	}): boolean
}

/** @public */
export const defaultTldrawOptions = {
	maxShapesPerPage: 4000,
	maxFilesAtOnce: 100,
	maxPages: 40,
	animationMediumMs: 320,
	followChaseViewportSnap: 2,
	doubleClickDurationMs: 450,
	multiClickDurationMs: 200,
	coarseDragDistanceSquared: 36, // 6 squared
	dragDistanceSquared: 16, // 4 squared
	uiDragDistanceSquared: 16, // 4 squared
	// it's really easy to accidentally drag from the toolbar on mobile, so we use a much larger
	// threshold than usual here to try and prevent accidental drags.
	uiCoarseDragDistanceSquared: 625, // 25 squared
	defaultSvgPadding: 32,
	cameraSlideFriction: 0.09,
	gridSteps: [
		{ min: -1, mid: 0.15, step: 64 },
		{ min: 0.05, mid: 0.375, step: 16 },
		{ min: 0.15, mid: 1, step: 4 },
		{ min: 0.7, mid: 2.5, step: 1 },
	],
	collaboratorInactiveTimeoutMs: 60000,
	collaboratorIdleTimeoutMs: 3000,
	collaboratorCheckIntervalMs: 1200,
	cameraMovingTimeoutMs: 64,
	hitTestMargin: 8,
	edgeScrollDelay: 200,
	edgeScrollEaseDuration: 200,
	edgeScrollSpeed: 25,
	edgeScrollDistance: 8,
	coarsePointerWidth: 12,
	coarseHandleRadius: 20,
	handleRadius: 12,
	longPressDurationMs: 500,
	textShadowLod: 0.35,
	adjacentShapeMargin: 10,
	flattenImageBoundsExpand: 64,
	flattenImageBoundsPadding: 16,
	laserDelayMs: 1200,
	laserFadeoutMs: 500,
	maxExportDelayMs: 5000,
	tooltipDelayMs: 700,
	temporaryAssetPreviewLifetimeMs: 180000,
	actionShortcutsLocation: 'swap',
	createTextOnCanvasDoubleClick: true,
	exportProvider: Fragment,
	enableToolbarKeyboardShortcuts: true,
	maxFontsToLoadBeforeRender: Infinity,
	nonce: undefined,
	debouncedZoom: true,
	debouncedZoomThreshold: 500,
	spacebarPanning: true,
	rightClickPanning: true,
	zoomToFitPadding: 128,
	snapThreshold: 8,
	camera: DEFAULT_CAMERA_OPTIONS,
	text: {},
	deepLinks: undefined,
	quickZoomPreservesScreenBounds: true,
	onBeforeCopyToClipboard: undefined,
	onBeforePasteFromClipboard: undefined,
	onClipboardPasteRaw: undefined,
	experimental__onDropOnCanvas: undefined,
} as const satisfies TldrawOptions
