/**
 * 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 log from "../../../../log";
import type { IAdaptation } from "../../../../manifest";
import type { IHDRInformation } from "../../../../public_types";
import arrayFind from "../../../../utils/array_find";
import objectAssign from "../../../../utils/object_assign";
import type { IParsedRepresentation } from "../../types";
import type {
  IAdaptationSetIntermediateRepresentation,
  IRepresentationIntermediateRepresentation,
  IScheme,
} from "../node_parser_types";
import type ContentProtectionParser from "./content_protection_parser";
import { convertSupplementalCodecsToRFC6381 } from "./convert_supplemental_codecs";
import { getWEBMHDRInformation } from "./get_hdr_information";
import type { IRepresentationIndexContext } from "./parse_representation_index";
import parseRepresentationIndex from "./parse_representation_index";
import resolveBaseURLs from "./resolve_base_urls";

/**
 * Combine inband event streams from representation and
 * adaptation data.
 * @param {Object} representation
 * @param {Object} adaptation
 * @returns {undefined | Array.<Object>}
 */
function combineInbandEventStreams(
  representation: IRepresentationIntermediateRepresentation,
  adaptation: IAdaptationSetIntermediateRepresentation,
): IScheme[] | undefined {
  const newSchemeId = [];
  if (representation.children.inbandEventStreams !== undefined) {
    newSchemeId.push(...representation.children.inbandEventStreams);
  }
  if (adaptation.children.inbandEventStreams !== undefined) {
    newSchemeId.push(...adaptation.children.inbandEventStreams);
  }
  if (newSchemeId.length === 0) {
    return undefined;
  }
  return newSchemeId;
}

/**
 * Extract HDR information from manifest and codecs.
 * @param {Object}
 * @returns {Object | undefined}
 */
function getHDRInformation({
  adaptationProfiles,
  essentialProperties,
  supplementalProperties,
  manifestProfiles,
  codecs,
}: {
  adaptationProfiles?: string | undefined;
  essentialProperties?: IScheme[] | undefined;
  supplementalProperties?: IScheme[] | undefined;
  manifestProfiles?: string | undefined;
  codecs?: string | undefined;
}): undefined | IHDRInformation {
  const profiles = (adaptationProfiles ?? "") + (manifestProfiles ?? "");
  if (profiles.indexOf("http://dashif.org/guidelines/dash-if-uhd#hevc-hdr-pq10") !== -1) {
    if (codecs === "hvc1.2.4.L153.B0" || codecs === "hev1.2.4.L153.B0") {
      return { colorDepth: 10, eotf: "pq", colorSpace: "rec2020" };
    }
  }
  const transferCharacteristicScheme = arrayFind(
    [...(essentialProperties ?? []), ...(supplementalProperties ?? [])],
    (p) => p.schemeIdUri === "urn:mpeg:mpegB:cicp:TransferCharacteristics",
  );
  if (transferCharacteristicScheme !== undefined) {
    // 1: ITU-R BT.709
    // 2: Unspecified
    // 4: Gamma 2.2 curve
    // 5: Gamma 2.8 curve
    // 6: SMPTE 170M
    // 7: SMPTE 240M
    // 8: Linear
    // 9: Logarithmic (100:1 range)
    // 10: Logarithmic (100 * Sqrt(10) : 1 range)
    // 11: IEC 61966-2-4
    // 12: ITU-R BT.1361 Extended Colour Gamut
    // 13: IEC 61966-2-1 (sRGB or sYCC)
    // 14: ITU-R BT.2020 10-bit system
    // 15: ITU-R BT.2020 12-bit system
    // 16: SMPTE ST 2084, ITU-R BT.2100 PQ
    // 17: SMPTE ST 428-1
    // 18: ARIB STD-B67 (HLG)
    switch (transferCharacteristicScheme.value) {
      case "15":
        return undefined; // SDR
      case "16":
        return { eotf: "pq" };
      case "18":
        return { eotf: "hlg" };
    }
  }
  if (codecs !== undefined && /^vp(08|09|10)/.test(codecs)) {
    return getWEBMHDRInformation(codecs);
  }
}

