import AbrController from './controller/abr-controller';
import AudioStreamController from './controller/audio-stream-controller';
import AudioTrackController from './controller/audio-track-controller';
import { SubtitleStreamController } from './controller/subtitle-stream-controller';
import SubtitleTrackController from './controller/subtitle-track-controller';
import BufferController from './controller/buffer-controller';
import { TimelineController } from './controller/timeline-controller';
import CapLevelController from './controller/cap-level-controller';
import FPSController from './controller/fps-controller';
import EMEController, {
  MediaKeySessionContext,
} from './controller/eme-controller';
import CMCDController from './controller/cmcd-controller';
import ContentSteeringController from './controller/content-steering-controller';
import ErrorController from './controller/error-controller';
import XhrLoader from './utils/xhr-loader';
import FetchLoader, { fetchSupported } from './utils/fetch-loader';
import Cues from './utils/cues';
import { requestMediaKeySystemAccess } from './utils/mediakeys-helper';
import { ILogger, logger } from './utils/logger';

import type Hls from './hls';
import type { CuesInterface } from './utils/cues';
import type { MediaKeyFunc, KeySystems } from './utils/mediakeys-helper';
import type {
  FragmentLoaderContext,
  Loader,
  LoaderContext,
  LoaderResponse,
  PlaylistLoaderContext,
} from './types/loader';
import type {
  AudioSelectionOption,
  SubtitleSelectionOption,
  VideoSelectionOption,
} from './types/media-playlist';

export type ABRControllerConfig = {
  abrEwmaFastLive: number;
  abrEwmaSlowLive: number;
  abrEwmaFastVoD: number;
  abrEwmaSlowVoD: number;
  /**
   * Default bandwidth estimate in bits/s prior to collecting fragment bandwidth samples
   */
  abrEwmaDefaultEstimate: number;
  abrEwmaDefaultEstimateMax: number;
  abrBandWidthFactor: number;
  abrBandWidthUpFactor: number;
  abrMaxWithRealBitrate: boolean;
  maxStarvationDelay: number;
  maxLoadingDelay: number;
};

export type BufferControllerConfig = {
  appendErrorMaxRetry: number;
  backBufferLength: number;
  frontBufferFlushThreshold: number;
  liveDurationInfinity: boolean;
  /**
   * @deprecated use backBufferLength
   */
  liveBackBufferLength: number | null;
};

export type CapLevelControllerConfig = {
  capLevelToPlayerSize: boolean;
};

export type CMCDControllerConfig = {
  sessionId?: string;
  contentId?: string;
  useHeaders?: boolean;
  includeKeys?: string[];
};

export type DRMSystemOptions = {
  audioRobustness?: string;
  videoRobustness?: string;
  audioEncryptionScheme?: string | null;
  videoEncryptionScheme?: string | null;
  persistentState?: MediaKeysRequirement;
  distinctiveIdentifier?: MediaKeysRequirement;
  sessionTypes?: string[];
  sessionType?: string;
};

export type DRMSystemConfiguration = {
  licenseUrl: string;
  serverCertificateUrl?: string;
  generateRequest?: (
    this: Hls,
    initDataType: string,
    initData: ArrayBuffer | null,
    keyContext: MediaKeySessionContext,
  ) =>
    | { initDataType: string; initData: ArrayBuffer | null }
    | undefined
    | never;
};

export type DRMSystemsConfiguration = Partial<
  Record<KeySystems, DRMSystemConfiguration>
>;

export type EMEControllerConfig = {
  licenseXhrSetup?: (
    this: Hls,
    xhr: XMLHttpRequest,
    url: string,
    keyContext: MediaKeySessionContext,
    licenseChallenge: Uint8Array,
  ) => void | Uint8Array | Promise<Uint8Array | void>;
  licenseResponseCallback?: (
    this: Hls,
    xhr: XMLHttpRequest,
    url: string,
    keyContext: MediaKeySessionContext,
  ) => ArrayBuffer;
  emeEnabled: boolean;
  widevineLicenseUrl?: string;
  drmSystems: DRMSystemsConfiguration;
  drmSystemOptions: DRMSystemOptions;
  requestMediaKeySystemAccessFunc: MediaKeyFunc | null;
};

