import Error from '../../../../error/error';
import getLogger from '../../../../utils/logger';
import * as Utils from '../../../../utils/util';
import {RequestType} from '../../../../enums/request-type';
import {DrmScheme} from '../../../../drm/drm-scheme';
import { EventManager, ListenerType} from '../../../../event/event-manager';
import {PKDrmDataObject, PKRequestObject, PKResponseObject} from '../../../../types';

type WebkitEventsType = {[name: string]: string};

const KeySystem: string = 'com.apple.fps.1_0';
const WebkitEvents: WebkitEventsType = {
  NEED_KEY: 'webkitneedkey',
  KEY_MESSAGE: 'webkitkeymessage',
  KEY_ADDED: 'webkitkeyadded',
  KEY_ERROR: 'webkitkeyerror'
};

type FairPlayDrmConfigType = {licenseUrl: string, certificate?: string, network: {requestFilter?: (...args: any[]) => any, responseFilter: (...args: any[]) => any}, useKIDHeader?: boolean};

class FairPlayDrmHandler {
  public static WebkitEvents: WebkitEventsType = WebkitEvents;

  private _logger = getLogger('FairPlayDrmHandler');
  private _eventManager: EventManager;
  private _keySession: any;
  private _config: FairPlayDrmConfigType;
  private _onWebkitNeedKeyHandler: ListenerType;
  private _errorCallback: (...args: any[]) => any;
  private _drmResponseCallback: (...args: any[]) => any;
  private _videoElement: HTMLVideoElement;
  private _retryLicenseRequest: number = 4;
  private _licenseRequestTime: number | undefined;
  private _defaultConfig: FairPlayDrmConfigType = {
    licenseUrl: '',
    certificate: '',
    network: {
      responseFilter: (type, response) => {
        let responseObj: any = {};
        try {
          const dataView = new DataView(response.data);
          const decoder = new TextDecoder();
          const keyText = decoder.decode(dataView).trim();
          responseObj = JSON.parse(keyText);
        } catch (error) {
          this._onError(Error.Code.BAD_FAIRPLAY_RESPONSE, {
            error,
            responseText: response.data
          });
          return;
        }
        const isValidResponse = FairPlayDrmHandler._validateResponse(responseObj);
        if (isValidResponse.valid) {
          response.data = FairPlayDrmHandler._base64DecodeUint8Array(responseObj.ckc);
        } else {
          this._onError(Error.Code.BAD_FAIRPLAY_RESPONSE, isValidResponse);
        }
      }
    }
  };

  /**
   * Fairplay DRM handler
   * @param {HTMLVideoElement} videoElement - the video element
   * @param {FairPlayDrmConfigType} config - config object
   * @param {Function} errorCallback - error callback function
   * @param {Function} drmResponseCallback - drm license response callback function
   */
  constructor(videoElement: HTMLVideoElement, config: FairPlayDrmConfigType, errorCallback: (...args: any[]) => any, drmResponseCallback: (...args: any[]) => any) {
    this._config = Utils.Object.mergeDeep({}, this._defaultConfig, config);
    this._errorCallback = errorCallback;
    this._drmResponseCallback = drmResponseCallback;
    this._videoElement = videoElement;
    this._onWebkitNeedKeyHandler = (e: Event): void => this._onWebkitNeedKey(e);
    this._eventManager = new EventManager();
    this._eventManager.listen(this._videoElement, WebkitEvents.NEED_KEY, this._onWebkitNeedKeyHandler);
  }

  private _onWebkitNeedKey(event: any): void {
    this._logger.debug('Webkit need key triggered');
    const videoElement = event.target;
    let initData = event.initData;

    const contentId = FairPlayDrmHandler._extractContentId(initData, !!this._config.useKIDHeader);
    const fpsCertificate = FairPlayDrmHandler._base64DecodeUint8Array(this._config.certificate);

    initData = FairPlayDrmHandler._concatInitDataIdAndCertificate(initData, contentId, fpsCertificate);

    if (!videoElement.webkitKeys) {
      const keySystem = this._selectKeySystem();
      this._logger.debug('Sets media keys');
      videoElement.webkitSetMediaKeys(new WebKitMediaKeys(keySystem!));
    }
    if (!videoElement.webkitKeys) {
      this._onError(Error.Code.COULD_NOT_CREATE_MEDIA_KEYS);
    }
    this._logger.debug('Creates session');
    this._keySession = videoElement.webkitKeys.createSession('video/mp4', initData);
    if (!this._keySession) {
      this._onError(Error.Code.COULD_NOT_CREATE_KEY_SESSION);
    }
    this._keySession.contentId = contentId;
    this._eventManager.listen(this._keySession, WebkitEvents.KEY_MESSAGE, (e: Event) => this._onWebkitKeyMessage(e));
    this._eventManager.listen(this._keySession, WebkitEvents.KEY_ADDED, () => this._onWebkitKeyAdded());
    this._eventManager.listen(this._keySession, WebkitEvents.KEY_ERROR, (e: Event) => this._onWebkitKeyError(e));
  }

