import { createMotionGPUError } from './error-report.js';

/** Storage access modes described by the WebGPU texture capability table. */
export type MotionGPUStorageTextureAccess = 'write-only' | 'read-only' | 'read-write';

/** Device-aware capabilities for one WebGPU texture format. */
export interface TextureFormatCapabilities {
	format: GPUTextureFormat;
	supported: boolean;
	renderable: boolean;
	colorRenderable: boolean;
	sampleType: GPUTextureSampleType | null;
	filterable: boolean;
	storageAccess: readonly MotionGPUStorageTextureAccess[];
	requiredFeatures: Readonly<{
		format?: GPUFeatureName;
		renderable?: GPUFeatureName;
		filterable?: GPUFeatureName;
		storageAccess?: Readonly<Partial<Record<MotionGPUStorageTextureAccess, GPUFeatureName>>>;
	}>;
}

export interface FormatCapabilityDiagnostic {
	target: string;
	format: unknown;
	pass: string;
	capability: string;
	detail?: string;
}

/** Texture formats recognized by the WebGPU typings supported in this release. */
export const TEXTURE_FORMATS: ReadonlySet<string> = new Set([
	'r8unorm',
	'r8snorm',
	'r8uint',
	'r8sint',
	'r16unorm',
	'r16snorm',
	'r16uint',
	'r16sint',
	'r16float',
	'rg8unorm',
	'rg8snorm',
	'rg8uint',
	'rg8sint',
	'r32uint',
	'r32sint',
	'r32float',
	'rg16unorm',
	'rg16snorm',
	'rg16uint',
	'rg16sint',
	'rg16float',
	'rgba8unorm',
	'rgba8unorm-srgb',
	'rgba8snorm',
	'rgba8uint',
	'rgba8sint',
	'bgra8unorm',
	'bgra8unorm-srgb',
	'rgb9e5ufloat',
	'rgb10a2uint',
	'rgb10a2unorm',
	'rg11b10ufloat',
	'rg32uint',
	'rg32sint',
	'rg32float',
	'rgba16unorm',
	'rgba16snorm',
	'rgba16uint',
	'rgba16sint',
	'rgba16float',
	'rgba32uint',
	'rgba32sint',
	'rgba32float',
	'stencil8',
	'depth16unorm',
	'depth24plus',
	'depth24plus-stencil8',
	'depth32float',
	'depth32float-stencil8',
	'bc1-rgba-unorm',
	'bc1-rgba-unorm-srgb',
	'bc2-rgba-unorm',
	'bc2-rgba-unorm-srgb',
	'bc3-rgba-unorm',
	'bc3-rgba-unorm-srgb',
	'bc4-r-unorm',
	'bc4-r-snorm',
	'bc5-rg-unorm',
	'bc5-rg-snorm',
	'bc6h-rgb-ufloat',
	'bc6h-rgb-float',
	'bc7-rgba-unorm',
	'bc7-rgba-unorm-srgb',
	'etc2-rgb8unorm',
	'etc2-rgb8unorm-srgb',
	'etc2-rgb8a1unorm',
	'etc2-rgb8a1unorm-srgb',
	'etc2-rgba8unorm',
	'etc2-rgba8unorm-srgb',
	'eac-r11unorm',
	'eac-r11snorm',
	'eac-rg11unorm',
	'eac-rg11snorm',
	'astc-4x4-unorm',
	'astc-4x4-unorm-srgb',
	'astc-5x4-unorm',
	'astc-5x4-unorm-srgb',
	'astc-5x5-unorm',
	'astc-5x5-unorm-srgb',
	'astc-6x5-unorm',
	'astc-6x5-unorm-srgb',
	'astc-6x6-unorm',
	'astc-6x6-unorm-srgb',
	'astc-8x5-unorm',
	'astc-8x5-unorm-srgb',
	'astc-8x6-unorm',
	'astc-8x6-unorm-srgb',
	'astc-8x8-unorm',
	'astc-8x8-unorm-srgb',
	'astc-10x5-unorm',
	'astc-10x5-unorm-srgb',
	'astc-10x6-unorm',
	'astc-10x6-unorm-srgb',
	'astc-10x8-unorm',
	'astc-10x8-unorm-srgb',
	'astc-10x10-unorm',
	'astc-10x10-unorm-srgb',
	'astc-12x10-unorm',
	'astc-12x10-unorm-srgb',
	'astc-12x12-unorm',
	'astc-12x12-unorm-srgb'
]);

