/**
 * 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 { MediaError } from "../../errors";
import log from "../../log";
import type { IPeriodMetadata } from "../../manifest";
import type { IPeriod } from "../../public_types";
import arrayFindIndex from "../../utils/array_find_index";
import objectAssign from "../../utils/object_assign";
import type Period from "./period";
import { MANIFEST_UPDATE_TYPE } from "./types";
import type { IUpdatedPeriodResult } from "./update_period_in_place";
import updatePeriodInPlace from "./update_period_in_place";

/**
 * Update old periods by adding new periods and removing
 * not available ones.
 * @param {Array.<Object>} oldPeriods
 * @param {Array.<Object>} newPeriods
 * @returns {Object}
 */
export function replacePeriods(
  oldPeriods: Period[],
  newPeriods: Period[],
): IPeriodsUpdateResult {
  const res: IPeriodsUpdateResult = {
    updatedPeriods: [],
    addedPeriods: [],
    removedPeriods: [],
  };
  let firstUnhandledPeriodIdx = 0;
  for (let i = 0; i < newPeriods.length; i++) {
    const newPeriod = newPeriods[i];
    let j = firstUnhandledPeriodIdx;
    let oldPeriod = oldPeriods[j];
    while (oldPeriod !== undefined && oldPeriod.id !== newPeriod.id) {
      j++;
      oldPeriod = oldPeriods[j];
    }
    if (oldPeriod !== undefined) {
      const result = updatePeriodInPlace(oldPeriod, newPeriod, MANIFEST_UPDATE_TYPE.Full);
      res.updatedPeriods.push({
        period: {
          id: oldPeriod.id,
          start: oldPeriod.start,
          end: oldPeriod.end,
          duration: oldPeriod.duration,
          streamEvents: oldPeriod.streamEvents,
        },
        result,
      });
      const periodsToInclude = newPeriods.slice(firstUnhandledPeriodIdx, i);
      const nbrOfPeriodsToRemove = j - firstUnhandledPeriodIdx;
      const removed = oldPeriods.splice(
        firstUnhandledPeriodIdx,
        nbrOfPeriodsToRemove,
        ...periodsToInclude,
      );
      res.removedPeriods.push(
        ...removed.map((p) => ({
          id: p.id,
          start: p.start,
          end: p.end,
        })),
      );
      res.addedPeriods.push(...periodsToInclude.map((p) => p.getMetadataSnapshot()));
      firstUnhandledPeriodIdx = i + 1;
    }
  }

  if (firstUnhandledPeriodIdx > oldPeriods.length) {
    log.error("manifest", "error when updating Periods");
    return res;
  }
  if (firstUnhandledPeriodIdx < oldPeriods.length) {
    const removed = oldPeriods.splice(
      firstUnhandledPeriodIdx,
      oldPeriods.length - firstUnhandledPeriodIdx,
    );
    res.removedPeriods.push(
      ...removed.map((p) => ({
        id: p.id,
        start: p.start,
        end: p.end,
      })),
    );
  }
  const remainingNewPeriods = newPeriods.slice(
    firstUnhandledPeriodIdx,
    newPeriods.length,
  );
  if (remainingNewPeriods.length > 0) {
    oldPeriods.push(...remainingNewPeriods);
    res.addedPeriods.push(...remainingNewPeriods.map((p) => p.getMetadataSnapshot()));
  }
  return res;
}

/**
 * Update old periods by adding new periods and removing
 * not available ones.
 * @param {Array.<Object>} oldPeriods
 * @param {Array.<Object>} newPeriods
 * @returns {Object}
 */
