import type { Track } from './Track';
import type {
  AudioProcessorOptions,
  TrackProcessor,
  VideoProcessorOptions,
} from './processor/types';

export interface TrackPublishDefaults {
  /**
   * encoding parameters for camera track
   */
  videoEncoding?: VideoEncoding;

  /**
   * Multi-codec Simulcast
   * VP9 and AV1 are not supported by all browser clients. When backupCodec is
   * set, when an incompatible client attempts to subscribe to the track, LiveKit
   * will automatically publish a secondary track encoded with the backup codec.
   *
   * You could customize specific encoding parameters of the backup track by
   * explicitly setting codec and encoding fields.
   *
   * Defaults to `true`
   */
  backupCodec?: true | false | { codec: BackupVideoCodec; encoding?: VideoEncoding };

  /**
   * encoding parameters for screen share track
   */
  screenShareEncoding?: VideoEncoding;

  /**
   * codec, defaults to vp8; for svc codecs, auto enable vp8
   * as backup. (TBD)
   */
  videoCodec?: VideoCodec;

  /**
   * which audio preset should be used for publishing (audio) tracks
   * defaults to [[AudioPresets.music]]
   */
  audioPreset?: AudioPreset;

  /**
   * dtx (Discontinuous Transmission of audio), enabled by default for mono tracks.
   */
  dtx?: boolean;

  /**
   * red (Redundant Audio Data), enabled by default for mono tracks.
   */
  red?: boolean;

  /**
   * publish track in stereo mode (or set to false to disable). defaults determined by capture channel count.
   */
  forceStereo?: boolean;

  /**
   * use simulcast, defaults to true.
   * When using simulcast, LiveKit will publish up to three versions of the stream
   * at various resolutions.
   */
  simulcast?: boolean;

  /**
   * scalability mode for svc codecs, defaults to 'L3T3_KEY'.
   * for svc codecs, simulcast is disabled.
   */
  scalabilityMode?: ScalabilityMode;

  /**
   * degradation preference
   */
  degradationPreference?: RTCDegradationPreference;

  /**
   * Up to two additional simulcast layers to publish in addition to the original
   * Track.
   * When left blank, it defaults to h180, h360.
   * If a SVC codec is used (VP9 or AV1), this field has no effect.
   *
   * To publish three total layers, you would specify:
   * {
   *   videoEncoding: {...}, // encoding of the primary layer
   *   videoSimulcastLayers: [
   *     VideoPresets.h540,
   *     VideoPresets.h216,
   *   ],
   * }
   */
  videoSimulcastLayers?: Array<VideoPreset>;

  /**
   * custom video simulcast layers for screen tracks
   * Note: the layers need to be ordered from lowest to highest quality
   */
  screenShareSimulcastLayers?: Array<VideoPreset>;

  /**
   * For local tracks, stop the underlying MediaStreamTrack when the track is muted (or paused)
   * on some platforms, this option is necessary to disable the microphone recording indicator.
   * Note: when this is enabled, and BT devices are connected, they will transition between
   * profiles (e.g. HFP to A2DP) and there will be an audible difference in playback.
   *
   * defaults to false
   */
  stopMicTrackOnMute?: boolean;
}

/**
 * Options when publishing tracks
 */
export interface TrackPublishOptions extends TrackPublishDefaults {
  /**
   * set a track name
   */
  name?: string;

  /**
   * Source of track, camera, microphone, or screen
   */
  source?: Track.Source;

  /**
   * Set stream name for the track. Audio and video tracks with the same stream name
   * will be placed in the same `MediaStream` and offer better synchronization.
   * By default, camera and microphone will be placed in a stream; as would screen_share and screen_share_audio
   */
  stream?: string;
}

export interface CreateLocalTracksOptions {
  /**
   * audio track options, true to create with defaults. false if audio shouldn't be created
   * default true
   */
  audio?: boolean | AudioCaptureOptions;

  /**
   * video track options, true to create with defaults. false if video shouldn't be created
   * default true
   */
  video?: boolean | VideoCaptureOptions;
}

export interface VideoCaptureOptions {
  /**
   * A ConstrainDOMString object specifying a device ID or an array of device
   * IDs which are acceptable and/or required.
   */
  deviceId?: ConstrainDOMString;

  /**
   * a facing or an array of facings which are acceptable and/or required.
   */
  facingMode?: 'user' | 'environment' | 'left' | 'right';

  resolution?: VideoResolution;

  /**
   * initialize the track with a given processor
   */
  processor?: TrackProcessor<Track.Kind.Video, VideoProcessorOptions>;
}

