import log from "../log";
import type { IProcessedProtectionData } from "../main_thread/types";
import type { IManifest, IPeriod, IAdaptation, IPeriodsUpdateResult } from "../manifest";
import type {
  IAudioRepresentation,
  IAudioTrack,
  IRepresentationFilter,
  ITextTrack,
  ITrackType,
  IVideoRepresentation,
  IVideoTrack,
} from "../public_types";
import areArraysOfNumbersEqual from "../utils/are_arrays_of_numbers_equal";
import arrayFind from "../utils/array_find";
import isNullOrUndefined from "../utils/is_null_or_undefined";
import getMonotonicTimeStamp from "../utils/monotonic_timestamp";
import { objectValues } from "../utils/object_values";
import { ManifestMetadataFormat } from "./types";
import type {
  IAdaptationMetadata,
  IManifestMetadata,
  IPeriodMetadata,
  IRepresentationMetadata,
  IThumbnailTrackMetadata,
} from "./types";

/** List in an array every possible value for the Adaptation's `type` property. */
export const SUPPORTED_ADAPTATIONS_TYPE: ITrackType[] = ["audio", "video", "text"];

/**
 * Returns the theoretical minimum playable position on the content
 * regardless of the current Adaptation chosen, as estimated at parsing
 * time.
 * @param {Object} manifest
 * @returns {number}
 */
export function getMinimumSafePosition(manifest: IManifestMetadata): number {
  const windowData = manifest.timeBounds;
  if (windowData.timeshiftDepth === null) {
    return windowData.minimumSafePosition ?? 0;
  }

  const { maximumTimeData } = windowData;
  let maximumTime: number;
  if (!windowData.maximumTimeData.isLinear) {
    maximumTime = maximumTimeData.maximumSafePosition;
  } else {
    const timeDiff = getMonotonicTimeStamp() - maximumTimeData.time;
    maximumTime = maximumTimeData.maximumSafePosition + timeDiff / 1000;
  }
  const theoricalMinimum = maximumTime - windowData.timeshiftDepth;
  return Math.max(windowData.minimumSafePosition ?? 0, theoricalMinimum);
}

/**
 * Get the position of the live edge - that is, the position of what is
 * currently being broadcasted, in seconds.
 * @param {Object} manifest
 * @returns {number|undefined}
 */
export function getLivePosition(manifest: IManifestMetadata): number | undefined {
  const { maximumTimeData } = manifest.timeBounds;
  if (!manifest.isLive || maximumTimeData.livePosition === undefined) {
    return undefined;
  }
  if (!maximumTimeData.isLinear) {
    return maximumTimeData.livePosition;
  }
  const timeDiff = getMonotonicTimeStamp() - maximumTimeData.time;
  return maximumTimeData.livePosition + timeDiff / 1000;
}

/**
 * Returns the theoretical maximum playable position on the content
 * regardless of the current Adaptation chosen, as estimated at parsing
 * time.
 * @param {Object} manifest
 * @returns {number}
 */
export function getMaximumSafePosition(manifest: IManifestMetadata): number {
  const { maximumTimeData } = manifest.timeBounds;
  if (!maximumTimeData.isLinear) {
    return maximumTimeData.maximumSafePosition;
  }
  const timeDiff = getMonotonicTimeStamp() - maximumTimeData.time;
  return maximumTimeData.maximumSafePosition + timeDiff / 1000;
}

/**
 * Returns Adaptations that contain supported Representation(s).
 * @param {string|undefined} type - If set filter on a specific Adaptation's
 * type. Will return for all types if `undefined`.
 * @returns {Array.<Adaptation>}
 */
export function getSupportedAdaptations(
  period: IPeriod,
  type?: ITrackType | undefined,
): IAdaptation[];
export function getSupportedAdaptations(
  period: IPeriodMetadata,
  type?: ITrackType | undefined,
): IAdaptationMetadata[];
export function getSupportedAdaptations(
  period: IPeriod | IPeriodMetadata,
  type?: ITrackType | undefined,
): IAdaptationMetadata[] | IAdaptation[] {
  if (type === undefined) {
    return getAdaptations(period).filter((ada) => {
      return (
        ada.supportStatus.hasSupportedCodec !== false &&
        ada.supportStatus.isDecipherable !== false
      );
    });
  }
  const adaptationsForType = period.adaptations[type];
  if (adaptationsForType === undefined) {
    return [];
  }
  return adaptationsForType.filter((ada) => {
    return (
      ada.supportStatus.hasSupportedCodec !== false &&
      ada.supportStatus.isDecipherable !== false
    );
  });
}

