export const DEFAULT_MINIMUM_CLIP_DURATION_SECONDS = 0.001;
export const TIME_COMPARISON_EPSILON_SECONDS = 1e-6;

export type SourceRangeIssueCode =
  | 'invalid-source-in'
  | 'invalid-source-out'
  | 'source-out-before-in'
  | 'clip-too-short'
  | 'source-out-after-duration';

export interface SourceRangeIssue {
  code: SourceRangeIssueCode;
  message: string;
}

export interface SourceRangeValidation {
  valid: boolean;
  durationSeconds?: number;
  issues: SourceRangeIssue[];
}

export function isValidDuration(value: unknown): value is number {
  return typeof value === 'number' && Number.isFinite(value) && value >= 0;
}

export function roundSeconds(seconds: number, decimalPlaces = 6): number {
  if (!Number.isFinite(seconds)) {
    throw new RangeError('Seconds must be finite.');
  }
  if (
    !Number.isInteger(decimalPlaces) ||
    decimalPlaces < 0 ||
    decimalPlaces > 9
  ) {
    throw new RangeError('Decimal places must be an integer from 0 to 9.');
  }

  const factor = 10 ** decimalPlaces;
  const rounded =
    Math.round((seconds + Math.sign(seconds) * Number.EPSILON) * factor) /
    factor;
  return Object.is(rounded, -0) ? 0 : rounded;
}

export function addDurations(
  durations: readonly number[],
  decimalPlaces = 6
): number {
  let total = 0;
  let compensation = 0;

  for (const duration of durations) {
    if (!isValidDuration(duration)) {
      throw new RangeError('Every duration must be finite and non-negative.');
    }
    const adjusted = duration - compensation;
    const next = total + adjusted;
    compensation = next - total - adjusted;
    total = next;
  }

  return roundSeconds(total, decimalPlaces);
}

export function subtractDurations(
  endSeconds: number,
  startSeconds: number,
  decimalPlaces = 6
): number {
  if (!isValidDuration(startSeconds) || !isValidDuration(endSeconds)) {
    throw new RangeError(
      'Start and end times must be finite and non-negative.'
    );
  }
  return roundSeconds(endSeconds - startSeconds, decimalPlaces);
}

export function clampTime(seconds: number, durationSeconds: number): number {
  if (!Number.isFinite(seconds) || !isValidDuration(durationSeconds)) {
    throw new RangeError('Time and duration must be finite.');
  }
  return Math.min(Math.max(seconds, 0), durationSeconds);
}

export function validateSourceRange(
  sourceInSeconds: unknown,
  sourceOutSeconds: unknown,
  sourceDurationSeconds?: unknown,
  minimumDurationSeconds = DEFAULT_MINIMUM_CLIP_DURATION_SECONDS
): SourceRangeValidation {
  const issues: SourceRangeIssue[] = [];
  const sourceIn = isValidDuration(sourceInSeconds)
    ? sourceInSeconds
    : undefined;
  const sourceOut = isValidDuration(sourceOutSeconds)
    ? sourceOutSeconds
    : undefined;

  if (sourceIn === undefined) {
    issues.push({
      code: 'invalid-source-in',
      message: 'The source in-point must be finite and non-negative.',
    });
  }
  if (sourceOut === undefined) {
    issues.push({
      code: 'invalid-source-out',
      message: 'The source out-point must be finite and non-negative.',
    });
  }
  if (!Number.isFinite(minimumDurationSeconds) || minimumDurationSeconds <= 0) {
    throw new RangeError('Minimum duration must be finite and positive.');
  }

  let durationSeconds: number | undefined;
  if (sourceIn !== undefined && sourceOut !== undefined) {
    if (sourceOut < sourceIn) {
      issues.push({
        code: 'source-out-before-in',
        message: 'The source out-point must not be before the in-point.',
      });
    } else {
      durationSeconds = subtractDurations(sourceOut, sourceIn);
      if (
        durationSeconds + TIME_COMPARISON_EPSILON_SECONDS <
        minimumDurationSeconds
      ) {
        issues.push({
          code: 'clip-too-short',
          message: `The clip must be at least ${minimumDurationSeconds} seconds long.`,
        });
      }
    }
  }

  if (
    sourceOut !== undefined &&
    isValidDuration(sourceDurationSeconds) &&
    sourceOut > sourceDurationSeconds + TIME_COMPARISON_EPSILON_SECONDS
  ) {
    issues.push({
      code: 'source-out-after-duration',
      message: 'The source out-point exceeds the source duration.',
    });
  }

  return {
    valid: issues.length === 0,
    ...(durationSeconds === undefined ? {} : { durationSeconds }),
    issues,
  };
}
