/**
 * 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 cancellableSleep from "../../utils/cancellable_sleep";
import TaskCanceller, { CancellationError } from "../../utils/task_canceller";
import type { IMediaKeySession } from "../browser_compatibility_types";

/**
 * Close the given `MediaKeySession` and returns a Promise resolving when the
 * session is closed.
 * This promise does not reject, even if we're unable to close the
 * `MediaKeySession`.
 *
 * Note that there is a lot of browser issues linked to the impossibility to
 * either close a MediaKeySession or to know if a MediaKeySession was closed.
 * Due to this, the returned Promise might take some time before resolving on
 * some devices.
 * @param {MediaKeySession|Object} session
 * @returns {Promise.<undefined>}
 */
export default function closeSession(session: IMediaKeySession): Promise<void> {
  const timeoutCanceller = new TaskCanceller("MediaKeySession close timeout");

  return Promise.race([
    session.close().then(() => {
      timeoutCanceller.cancel("MediaKeySession close method resolved");
    }),
    // The `closed` promise may resolve, even if `close()` result has not
    // (seen at some point on Firefox).
    session.closed.then(() => {
      timeoutCanceller.cancel("MediaKeySession closed property resolved");
    }),
    waitTimeoutAndCheck(),
  ]);

  /**
   * If the session is not closed after 1000ms, try to communicate with the
   * MediaKeySession and check if an error is returned.
   * This is needed because on some browsers with poor EME implementation,
   * knowing when a MediaKeySession is closed is actually a hard task.
   *
   * The returned Promise will never reject.
   * @returns {Promise.<undefined>}
   */
  async function waitTimeoutAndCheck(): Promise<void> {
    try {
      await cancellableSleep(1000, timeoutCanceller.signal);
      await tryUpdatingSession();
    } catch (err) {
      if (err instanceof CancellationError) {
        // cancelled == session closed
        return;
      }
      const message =
        err instanceof Error
          ? err.message
          : "Unknown error made it impossible to close the session";
      log.error(`DRM`, "Error made it impossible to close the session", `${message}`);
    }
  }

  /**
   * Try to update `MediaKeySession` and check its error if it failed.
   * If we still don't know whether it closed yet, wait a second
   * timeout then quit.
   *
   * The returned Promise resolves if the `MediaKeySession` seems closed and
   * rejects if we couldn't know or it doesn't.
   * @returns {Promise.<undefined>}
   */
  async function tryUpdatingSession(): Promise<void> {
    try {
      await session.update(new Uint8Array(1));
    } catch (err) {
      if (timeoutCanceller.isUsed()) {
        // Reminder: cancelled == session closed
        return;
      }

      // The caught error can tell if session is closed
      // (Chrome may throw this error)
      // I know... Checking the error message is not the best practice ever.
      if (err instanceof Error && err.message === "The session is already closed.") {
        return;
      }

      await cancellableSleep(1000, timeoutCanceller.signal);
    }

    if (timeoutCanceller.isUsed()) {
      // Reminder: cancelled == session closed
      return;
    }

    throw new Error("Compat: Couldn't know if session is closed");
  }
}
