import {
  aggregatePcmPeaks,
  validateWaveformPeakData,
} from './peak-aggregation';
import {
  WaveformCancellationError,
  WaveformDataError,
  WaveformRuntimeBusyError,
  WaveformRuntimeTerminatedError,
} from './waveform-errors';
import type {
  WaveformAggregateOptions,
  WaveformPcmInput,
  WaveformPeakData,
  WaveformRuntimeOptions,
  WaveformWorkerAggregateRequest,
  WaveformWorkerFactory,
  WaveformWorkerLike,
  WaveformWorkerResponse,
} from './waveform.types';

interface ActiveWorkerTask {
  readonly requestId: number;
  readonly worker: WaveformWorkerLike;
  readonly reject: (reason: unknown) => void;
  readonly removeListeners: () => void;
  readonly removeAbortListener: () => void;
}

interface ActiveSynchronousTask {
  readonly requestId: number;
  cancelled: boolean;
  readonly reject: (reason: unknown) => void;
  readonly removeAbortListener: () => void;
}

/**
 * Runs at most one aggregation at a time. Worker failure during construction
 * falls back to the synchronous implementation; a worker-reported data error
 * is surfaced rather than repeated on the main thread.
 */
export class WaveformRuntime {
  readonly #workerFactory: WaveformWorkerFactory | null;
  #nextRequestId = 1;
  #active: ActiveWorkerTask | undefined;
  #activeSynchronous: ActiveSynchronousTask | undefined;
  #terminated = false;
  #workerUnavailable = false;

  constructor(options: WaveformRuntimeOptions = {}) {
    this.#workerFactory =
      options.workerFactory === undefined
        ? createDefaultWaveformWorkerFactory()
        : options.workerFactory;
  }

  get mode(): 'worker' | 'synchronous' {
    return this.#workerFactory === null || this.#workerUnavailable
      ? 'synchronous'
      : 'worker';
  }

  get isActive(): boolean {
    return this.#active !== undefined || this.#activeSynchronous !== undefined;
  }

  async aggregate(
    input: WaveformPcmInput,
    options: WaveformAggregateOptions
  ): Promise<WaveformPeakData> {
    if (this.#terminated) {
      throw new WaveformRuntimeTerminatedError();
    }
    if (this.isActive) {
      throw new WaveformRuntimeBusyError();
    }
    throwIfAborted(options.signal);

    if (this.#workerFactory === null || this.#workerUnavailable) {
      return this.#aggregateSynchronously(input, options);
    }

    let worker: WaveformWorkerLike;
    try {
      worker = this.#workerFactory();
    } catch {
      this.#workerUnavailable = true;
      return this.#aggregateSynchronously(input, options);
    }
    return this.#aggregateInWorker(worker, input, options);
  }

  cancelActive(): boolean {
    const active = this.#active;
    if (active !== undefined) {
      active.removeListeners();
      active.removeAbortListener();
      active.worker.terminate();
      this.#active = undefined;
      active.reject(new WaveformCancellationError());
      return true;
    }
    const synchronous = this.#activeSynchronous;
    if (synchronous !== undefined) {
      synchronous.cancelled = true;
      synchronous.removeAbortListener();
      this.#activeSynchronous = undefined;
      synchronous.reject(new WaveformCancellationError());
      return true;
    }
    return false;
  }

  terminate(): void {
    this.cancelActive();
    this.#terminated = true;
  }