const COLOR_RENDERABLE_FORMATS: ReadonlySet<GPUTextureFormat> = new Set([
	'r8unorm',
	'r8snorm',
	'r8uint',
	'r8sint',
	'r16unorm',
	'r16snorm',
	'r16uint',
	'r16sint',
	'r16float',
	'rg8unorm',
	'rg8snorm',
	'rg8uint',
	'rg8sint',
	'r32uint',
	'r32sint',
	'r32float',
	'rg16unorm',
	'rg16snorm',
	'rg16uint',
	'rg16sint',
	'rg16float',
	'rgba8unorm',
	'rgba8unorm-srgb',
	'rgba8snorm',
	'rgba8uint',
	'rgba8sint',
	'bgra8unorm',
	'bgra8unorm-srgb',
	'rgb10a2uint',
	'rgb10a2unorm',
	'rg11b10ufloat',
	'rg32uint',
	'rg32sint',
	'rg32float',
	'rgba16unorm',
	'rgba16snorm',
	'rgba16uint',
	'rgba16sint',
	'rgba16float',
	'rgba32uint',
	'rgba32sint',
	'rgba32float'
]);

const FORMAT_REQUIRED_FEATURE: Readonly<Partial<Record<GPUTextureFormat, GPUFeatureName>>> =
	Object.freeze({
		r16unorm: 'texture-formats-tier1',
		r16snorm: 'texture-formats-tier1',
		rg16unorm: 'texture-formats-tier1',
		rg16snorm: 'texture-formats-tier1',
		rgba16unorm: 'texture-formats-tier1',
		rgba16snorm: 'texture-formats-tier1',
		'bgra8unorm-srgb': 'core-features-and-limits',
		'depth32float-stencil8': 'depth32float-stencil8',
		'bc1-rgba-unorm': 'texture-compression-bc',
		'bc1-rgba-unorm-srgb': 'texture-compression-bc',
		'bc2-rgba-unorm': 'texture-compression-bc',
		'bc2-rgba-unorm-srgb': 'texture-compression-bc',
		'bc3-rgba-unorm': 'texture-compression-bc',
		'bc3-rgba-unorm-srgb': 'texture-compression-bc',
		'bc4-r-unorm': 'texture-compression-bc',
		'bc4-r-snorm': 'texture-compression-bc',
		'bc5-rg-unorm': 'texture-compression-bc',
		'bc5-rg-snorm': 'texture-compression-bc',
		'bc6h-rgb-ufloat': 'texture-compression-bc',
		'bc6h-rgb-float': 'texture-compression-bc',
		'bc7-rgba-unorm': 'texture-compression-bc',
		'bc7-rgba-unorm-srgb': 'texture-compression-bc',
		'etc2-rgb8unorm': 'texture-compression-etc2',
		'etc2-rgb8unorm-srgb': 'texture-compression-etc2',
		'etc2-rgb8a1unorm': 'texture-compression-etc2',
		'etc2-rgb8a1unorm-srgb': 'texture-compression-etc2',
		'etc2-rgba8unorm': 'texture-compression-etc2',
		'etc2-rgba8unorm-srgb': 'texture-compression-etc2',
		'eac-r11unorm': 'texture-compression-etc2',
		'eac-r11snorm': 'texture-compression-etc2',
		'eac-rg11unorm': 'texture-compression-etc2',
		'eac-rg11snorm': 'texture-compression-etc2',
		'astc-4x4-unorm': 'texture-compression-astc',
		'astc-4x4-unorm-srgb': 'texture-compression-astc',
		'astc-5x4-unorm': 'texture-compression-astc',
		'astc-5x4-unorm-srgb': 'texture-compression-astc',
		'astc-5x5-unorm': 'texture-compression-astc',
		'astc-5x5-unorm-srgb': 'texture-compression-astc',
		'astc-6x5-unorm': 'texture-compression-astc',
		'astc-6x5-unorm-srgb': 'texture-compression-astc',
		'astc-6x6-unorm': 'texture-compression-astc',
		'astc-6x6-unorm-srgb': 'texture-compression-astc',
		'astc-8x5-unorm': 'texture-compression-astc',
		'astc-8x5-unorm-srgb': 'texture-compression-astc',
		'astc-8x6-unorm': 'texture-compression-astc',
		'astc-8x6-unorm-srgb': 'texture-compression-astc',
		'astc-8x8-unorm': 'texture-compression-astc',
		'astc-8x8-unorm-srgb': 'texture-compression-astc',
		'astc-10x5-unorm': 'texture-compression-astc',
		'astc-10x5-unorm-srgb': 'texture-compression-astc',
		'astc-10x6-unorm': 'texture-compression-astc',
		'astc-10x6-unorm-srgb': 'texture-compression-astc',
		'astc-10x8-unorm': 'texture-compression-astc',
		'astc-10x8-unorm-srgb': 'texture-compression-astc',
		'astc-10x10-unorm': 'texture-compression-astc',
		'astc-10x10-unorm-srgb': 'texture-compression-astc',
		'astc-12x10-unorm': 'texture-compression-astc',
		'astc-12x10-unorm-srgb': 'texture-compression-astc',
		'astc-12x12-unorm': 'texture-compression-astc',
		'astc-12x12-unorm-srgb': 'texture-compression-astc'
	});

