/**
 * 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 type {
  IMediaSource,
  ISourceBuffer,
  ISourceBufferList,
} from "../../compat/browser_compatibility_types";
import {
  onRemoveSourceBuffers,
  onSourceOpen,
  onSourceBufferUpdate,
} from "../../compat/event_listeners";
import log from "../../log";
import type { CancellationSignal } from "../../utils/task_canceller";
import TaskCanceller from "../../utils/task_canceller";

/**
 * Get "updating" SourceBuffers from a SourceBufferList.
 * @param {SourceBufferList} sourceBuffers
 * @returns {Array.<SourceBuffer>}
 */
function getUpdatingSourceBuffers(sourceBuffers: ISourceBufferList): ISourceBuffer[] {
  const updatingSourceBuffers: ISourceBuffer[] = [];
  for (let i = 0; i < sourceBuffers.length; i++) {
    const SourceBuffer = sourceBuffers[i];
    if (SourceBuffer.updating) {
      updatingSourceBuffers.push(SourceBuffer);
    }
  }
  return updatingSourceBuffers;
}

/**
 * Trigger the `endOfStream` method of a MediaSource.
 *
 * If the MediaSource is ended/closed, do not call this method.
 * If SourceBuffers are updating, wait for them to be updated before closing
 * it.
 * @param {MediaSource} mediaSource
 * @param {Object} cancelSignal
 */
export default function triggerEndOfStream(
  mediaSource: IMediaSource,
  cancelSignal: CancellationSignal,
): void {
  log.debug("mse", "Trying to call endOfStream");
  if (mediaSource.readyState !== "open") {
    log.debug("mse", "MediaSource not open, cancel endOfStream");
    return;
  }

  const { sourceBuffers } = mediaSource;
  const updatingSourceBuffers = getUpdatingSourceBuffers(sourceBuffers);

  if (updatingSourceBuffers.length === 0) {
    log.info("mse", "Triggering end of stream");
    try {
      mediaSource.endOfStream();
    } catch (err) {
      log.error(
        "mse",
        "Unable to call endOfStream",
        err instanceof Error ? err : new Error("Unknown error"),
      );
    }
    return;
  }

  log.debug("mse", "Waiting SourceBuffers to be updated before calling endOfStream.");

  const innerCanceller = new TaskCanceller("EndOfStream current iteration");
  innerCanceller.linkToSignal(cancelSignal);
  for (const sourceBuffer of updatingSourceBuffers) {
    onSourceBufferUpdate(
      sourceBuffer,
      () => {
        innerCanceller.cancel("SourceBuffer update");
        triggerEndOfStream(mediaSource, cancelSignal);
      },
      innerCanceller.signal,
    );
  }

  onRemoveSourceBuffers(
    sourceBuffers,
    () => {
      innerCanceller.cancel("SourceBuffer remove");
      triggerEndOfStream(mediaSource, cancelSignal);
    },
    innerCanceller.signal,
  );
}

/**
 * Trigger the `endOfStream` method of a MediaSource each times it opens.
 * @see triggerEndOfStream
 * @param {MediaSource} mediaSource
 * @param {Object} cancelSignal
 */
export function maintainEndOfStream(
  mediaSource: IMediaSource,
  cancelSignal: CancellationSignal,
): void {
  let endOfStreamCanceller = new TaskCanceller("EndOfStream");
  endOfStreamCanceller.linkToSignal(cancelSignal);
  onSourceOpen(
    mediaSource,
    () => {
      log.debug("mse", "MediaSource re-opened while end-of-stream is active");
      endOfStreamCanceller.cancel("MediaSource re-opened");
      endOfStreamCanceller = new TaskCanceller("EndOfStream");
      endOfStreamCanceller.linkToSignal(cancelSignal);
      triggerEndOfStream(mediaSource, endOfStreamCanceller.signal);
    },
    cancelSignal,
  );
  triggerEndOfStream(mediaSource, endOfStreamCanceller.signal);
}