/**
 * Returns the Period encountered at the given time.
 * Returns `undefined` if there is no Period exactly at the given time.
 * @param {Object} manifest
 * @param {number} time
 * @returns {Object|undefined}
 */
export function getPeriodForTime(manifest: IManifest, time: number): IPeriod | undefined;
export function getPeriodForTime(
  manifest: IManifestMetadata,
  time: number,
): IPeriodMetadata | undefined;
export function getPeriodForTime(
  manifest: IManifestMetadata | IManifest,
  time: number,
): IPeriod | IPeriodMetadata | undefined {
  let nextPeriod = null;
  for (let i = manifest.periods.length - 1; i >= 0; i--) {
    const period = manifest.periods[i];
    if (periodContainsTime(period, time, nextPeriod)) {
      return period;
    }
    nextPeriod = period;
  }
}

/**
 * Returns the Period coming chronologically just after another given Period.
 * Returns `undefined` if not found.
 * @param {Object} manifest
 * @param {Object} period
 * @returns {Object|null}
 */
export function getPeriodAfter(manifest: IManifest, period: IPeriod): IPeriod | null;
export function getPeriodAfter(
  manifest: IManifestMetadata,
  period: IPeriodMetadata,
): IPeriodMetadata | null;
export function getPeriodAfter(
  manifest: IManifestMetadata | IManifest,
  period: IPeriodMetadata | IPeriod,
): IPeriod | IPeriodMetadata | null {
  const endOfPeriod = period.end;
  if (endOfPeriod === undefined) {
    return null;
  }
  const nextPeriod = arrayFind(manifest.periods, (_period) => {
    return _period.end === undefined || endOfPeriod < _period.end;
  });
  return nextPeriod === undefined ? null : nextPeriod;
}

/**
 * Returns true if the give time is in the time boundaries of this `Period`.
 * @param {Object} period - The `Period` which we want to check.
 * @param {number} time
 * @param {object|null} nextPeriod - Period coming chronologically just
 * after in the same Manifest. `null` if this instance is the last `Period`.
 * @returns {boolean}
 */
export function periodContainsTime(
  period: IPeriodMetadata,
  time: number,
  nextPeriod: IPeriodMetadata | null,
): boolean {
  if (time >= period.start && (period.end === undefined || time < period.end)) {
    return true;
  } else if (
    time === period.end &&
    (nextPeriod === null || nextPeriod.start > period.end)
  ) {
    // The last possible timed position of a Period is ambiguous as it is
    // frequently in common with the start of the next one: is it part of
    // the current or of the next Period?
    // Here we only consider it part of the current Period if it is the
    // only one with that position.
    return true;
  }
  return false;
}

/**
 * Returns every `Adaptations` (or `tracks`) linked to that Period, in an
 * Array.
 * @returns {Array.<Object>}
 */
export function getAdaptations(period: IPeriod): IAdaptation[];
export function getAdaptations(period: IPeriodMetadata): IAdaptationMetadata[];
export function getAdaptations(
  period: IPeriodMetadata | IPeriod,
): IAdaptationMetadata[] | IAdaptation[] {
  const adaptationsByType = period.adaptations;
  return objectValues(adaptationsByType).reduce<IAdaptationMetadata[]>(
    // Note: the second case cannot happen. TS is just being dumb here
    (acc, adaptations) =>
      !isNullOrUndefined(adaptations) ? acc.concat(adaptations) : acc,
    [],
  );
}

/**
 * Format an `Adaptation`, generally of type `"audio"`, as an `IAudioTrack`.
 * @param {Object} adaptation
 * @param {boolean} filterPlayable - If `true` only "playable" Representation
 * will be returned.
 * @returns {Object}
 */
export function toAudioTrack(
  adaptation: IAdaptationMetadata,
  filterPlayable: boolean,
): IAudioTrack {
  const formatted: IAudioTrack = {
    language: adaptation.language ?? "",
    normalized: adaptation.normalizedLanguage ?? "",
    audioDescription: adaptation.isAudioDescription === true,
    id: adaptation.id,
    representations: (filterPlayable
      ? adaptation.representations.filter((r) => isRepresentationPlayable(r) === true)
      : adaptation.representations
    ).map(toAudioRepresentation),
    label: adaptation.label,
  };
  if (adaptation.isDub === true) {
    formatted.dub = true;
  }
  return formatted;
}