export interface ScreenShareCaptureOptions {
  /**
   * true to capture audio shared. browser support for audio capturing in
   * screenshare is limited: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#browser_compatibility
   */
  audio?: boolean | AudioCaptureOptions;

  /**
   * only allows for 'true' and chrome allows for additional options to be passed in
   * https://developer.chrome.com/docs/web-platform/screen-sharing-controls/#displaySurface
   */
  video?: true | { displaySurface?: 'window' | 'browser' | 'monitor' };

  /**
   * capture resolution, defaults to 1080 for all browsers other than Safari
   * On Safari 17, default resolution is not capped, due to a bug, specifying
   * any resolution at all would lead to a low-resolution capture.
   * https://bugs.webkit.org/show_bug.cgi?id=263015
   */
  resolution?: VideoResolution;

  /** a CaptureController object instance containing methods that can be used to further manipulate the capture session if included. */
  controller?: unknown; // TODO replace type with CaptureController once it lands in TypeScript

  /** specifies whether the browser should allow the user to select the current tab for capture */
  selfBrowserSurface?: 'include' | 'exclude';

  /** specifies whether the browser should display a control to allow the user to dynamically switch the shared tab during screen-sharing. */
  surfaceSwitching?: 'include' | 'exclude';

  /** specifies whether the browser should include the system audio among the possible audio sources offered to the user */
  systemAudio?: 'include' | 'exclude';

  /** specify the type of content, see: https://www.w3.org/TR/mst-content-hint/#video-content-hints */
  contentHint?: 'detail' | 'text' | 'motion';

  /**
   * Experimental option to control whether the audio playing in a tab will continue to be played out of a user's
   * local speakers when the tab is captured.
   */
  suppressLocalAudioPlayback?: boolean;

  /**
   * Experimental option to instruct the browser to offer the current tab as the most prominent capture source
   * @experimental
   * @see https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia#prefercurrenttab
   */
  preferCurrentTab?: boolean;
}

export interface AudioCaptureOptions {
  /**
   * specifies whether automatic gain control is preferred and/or required
   */
  autoGainControl?: ConstrainBoolean;

  /**
   * the channel count or range of channel counts which are acceptable and/or required
   */
  channelCount?: ConstrainULong;

  /**
   * A ConstrainDOMString object specifying a device ID or an array of device
   * IDs which are acceptable and/or required.
   */
  deviceId?: ConstrainDOMString;

  /**
   * whether or not echo cancellation is preferred and/or required
   */
  echoCancellation?: ConstrainBoolean;

  /**
   * the latency or range of latencies which are acceptable and/or required.
   */
  latency?: ConstrainDouble;

  /**
   * whether noise suppression is preferred and/or required.
   */
  noiseSuppression?: ConstrainBoolean;

  /**
   * the sample rate or range of sample rates which are acceptable and/or required.
   */
  sampleRate?: ConstrainULong;

  /**
   * sample size or range of sample sizes which are acceptable and/or required.
   */
  sampleSize?: ConstrainULong;

  /**
   * initialize the track with a given processor
   */
  processor?: TrackProcessor<Track.Kind.Audio, AudioProcessorOptions>;
}

export interface AudioOutputOptions {
  /**
   * deviceId to output audio
   *
   * Only supported on browsers where `setSinkId` is available
   */
  deviceId?: string;
}

export interface VideoResolution {
  width: number;
  height: number;
  frameRate?: number;
  aspectRatio?: number;
}

export interface VideoEncoding {
  maxBitrate: number;
  maxFramerate?: number;
  priority?: RTCPriorityType;
}

export interface VideoPresetOptions {
  width: number;
  height: number;
  aspectRatio?: number;
  maxBitrate: number;
  maxFramerate?: number;
  priority?: RTCPriorityType;
}

export class VideoPreset {
  encoding: VideoEncoding;

  width: number;

  height: number;

  aspectRatio?: number;

  constructor(videoPresetOptions: VideoPresetOptions);
  constructor(
    width: number,
    height: number,
    maxBitrate: number,
    maxFramerate?: number,
    priority?: RTCPriorityType,
  );
  constructor(
    widthOrOptions: number | VideoPresetOptions,
    height?: number,
    maxBitrate?: number,
    maxFramerate?: number,
    priority?: RTCPriorityType,
  ) {
    if (typeof widthOrOptions === 'object') {
      this.width = widthOrOptions.width;
      this.height = widthOrOptions.height;
      this.aspectRatio = widthOrOptions.aspectRatio;
      this.encoding = {
        maxBitrate: widthOrOptions.maxBitrate,
        maxFramerate: widthOrOptions.maxFramerate,
        priority: widthOrOptions.priority,
      };
    } else if (height !== undefined && maxBitrate !== undefined) {
      this.width = widthOrOptions;
      this.height = height;
      this.aspectRatio = widthOrOptions / height;
      this.encoding = {
        maxBitrate,
        maxFramerate,
        priority,
      };
    } else {
      throw new TypeError('Unsupported options: provide at least width, height and maxBitrate');
    }
  }

