import {
  createVideoFiltergraph,
  type FilterNode,
  type VideoFilterStage,
} from './filtergraph-builder';
import type { ExportPreset } from './convert';
import { buildConvertPlan } from './convert';
import type { SourceReference } from './command-utils';
import type { FFmpegCommandPlan } from './command-plan';

export interface CropSettings {
  readonly x: number;
  readonly y: number;
  readonly width: number;
  readonly height: number;
  readonly sourceWidth: number;
  readonly sourceHeight: number;
  readonly requireEvenDimensions?: boolean;
}

export type ResizeMode = 'fit' | 'contain' | 'cover' | 'stretch';

export interface ResizeSettings {
  readonly sourceWidth: number;
  readonly sourceHeight: number;
  readonly width?: number;
  readonly height?: number;
  readonly mode: ResizeMode;
  readonly noUpscaling?: boolean;
  readonly algorithm?: 'fast_bilinear' | 'bilinear' | 'bicubic' | 'lanczos';
  readonly paddingColor?: string;
}

export interface TransformSettings {
  readonly crop?: CropSettings;
  readonly resize?: ResizeSettings;
  readonly rotation?: 0 | 90 | 180 | 270;
  readonly frameRate?: number;
  readonly pixelFormat?: 'yuv420p' | 'yuv422p' | 'yuv444p' | 'rgba';
  readonly subtitleFilterExpression?: string;
  readonly fadeNodes?: readonly FilterNode<VideoFilterStage>[];
}

export interface Dimensions {
  readonly width: number;
  readonly height: number;
}

export interface TransformPlanOptions {
  readonly jobId: string;
  readonly source: SourceReference;
  readonly preset: ExportPreset;
  readonly settings: TransformSettings;
  readonly expectedDurationSeconds?: number;
}

export function buildTransformPlan(
  options: TransformPlanOptions
): FFmpegCommandPlan {
  const filtergraph = buildTransformFiltergraph(options.settings);
  if (!filtergraph.graph) {
    throw new TypeError(
      'At least one crop, resize, rotation, frame-rate, or format transform is required'
    );
  }
  return buildConvertPlan({
    jobId: options.jobId,
    source: options.source,
    preset: options.preset,
    operation: 'transform',
    videoFiltergraph: filtergraph.graph,
    additionalRequirements: filtergraph.requirements,
    ...(options.expectedDurationSeconds !== undefined
      ? { expectedDurationSeconds: options.expectedDurationSeconds }
      : {}),
  });
}

export function validateCrop(settings: CropSettings): CropSettings {
  const fields = [
    settings.x,
    settings.y,
    settings.width,
    settings.height,
    settings.sourceWidth,
    settings.sourceHeight,
  ];
  if (fields.some((value) => !Number.isSafeInteger(value))) {
    throw new RangeError('Crop coordinates and dimensions must be integers');
  }
  if (
    settings.x < 0 ||
    settings.y < 0 ||
    settings.width <= 0 ||
    settings.height <= 0 ||
    settings.x + settings.width > settings.sourceWidth ||
    settings.y + settings.height > settings.sourceHeight
  ) {
    throw new RangeError('Crop rectangle must be inside the source dimensions');
  }
  if (!settings.requireEvenDimensions) {
    return Object.freeze({ ...settings });
  }
  const width = settings.width - (settings.width % 2);
  const height = settings.height - (settings.height % 2);
  const x = settings.x - (settings.x % 2);
  const y = settings.y - (settings.y % 2);
  if (width < 2 || height < 2) {
    throw new RangeError('Even crop dimensions must be at least 2 × 2');
  }
  return Object.freeze({ ...settings, x, y, width, height });
}