/**
 * Format an `Adaptation`, generally of type `"audio"`, as an `IAudioTrack`.
 * @param {Object} adaptation
 * @returns {Object}
 */
export function toTextTrack(adaptation: IAdaptationMetadata): ITextTrack {
  return {
    language: adaptation.language ?? "",
    normalized: adaptation.normalizedLanguage ?? "",
    closedCaption: adaptation.isClosedCaption === true,
    id: adaptation.id,
    label: adaptation.label,
    forced: adaptation.isForcedSubtitles,
  };
}

/**
 * Format an `Adaptation`, generally of type `"video"`, as an `IAudioTrack`.
 * @param {Object} adaptation
 * @param {boolean} filterPlayable - If `true` only "playable" Representation
 * will be returned.
 * @returns {Object}
 */
export function toVideoTrack(
  adaptation: IAdaptationMetadata,
  filterPlayable: boolean,
): IVideoTrack {
  const trickModeTracks =
    adaptation.trickModeTracks !== undefined
      ? adaptation.trickModeTracks.map((trickModeAdaptation) => {
          const representations = (
            filterPlayable
              ? trickModeAdaptation.representations.filter(
                  (r) => isRepresentationPlayable(r) === true,
                )
              : trickModeAdaptation.representations
          ).map(toVideoRepresentation);
          const trickMode: IVideoTrack = {
            id: trickModeAdaptation.id,
            representations,
            isTrickModeTrack: true,
          };
          if (trickModeAdaptation.isSignInterpreted === true) {
            trickMode.signInterpreted = true;
          }
          return trickMode;
        })
      : undefined;

  const videoTrack: IVideoTrack = {
    id: adaptation.id,
    representations: (filterPlayable
      ? adaptation.representations.filter((r) => isRepresentationPlayable(r) === true)
      : adaptation.representations
    ).map(toVideoRepresentation),
    label: adaptation.label,
  };
  if (adaptation.isSignInterpreted === true) {
    videoTrack.signInterpreted = true;
  }
  if (adaptation.isTrickModeTrack === true) {
    videoTrack.isTrickModeTrack = true;
  }
  if (trickModeTracks !== undefined) {
    videoTrack.trickModeTracks = trickModeTracks;
  }
  return videoTrack;
}

/**
 * Format Representation as an `IAudioRepresentation`.
 * @returns {Object}
 */
export function toAudioRepresentation(
  representation: IRepresentationMetadata,
): IAudioRepresentation {
  const { id, bitrate, chosenCodec, isSpatialAudio, isSupported, decipherable } =
    representation;
  return {
    id,
    bitrate,
    codec: chosenCodec,
    isSpatialAudio,
    isCodecSupported: isSupported,
    decipherable,
  };
}

/**
 * Format Representation as an `IVideoRepresentation`.
 * @returns {Object}
 */
export function toVideoRepresentation(
  representation: IRepresentationMetadata,
): IVideoRepresentation {
  const {
    id,
    bitrate,
    frameRate,
    width,
    height,
    chosenCodec,
    hdrInfo,
    isSupported,
    decipherable,
    contentProtections,
  } = representation;
  return {
    id,
    bitrate,
    frameRate,
    width,
    height,
    codec: chosenCodec,
    hdrInfo,
    isCodecSupported: isSupported,
    decipherable,
    contentProtections:
      contentProtections !== undefined
        ? {
            keyIds: contentProtections.keyIds,
          }
        : undefined,
  };
}

export function toTaggedTrack(adaptation: IAdaptation): ITaggedTrack {
  switch (adaptation.type) {
    case "audio":
      return { type: "audio", track: toAudioTrack(adaptation, false) };
    case "video":
      return { type: "video", track: toVideoTrack(adaptation, false) };
    case "text":
      return { type: "text", track: toTextTrack(adaptation) };
  }
}

/**
 * Returns `true` if the `Representation` has a high chance of being playable on
 * the current device (its codec seems supported and we don't consider it to be
 * un-decipherable).
 *
 * Returns `false` if the `Representation` has a high chance of being unplayable
 * on the current device (its codec seems unsupported and/or we consider it to
 * be un-decipherable).
 *
 * Returns `undefined` if we don't know as the codec has not been checked yet.
 *
 * @param {Object} representation
 * @returns {boolean|undefined}
 */