  public getDrmInfo(): PKDrmDataObject {
    const {certificate, licenseUrl} = this._config;
    return {certificate, licenseUrl, scheme: DrmScheme.FAIRPLAY};
  }

  public destroy(): void {
    this._eventManager.destroy();
    this._keySession.close();
    this._keySession = null;
  }

  private _onWebkitKeyMessage(event: any): void {
    this._logger.debug('Webkit key message triggered');
    const message = event.message;
    // WebkitMediaKeySession has attribute contentId (which is the mykeyid from playlist's skd://mykeyid)
    const session = event.target;
    const kid = session?.contentId;
    const kidBase64 = (kid !== undefined && kid !== null) ? btoa(kid) : '';

    const request = new XMLHttpRequest();
    request.responseType = 'arraybuffer';
    this._eventManager.listenOnce(request, 'load', (e: Event) => this._licenseRequestLoaded(e));
    const pkRequest: PKRequestObject = {
      url: this._config.licenseUrl,
      body: FairPlayDrmHandler._base64EncodeUint8Array(message),
      headers: this._config.useKIDHeader ? {'X-Kaltura-InitData': kidBase64} : {} // pass the kid to uDRM
    };
    let requestFilterPromise;
    const requestFilter = this._config.network.requestFilter;
    if (requestFilter) {
      this._logger.debug('Apply request filter');
      try {
        requestFilterPromise = requestFilter(RequestType.LICENSE, pkRequest);
      } catch (error) {
        requestFilterPromise = Promise.reject(error);
      }
    }
    requestFilterPromise = requestFilterPromise || Promise.resolve(pkRequest);
    requestFilterPromise
      .then(updatedRequest => {
        request.open('POST', updatedRequest.url, true);
        let setContentType = true;
        if (updatedRequest.headers) {
          Object.entries(updatedRequest.headers).forEach(([header, value]) => {
            typeof value === 'string' && request.setRequestHeader(header, value);
            setContentType && (setContentType = header.toLowerCase() !== 'content-type');
          });
        }
        if (typeof updatedRequest.withCredentials === 'boolean') {
          request.withCredentials = updatedRequest.withCredentials;
        }
        setContentType && request.setRequestHeader('Content-type', 'application/json');
        this._logger.debug('Ready for license request');
        request.onerror = (): void => {
          this._onError(Error.Code.LICENSE_REQUEST_FAILED, {
            status: request.status,
            responseText: request.responseText
          });
        };
        this._licenseRequestTime = Date.now();
        request.send(updatedRequest.body);
      })
      .catch(error => {
        this._errorCallback(
          new Error(
            Error.Severity.CRITICAL,
            Error.Category.NETWORK,
            Error.Code.REQUEST_FILTER_ERROR,
            error
          )
        );
        this.destroy();
      });
  }

  private _onWebkitKeyAdded(): void {
    this._logger.debug('Decryption key was added to session');
  }

  private _onWebkitKeyError(e: any): void {
    this._logger.error('A decryption key error was encountered', e);
    if (this._retryLicenseRequest <= 0) {
      this._onError(Error.Code.LICENSE_REQUEST_FAILED, e.target.error);
    }
    this._retryLicenseRequest--;
  }

  private _licenseRequestLoaded(event: any): void {
    this._logger.debug('License request loaded');
    const request = event.target;
    if (request.status > 299) {
      this._onError(Error.Code.LICENSE_REQUEST_FAILED, {
        status: request.status,
        error: request.responseText
      });
      return;
    }
    if (this._drmResponseCallback) {
      const licenseTime = Date.now() - this._licenseRequestTime!;
      this._drmResponseCallback({licenseTime: licenseTime / 1000, scheme: DrmScheme.FAIRPLAY});
    }
    const {responseURL: url, response: data} = request;
    const originalUrl = this._config.licenseUrl;
    const headers = Utils.Http.convertHeadersToDictionary(request.getAllResponseHeaders());

    const pkResponse: PKResponseObject = {url, originalUrl, data, headers};
    this._logger.debug('Apply response filter');
    let responseFilterPromise;
    try {
      responseFilterPromise = this._config.network.responseFilter(RequestType.LICENSE, pkResponse);
    } catch (error) {
      responseFilterPromise = Promise.reject(error);
    }
    responseFilterPromise = responseFilterPromise || Promise.resolve(pkResponse);
    responseFilterPromise
      .then(updatedResponse => {
        this._keySession.update(updatedResponse.data);
      })
      .catch(error => {
        this._errorCallback(
          new Error(
            Error.Severity.CRITICAL,
            Error.Category.NETWORK,
            Error.Code.RESPONSE_FILTER_ERROR,
            error
          )
        );
        this.destroy();
      });
  }

