/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import shouldPatchOutDolbyVisionConfigDataFromMp4 from "../../compat/should_patch_out_dolby_vision_config_data_from_mp4";
import logger from "../../log";
import {
  getMDHDTimescale,
  getSegmentsFromSidx,
  extractPssh,
  removeDolbyVisionConfigData,
} from "../../parsers/containers/isobmff";
import {
  getKeyIdFromInitSegment,
  parseEmsgBoxes,
} from "../../parsers/containers/isobmff/utils";
import { getSegmentsFromCues, getTimeCodeScale } from "../../parsers/containers/matroska";
import { toUint8Array } from "../../utils/byte_parsing";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import type {
  IProtectionDataInfo,
  ISegmentContext,
  ISegmentParser,
  ISegmentParserParsedInitChunk,
  ISegmentParserParsedMediaChunk,
} from "../types";
import getISOBMFFTimingInfos from "../utils/get_isobmff_timing_infos";
import inferSegmentContainer from "../utils/infer_segment_container";
import getEventsOutOfEMSGs from "./get_events_out_of_emsgs";

/**
 * @param {Object} config
 * @returns {Function}
 */
export default function generateAudioVideoSegmentParser({
  __priv_patchLastSegmentInSidx,
}: {
  __priv_patchLastSegmentInSidx?: boolean | undefined;
}): ISegmentParser<
  ArrayBuffer | Uint8Array<ArrayBuffer> | null,
  ArrayBuffer | Uint8Array<ArrayBuffer> | null
> {
  return function audioVideoSegmentParser(
    loadedSegment: {
      data: ArrayBuffer | Uint8Array<ArrayBuffer> | null;
      isChunked: boolean;
    },
    context: ISegmentContext,
    initTimescale: number | undefined,
  ):
    | ISegmentParserParsedMediaChunk<Uint8Array<ArrayBuffer> | ArrayBuffer | null>
    | ISegmentParserParsedInitChunk<Uint8Array<ArrayBuffer> | ArrayBuffer | null> {
    const { segment, periodStart, periodEnd } = context;
    const { data, isChunked } = loadedSegment;
    const appendWindow: [number, number | undefined] = [periodStart, periodEnd];

    if (data === null) {
      if (segment.isInit) {
        return {
          segmentType: "init",
          initializationData: null,
          initializationDataSize: 0,
          protectionData: [],
          initTimescale: undefined,
        };
      }
      return {
        segmentType: "media",
        chunkData: null,
        chunkSize: 0,
        chunkInfos: null,
        chunkOffset: 0,
        protectionData: [],
        appendWindow,
      };
    }

    const chunkData = toUint8Array(data);

    const containerType = inferSegmentContainer(context.type, context.mimeType);

    // TODO take a look to check if this is an ISOBMFF/webm?
    const seemsToBeMP4 = containerType === "mp4" || containerType === undefined;

    const protectionData: IProtectionDataInfo[] = [];
    if (seemsToBeMP4) {
      const psshInfo = extractPssh(chunkData);
      let keyId;
      if (segment.isInit) {
        keyId = getKeyIdFromInitSegment(chunkData) ?? undefined;
      }
      if (psshInfo.length > 0 || keyId !== undefined) {
        protectionData.push({
          initDataType: "cenc",
          keyId,
          initData: psshInfo,
        });
      }
    }

    if (!segment.isInit) {
      const chunkInfos = seemsToBeMP4
        ? getISOBMFFTimingInfos(chunkData, isChunked, segment, initTimescale)
        : null; // TODO extract time info from webm
      const chunkOffset = segment.timestampOffset ?? 0;

      if (seemsToBeMP4) {
        const parsedEMSGs = parseEmsgBoxes(chunkData);
        if (parsedEMSGs !== undefined) {
          const whitelistedEMSGs = parsedEMSGs.filter((evt) => {
            if (
              segment.privateInfos === undefined ||
              segment.privateInfos.isEMSGWhitelisted === undefined
            ) {
              return false;
            }
            return segment.privateInfos.isEMSGWhitelisted(evt);
          });
          const events = getEventsOutOfEMSGs(
            whitelistedEMSGs,
            context.manifestPublishTime,
          );
          if (events !== undefined) {
            const { needsManifestRefresh, inbandEvents } = events;
            return {
              segmentType: "media",
              chunkData,
              chunkSize: chunkData.length,
              chunkInfos,
              chunkOffset,
              appendWindow,
              inbandEvents,
              protectionData,
              needsManifestRefresh,
            };
          }
        }
      }

      return {
        segmentType: "media",
        chunkData,
        chunkSize: chunkData.length,
        chunkInfos,
        chunkOffset,
        protectionData,
        appendWindow,
      };
    }
    // we're handling an initialization segment
    const { indexRange } = segment;

    let segmentList;
    if (containerType === "webm") {
      segmentList = getSegmentsFromCues(chunkData, 0);
    } else if (seemsToBeMP4) {
      if (
        shouldPatchOutDolbyVisionConfigDataFromMp4(
          context.baseCodecs,
          context.chosenCodec,
        )
      ) {
        logger.debug("dash", "patching out Dolby Vision metadata from segment");
        removeDolbyVisionConfigData(chunkData);
      }

      segmentList = getSegmentsFromSidx(
        chunkData,
        Array.isArray(indexRange) ? indexRange[0] : 0,
      );

      // This is a very specific handling for streams we know have a very
      // specific problem at Canal+: The last reference gives a truncated
      // segment.
      // Sadly, people on the packaging side could not fix all legacy contents.
      // This is an easy-but-ugly fix for those.
      // TODO Cleaner way? I tried to always check the obtained segment after
      // a byte-range request but it leads to a lot of code.
      if (
        __priv_patchLastSegmentInSidx === true &&
        segmentList !== null &&
        segmentList.length > 0
      ) {
        const lastSegment = segmentList[segmentList.length - 1];
        if (Array.isArray(lastSegment.range)) {
          lastSegment.range[1] = Infinity;
        }
      }
    }

    let timescale;
    if (seemsToBeMP4) {
      timescale = getMDHDTimescale(chunkData);
    } else if (containerType === "webm") {
      timescale = getTimeCodeScale(chunkData, 0);
    }

    const parsedTimescale = isNullOrUndefined(timescale) ? undefined : timescale;

    return {
      segmentType: "init",
      initializationData: chunkData,
      initializationDataSize: chunkData.length,
      protectionData,
      initTimescale: parsedTimescale,
      segmentList: segmentList ?? undefined,
    };
  };
}