export function isRepresentationPlayable(
  representation: IRepresentationMetadata,
): boolean | undefined {
  if (representation.decipherable === false) {
    return false;
  }
  return representation.isSupported;
}

/**
 * Information on a Representation affected by a `decipherabilityUpdates` event.
 */
export interface IDecipherabilityStatusChangedElement {
  manifest: IManifestMetadata;
  period: IPeriodMetadata;
  adaptation: IAdaptationMetadata;
  representation: IRepresentationMetadata;
}

/**
 * Change the decipherability of Representations which have their key id in one
 * of the given Arrays:
 *
 *   - Those who have a key id listed in `whitelistedKeyIds` will have their
 *     decipherability updated to `true`
 *
 *   - Those who have a key id listed in `blacklistedKeyIds` will have their
 *     decipherability updated to `false`
 *
 *   - Those who have a key id listed in `delistedKeyIds` will have their
 *     decipherability updated to `undefined`.
 *
 * @param {Object} manifest
 * @param {Object} updates
 * @param {Array.<Uint8Array>} updates.whitelistedKeyIds
 * @param {Array.<Uint8Array>} updates.blacklistedKeyIds
 * @param {Array.<Uint8Array>} updates.delistedKeyIds
 */
export function updateDecipherabilityFromKeyIds(
  manifest: IManifestMetadata,
  updates: {
    whitelistedKeyIds: Uint8Array[];
    blacklistedKeyIds: Uint8Array[];
    delistedKeyIds: Uint8Array[];
  },
): IDecipherabilityStatusChangedElement[] {
  const { whitelistedKeyIds, blacklistedKeyIds, delistedKeyIds } = updates;
  const isDecipherable = (
    representation: IRepresentationMetadata,
  ): boolean | undefined => {
    if (representation.contentProtections === undefined) {
      return representation.decipherable;
    }
    const contentKIDs = representation.contentProtections.keyIds;
    if (contentKIDs !== undefined) {
      for (const elt of contentKIDs) {
        for (const blacklistedKeyId of blacklistedKeyIds) {
          if (areArraysOfNumbersEqual(blacklistedKeyId, elt)) {
            return false;
          }
        }
        for (const whitelistedKeyId of whitelistedKeyIds) {
          if (areArraysOfNumbersEqual(whitelistedKeyId, elt)) {
            return true;
          }
        }
        for (const delistedKeyId of delistedKeyIds) {
          if (areArraysOfNumbersEqual(delistedKeyId, elt)) {
            return undefined;
          }
        }
      }
    }
    return representation.decipherable;
  };

  if (manifest.manifestFormat === ManifestMetadataFormat.Class) {
    (manifest as IManifest).updateRepresentationsDeciperability(({ representation }) => {
      return isDecipherable(representation);
    });
  }
  return updateRepresentationsDeciperability(manifest, isDecipherable);
}

/**
 * Update decipherability to `false` to any Representation which is linked to
 * the given initialization data.
 * @param {Object} manifest
 * @param {Object} initData
 */
export function updateDecipherabilityFromProtectionData(
  manifest: IManifestMetadata,
  initData: IProcessedProtectionData,
): IDecipherabilityStatusChangedElement[] {
  const isDecipherable = (representation: IRepresentationMetadata) => {
    if (representation.decipherable === false) {
      return false;
    }
    const segmentProtections = representation.contentProtections?.initData ?? [];
    for (const protection of segmentProtections) {
      if (initData.type === undefined || protection.type === initData.type) {
        const containedInitData = initData.values
          .getFormattedValues()
          .every((undecipherableVal) => {
            return protection.values.some((currVal) => {
              return (
                (undecipherableVal.systemId === undefined ||
                  currVal.systemId === undecipherableVal.systemId) &&
                areArraysOfNumbersEqual(currVal.data, undecipherableVal.data)
              );
            });
          });
        if (containedInitData) {
          return false;
        }
      }
    }
    return representation.decipherable;
  };
  if (manifest.manifestFormat === ManifestMetadataFormat.Class) {
    (manifest as IManifest).updateRepresentationsDeciperability(({ representation }) => {
      return isDecipherable(representation);
    });
  }
  return updateRepresentationsDeciperability(manifest, isDecipherable);
}