export interface FragmentLoaderConstructor {
  new (confg: HlsConfig): Loader<FragmentLoaderContext>;
}

/**
 * @deprecated use fragLoadPolicy.default
 */
export type FragmentLoaderConfig = {
  fragLoadingTimeOut: number;
  fragLoadingMaxRetry: number;
  fragLoadingRetryDelay: number;
  fragLoadingMaxRetryTimeout: number;
};

export type FPSControllerConfig = {
  capLevelOnFPSDrop: boolean;
  fpsDroppedMonitoringPeriod: number;
  fpsDroppedMonitoringThreshold: number;
};

export type LevelControllerConfig = {
  startLevel?: number;
};

export type MP4RemuxerConfig = {
  stretchShortVideoTrack: boolean;
  maxAudioFramesDrift: number;
};

export interface PlaylistLoaderConstructor {
  new (confg: HlsConfig): Loader<PlaylistLoaderContext>;
}

/**
 * @deprecated use manifestLoadPolicy.default and playlistLoadPolicy.default
 */
export type PlaylistLoaderConfig = {
  manifestLoadingTimeOut: number;
  manifestLoadingMaxRetry: number;
  manifestLoadingRetryDelay: number;
  manifestLoadingMaxRetryTimeout: number;

  levelLoadingTimeOut: number;
  levelLoadingMaxRetry: number;
  levelLoadingRetryDelay: number;
  levelLoadingMaxRetryTimeout: number;
};

export type HlsLoadPolicies = {
  fragLoadPolicy: LoadPolicy;
  keyLoadPolicy: LoadPolicy;
  certLoadPolicy: LoadPolicy;
  playlistLoadPolicy: LoadPolicy;
  manifestLoadPolicy: LoadPolicy;
  steeringManifestLoadPolicy: LoadPolicy;
};

export type LoadPolicy = {
  default: LoaderConfig;
};

export type LoaderConfig = {
  maxTimeToFirstByteMs: number; // Max time to first byte
  maxLoadTimeMs: number; // Max time for load completion
  timeoutRetry: RetryConfig | null;
  errorRetry: RetryConfig | null;
};

export type RetryConfig = {
  maxNumRetry: number; // Maximum number of retries
  retryDelayMs: number; // Retry delay = 2^retryCount * retryDelayMs (exponential) or retryCount * retryDelayMs (linear)
  maxRetryDelayMs: number; // Maximum delay between retries
  backoff?: 'exponential' | 'linear'; // used to determine retry backoff duration (see retryDelayMs)
  shouldRetry?: (
    retryConfig: RetryConfig | null | undefined,
    retryCount: number,
    isTimeout: boolean,
    loaderResponse: LoaderResponse | undefined,
    retry: boolean,
  ) => boolean;
};

export type StreamControllerConfig = {
  autoStartLoad: boolean;
  startPosition: number;
  defaultAudioCodec?: string;
  initialLiveManifestSize: number;
  maxBufferLength: number;
  maxBufferSize: number;
  maxBufferHole: number;
  highBufferWatchdogPeriod: number;
  nudgeOffset: number;
  nudgeMaxRetry: number;
  maxFragLookUpTolerance: number;
  maxMaxBufferLength: number;
  startFragPrefetch: boolean;
  testBandwidth: boolean;
};

export type SelectionPreferences = {
  videoPreference?: VideoSelectionOption;
  audioPreference?: AudioSelectionOption;
  subtitlePreference?: SubtitleSelectionOption;
};

export type LatencyControllerConfig = {
  liveSyncDurationCount: number;
  liveMaxLatencyDurationCount: number;
  liveSyncDuration?: number;
  liveMaxLatencyDuration?: number;
  maxLiveSyncPlaybackRate: number;
};

export type MetadataControllerConfig = {
  enableDateRangeMetadataCues: boolean;
  enableEmsgMetadataCues: boolean;
  enableID3MetadataCues: boolean;
};

