import { ContainerConfig, Container } from '../Container';
import { Label, LabelConfig } from '../labels/Label';
import { UIInstanceManager } from '../../UIManager';
import { TvNoiseCanvas } from '../TvNoiseCanvas';
import { ErrorUtils } from '../../utils/ErrorUtils';
import { ErrorEvent, PlayerAPI, PlayerEventBase } from 'bitmovin-player';
import {
  isMobileV3PlayerAPI,
  MobileV3PlayerAPI,
  MobileV3PlayerErrorEvent,
  MobileV3PlayerEvent,
  MobileV3SourceErrorEvent,
} from '../../utils/MobileV3PlayerAPI';

export interface ErrorMessageTranslator {
  (error: ErrorEvent | MobileV3PlayerErrorEvent): string;
}

export interface ErrorMessageMap {
  [code: number]: string | ErrorMessageTranslator;
}

/**
 * Configuration interface for the {@link ErrorMessageOverlay}.
 *
 * @category Configs
 */
export interface ErrorMessageOverlayConfig extends ContainerConfig {
  /**
   * Allows overwriting of the error messages displayed in the overlay for customization and localization.
   * This is either a function that receives any {@link ErrorEvent} as parameter and translates error messages,
   * or a map of error codes that overwrites specific error messages with a plain string or a function that
   * receives the {@link ErrorEvent} as parameter and returns a customized string.
   * The translation functions can be used to extract data (e.g. parameters) from the original error message.
   *
   * Example 1 (catch-all translation function):
   * <code>
   * errorMessageOverlayConfig = {
   *   messages: function(error) {
   *     switch (error.code) {
   *       // Overwrite error 1000 'Unknown error'
   *       case 1000:
   *         return 'Houston, we have a problem'
   *
   *       // Transform error 1201 'The downloaded manifest is invalid' to uppercase
   *       case 1201:
   *         var description = ErrorUtils.defaultErrorMessages[error.code];
   *         return description.toUpperCase();
   *
   *       // Customize error 1207 'The manifest could not be loaded'
   *       case 1207:
   *         var statusCode = error.data.statusCode;
   *         return 'Manifest loading failed with HTTP error ' + statusCode;
   *     }
   *     // Return unmodified error message for all other errors
   *     return error.message;
   *   }
   * };
   * </code>
   *
   * Example 2 (translating specific errors):
   * <code>
   * errorMessageOverlayConfig = {
   *   messages: {
   *     // Overwrite error 1000 'Unknown error'
   *     1000: 'Houston, we have a problem',
   *
   *     // Transform error 1201 'Unsupported manifest format' to uppercase
   *     1201: function(error) {
   *       var description = ErrorUtils.defaultErrorMessages[error.code];
   *       return description.toUpperCase();
   *     },
   *
   *     // Customize error 1207 'The manifest could not be loaded'
   *     1207: function(error) {
   *       var statusCode = error.data.statusCode;
   *       return 'Manifest loading failed with HTTP error ' + statusCode;
   *     }
   *   }
   * };
   * </code>
   */
  messages?: ErrorMessageMap | ErrorMessageTranslator;
}

/**
 * Overlays the player and displays error messages.
 *
 * @category Components
 */
export class ErrorMessageOverlay extends Container<ErrorMessageOverlayConfig> {
  private errorLabel: Label<LabelConfig>;
  private tvNoiseBackground: TvNoiseCanvas;

  constructor(config: ErrorMessageOverlayConfig = {}) {
    super(config);

    this.errorLabel = new Label<LabelConfig>({ cssClass: 'ui-errormessage-label' });
    this.tvNoiseBackground = new TvNoiseCanvas();

    this.config = this.mergeConfig(
      config,
      {
        cssClass: 'ui-errormessage-overlay',
        components: [this.tvNoiseBackground, this.errorLabel],
        hidden: true,
        role: 'status',
      },
      this.config,
    );
  }

  configure(player: PlayerAPI | MobileV3PlayerAPI, uimanager: UIInstanceManager): void {
    super.configure(player, uimanager);

    const config = this.getConfig();

    const handleErrorMessage = (
      event: ErrorEvent | MobileV3SourceErrorEvent | MobileV3PlayerErrorEvent,
      message: string,
    ) => {
      const customizedMessage = customizeErrorMessage(uimanager.getConfig().errorMessages || config.messages, event);
      if (customizedMessage) {
        message = customizedMessage;
      }

      this.display(message);
    };

    if (isMobileV3PlayerAPI(player)) {
      const errorEventHandler = (event: MobileV3SourceErrorEvent | MobileV3PlayerErrorEvent) => {
        const message = ErrorUtils.defaultMobileV3ErrorMessageTranslator(event);
        handleErrorMessage(event, message);
      };

      player.on(MobileV3PlayerEvent.PlayerError, errorEventHandler);
      player.on(MobileV3PlayerEvent.SourceError, errorEventHandler);
    } else {
      player.on(player.exports.PlayerEvent.Error, (event: ErrorEvent) => {
        const message = ErrorUtils.defaultWebErrorMessageTranslator(event);
        handleErrorMessage(event, message);
      });
    }

    player.on(player.exports.PlayerEvent.SourceLoaded, (event: PlayerEventBase) => {
      if (this.isShown()) {
        this.clear();
      }
    });
  }

  display(errorMessage: string): void {
    this.errorLabel.setText(errorMessage);
    this.tvNoiseBackground.start();
    this.show();
  }

  private clear(): void {
    this.errorLabel.setText('');

    // Canvas rendering must be explicitly stopped, else it just continues forever and hogs resources
    this.tvNoiseBackground.stop();
    this.hide();
  }

  release(): void {
    super.release();

    this.clear();
  }
}

function customizeErrorMessage(
  errorMessages: ErrorMessageTranslator | ErrorMessageMap,
  event: ErrorEvent | MobileV3PlayerErrorEvent | MobileV3SourceErrorEvent,
): string | undefined {
  if (!errorMessages) {
    return undefined;
  }

  // Process message vocabularies
  if (typeof errorMessages === 'function') {
    // Translation function for all errors
    return errorMessages(event);
  }
  if (errorMessages[event.code]) {
    // It's not a translation function, so it must be a map of strings or translation functions
    const customMessage = errorMessages[event.code];

    return typeof customMessage === 'string' ? customMessage : customMessage(event);
  }
}