/**
 * Update `decipherable` property of every `Representation` found in the
 * Manifest based on the result of a `isDecipherable` callback:
 *   - When that callback returns `true`, update `decipherable` to `true`
 *   - When that callback returns `false`, update `decipherable` to `false`
 *   - When that callback returns `undefined`, update `decipherable` to
 *     `undefined`
 * @param {Manifest} manifest
 * @param {Function} isDecipherable
 * @returns {Array.<Object>}
 */
function updateRepresentationsDeciperability(
  manifest: IManifestMetadata,
  isDecipherable: (rep: IRepresentationMetadata) => boolean | undefined,
): IDecipherabilityStatusChangedElement[] {
  const updates: IDecipherabilityStatusChangedElement[] = [];
  for (const period of manifest.periods) {
    const adaptationsByType = period.adaptations;
    const adaptations = objectValues(adaptationsByType).reduce<IAdaptationMetadata[]>(
      // Note: the second case cannot happen. TS is just being dumb here
      (acc, adaps) => (!isNullOrUndefined(adaps) ? acc.concat(adaps) : acc),
      [],
    );
    for (const adaptation of adaptations) {
      let hasOnlyUndecipherableRepresentations = true;
      for (const representation of adaptation.representations) {
        const result = isDecipherable(representation);
        if (result !== false) {
          hasOnlyUndecipherableRepresentations = false;
        }
        if (result !== representation.decipherable) {
          if (result === true) {
            adaptation.supportStatus.isDecipherable = true;
          } else if (
            result === undefined &&
            adaptation.supportStatus.isDecipherable === false
          ) {
            adaptation.supportStatus.isDecipherable = undefined;
          }
          updates.push({ manifest, period, adaptation, representation });
          representation.decipherable = result;
        }
      }
      if (hasOnlyUndecipherableRepresentations) {
        adaptation.supportStatus.isDecipherable = false;
      }
    }
  }
  return updates;
}

/**
 *
 * TODO that function is kind of very ugly, yet should work.
 * Maybe find out a better system for Manifest updates.
 * @param {Object} baseManifest
 * @param {Object} newManifest
 * @param {Array.<Object>} updates
 */