export type TimelineControllerConfig = {
  cueHandler: CuesInterface;
  enableWebVTT: boolean;
  enableIMSC1: boolean;
  enableCEA708Captions: boolean;
  captionsTextTrack1Label: string;
  captionsTextTrack1LanguageCode: string;
  captionsTextTrack2Label: string;
  captionsTextTrack2LanguageCode: string;
  captionsTextTrack3Label: string;
  captionsTextTrack3LanguageCode: string;
  captionsTextTrack4Label: string;
  captionsTextTrack4LanguageCode: string;
  renderTextTracksNatively: boolean;
};

export type TSDemuxerConfig = {
  forceKeyFrameOnDiscontinuity: boolean;
};

export type HlsConfig = {
  debug: boolean | ILogger;
  enableWorker: boolean;
  workerPath: null | string;
  enableSoftwareAES: boolean;
  minAutoBitrate: number;
  ignoreDevicePixelRatio: boolean;
  preferManagedMediaSource: boolean;
  loader: { new (confg: HlsConfig): Loader<LoaderContext> };
  fLoader?: FragmentLoaderConstructor;
  pLoader?: PlaylistLoaderConstructor;
  fetchSetup?: (context: LoaderContext, initParams: any) => Request;
  xhrSetup?: (xhr: XMLHttpRequest, url: string) => Promise<void> | void;

  // Alt Audio
  audioStreamController?: typeof AudioStreamController;
  audioTrackController?: typeof AudioTrackController;
  // Subtitle
  subtitleStreamController?: typeof SubtitleStreamController;
  subtitleTrackController?: typeof SubtitleTrackController;
  timelineController?: typeof TimelineController;
  // EME
  emeController?: typeof EMEController;
  // CMCD
  cmcd?: CMCDControllerConfig;
  cmcdController?: typeof CMCDController;
  // Content Steering
  contentSteeringController?: typeof ContentSteeringController;

  // MediaCapabilies API for level, track, and switch filtering
  useMediaCapabilities: boolean;

  abrController: typeof AbrController;
  bufferController: typeof BufferController;
  capLevelController: typeof CapLevelController;
  errorController: typeof ErrorController;
  fpsController: typeof FPSController;
  progressive: boolean;
  lowLatencyMode: boolean;
} & ABRControllerConfig &
  BufferControllerConfig &
  CapLevelControllerConfig &
  EMEControllerConfig &
  FPSControllerConfig &
  LevelControllerConfig &
  MP4RemuxerConfig &
  StreamControllerConfig &
  SelectionPreferences &
  LatencyControllerConfig &
  MetadataControllerConfig &
  TimelineControllerConfig &
  TSDemuxerConfig &
  HlsLoadPolicies &
  FragmentLoaderConfig &
  PlaylistLoaderConfig;

const defaultLoadPolicy: LoaderConfig = {
  maxTimeToFirstByteMs: 8000,
  maxLoadTimeMs: 20000,
  timeoutRetry: null,
  errorRetry: null,
};

/**
 * @ignore
 * If possible, keep hlsDefaultConfig shallow
 * It is cloned whenever a new Hls instance is created, by keeping the config
 * shallow the properties are cloned, and we don't end up manipulating the default
 */