const RENDER_REQUIRED_FEATURE: Readonly<Partial<Record<GPUTextureFormat, GPUFeatureName>>> =
	Object.freeze({
		r8snorm: 'texture-formats-tier1',
		rg8snorm: 'texture-formats-tier1',
		rgba8snorm: 'texture-formats-tier1',
		rg11b10ufloat: 'rg11b10ufloat-renderable'
	});

const STORAGE_FORMATS: ReadonlySet<GPUTextureFormat> = new Set([
	'r8unorm',
	'r8snorm',
	'r8uint',
	'r8sint',
	'r16unorm',
	'r16snorm',
	'r16uint',
	'r16sint',
	'r16float',
	'rg8unorm',
	'rg8snorm',
	'rg8uint',
	'rg8sint',
	'rg16unorm',
	'rg16snorm',
	'rg16uint',
	'rg16sint',
	'rg16float',
	'rgba8unorm',
	'rgba8snorm',
	'rgba8uint',
	'rgba8sint',
	'rgba16unorm',
	'rgba16snorm',
	'rgba16float',
	'rgba16uint',
	'rgba16sint',
	'r32float',
	'r32sint',
	'r32uint',
	'rg32float',
	'rg32sint',
	'rg32uint',
	'rgba32float',
	'rgba32uint',
	'rgba32sint',
	'bgra8unorm',
	'rgb10a2uint',
	'rgb10a2unorm',
	'rg11b10ufloat'
]);

const EMPTY_STORAGE_ACCESS = Object.freeze([]) as readonly MotionGPUStorageTextureAccess[];

const STORAGE_WRITE_REQUIRED_TIER1: ReadonlySet<GPUTextureFormat> = new Set([
	'r8unorm',
	'r8snorm',
	'r8uint',
	'r8sint',
	'r16uint',
	'r16sint',
	'r16float',
	'rg8unorm',
	'rg8snorm',
	'rg8uint',
	'rg8sint',
	'rg16uint',
	'rg16sint',
	'rg16float',
	'rgb10a2uint',
	'rgb10a2unorm',
	'rg11b10ufloat'
]);