  get resolution(): VideoResolution {
    return {
      width: this.width,
      height: this.height,
      frameRate: this.encoding.maxFramerate,
      aspectRatio: this.aspectRatio,
    };
  }
}

export interface AudioPreset {
  maxBitrate: number;
  priority?: RTCPriorityType;
}

const backupCodecs = ['vp8', 'h264'] as const;

export const videoCodecs = ['vp8', 'h264', 'vp9', 'av1'] as const;

export type VideoCodec = (typeof videoCodecs)[number];

export type BackupVideoCodec = (typeof backupCodecs)[number];

export function isBackupCodec(codec: string): codec is BackupVideoCodec {
  return !!backupCodecs.find((backup) => backup === codec);
}

/**
 * scalability modes for svc.
 */
export type ScalabilityMode =
  | 'L1T1'
  | 'L1T2'
  | 'L1T3'
  | 'L2T1'
  | 'L2T1h'
  | 'L2T1_KEY'
  | 'L2T2'
  | 'L2T2h'
  | 'L2T2_KEY'
  | 'L2T3'
  | 'L2T3h'
  | 'L2T3_KEY'
  | 'L3T1'
  | 'L3T1h'
  | 'L3T1_KEY'
  | 'L3T2'
  | 'L3T2h'
  | 'L3T2_KEY'
  | 'L3T3'
  | 'L3T3h'
  | 'L3T3_KEY';

export namespace AudioPresets {
  export const telephone: AudioPreset = {
    maxBitrate: 12_000,
  };
  export const speech: AudioPreset = {
    maxBitrate: 20_000,
  };
  export const music: AudioPreset = {
    maxBitrate: 32_000,
  };
  export const musicStereo: AudioPreset = {
    maxBitrate: 48_000,
  };
  export const musicHighQuality: AudioPreset = {
    maxBitrate: 64_000,
  };
  export const musicHighQualityStereo: AudioPreset = {
    maxBitrate: 96_000,
  };
}

/**
 * Sane presets for video resolution/encoding
 */
export const VideoPresets = {
  h90: new VideoPreset(160, 90, 90_000, 20),
  h180: new VideoPreset(320, 180, 160_000, 20),
  h216: new VideoPreset(384, 216, 180_000, 20),
  h360: new VideoPreset(640, 360, 450_000, 20),
  h540: new VideoPreset(960, 540, 800_000, 25),
  h720: new VideoPreset(1280, 720, 1_700_000, 30),
  h1080: new VideoPreset(1920, 1080, 3_000_000, 30),
  h1440: new VideoPreset(2560, 1440, 5_000_000, 30),
  h2160: new VideoPreset(3840, 2160, 8_000_000, 30),
} as const;

/**
 * Four by three presets
 */
export const VideoPresets43 = {
  h120: new VideoPreset(160, 120, 70_000, 20),
  h180: new VideoPreset(240, 180, 125_000, 20),
  h240: new VideoPreset(320, 240, 140_000, 20),
  h360: new VideoPreset(480, 360, 330_000, 20),
  h480: new VideoPreset(640, 480, 500_000, 20),
  h540: new VideoPreset(720, 540, 600_000, 25),
  h720: new VideoPreset(960, 720, 1_300_000, 30),
  h1080: new VideoPreset(1440, 1080, 2_300_000, 30),
  h1440: new VideoPreset(1920, 1440, 3_800_000, 30),
} as const;

export const ScreenSharePresets = {
  h360fps3: new VideoPreset(640, 360, 200_000, 3, 'medium'),
  h360fps15: new VideoPreset(640, 360, 400_000, 15, 'medium'),
  h720fps5: new VideoPreset(1280, 720, 800_000, 5, 'medium'),
  h720fps15: new VideoPreset(1280, 720, 1_500_000, 15, 'medium'),
  h720fps30: new VideoPreset(1280, 720, 2_000_000, 30, 'medium'),
  h1080fps15: new VideoPreset(1920, 1080, 2_500_000, 15, 'medium'),
  h1080fps30: new VideoPreset(1920, 1080, 5_000_000, 30, 'medium'),
  // original resolution, without resizing
  original: new VideoPreset(0, 0, 7_000_000, 30, 'medium'),
} as const;