export const hlsDefaultConfig: HlsConfig = {
  autoStartLoad: true, // used by stream-controller
  startPosition: -1, // used by stream-controller
  defaultAudioCodec: undefined, // used by stream-controller
  debug: false, // used by logger
  capLevelOnFPSDrop: false, // used by fps-controller
  capLevelToPlayerSize: false, // used by cap-level-controller
  ignoreDevicePixelRatio: false, // used by cap-level-controller
  preferManagedMediaSource: true,
  initialLiveManifestSize: 1, // used by stream-controller
  maxBufferLength: 30, // used by stream-controller
  backBufferLength: Infinity, // used by buffer-controller
  frontBufferFlushThreshold: Infinity,
  maxBufferSize: 60 * 1000 * 1000, // used by stream-controller
  maxBufferHole: 0.1, // used by stream-controller
  highBufferWatchdogPeriod: 2, // used by stream-controller
  nudgeOffset: 0.1, // used by stream-controller
  nudgeMaxRetry: 3, // used by stream-controller
  maxFragLookUpTolerance: 0.25, // used by stream-controller
  liveSyncDurationCount: 3, // used by latency-controller
  liveMaxLatencyDurationCount: Infinity, // used by latency-controller
  liveSyncDuration: undefined, // used by latency-controller
  liveMaxLatencyDuration: undefined, // used by latency-controller
  maxLiveSyncPlaybackRate: 1, // used by latency-controller
  liveDurationInfinity: false, // used by buffer-controller
  /**
   * @deprecated use backBufferLength
   */
  liveBackBufferLength: null, // used by buffer-controller
  maxMaxBufferLength: 600, // used by stream-controller
  enableWorker: true, // used by transmuxer
  workerPath: null, // used by transmuxer
  enableSoftwareAES: true, // used by decrypter
  startLevel: undefined, // used by level-controller
  startFragPrefetch: false, // used by stream-controller
  fpsDroppedMonitoringPeriod: 5000, // used by fps-controller
  fpsDroppedMonitoringThreshold: 0.2, // used by fps-controller
  appendErrorMaxRetry: 3, // used by buffer-controller
  loader: XhrLoader,
  // loader: FetchLoader,
  fLoader: undefined, // used by fragment-loader
  pLoader: undefined, // used by playlist-loader
  xhrSetup: undefined, // used by xhr-loader
  licenseXhrSetup: undefined, // used by eme-controller
  licenseResponseCallback: undefined, // used by eme-controller
  abrController: AbrController,
  bufferController: BufferController,
  capLevelController: CapLevelController,
  errorController: ErrorController,
  fpsController: FPSController,
  stretchShortVideoTrack: false, // used by mp4-remuxer
  maxAudioFramesDrift: 1, // used by mp4-remuxer
  forceKeyFrameOnDiscontinuity: true, // used by ts-demuxer
  abrEwmaFastLive: 3, // used by abr-controller
  abrEwmaSlowLive: 9, // used by abr-controller
  abrEwmaFastVoD: 3, // used by abr-controller
  abrEwmaSlowVoD: 9, // used by abr-controller
  abrEwmaDefaultEstimate: 5e5, // 500 kbps  // used by abr-controller
  abrEwmaDefaultEstimateMax: 5e6, // 5 mbps
  abrBandWidthFactor: 0.95, // used by abr-controller
  abrBandWidthUpFactor: 0.7, // used by abr-controller
  abrMaxWithRealBitrate: false, // used by abr-controller
  maxStarvationDelay: 4, // used by abr-controller
  maxLoadingDelay: 4, // used by abr-controller
  minAutoBitrate: 0, // used by hls
  emeEnabled: false, // used by eme-controller
  widevineLicenseUrl: undefined, // used by eme-controller
  drmSystems: {}, // used by eme-controller
  drmSystemOptions: {}, // used by eme-controller
  requestMediaKeySystemAccessFunc: __USE_EME_DRM__
    ? requestMediaKeySystemAccess
    : null, // used by eme-controller
  testBandwidth: true,
  progressive: false,
  lowLatencyMode: true,
  cmcd: undefined,
  enableDateRangeMetadataCues: true,
  enableEmsgMetadataCues: true,
  enableID3MetadataCues: true,
  useMediaCapabilities: __USE_MEDIA_CAPABILITIES__,

  certLoadPolicy: {
    default: defaultLoadPolicy,
  },
  keyLoadPolicy: {
    default: {
      maxTimeToFirstByteMs: 8000,
      maxLoadTimeMs: 20000,
      timeoutRetry: {
        maxNumRetry: 1,
        retryDelayMs: 1000,
        maxRetryDelayMs: 20000,
        backoff: 'linear',
      },
      errorRetry: {
        maxNumRetry: 8,
        retryDelayMs: 1000,
        maxRetryDelayMs: 20000,
        backoff: 'linear',
      },
    },
  },
  manifestLoadPolicy: {
    default: {
      maxTimeToFirstByteMs: Infinity,
      maxLoadTimeMs: 20000,
      timeoutRetry: {
        maxNumRetry: 2,
        retryDelayMs: 0,
        maxRetryDelayMs: 0,
      },
      errorRetry: {
        maxNumRetry: 1,
        retryDelayMs: 1000,
        maxRetryDelayMs: 8000,
      },
    },
  },
  playlistLoadPolicy: {
    default: {
      maxTimeToFirstByteMs: 10000,
      maxLoadTimeMs: 20000,
      timeoutRetry: {
        maxNumRetry: 2,
        retryDelayMs: 0,
        maxRetryDelayMs: 0,
      },
      errorRetry: {
        maxNumRetry: 2,
        retryDelayMs: 1000,
        maxRetryDelayMs: 8000,
      },
    },
  },
  fragLoadPolicy: {
    default: {
      maxTimeToFirstByteMs: 10000,
      maxLoadTimeMs: 120000,
      timeoutRetry: {
        maxNumRetry: 4,
        retryDelayMs: 0,
        maxRetryDelayMs: 0,
      },
      errorRetry: {
        maxNumRetry: 6,
        retryDelayMs: 1000,
        maxRetryDelayMs: 8000,
      },
    },
  },
  steeringManifestLoadPolicy: {
    default: __USE_CONTENT_STEERING__
      ? {
          maxTimeToFirstByteMs: 10000,
          maxLoadTimeMs: 20000,
          timeoutRetry: {
            maxNumRetry: 2,
            retryDelayMs: 0,
            maxRetryDelayMs: 0,
          },
          errorRetry: {
            maxNumRetry: 1,
            retryDelayMs: 1000,
            maxRetryDelayMs: 8000,
          },
        }
      : defaultLoadPolicy,
  },

  // These default settings are deprecated in favor of the above policies
  // and are maintained for backwards compatibility
  manifestLoadingTimeOut: 10000,
  manifestLoadingMaxRetry: 1,
  manifestLoadingRetryDelay: 1000,
  manifestLoadingMaxRetryTimeout: 64000,
  levelLoadingTimeOut: 10000,
  levelLoadingMaxRetry: 4,
  levelLoadingRetryDelay: 1000,
  levelLoadingMaxRetryTimeout: 64000,
  fragLoadingTimeOut: 20000,
  fragLoadingMaxRetry: 6,
  fragLoadingRetryDelay: 1000,
  fragLoadingMaxRetryTimeout: 64000,

  // Dynamic Modules
  ...timelineConfig(),
  subtitleStreamController: __USE_SUBTITLES__
    ? SubtitleStreamController
    : undefined,
  subtitleTrackController: __USE_SUBTITLES__
    ? SubtitleTrackController
    : undefined,
  timelineController: __USE_SUBTITLES__ ? TimelineController : undefined,
  audioStreamController: __USE_ALT_AUDIO__ ? AudioStreamController : undefined,
  audioTrackController: __USE_ALT_AUDIO__ ? AudioTrackController : undefined,
  emeController: __USE_EME_DRM__ ? EMEController : undefined,
  cmcdController: __USE_CMCD__ ? CMCDController : undefined,
  contentSteeringController: __USE_CONTENT_STEERING__
    ? ContentSteeringController
    : undefined,
};

