/**
 * 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 {
  IMediaElement,
  IMediaKeySystemAccess,
  IMediaKeys,
} from "../../compat/browser_compatibility_types";
import canReuseMediaKeys from "../../compat/can_reuse_media_keys";
import type { IEmeApiImplementation } from "../../compat/eme";
import { EncryptedMediaError } from "../../errors";
import log from "../../log";
import type { IKeySystemOption } from "../../public_types";
import isNullOrUndefined from "../../utils/is_null_or_undefined";
import type { CancellationSignal } from "../../utils/task_canceller";
import getMediaKeySystemAccess from "./find_key_system";
import type { ICodecSupportList } from "./find_key_system";
import type { IMediaKeySessionStores } from "./types";
import LoadedSessionsStore from "./utils/loaded_sessions_store";
import MediaKeysAttacher from "./utils/media_keys_attacher";
import PersistentSessionsStore from "./utils/persistent_sessions_store";
import ServerCertificateStore from "./utils/server_certificate_store";

/**
 * @throws {EncryptedMediaError}
 * @param {Object} keySystemOptions
 * @returns {Object|null}
 */
function createPersistentSessionsStorage(
  keySystemOptions: IKeySystemOption,
): PersistentSessionsStore | null {
  const { persistentLicenseConfig } = keySystemOptions;
  if (isNullOrUndefined(persistentLicenseConfig)) {
    return null;
  }

  log.debug("DRM", "Set the given license storage");
  return new PersistentSessionsStore(persistentLicenseConfig);
}

/** Object returned by `getMediaKeysInfos`. */
export interface IMediaKeysInfos {
  /** The MediaKeySystemAccess which allowed to create the MediaKeys instance. */
  mediaKeySystemAccess: IMediaKeySystemAccess;
  /**
   * The MediaKeySystemConfiguration that has been provided to the
   * `requestMediaKeySystemAccess` API.
   */
  askedConfiguration: MediaKeySystemConfiguration;
  /** The MediaKeys instance. */
  mediaKeys: IMediaKeys;
  /** Stores allowing to create and retrieve MediaKeySessions. */
  stores: IMediaKeySessionStores;
  /** IKeySystemOption compatible to the created MediaKeys instance. */
  options: IKeySystemOption;
  /** The codecs support */
  codecSupport: ICodecSupportList;
}

/**
 * Create a MediaKeys instance and associated structures (or just return the
 * current ones if sufficient) based on a wanted configuration.
 * @param {Object} eme - current EME implementation
 * @param {HTMLMediaElement} mediaElement - The HTMLMediaElement on which you
 * will attach the MediaKeys instance.
 * This Element is here only used to check if the current MediaKeys and
 * MediaKeySystemAccess instances are sufficient
 * @param {Array.<Object>} keySystemsConfigs - The key system configuration.
 * Needed to ask the right MediaKeySystemAccess.
 * @param {Object} cancelSignal - CancellationSignal allowing to cancel the
 * creation of the MediaKeys instance while the task is still pending.
 * @returns {Promise.<Object>}
 */
export default async function getMediaKeysInfos(
  eme: IEmeApiImplementation,
  mediaElement: IMediaElement,
  keySystemsConfigs: IKeySystemOption[],
  cancelSignal: CancellationSignal,
): Promise<IMediaKeysInfos> {
  const evt = await getMediaKeySystemAccess(
    eme,
    mediaElement,
    keySystemsConfigs,
    cancelSignal,
  );
  if (cancelSignal.cancellationError !== null) {
    throw cancelSignal.cancellationError;
  }

  const { options, mediaKeySystemAccess, askedConfiguration, codecSupport } = evt.value;
  const currentState = await MediaKeysAttacher.getAttachedMediaKeysState(mediaElement);
  const persistentSessionsStore = createPersistentSessionsStorage(options);

  if (
    evt.value.options.reuseMediaKeys !== false &&
    canReuseMediaKeys() &&
    currentState !== null &&
    evt.type === "reuse-media-key-system-access"
  ) {
    log.debug("DRM", "Reusing already created MediaKeys");
    const { mediaKeys, loadedSessionsStore } = currentState;

    // We might just rely on the currently attached MediaKeys instance.
    // First check if server certificate parameters are the same than in the
    // current MediaKeys instance. If not, re-create MediaKeys from scratch.
    if (
      ServerCertificateStore.hasOne(mediaKeys) === false ||
      (!isNullOrUndefined(options.serverCertificate) &&
        ServerCertificateStore.has(mediaKeys, options.serverCertificate))
    ) {
      return {
        mediaKeys,
        mediaKeySystemAccess,
        askedConfiguration,
        stores: { loadedSessionsStore, persistentSessionsStore },
        options,
        codecSupport,
      };
    }
  }

  const mediaKeys = await createMediaKeys(mediaKeySystemAccess);
  log.info("DRM", "MediaKeys created with success");
  const loadedSessionsStore = new LoadedSessionsStore(mediaKeys);
  return {
    mediaKeys,
    mediaKeySystemAccess,
    askedConfiguration,
    stores: { loadedSessionsStore, persistentSessionsStore },
    options,
    codecSupport,
  };
}

/**
 * Create `MediaKeys` from the `MediaKeySystemAccess` given.
 * Throws the right formatted error if it fails.
 * @param {MediaKeySystemAccess} mediaKeySystemAccess
 * @returns {Promise.<MediaKeys>}
 */
async function createMediaKeys(
  mediaKeySystemAccess: IMediaKeySystemAccess,
): Promise<IMediaKeys> {
  log.info("DRM", "Calling createMediaKeys on the MediaKeySystemAccess");
  try {
    const mediaKeys = await mediaKeySystemAccess.createMediaKeys();
    return mediaKeys;
  } catch (error) {
    const message =
      error instanceof Error ? error.message : "Unknown error when creating MediaKeys.";
    throw new EncryptedMediaError("CREATE_MEDIA_KEYS_ERROR", message, {
      keyStatuses: undefined,
      keySystemConfiguration: mediaKeySystemAccess.getConfiguration(),
      keySystem: mediaKeySystemAccess.keySystem,
    });
  }
}