  #aggregateSynchronously(
    input: WaveformPcmInput,
    options: WaveformAggregateOptions
  ): Promise<WaveformPeakData> {
    const requestId = this.#nextRequestId;
    this.#nextRequestId += 1;
    return new Promise<WaveformPeakData>((resolve, reject) => {
      const onAbort = (): void => {
        const active = this.#activeSynchronous;
        if (active?.requestId !== requestId) {
          return;
        }
        active.cancelled = true;
        active.removeAbortListener();
        this.#activeSynchronous = undefined;
        reject(new WaveformCancellationError());
      };
      options.signal?.addEventListener('abort', onAbort, { once: true });
      const active: ActiveSynchronousTask = {
        requestId,
        cancelled: false,
        reject,
        removeAbortListener: () => {
          options.signal?.removeEventListener('abort', onAbort);
        },
      };
      this.#activeSynchronous = active;

      queueMicrotask(() => {
        if (active.cancelled) {
          return;
        }
        try {
          throwIfAborted(options.signal);
          const peaks = aggregatePcmPeaks(input, {
            bucketCount: options.bucketCount,
            ...(options.maxSamples === undefined
              ? {}
              : { maxSamples: options.maxSamples }),
            isCancelled: () =>
              active.cancelled || options.signal?.aborted === true,
            ...(options.onProgress === undefined
              ? {}
              : { onProgress: options.onProgress }),
          });
          resolve(peaks);
        } catch (error) {
          reject(error);
        } finally {
          active.removeAbortListener();
          if (this.#activeSynchronous?.requestId === requestId) {
            this.#activeSynchronous = undefined;
          }
        }
      });
    });
  }

  #aggregateInWorker(
    worker: WaveformWorkerLike,
    input: WaveformPcmInput,
    options: WaveformAggregateOptions
  ): Promise<WaveformPeakData> {
    const requestId = this.#nextRequestId;
    this.#nextRequestId += 1;
    const ownedData = copyByteSource(input.data);

    return new Promise<WaveformPeakData>((resolve, reject) => {
      const cleanup = (): void => {
        worker.removeEventListener('message', onMessage);
        worker.removeEventListener('error', onWorkerFailure);
        worker.removeEventListener('messageerror', onWorkerFailure);
        options.signal?.removeEventListener('abort', onAbort);
        if (this.#active?.requestId === requestId) {
          this.#active = undefined;
        }
      };

      const finish = (callback: () => void, shouldTerminate = true): void => {
        cleanup();
        if (shouldTerminate) {
          worker.terminate();
        }
        callback();
      };

      const onAbort = (): void => {
        finish(() => reject(new WaveformCancellationError()));
      };

      const onWorkerFailure = (): void => {
        finish(() =>
          reject(new WaveformDataError('Waveform worker failed unexpectedly'))
        );
      };

      const onMessage = (event: MessageEvent<unknown>): void => {
        const response = parseWorkerResponse(event.data, requestId);
        if (response === undefined) {
          return;
        }
        try {
          if (response.type === 'progress') {
            options.onProgress?.(clampProgress(response.progress));
            return;
          }
          if (response.type === 'error') {
            const error = isSerializedError(response.error)
              ? createWorkerError(response.error)
              : new WaveformDataError(
                  'Waveform worker returned a malformed error'
                );
            finish(() => reject(error));
            return;
          }
          const peaks = validateWaveformPeakData(response.peaks);
          options.onProgress?.(1);
          finish(() => resolve(peaks));
        } catch (error) {
          finish(() => reject(error));
        }
      };

      worker.addEventListener('message', onMessage);
      worker.addEventListener('error', onWorkerFailure);
      worker.addEventListener('messageerror', onWorkerFailure);
      options.signal?.addEventListener('abort', onAbort, { once: true });
      this.#active = {
        requestId,
        worker,
        reject,
        removeListeners: () => {
          worker.removeEventListener('message', onMessage);
          worker.removeEventListener('error', onWorkerFailure);
          worker.removeEventListener('messageerror', onWorkerFailure);
        },
        removeAbortListener: () => {
          options.signal?.removeEventListener('abort', onAbort);
        },
      };

      const request: WaveformWorkerAggregateRequest = {
        type: 'aggregate',
        requestId,
        input: {
          container: input.container,
          data: ownedData,
          ...(input.container === 'raw'
            ? {
                encoding: input.encoding,
                sampleRate: input.sampleRate,
              }
            : {}),
        },
        bucketCount: options.bucketCount,
        ...(options.maxSamples === undefined
          ? {}
          : { maxSamples: options.maxSamples }),
      };
      try {
        worker.postMessage(request, [ownedData]);
      } catch {
        this.#workerUnavailable = true;
        cleanup();
        worker.terminate();
        void this.#aggregateSynchronously(input, options).then(resolve, reject);
      }
    });
  }
}

export function createWaveformRuntime(
  options: WaveformRuntimeOptions = {}
): WaveformRuntime {
  return new WaveformRuntime(options);
}

export function createDefaultWaveformWorkerFactory(): WaveformWorkerFactory | null {
  if (typeof Worker === 'undefined') {
    return null;
  }
  return () =>
    new Worker(new URL('./waveform.worker.ts', import.meta.url), {
      type: 'module',
      name: 'waveform-peak-aggregation',
    });
}

function parseWorkerResponse(
  value: unknown,
  requestId: number
): WaveformWorkerResponse | undefined {
  if (
    typeof value !== 'object' ||
    value === null ||
    !('type' in value) ||
    !('requestId' in value) ||
    value.requestId !== requestId
  ) {
    return undefined;
  }
  const type = value.type;
  if (type !== 'progress' && type !== 'result' && type !== 'error') {
    return undefined;
  }
  return value as WaveformWorkerResponse;
}

function createWorkerError(error: {
  readonly name: string;
  readonly message: string;
}): Error {
  const result =
    error.name === 'AbortError'
      ? new WaveformCancellationError(error.message)
      : new WaveformDataError(error.message);
  if (error.name && error.name !== 'AbortError') {
    result.name = error.name;
  }
  return result;
}

function isSerializedError(
  value: unknown
): value is { readonly name: string; readonly message: string } {
  return (
    typeof value === 'object' &&
    value !== null &&
    'name' in value &&
    typeof value.name === 'string' &&
    'message' in value &&
    typeof value.message === 'string'
  );
}

function copyByteSource(source: ArrayBuffer | ArrayBufferView): ArrayBuffer {
  const bytes =
    source instanceof ArrayBuffer
      ? new Uint8Array(source)
      : new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
  const copy = new Uint8Array(bytes.byteLength);
  copy.set(bytes);
  return copy.buffer;
}

function throwIfAborted(signal?: AbortSignal): void {
  if (signal?.aborted === true) {
    throw new WaveformCancellationError();
  }
}

function clampProgress(progress: number): number {
  return Number.isFinite(progress) ? Math.max(0, Math.min(1, progress)) : 0;
}