const STORAGE_WRITE_REQUIRED_CORE: ReadonlySet<GPUTextureFormat> = new Set([
	'rg32uint',
	'rg32sint',
	'rg32float'
]);

const STORAGE_READ_WRITE_FORMATS: ReadonlySet<GPUTextureFormat> = new Set([
	'r8unorm',
	'r8uint',
	'r8sint',
	'r16uint',
	'r16sint',
	'r16float',
	'rgba8unorm',
	'rgba8uint',
	'rgba8sint',
	'rgba16uint',
	'rgba16sint',
	'rgba16float',
	'r32uint',
	'r32sint',
	'r32float',
	'rgba32uint',
	'rgba32sint',
	'rgba32float'
]);

const STORAGE_READ_WRITE_BASELINE: ReadonlySet<GPUTextureFormat> = new Set([
	'r32uint',
	'r32sint',
	'r32float'
]);

function hasFeature(features: ReadonlySet<string> | undefined, feature: GPUFeatureName): boolean {
	if (features?.has(feature) === true) return true;
	if (feature === 'texture-formats-tier1' && features?.has('texture-formats-tier2')) return true;
	if (
		feature === 'rg11b10ufloat-renderable' &&
		(features?.has('texture-formats-tier1') || features?.has('texture-formats-tier2'))
	) {
		return true;
	}
	return false;
}

/** Rejects texture format strings outside the WebGPU contract known to this release. */
export function assertTextureFormat(
	format: unknown,
	label = 'Texture'
): asserts format is GPUTextureFormat {
	if (typeof format !== 'string' || !TEXTURE_FORMATS.has(format)) {
		throw new Error(`${label} format "${String(format)}" is not a recognized GPUTextureFormat.`);
	}
}

/** Reports whether a texture format uses 32-bit floating-point channels. */
export function isFloat32TextureFormat(format: GPUTextureFormat): boolean {
	return format === 'r32float' || format === 'rg32float' || format === 'rgba32float';
}

/** Reports whether the format has any storage-texture access in the WebGPU capability table. */
export function isStorageTextureFormat(format: GPUTextureFormat): boolean {
	assertTextureFormat(format);
	return STORAGE_FORMATS.has(format);
}

/** Resolves the WGSL sampled scalar represented by a texture format. */
export function textureSampleScalarType(
	format: GPUTextureFormat
): 'f32' | 'u32' | 'i32' | 'depth' | null {
	if (format === 'stencil8') return 'u32';
	if (format.startsWith('depth')) return 'depth';
	if (format.endsWith('uint')) return 'u32';
	if (format.endsWith('sint')) return 'i32';
	return 'f32';
}

/**
 * Returns the effective capabilities of a texture format on the active device.
 * This is the sole format classification table used by render, sampling and compute paths.
 */