/**
 * Process intermediate representations to create final parsed representations.
 * In the same order.
 * @param {Array.<Object>} representationsIR
 * @param {Object} context
 * @returns {Array.<Object>}
 */
export default function parseRepresentations(
  representationsIR: IRepresentationIntermediateRepresentation[],
  adaptation: IAdaptationSetIntermediateRepresentation,
  context: IRepresentationContext,
): IParsedRepresentation[] {
  const parsedRepresentations: IParsedRepresentation[] = [];
  for (const representation of representationsIR) {
    // Compute Representation ID
    let representationID =
      representation.attributes.id !== undefined
        ? representation.attributes.id
        : String(representation.attributes.bitrate) +
          (representation.attributes.height !== undefined
            ? `-${representation.attributes.height}`
            : "") +
          (representation.attributes.width !== undefined
            ? `-${representation.attributes.width}`
            : "") +
          (representation.attributes.mimeType !== undefined
            ? `-${representation.attributes.mimeType}`
            : "") +
          (representation.attributes.codecs !== undefined
            ? `-${representation.attributes.codecs}`
            : "");

    // Avoid duplicate IDs
    while (parsedRepresentations.some((r) => r.id === representationID)) {
      representationID += "-dup";
    }

    // Retrieve previous version of the Representation, if one.
    const unsafelyBaseOnPreviousRepresentation =
      context.unsafelyBaseOnPreviousAdaptation?.getRepresentation(representationID) ??
      null;

    const inbandEventStreams = combineInbandEventStreams(representation, adaptation);

    const availabilityTimeComplete =
      representation.attributes.availabilityTimeComplete ??
      context.availabilityTimeComplete;

    let availabilityTimeOffset: number | undefined;
    if (
      representation.attributes.availabilityTimeOffset !== undefined ||
      context.availabilityTimeOffset !== undefined
    ) {
      availabilityTimeOffset =
        (representation.attributes.availabilityTimeOffset ?? 0) +
        (context.availabilityTimeOffset ?? 0);
    }
    const reprIndexCtxt = objectAssign({}, context, {
      availabilityTimeOffset,
      availabilityTimeComplete,
      unsafelyBaseOnPreviousRepresentation,
      adaptation,
      inbandEventStreams,
    });
    const representationIndex = parseRepresentationIndex(representation, reprIndexCtxt);

    // Find bitrate
    let representationBitrate: number;
    if (representation.attributes.bitrate === undefined) {
      log.warn("dash", "No usable bitrate found in the Representation.");
      representationBitrate = 0;
    } else {
      representationBitrate = representation.attributes.bitrate;
    }

    const representationBaseURLs = resolveBaseURLs(
      context.baseURLs,
      representation.children.baseURLs,
    );

    const cdnMetadata =
      representationBaseURLs.length === 0
        ? // No BaseURL seems to be associated to this Representation, nor to the MPD,
          // but underlying segments might have one. To indicate that segments should
          // still be available through a CDN without giving any root CDN URL here,
          // we just communicate about an empty `baseUrl`, as documented.
          [{ baseUrl: "", id: undefined }]
        : representationBaseURLs.map((x) => ({
            baseUrl: x.url,
            id: x.serviceLocation,
          }));

    // Construct Representation Base
    const parsedRepresentation: IParsedRepresentation = {
      bitrate: representationBitrate,
      cdnMetadata,
      index: representationIndex,
      id: representationID,
    };

    if (
      representation.children.supplementalProperties !== undefined &&
      arrayFind(
        representation.children.supplementalProperties,
        (r) =>
          r.schemeIdUri === "tag:dolby.com,2018:dash:EC3_ExtensionType:2018" &&
          r.value === "JOC",
      ) !== undefined
    ) {
      parsedRepresentation.isSpatialAudio = true;
    }

    // Add optional attributes
    let codecs: string | undefined;
    if (representation.attributes.codecs !== undefined) {
      codecs = representation.attributes.codecs;
    } else if (adaptation.attributes.codecs !== undefined) {
      codecs = adaptation.attributes.codecs;
    }
    if (codecs !== undefined) {
      codecs = codecs === "mp4a.40.02" ? "mp4a.40.2" : codecs;
      parsedRepresentation.codecs = codecs;
    }

    let supplementalCodecs: string | undefined;
    if (representation.attributes.supplementalCodecs !== undefined) {
      supplementalCodecs = representation.attributes.supplementalCodecs;
    } else if (adaptation.attributes.supplementalCodecs !== undefined) {
      supplementalCodecs = adaptation.attributes.supplementalCodecs;
    }
    if (supplementalCodecs !== undefined) {
      parsedRepresentation.supplementalCodecs =
        convertSupplementalCodecsToRFC6381(supplementalCodecs);
    }

    if (representation.attributes.frameRate !== undefined) {
      parsedRepresentation.frameRate = representation.attributes.frameRate;
    } else if (adaptation.attributes.frameRate !== undefined) {
      parsedRepresentation.frameRate = adaptation.attributes.frameRate;
    }
    if (representation.attributes.height !== undefined) {
      parsedRepresentation.height = representation.attributes.height;
    } else if (adaptation.attributes.height !== undefined) {
      parsedRepresentation.height = adaptation.attributes.height;
    }
    if (representation.attributes.mimeType !== undefined) {
      parsedRepresentation.mimeType = representation.attributes.mimeType;
    } else if (adaptation.attributes.mimeType !== undefined) {
      parsedRepresentation.mimeType = adaptation.attributes.mimeType;
    }
    if (representation.attributes.width !== undefined) {
      parsedRepresentation.width = representation.attributes.width;
    } else if (adaptation.attributes.width !== undefined) {
      parsedRepresentation.width = adaptation.attributes.width;
    }

    // Content Protection parsing
    {
      const contentProtIrArr = [
        ...(adaptation.children.contentProtections ?? []),
        ...(representation.children.contentProtections ?? []),
      ];
      for (const contentProtIr of contentProtIrArr) {
        context.contentProtectionParser.add(parsedRepresentation, contentProtIr);
      }
    }

    parsedRepresentation.hdrInfo = getHDRInformation({
      adaptationProfiles: adaptation.attributes.profiles,
      supplementalProperties: adaptation.children.supplementalProperties,
      essentialProperties: adaptation.children.essentialProperties,
      manifestProfiles: context.manifestProfiles,
      codecs,
    });

    parsedRepresentations.push(parsedRepresentation);
  }
  return parsedRepresentations;
}

/** Supplementary context needed to parse a Representation. */
export interface IRepresentationContext extends IInheritedRepresentationIndexContext {
  /** Manifest DASH profiles used for signalling some features */
  manifestProfiles?: string | undefined;
  /**
   * The parser should take this Adaptation - which is from a previously parsed
   * Manifest for the same dynamic content - as a base to speed-up the parsing
   * process.
   * /!\ If unexpected differences exist between both, there is a risk of
   * de-synchronization with what is actually on the server,
   * Use with moderation.
   */
  unsafelyBaseOnPreviousAdaptation: IAdaptation | null;
  /** Parses contentProtection elements. */
  contentProtectionParser: ContentProtectionParser;
}

/**
 * Supplementary context needed to parse a Representation common with
 * `IRepresentationIndexContext`.
 */
type IInheritedRepresentationIndexContext = Omit<
  IRepresentationIndexContext,
  "adaptation" | "unsafelyBaseOnPreviousRepresentation" | "inbandEventStreams"
>;