export function calculateResize(settings: ResizeSettings): Dimensions {
  const { sourceWidth, sourceHeight } = settings;
  if (
    !Number.isFinite(sourceWidth) ||
    !Number.isFinite(sourceHeight) ||
    sourceWidth <= 0 ||
    sourceHeight <= 0
  ) {
    throw new RangeError('Source dimensions must be positive');
  }
  if (settings.width === undefined && settings.height === undefined) {
    throw new RangeError('At least one output dimension is required');
  }
  for (const dimension of [settings.width, settings.height]) {
    if (
      dimension !== undefined &&
      (!Number.isSafeInteger(dimension) || dimension < 1 || dimension > 8192)
    ) {
      throw new RangeError(
        'Requested output dimensions must be integers from 1 to 8192'
      );
    }
  }
  const requestedWidth =
    settings.width ??
    (sourceWidth * (settings.height as number)) / sourceHeight;
  const requestedHeight =
    settings.height ??
    (sourceHeight * (settings.width as number)) / sourceWidth;
  if (
    !Number.isFinite(requestedWidth) ||
    !Number.isFinite(requestedHeight) ||
    requestedWidth < 1 ||
    requestedHeight < 1 ||
    requestedWidth > 8192 ||
    requestedHeight > 8192
  ) {
    throw new RangeError('Output dimensions must be between 1 and 8192');
  }
  if (settings.mode === 'stretch') {
    return {
      width: even(requestedWidth),
      height: even(requestedHeight),
    };
  }
  const scale =
    settings.mode === 'cover'
      ? Math.max(requestedWidth / sourceWidth, requestedHeight / sourceHeight)
      : Math.min(requestedWidth / sourceWidth, requestedHeight / sourceHeight);
  const boundedScale = settings.noUpscaling ? Math.min(1, scale) : scale;
  if (
    settings.mode === 'cover' &&
    settings.noUpscaling &&
    boundedScale < scale
  ) {
    throw new RangeError(
      'Cover resize cannot fill the requested dimensions without upscaling'
    );
  }
  return {
    width: even(sourceWidth * boundedScale),
    height: even(sourceHeight * boundedScale),
  };
}

export function buildTransformFiltergraph(settings: TransformSettings): {
  readonly graph?: string;
  readonly requirements: ReturnType<
    ReturnType<typeof createVideoFiltergraph>['build']
  >['requirements'];
} {
  const builder = createVideoFiltergraph();
  if (settings.crop) {
    const crop = validateCrop(settings.crop);
    builder.add({
      id: 'crop',
      stage: 'crop',
      expression: `crop=${crop.width}:${crop.height}:${crop.x}:${crop.y}`,
      requiredCapabilities: { filters: ['crop'] },
    });
  }
  if (settings.rotation) {
    const expression =
      settings.rotation === 90
        ? 'transpose=clock'
        : settings.rotation === 270
          ? 'transpose=cclock'
          : 'hflip,vflip';
    builder.add({
      id: 'rotation',
      stage: 'rotate',
      expression,
      requiredCapabilities: {
        filters: settings.rotation === 180 ? ['hflip', 'vflip'] : ['transpose'],
      },
    });
  }
  if (settings.resize) {
    if (
      settings.resize.paddingColor !== undefined &&
      !/^(?:#[0-9a-f]{6}|[a-z]{3,20})$/iu.test(settings.resize.paddingColor)
    ) {
      throw new TypeError(
        'Resize padding colour must be a named colour or #RRGGBB'
      );
    }
    const size = calculateResize(settings.resize);
    const flags = settings.resize.algorithm ?? 'lanczos';
    let expression = `scale=${size.width}:${size.height}:flags=${flags}`;
    if (
      settings.resize.mode === 'contain' &&
      settings.resize.width &&
      settings.resize.height
    ) {
      expression += `,pad=${settings.resize.width}:${settings.resize.height}:(ow-iw)/2:(oh-ih)/2:${settings.resize.paddingColor ?? 'black'}`;
    } else if (
      settings.resize.mode === 'cover' &&
      settings.resize.width &&
      settings.resize.height
    ) {
      expression += `,crop=${settings.resize.width}:${settings.resize.height}`;
    }
    builder.add({
      id: 'resize',
      stage: 'resize',
      expression,
      requiredCapabilities: {
        filters:
          settings.resize.mode === 'contain'
            ? ['scale', 'pad']
            : settings.resize.mode === 'cover'
              ? ['scale', 'crop']
              : ['scale'],
      },
    });
  }
  if (settings.frameRate !== undefined) {
    if (
      !Number.isFinite(settings.frameRate) ||
      settings.frameRate <= 0 ||
      settings.frameRate > 240
    ) {
      throw new RangeError('Frame rate must be between 0 and 240');
    }
    builder.add({
      id: 'frame-rate',
      stage: 'frame-rate',
      expression: `fps=${settings.frameRate}`,
      requiredCapabilities: { filters: ['fps'] },
    });
  }
  if (settings.pixelFormat) {
    builder.add({
      id: 'pixel-format',
      stage: 'format',
      expression: `format=${settings.pixelFormat}`,
      requiredCapabilities: { filters: ['format'] },
    });
  }
  if (settings.subtitleFilterExpression) {
    builder.add({
      id: 'subtitles',
      stage: 'subtitles',
      expression: settings.subtitleFilterExpression,
      requiredCapabilities: { filters: ['subtitles'] },
    });
  }
  for (const node of settings.fadeNodes ?? []) {
    builder.add(node);
  }
  return builder.build();
}

function even(value: number): number {
  const rounded = Math.max(2, Math.round(value));
  return rounded - (rounded % 2);
}