export function resolveTextureFormatCapabilities(
	format: GPUTextureFormat,
	deviceFeatures?: ReadonlySet<string>
): TextureFormatCapabilities {
	assertTextureFormat(format);
	const scalarType = textureSampleScalarType(format);
	const formatFeature = FORMAT_REQUIRED_FEATURE[format];
	const supported = !formatFeature || hasFeature(deviceFeatures, formatFeature);
	const float32Filterable =
		isFloat32TextureFormat(format) && hasFeature(deviceFeatures, 'float32-filterable');
	const isUnfilterableNormalized16 =
		format === 'r16unorm' ||
		format === 'r16snorm' ||
		format === 'rg16unorm' ||
		format === 'rg16snorm' ||
		format === 'rgba16unorm' ||
		format === 'rgba16snorm';
	const sampleType: GPUTextureSampleType | null =
		scalarType === null
			? null
			: scalarType === 'depth'
				? 'depth'
				: scalarType === 'u32'
					? 'uint'
					: scalarType === 'i32'
						? 'sint'
						: (isFloat32TextureFormat(format) && !float32Filterable) || isUnfilterableNormalized16
							? 'unfilterable-float'
							: 'float';
	const renderFeature = RENDER_REQUIRED_FEATURE[format];
	const storageFeature =
		format === 'bgra8unorm'
			? ('bgra8unorm-storage' as GPUFeatureName)
			: STORAGE_WRITE_REQUIRED_TIER1.has(format)
				? ('texture-formats-tier1' as GPUFeatureName)
				: STORAGE_WRITE_REQUIRED_CORE.has(format)
					? ('core-features-and-limits' as GPUFeatureName)
					: undefined;
	const staticallyRenderable = COLOR_RENDERABLE_FORMATS.has(format);
	const colorRenderable =
		supported &&
		staticallyRenderable &&
		(!renderFeature || hasFeature(deviceFeatures, renderFeature));
	const renderable =
		colorRenderable || (supported && (format === 'stencil8' || format.startsWith('depth')));
	const storageWritable =
		supported &&
		STORAGE_FORMATS.has(format) &&
		(!storageFeature || hasFeature(deviceFeatures, storageFeature));
	const storageReadWriteFeature = STORAGE_READ_WRITE_BASELINE.has(format)
		? undefined
		: STORAGE_READ_WRITE_FORMATS.has(format)
			? ('texture-formats-tier2' as GPUFeatureName)
			: undefined;
	const storageReadWrite =
		storageWritable &&
		STORAGE_READ_WRITE_FORMATS.has(format) &&
		(!storageReadWriteFeature || hasFeature(deviceFeatures, storageReadWriteFeature));
	const storageAccess: MotionGPUStorageTextureAccess[] = [];
	if (storageWritable) {
		storageAccess.push('write-only');
		if (format !== 'bgra8unorm') storageAccess.push('read-only');
		if (storageReadWrite) storageAccess.push('read-write');
	}
	const filterable =
		supported && (sampleType === 'float' || (isFloat32TextureFormat(format) && float32Filterable));

	return {
		format,
		supported,
		renderable,
		colorRenderable,
		sampleType,
		filterable,
		storageAccess:
			storageAccess.length > 0
				? (Object.freeze(storageAccess) as readonly MotionGPUStorageTextureAccess[])
				: EMPTY_STORAGE_ACCESS,
		requiredFeatures: Object.freeze({
			...(formatFeature ? { format: formatFeature } : {}),
			...(renderFeature ? { renderable: renderFeature } : {}),
			...(isFloat32TextureFormat(format)
				? { filterable: 'float32-filterable' as GPUFeatureName }
				: {}),
			...(storageFeature || storageReadWriteFeature
				? {
						storageAccess: Object.freeze({
							...(storageFeature
								? {
										'write-only': storageFeature,
										...(format === 'bgra8unorm' ? {} : { 'read-only': storageFeature })
									}
								: {}),
							...(storageReadWriteFeature ? { 'read-write': storageReadWriteFeature } : {})
						})
					}
				: {})
		})
	};
}

/** Creates a stable classified error for an unmet format capability. */
export function createFormatCapabilityError(input: FormatCapabilityDiagnostic): Error {
	return createMotionGPUError(
		'FORMAT_CAPABILITY_MISSING',
		`${input.pass} target "${input.target}" uses format "${String(input.format)}" but is missing capability "${input.capability}".${input.detail ? ` ${input.detail}` : ''}`
	);
}