export function updatePeriods(
  oldPeriods: Period[],
  newPeriods: Period[],
): IPeriodsUpdateResult {
  const res: IPeriodsUpdateResult = {
    updatedPeriods: [],
    addedPeriods: [],
    removedPeriods: [],
  };
  if (oldPeriods.length === 0) {
    oldPeriods.splice(0, 0, ...newPeriods);
    res.addedPeriods.push(...newPeriods.map((p) => p.getMetadataSnapshot()));
    return res;
  }
  if (newPeriods.length === 0) {
    return res;
  }
  const oldLastPeriod = oldPeriods[oldPeriods.length - 1];
  if (oldLastPeriod.start < newPeriods[0].start) {
    if (oldLastPeriod.end !== newPeriods[0].start) {
      throw new MediaError(
        "MANIFEST_UPDATE_ERROR",
        "Cannot perform partial update: not enough data",
      );
    }
    oldPeriods.push(...newPeriods);
    res.addedPeriods.push(...newPeriods.map((p) => p.getMetadataSnapshot()));
    return res;
  }

  /** Index, in `oldPeriods` of the first element of `newPeriods` */
  const indexOfNewFirstPeriod = arrayFindIndex(
    oldPeriods,
    ({ id }) => id === newPeriods[0].id,
  );
  if (indexOfNewFirstPeriod < 0) {
    throw new MediaError(
      "MANIFEST_UPDATE_ERROR",
      "Cannot perform partial update: incoherent data",
    );
  }

  // The first updated Period can only be a partial part
  const updateRes = updatePeriodInPlace(
    oldPeriods[indexOfNewFirstPeriod],
    newPeriods[0],
    MANIFEST_UPDATE_TYPE.Partial,
  );
  res.updatedPeriods.push({
    period: objectAssign(oldPeriods[indexOfNewFirstPeriod].getMetadataSnapshot(), {
      adaptations: undefined,
    }),
    result: updateRes,
  });

  // Search each consecutive elements of `newPeriods` - after the initial one already
  // processed - in `oldPeriods`, removing and adding unfound Periods in the process
  let prevIndexOfNewPeriod = indexOfNewFirstPeriod + 1;
  for (let i = 1; i < newPeriods.length; i++) {
    const newPeriod = newPeriods[i];
    let indexOfNewPeriod = -1;
    for (let j = prevIndexOfNewPeriod; j < oldPeriods.length; j++) {
      if (newPeriod.id === oldPeriods[j].id) {
        indexOfNewPeriod = j;
        break; // end the loop
      }
    }

    if (indexOfNewPeriod < 0) {
      // Next element of `newPeriods` not found: insert it
      let toRemoveUntil = -1;
      for (let j = prevIndexOfNewPeriod; j < oldPeriods.length; j++) {
        if (newPeriod.start < oldPeriods[j].start) {
          toRemoveUntil = j;
          break; // end the loop
        }
      }
      const nbElementsToRemove = toRemoveUntil - prevIndexOfNewPeriod;
      const removed = oldPeriods.splice(
        prevIndexOfNewPeriod,
        nbElementsToRemove,
        newPeriod,
      );
      res.addedPeriods.push(newPeriod.getMetadataSnapshot());
      res.removedPeriods.push(
        ...removed.map((p) => ({
          id: p.id,
          start: p.start,
          end: p.end,
        })),
      );
    } else {
      if (indexOfNewPeriod > prevIndexOfNewPeriod) {
        // Some old periods were not found: remove
        log.warn("manifest", "old Periods not found in new when updating, removing");
        const removed = oldPeriods.splice(
          prevIndexOfNewPeriod,
          indexOfNewPeriod - prevIndexOfNewPeriod,
        );
        res.removedPeriods.push(
          ...removed.map((p) => ({
            id: p.id,
            start: p.start,
            end: p.end,
          })),
        );
        indexOfNewPeriod = prevIndexOfNewPeriod;
      }

      // Later Periods can be fully replaced
      const result = updatePeriodInPlace(
        oldPeriods[indexOfNewPeriod],
        newPeriod,
        MANIFEST_UPDATE_TYPE.Full,
      );
      res.updatedPeriods.push({
        period: objectAssign(oldPeriods[indexOfNewPeriod].getMetadataSnapshot(), {
          adaptations: undefined,
        }),
        result,
      });
    }
    prevIndexOfNewPeriod++;
  }

  if (prevIndexOfNewPeriod < oldPeriods.length) {
    log.warn("manifest", "Ending Periods not found in new when updating, removing");
    const removed = oldPeriods.splice(
      prevIndexOfNewPeriod,
      oldPeriods.length - prevIndexOfNewPeriod,
    );
    res.removedPeriods.push(
      ...removed.map((p) => ({
        id: p.id,
        start: p.start,
        end: p.end,
      })),
    );
  }
  return res;
}

export type IUpdatePeriodInformation = Pick<
  IPeriodMetadata,
  "id" | "start" | "end" | "duration" | "streamEvents"
>;

/** Object describing a Manifest update at the Periods level. */
export interface IPeriodsUpdateResult {
  /** Information on Periods that have been updated. */
  updatedPeriods: Array<{
    /** The concerned Period's information. */
    period: IUpdatePeriodInformation;
    /** The updates performed. */
    result: IUpdatedPeriodResult;
  }>;
  /** Periods that have been added. */
  addedPeriods: IPeriodMetadata[];
  /** Periods that have been removed. */
  removedPeriods: IPeriod[];
}