function timelineConfig(): TimelineControllerConfig {
  return {
    cueHandler: Cues, // used by timeline-controller
    enableWebVTT: __USE_SUBTITLES__, // used by timeline-controller
    enableIMSC1: __USE_SUBTITLES__, // used by timeline-controller
    enableCEA708Captions: __USE_SUBTITLES__, // used by timeline-controller
    captionsTextTrack1Label: 'English', // used by timeline-controller
    captionsTextTrack1LanguageCode: 'en', // used by timeline-controller
    captionsTextTrack2Label: 'Spanish', // used by timeline-controller
    captionsTextTrack2LanguageCode: 'es', // used by timeline-controller
    captionsTextTrack3Label: 'Unknown CC', // used by timeline-controller
    captionsTextTrack3LanguageCode: '', // used by timeline-controller
    captionsTextTrack4Label: 'Unknown CC', // used by timeline-controller
    captionsTextTrack4LanguageCode: '', // used by timeline-controller
    renderTextTracksNatively: true,
  };
}

/**
 * @ignore
 */
export function mergeConfig(
  defaultConfig: HlsConfig,
  userConfig: Partial<HlsConfig>,
): HlsConfig {
  if (
    (userConfig.liveSyncDurationCount ||
      userConfig.liveMaxLatencyDurationCount) &&
    (userConfig.liveSyncDuration || userConfig.liveMaxLatencyDuration)
  ) {
    throw new Error(
      "Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration",
    );
  }

  if (
    userConfig.liveMaxLatencyDurationCount !== undefined &&
    (userConfig.liveSyncDurationCount === undefined ||
      userConfig.liveMaxLatencyDurationCount <=
        userConfig.liveSyncDurationCount)
  ) {
    throw new Error(
      'Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"',
    );
  }

  if (
    userConfig.liveMaxLatencyDuration !== undefined &&
    (userConfig.liveSyncDuration === undefined ||
      userConfig.liveMaxLatencyDuration <= userConfig.liveSyncDuration)
  ) {
    throw new Error(
      'Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"',
    );
  }

  const defaultsCopy = deepCpy(defaultConfig);

  // Backwards compatibility with deprecated config values
  const deprecatedSettingTypes = ['manifest', 'level', 'frag'];
  const deprecatedSettings = [
    'TimeOut',
    'MaxRetry',
    'RetryDelay',
    'MaxRetryTimeout',
  ];
  deprecatedSettingTypes.forEach((type) => {
    const policyName = `${type === 'level' ? 'playlist' : type}LoadPolicy`;
    const policyNotSet = userConfig[policyName] === undefined;
    const report: string[] = [];
    deprecatedSettings.forEach((setting) => {
      const deprecatedSetting = `${type}Loading${setting}`;
      const value = userConfig[deprecatedSetting];
      if (value !== undefined && policyNotSet) {
        report.push(deprecatedSetting);
        const settings: LoaderConfig = defaultsCopy[policyName].default;
        userConfig[policyName] = { default: settings };
        switch (setting) {
          case 'TimeOut':
            settings.maxLoadTimeMs = value;
            settings.maxTimeToFirstByteMs = value;
            break;
          case 'MaxRetry':
            settings.errorRetry!.maxNumRetry = value;
            settings.timeoutRetry!.maxNumRetry = value;
            break;
          case 'RetryDelay':
            settings.errorRetry!.retryDelayMs = value;
            settings.timeoutRetry!.retryDelayMs = value;
            break;
          case 'MaxRetryTimeout':
            settings.errorRetry!.maxRetryDelayMs = value;
            settings.timeoutRetry!.maxRetryDelayMs = value;
            break;
        }
      }
    });
    if (report.length) {
      logger.warn(
        `hls.js config: "${report.join(
          '", "',
        )}" setting(s) are deprecated, use "${policyName}": ${JSON.stringify(
          userConfig[policyName],
        )}`,
      );
    }
  });

  return {
    ...defaultsCopy,
    ...userConfig,
  };
}

function deepCpy(obj: any): any {
  if (obj && typeof obj === 'object') {
    if (Array.isArray(obj)) {
      return obj.map(deepCpy);
    }
    return Object.keys(obj).reduce((result, key) => {
      result[key] = deepCpy(obj[key]);
      return result;
    }, {});
  }
  return obj;
}

/**
 * @ignore
 */
export function enableStreamingMode(config) {
  const currentLoader = config.loader;
  if (currentLoader !== FetchLoader && currentLoader !== XhrLoader) {
    // If a developer has configured their own loader, respect that choice
    logger.log(
      '[config]: Custom loader detected, cannot enable progressive streaming',
    );
    config.progressive = false;
  } else {
    const canStreamProgressively = fetchSupported();
    if (canStreamProgressively) {
      config.loader = FetchLoader;
      config.progressive = true;
      config.enableSoftwareAES = true;
      logger.log('[config]: Progressive streaming enabled, using FetchLoader');
    }
  }
}