  private _onError(code: number, data?: any): void {
    this._errorCallback(new Error(Error.Severity.CRITICAL, Error.Category.DRM, code, data));
  }

  public static _validateResponse(responseObj: any): any {
    if ((responseObj.message && responseObj.message.indexOf('error') > 0) || responseObj.reference === null || responseObj.status_code === 500) {
      return {
        //todo: create & edit an error object
        valid: false,
        details: 'internal server error' // would be ERROR.INTERNAL or something like that
      };
    } else if (responseObj.ckc === '') {
      return {
        valid: false,
        details: 'ckc is missing' // would be ERROR.MISSING_CKC or something like that
      };
    } else {
      return {
        valid: true
      };
    }
  }

  private _selectKeySystem(): string | null {
    let keySystem: string | null = null;
    if (WebKitMediaKeys.isTypeSupported(KeySystem, 'video/mp4')) {
      keySystem = KeySystem;
    } else {
      this._logger.warn('Key System not supported');
    }
    return keySystem;
  }

  public static _extractContentId(initData: Uint8Array, useKIDHeader: boolean): string {
    if (useKIDHeader) {
      const url = FairPlayDrmHandler._arrayToString(initData);
      const hostname = url.slice(url.lastIndexOf("skd://")+6,url.length); // handle the skd://mykeyid format
      return hostname;
    } else {
      const link = document.createElement('a');
      link.href = FairPlayDrmHandler._arrayToString(initData);
      return link.hostname;
    }
  }

  public static _arrayToString(array: Uint8Array): string {
    return String.fromCharCode.apply(null, new Uint16Array(array.buffer));
  }

  public static _base64DecodeUint8Array(input?: string): Uint8Array {
    const raw = window.atob(input!);
    const rawLength = raw.length;
    const array = new Uint8Array(new ArrayBuffer(rawLength));
    for (let i = 0; i < rawLength; i++) {
      array[i] = raw.charCodeAt(i);
    }
    return array;
  }

  public static _concatInitDataIdAndCertificate(initData: Uint8Array, id: string | Uint16Array, cert: Uint8Array): Uint8Array {
    if (typeof id === 'string') {
      id = FairPlayDrmHandler._stringToArray(id);
    }
    let offset = 0;
    const buffer = new ArrayBuffer(initData.byteLength + 4 + id.byteLength + 4 + cert.byteLength);
    const dataView = new DataView(buffer);

    const initDataArray = new Uint8Array(buffer, offset, initData.byteLength);
    initDataArray.set(initData);
    offset += initData.byteLength;

    dataView.setUint32(offset, id.byteLength, true);
    offset += 4;

    const idArray = new Uint8Array(buffer, offset, id.byteLength);
    idArray.set(id);
    offset += idArray.byteLength;

    dataView.setUint32(offset, cert.byteLength, true);
    offset += 4;

    const certArray = new Uint8Array(buffer, offset, cert.byteLength);
    certArray.set(cert);

    return new Uint8Array(buffer, 0, buffer.byteLength);
  }

  public static _stringToArray(string: string): Uint16Array {
    const buffer = new ArrayBuffer(string.length * 2);
    const array = new Uint16Array(buffer);
    for (let i = 0, strLen = string.length; i < strLen; i++) {
      array[i] = string.charCodeAt(i);
    }
    return array;
  }

  public static _base64EncodeUint8Array(input: Uint8Array): string {
    const keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
    let output = '';
    let chr1, chr2, chr3, enc1, enc2, enc3, enc4;
    let i = 0;
    while (i < input.length) {
      chr1 = input[i++];
      chr2 = i < input.length ? input[i++] : Number.NaN;
      chr3 = i < input.length ? input[i++] : Number.NaN;

      enc1 = chr1 >> 2;
      enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
      enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
      enc4 = chr3 & 63;

      if (isNaN(chr2)) {
        enc3 = enc4 = 64;
      } else if (isNaN(chr3)) {
        enc4 = 64;
      }
      output += keyStr.charAt(enc1) + keyStr.charAt(enc2) + keyStr.charAt(enc3) + keyStr.charAt(enc4);
    }
    return output;
  }
}

FairPlayDrmHandler.WebkitEvents = WebkitEvents;

export {FairPlayDrmHandler};
export type {FairPlayDrmConfigType};