/** Validates a format used as a MotionGPU color render target. */
export function assertRenderableFormat(input: {
	format: unknown;
	target: string;
	pass: string;
	deviceFeatures?: ReadonlySet<string>;
}): TextureFormatCapabilities {
	try {
		assertTextureFormat(input.format);
	} catch {
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: 'recognized GPUTextureFormat'
		});
	}
	const capabilities = resolveTextureFormatCapabilities(input.format, input.deviceFeatures);
	if (!capabilities.colorRenderable) {
		const requiredFeature =
			capabilities.requiredFeatures.format ?? capabilities.requiredFeatures.renderable;
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: requiredFeature ?? 'color render attachment',
			...(requiredFeature
				? { detail: `Enable device feature "${requiredFeature}".` }
				: { detail: 'Use a color-renderable texture format.' })
		});
	}
	return capabilities;
}

/** Validates a built-in pass input declared as `texture_2d<f32>`. */
export function assertFloatSampledFormat(input: {
	format: unknown;
	target: string;
	pass: string;
	deviceFeatures?: ReadonlySet<string>;
}): TextureFormatCapabilities {
	let capabilities: TextureFormatCapabilities;
	try {
		assertTextureFormat(input.format);
		capabilities = resolveTextureFormatCapabilities(input.format, input.deviceFeatures);
	} catch {
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: 'recognized GPUTextureFormat'
		});
	}
	if (!capabilities.supported) {
		const requiredFeature = capabilities.requiredFeatures.format!;
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: requiredFeature,
			detail: `Enable device feature "${requiredFeature}".`
		});
	}
	if (capabilities.sampleType !== 'float' && capabilities.sampleType !== 'unfilterable-float') {
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: 'float texture sampling',
			detail:
				'This built-in pass declares texture_2d<f32>; use a float-sampled format or a custom typed pass.'
		});
	}
	return capabilities;
}

/** Validates a built-in pass output written by a `vec4f` fragment result. */
export function assertFloatRenderableFormat(input: {
	format: unknown;
	target: string;
	pass: string;
	deviceFeatures?: ReadonlySet<string>;
}): TextureFormatCapabilities {
	const capabilities = assertRenderableFormat(input);
	if (capabilities.sampleType !== 'float' && capabilities.sampleType !== 'unfilterable-float') {
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: 'float color render attachment',
			detail:
				'This built-in pass writes vec4f; use a float/unorm/snorm format or a custom typed pass.'
		});
	}
	return capabilities;
}

/** Validates a storage texture access against the active device. */
export function assertStorageTextureAccess(input: {
	format: unknown;
	target: string;
	pass: string;
	access: MotionGPUStorageTextureAccess;
	deviceFeatures?: ReadonlySet<string>;
}): TextureFormatCapabilities {
	let capabilities: TextureFormatCapabilities;
	try {
		assertTextureFormat(input.format);
		capabilities = resolveTextureFormatCapabilities(input.format, input.deviceFeatures);
	} catch {
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: 'recognized GPUTextureFormat'
		});
	}
	if (!capabilities.storageAccess.includes(input.access)) {
		const requiredFeature =
			capabilities.requiredFeatures.format ??
			capabilities.requiredFeatures.storageAccess?.[input.access];
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: requiredFeature ?? `storage texture ${input.access}`,
			...(requiredFeature
				? { detail: `Enable device feature "${requiredFeature}".` }
				: { detail: `The format does not support ${input.access} storage access.` })
		});
	}
	return capabilities;
}

/** Validates that a device supports the format itself, independent of a specific usage. */
export function assertTextureFormatSupported(input: {
	format: unknown;
	target: string;
	pass: string;
	deviceFeatures?: ReadonlySet<string>;
}): TextureFormatCapabilities {
	let capabilities: TextureFormatCapabilities;
	try {
		assertTextureFormat(input.format);
		capabilities = resolveTextureFormatCapabilities(input.format, input.deviceFeatures);
	} catch {
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: 'recognized GPUTextureFormat'
		});
	}
	if (!capabilities.supported) {
		const requiredFeature = capabilities.requiredFeatures.format!;
		throw createFormatCapabilityError({
			target: input.target,
			format: input.format,
			pass: input.pass,
			capability: requiredFeature,
			detail: `Enable device feature "${requiredFeature}".`
		});
	}
	return capabilities;
}