export function replicateUpdatesOnManifestMetadata(
  baseManifest: IManifestMetadata,
  newManifest: Omit<IManifestMetadata, "periods">,
  updates: IPeriodsUpdateResult,
) {
  if (baseManifest.manifestFormat === ManifestMetadataFormat.Class) {
    // This function is not intended for and could break a `Manifest` instance, as
    // it might mutate it by adding metadata objects to it, whereas the `Manifest`
    // instance generally expect the "instance versions" of those objects instead.
    log.error(
      "manifest",
      "`replicateUpdatesOnManifestMetadata` called on a Manifest instance",
    );
    return;
  }
  for (const prop of Object.keys(newManifest)) {
    if (prop !== "periods") {
      // trust me bro
      // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
      (baseManifest as any)[prop] = (newManifest as any)[prop];
    }
  }

  for (const removedPeriod of updates.removedPeriods) {
    for (let periodIdx = 0; periodIdx < baseManifest.periods.length; periodIdx++) {
      if (baseManifest.periods[periodIdx].id === removedPeriod.id) {
        baseManifest.periods.splice(periodIdx, 1);
        break;
      }
    }
  }

  for (const updatedPeriod of updates.updatedPeriods) {
    for (let periodIdx = 0; periodIdx < baseManifest.periods.length; periodIdx++) {
      const newPeriod = updatedPeriod.period;
      if (baseManifest.periods[periodIdx].id === updatedPeriod.period.id) {
        const basePeriod = baseManifest.periods[periodIdx];
        for (const prop of Object.keys(newPeriod)) {
          if (prop !== "adaptations") {
            // trust me bro
            // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
            (basePeriod as any)[prop] = (newPeriod as any)[prop];
          }
        }

        for (const removedThumbnailTrack of updatedPeriod.result.removedThumbnailTracks) {
          for (
            let thumbIdx = 0;
            thumbIdx < basePeriod.thumbnailTracks.length;
            thumbIdx++
          ) {
            if (basePeriod.thumbnailTracks[thumbIdx].id === removedThumbnailTrack.id) {
              basePeriod.thumbnailTracks.splice(thumbIdx, 1);
              break;
            }
          }
        }
        for (const updatedThumbnailTrack of updatedPeriod.result.updatedThumbnailTracks) {
          const newThumbnailTrack = updatedThumbnailTrack;
          for (
            let thumbIdx = 0;
            thumbIdx < basePeriod.thumbnailTracks.length;
            thumbIdx++
          ) {
            if (basePeriod.thumbnailTracks[thumbIdx].id === newThumbnailTrack.id) {
              const baseThumbnailTrack = basePeriod.thumbnailTracks[thumbIdx];
              for (const prop of Object.keys(newThumbnailTrack) as Array<
                keyof IThumbnailTrackMetadata
              >) {
                // eslint-disable-next-line
                (baseThumbnailTrack as any)[prop] = newThumbnailTrack[prop];
              }
              break;
            }
          }
        }
        for (const addedThumbnailTrack of updatedPeriod.result.addedThumbnailTracks) {
          basePeriod.thumbnailTracks.push(addedThumbnailTrack);
        }

        for (const removedAdaptation of updatedPeriod.result.removedAdaptations) {
          const ttype = removedAdaptation.trackType;
          const adaptationsForType = basePeriod.adaptations[ttype] ?? [];
          for (let adapIdx = 0; adapIdx < adaptationsForType.length; adapIdx++) {
            if (adaptationsForType[adapIdx].id === removedAdaptation.id) {
              adaptationsForType.splice(adapIdx, 1);
              break;
            }
          }
        }

        for (const updatedAdaptation of updatedPeriod.result.updatedAdaptations) {
          const newAdaptation = updatedAdaptation.adaptation;
          const ttype = updatedAdaptation.trackType;
          const adaptationsForType = basePeriod.adaptations[ttype] ?? [];
          for (let adapIdx = 0; adapIdx < adaptationsForType.length; adapIdx++) {
            if (adaptationsForType[adapIdx].id === newAdaptation) {
              const baseAdaptation = adaptationsForType[adapIdx];
              for (const removedRepresentation of updatedAdaptation.removedRepresentations) {
                for (
                  let repIdx = 0;
                  repIdx < baseAdaptation.representations.length;
                  repIdx++
                ) {
                  if (
                    baseAdaptation.representations[repIdx].id === removedRepresentation
                  ) {
                    baseAdaptation.representations.splice(repIdx, 1);
                    break;
                  }
                }
              }

              for (const newRepresentation of updatedAdaptation.updatedRepresentations) {
                for (
                  let repIdx = 0;
                  repIdx < baseAdaptation.representations.length;
                  repIdx++
                ) {
                  if (
                    baseAdaptation.representations[repIdx].id === newRepresentation.id
                  ) {
                    const baseRepresentation = baseAdaptation.representations[repIdx];
                    for (const prop of Object.keys(newRepresentation) as Array<
                      keyof IRepresentationMetadata
                    >) {
                      if (prop !== "decipherable") {
                        // eslint-disable-next-line
                        (baseRepresentation as any)[prop] = newRepresentation[prop];
                      }
                    }
                    break;
                  }
                }
              }

              for (const addedRepresentation of updatedAdaptation.addedRepresentations) {
                baseAdaptation.representations.push(addedRepresentation);
              }
              break;
            }
          }
        }

        for (const addedAdaptation of updatedPeriod.result.addedAdaptations) {
          const ttype = addedAdaptation.type;
          const adaptationsForType = basePeriod.adaptations[ttype];
          if (adaptationsForType === undefined) {
            basePeriod.adaptations[ttype] = [addedAdaptation];
          } else {
            adaptationsForType.push(addedAdaptation);
          }
        }
        break;
      }
    }
  }

  for (const addedPeriod of updates.addedPeriods) {
    for (let periodIdx = 0; periodIdx < baseManifest.periods.length; periodIdx++) {
      if (baseManifest.periods[periodIdx].start > addedPeriod.start) {
        baseManifest.periods.splice(periodIdx, 0, addedPeriod);
        break;
      }
    }
    baseManifest.periods.push(addedPeriod);
  }
}

export function createRepresentationFilterFromFnString(
  fnString: string,
): IRepresentationFilter {
  // eslint-disable-next-line @typescript-eslint/no-implied-eval
  return new Function(
    `return (${fnString}(arguments[0], arguments[1]))`,
  ) as IRepresentationFilter;
}

interface ITaggedAudioTrack {
  type: "audio";
  track: IAudioTrack;
}

interface ITaggedVideoTrack {
  type: "video";
  track: IVideoTrack;
}

interface ITaggedTextTrack {
  type: "text";
  track: ITextTrack;
}

export type ITaggedTrack = ITaggedAudioTrack | ITaggedVideoTrack | ITaggedTextTrack;
