import { EventEmitter, ListenerFn } from "eventemitter3";
import { Howl, Howler } from "howler/dist/howler.core.min.js";

import { beepSound } from "./assets/base64assets";

import { userLicenseKey } from "../index";
import { BarcodePickerCameraManager } from "./barcodePickerCameraManager";
import { BarcodePickerGui } from "./barcodePickerGui";
import { BrowserCompatibility } from "./browserCompatibility";
import { BrowserHelper } from "./browserHelper";
import { Camera } from "./camera";
import { CameraManager } from "./cameraManager";
import { CameraSettings } from "./cameraSettings";
import { CustomError } from "./customError";
import { DummyCameraManager } from "./dummyCameraManager";
import { ImageSettings } from "./imageSettings";
import { Parser } from "./parser";
import { Scanner } from "./scanner";
import { ScanResult } from "./scanResult";
import { ScanSettings } from "./scanSettings";
import { SearchArea } from "./searchArea";
import { UnsupportedBrowserError } from "./unsupportedBrowserError";

/**
 * @hidden
 */
type EventName = "ready" | "submitFrame" | "processFrame" | "scan" | "scanError";

/**
 * @hidden
 */
class BarcodePickerEventEmitter extends EventEmitter<EventName> {}

/**
 * A barcode picker element used to get and show camera input and perform scanning operations.
 *
 * The barcode picker will automatically fit and scale inside the given *originElement*.
 *
 * Each barcode picker internally contains a [[Scanner]] object with its own WebWorker thread running a
 * separate copy of the external Scandit Engine library. To optimize loading times and performance it's
 * recommended to reuse the same picker and to already create the picker in advance (hidden) and just
 * display it when needed whenever possible.
 *
 * As the loading of the external Scandit Engine library can take some time the picker always starts inactive
 * (but showing GUI and video) and then activates, if not paused, as soon as the library is ready to scan.
 * The [[on]] method targeting the [[ready]] event can be used to set up a listener function to be called when the
 * library is loaded.
 *
 * The picker can also operate in "single image mode", letting the user click/tap to take a single image to be scanned
 * via the camera (mobile/tablet) or a file select dialog (desktop). This is provided automatically as fallback by
 * default when the OS/browser only supports part of the needed features and cannot provide direct access to the camera
 * for video streaming and continuous scanning, or can also be forced. This behaviour can be set up on creation. Note
 * that in this mode some of the functions provided by the picker will have no effect.
 *
 * By default an alert is shown if an internal error during scanning is encountered which prevents the scanning
 * procedure from continuing when running on a local IP address. As this uses the built-in [[scanError]] event
 * functionality, if unwanted it can be disabled by calling [[removeAllListeners]] on the BarcodePicker
 * instance (right after creation).
 *
 * You are not allowed to hide the Scandit logo present in the corner of the GUI.
 */
export class BarcodePicker {
  private readonly cameraManager: CameraManager;
  private readonly barcodePickerGui: BarcodePickerGui;
  private readonly eventEmitter: BarcodePickerEventEmitter;
  private readonly scanner: Scanner;
  private readonly beepSound: Howl;
  private readonly vibrateFunction: (pattern: number | number[]) => boolean;
  private readonly scannerReadyEventListener: () => void;

  private playSoundOnScan: boolean;
  private vibrateOnScan: boolean;
  private scanningPaused: boolean;
  private fatalError: Error;
  private latestVideoTimeProcessed: number;
  private destroyed: boolean;
  private isReadyToWork: boolean;
  private cameraAccess: boolean;
  private targetScanningFPS: number;
  private averageProcessingTime: number;

  private constructor(
    originElement: HTMLElement,
    {
      visible,
      singleImageMode,
      playSoundOnScan,
      vibrateOnScan,
      scanningPaused,
      guiStyle,
      videoFit,
      laserArea,
      viewfinderArea,
      scanner,
      scanSettings,
      targetScanningFPS,
      hideLogo
    }: {
      visible: boolean;
      singleImageMode: boolean;
      playSoundOnScan: boolean;
      vibrateOnScan: boolean;
      scanningPaused: boolean;
      guiStyle: BarcodePicker.GuiStyle;
      videoFit: BarcodePicker.ObjectFit;
      laserArea?: SearchArea;
      viewfinderArea?: SearchArea;
      scanner?: Scanner;
      scanSettings: ScanSettings;
      targetScanningFPS: number;
      hideLogo: boolean;
    }
  ) {
    this.isReadyToWork = false;
    this.destroyed = false;
    this.scanningPaused = scanningPaused;

    Howler.autoSuspend = false;
    this.beepSound = new Howl({
      src: beepSound
    });

    // istanbul ignore else
    if (navigator.vibrate != null) {
      this.vibrateFunction = navigator.vibrate;
    } else if (navigator.webkitVibrate != null) {
      this.vibrateFunction = navigator.webkitVibrate;
    } else if (navigator.mozVibrate != null) {
      this.vibrateFunction = navigator.mozVibrate;
    } else if (navigator.msVibrate != null) {
      this.vibrateFunction = navigator.msVibrate;
    }

    this.eventEmitter = new EventEmitter();

    this.setPlaySoundOnScanEnabled(playSoundOnScan);
    this.setVibrateOnScanEnabled(vibrateOnScan);
    this.setTargetScanningFPS(targetScanningFPS);

    if (scanner == null) {
      this.scanner = new Scanner({ scanSettings });
    } else {
      this.scanner = scanner;
      this.scanner.applyScanSettings(scanSettings);
    }
    this.scannerReadyEventListener = this.handleScannerReady.bind(this);
    this.scanner.on("ready", this.scannerReadyEventListener);

    this.barcodePickerGui = new BarcodePickerGui({
      scanner: this.scanner,
      originElement,
      singleImageMode,
      scanningPaused,
      visible,
      guiStyle,
      videoFit,
      hideLogo,
      laserArea,
      viewfinderArea,
      cameraUploadCallback: this.processVideoFrame.bind(this, true)
    });

    if (singleImageMode) {
      this.cameraManager = new DummyCameraManager();
    } else {
      this.cameraManager = new BarcodePickerCameraManager(this.triggerFatalError.bind(this), this.barcodePickerGui);
      this.scheduleVideoProcessing();
    }

    this.barcodePickerGui.setCameraManager(this.cameraManager);
  }

  /**
   * Create a [[BarcodePicker]] instance, creating the needed HTML in the given origin element.
   * If the *accessCamera* option is enabled (active by default) and the picker is not in "single image mode",
   * the available cameras are accessed and camera access permission is requested to the user if needed.
   * This object expects that at least a camera is available. The active camera is accessed and kept active during the
   * lifetime of the picker (also when hidden or scanning is paused), and is only released when [[destroy]] is called.
   *
   * It is required to having configured the library via [[configure]] before this object can be created.
   *
   * The "single image mode" behaviour of the picker can be set up via the
   * *singleImageMode* option, which accepts a configuration object of the form:
   * ```
   * {
   *   desktop: {
   *     always: false, allowFallback: true
   *   },
   *   mobile: {
   *     always: false, allowFallback: true
   *   }
   * }
   * ```
   *
   * Depending on parameters, device features and user permissions for camera access, any of the following errors
   * could be the rejected result of the returned promise:
   * - `LibraryNotConfiguredError`
   * - `NoOriginElementError`
   * - `UnsupportedBrowserError`
   * - `PermissionDeniedError`
   * - `NotAllowedError`
   * - `NotFoundError`
   * - `AbortError`
   * - `NotReadableError`
   * - `InternalError`
   * - `NoCameraAvailableError`
   *
   * @param originElement The HTMLElement inside which all the necessary elements for the picker will be added.
   * @param visible <div class="tsd-signature-symbol">Default =&nbsp;true</div>
   * Whether the picker starts in a visible state.
   * @param singleImageMode <div class="tsd-signature-symbol">Default =&nbsp;
   * { desktop: { always: false, allowFallback: true }, mobile: { always: false, allowFallback: true } }</div>
   * Whether to provide a UI to pick/snap a single image from the camera instead of accessing and using the persistent
   * video stream from a camera ("force"), or to allow to provide this as a fallback ("allowFallback") in case the
   * necessary features for direct camera access are not provided by the OS/browser.
   * @param playSoundOnScan <div class="tsd-signature-symbol">Default =&nbsp;false</div>
   * Whether a sound is played on barcode recognition (iOS requires user input).
   * @param vibrateOnScan <div class="tsd-signature-symbol">Default =&nbsp;false</div>
   * Whether the device vibrates on barcode recognition (only Chrome & Firefox, requires user input).
   * @param scanningPaused <div class="tsd-signature-symbol">Default =&nbsp;false</div>
   * Whether the picker starts in a paused scanning state.
   * @param guiStyle <div class="tsd-signature-symbol">Default =&nbsp;GuiStyle.LASER</div>
   * The GUI style for the picker.
   * @param videoFit <div class="tsd-signature-symbol">Default =&nbsp;ObjectFit.CONTAIN</div>
   * The fit type for the video element of the picker.
   * @param laserArea <div class="tsd-signature-symbol">Default =&nbsp;undefined</div>
   * The area of the laser displayed when the GUI style is set to *laser* (the laser will match the width and be
   * vertically centered), by default the area will match the current [[ScanSettings]]'s *searchArea* option.
   * @param viewfinderArea <div class="tsd-signature-symbol">Default =&nbsp;undefined</div>
   * The area of the viewfinder displayed when the GUI style is set to *viewfinder*, by default the area will match
   * the current [[ScanSettings]]'s *searchArea* option.
   * @param enableCameraSwitcher <div class="tsd-signature-symbol">Default =&nbsp;true</div>
   * Whether to show a GUI button to switch between different cameras (when available).
   * @param enableTorchToggle <div class="tsd-signature-symbol">Default =&nbsp;true</div>
   * Whether to show a GUI button to toggle device torch on/off (when available, only Chrome).
   * @param enableTapToFocus <div class="tsd-signature-symbol">Default =&nbsp;true</div>
   * Whether to trigger a manual focus of the camera when clicking/tapping on the video (when available, only Chrome).
   * @param enablePinchToZoom <div class="tsd-signature-symbol">Default =&nbsp;true</div>
   * Whether to control the zoom of the camera when doing a pinching gesture on the video (when available, only Chrome).
   * @param accessCamera <div class="tsd-signature-symbol">Default =&nbsp;true</div>
   * Whether to immediately access the camera (and requesting user permissions if needed) on picker creation.
   * @param camera <div class="tsd-signature-symbol">Default =&nbsp;undefined</div>
   * The camera to be used for video input, if not specified the back or only camera will be used.
   * @param cameraSettings <div class="tsd-signature-symbol">Default =&nbsp;undefined</div>
   * The camera options used when accessing the camera, by default HD resolution is used.
   * @param scanner <div class="tsd-signature-symbol">Default =&nbsp;undefined</div>
   * The scanner object responsible for scanning via the external Scandit Engine library
   * (a new scanner will be created and initialized if not provided).
   * @param scanSettings <div class="tsd-signature-symbol">Default =&nbsp;new ScanSettings()</div>
   * The configuration object for scanning options to be applied to the scanner (all symbologies disabled by default).
   * @param targetScanningFPS <div class="tsd-signature-symbol">Default =&nbsp;30</div>
   * The target frames per second to be processed, the final speed is limited by the camera framerate (usually 30 FPS)
   * and the frame processing time of the device. By setting this to lower numbers devices can save power by performing
   * less work during scanning operations, depending on device speed (faster devices can "sleep" for longer periods).
   * Must be a number bigger than 0.
   * @returns A promise resolving to the created ready [[BarcodePicker]] object.
   */
  public static create(
    originElement: HTMLElement,
    {
      visible = true,
      singleImageMode = {
        desktop: { always: false, allowFallback: true },
        mobile: { always: false, allowFallback: true }
      },
      playSoundOnScan = false,
      vibrateOnScan = false,
      scanningPaused = false,
      guiStyle = BarcodePicker.GuiStyle.LASER,
      videoFit = BarcodePicker.ObjectFit.CONTAIN,
      laserArea,
      viewfinderArea,
      scanner,
      scanSettings = new ScanSettings(),
      enableCameraSwitcher = true,
      enableTorchToggle = true,
      enableTapToFocus = true,
      enablePinchToZoom = true,
      accessCamera = true,
      camera,
      cameraSettings,
      targetScanningFPS = 30
    }: {
      visible?: boolean;
      singleImageMode?: {
        desktop: { always: boolean; allowFallback: boolean };
        mobile: { always: boolean; allowFallback: boolean };
      };
      playSoundOnScan?: boolean;
      vibrateOnScan?: boolean;
      scanningPaused?: boolean;
      guiStyle?: BarcodePicker.GuiStyle;
      videoFit?: BarcodePicker.ObjectFit;
      laserArea?: SearchArea;
      viewfinderArea?: SearchArea;
      scanner?: Scanner;
      scanSettings?: ScanSettings;
      enableCameraSwitcher?: boolean;
      enableTorchToggle?: boolean;
      enableTapToFocus?: boolean;
      enablePinchToZoom?: boolean;
      accessCamera?: boolean;
      camera?: Camera;
      cameraSettings?: CameraSettings;
      targetScanningFPS?: number;
    } = {}
  ): Promise<BarcodePicker> {
    let singleImageModeForced: boolean;
    let singleImageModeFallbackAllowed: boolean;
    const deviceType: string | undefined = BrowserHelper.userAgentInfo.getDevice().type;
    if (deviceType != null && ["mobile", "tablet"].includes(deviceType)) {
      singleImageModeForced = singleImageMode.mobile.always;
      singleImageModeFallbackAllowed = singleImageMode.mobile.allowFallback;
    } else {
      singleImageModeForced = singleImageMode.desktop.always;
      singleImageModeFallbackAllowed = singleImageMode.desktop.allowFallback;
    }

    const browserCompatibility: BrowserCompatibility = BrowserHelper.checkBrowserCompatibility();
    if (
      !browserCompatibility.scannerSupport ||
      (!singleImageModeForced && !singleImageModeFallbackAllowed && !browserCompatibility.fullSupport)
    ) {
      return Promise.reject(new UnsupportedBrowserError(browserCompatibility));
    }

    if (userLicenseKey == null) {
      return Promise.reject(
        new CustomError({
          name: "LibraryNotConfiguredError",
          message: "The library has not correctly been configured yet, please call 'configure' with valid parameters"
        })
      );
    }
    if (!BrowserHelper.isValidHTMLElement(originElement)) {
      return Promise.reject(
        new CustomError({
          name: "NoOriginElementError",
          message: "A valid origin HTML element must be given"
        })
      );
    }

    const barcodePicker: BarcodePicker = new BarcodePicker(originElement, {
      visible,
      singleImageMode: browserCompatibility.fullSupport ? singleImageModeForced : true,
      playSoundOnScan,
      vibrateOnScan,
      scanningPaused,
      guiStyle,
      videoFit,
      laserArea,
      viewfinderArea,
      scanner,
      scanSettings,
      targetScanningFPS,
      // tslint:disable-next-line:use-named-parameter
      hideLogo: arguments[1] == null ? false : arguments[1].hideLogo === true // Hidden parameter
    });

    barcodePicker.cameraManager.setInteractionOptions(
      enableCameraSwitcher,
      enableTorchToggle,
      enableTapToFocus,
      enablePinchToZoom
    );
    barcodePicker.cameraManager.setSelectedCamera(camera);
    barcodePicker.cameraManager.setSelectedCameraSettings(cameraSettings);

    barcodePicker.cameraAccess = accessCamera;

    // Show error in alert on ScanError by default when running on local IP address for easier customer debugging
    barcodePicker.on("scanError", error => {
      // istanbul ignore if
      if (["localhost", "127.0.0.1", ""].includes(window.location.hostname)) {
        alert(error);
      }
    });

    if (accessCamera) {
      return barcodePicker.cameraManager.setupCameras().then(() => {
        return barcodePicker;
      });
    }

    return Promise.resolve(barcodePicker);
  }

  /**
   * Stop scanning and displaying video output, remove HTML elements added to the page,
   * destroy the internal [[Scanner]] (by default) and destroy the barcode picker itself; ensuring complete cleanup.
   *
   * This method should be called after you don't plan to use the picker anymore,
   * before the object is automatically cleaned up by JavaScript.
   * The barcode picker must not be used in any way after this call.
   *
   * If the [[Scanner]] is or will be in use for other purposes, the relative option can be passed to prevent
   * its destruction.
   *
   * @param destroyScanner Whether to destroy the internally used [[Scanner]] or not.
   */
  public destroy(destroyScanner: boolean = true): void {
    this.pauseScanning(true);
    this.scanner.removeListener("ready", this.scannerReadyEventListener);
    this.destroyed = true;
    if (destroyScanner) {
      this.scanner.destroy();
    }
    this.barcodePickerGui.destroy();
    this.eventEmitter.removeAllListeners();
  }

  /**
   * Apply a new set of scan settings to the internal scanner (replacing old settings).
   *
   * @param scanSettings The scan configuration object to be applied to the scanner.
   * @returns The updated [[BarcodePicker]] object.
   */
  public applyScanSettings(scanSettings: ScanSettings): BarcodePicker {
    this.scanner.applyScanSettings(scanSettings);

    return this;
  }

  /**
   * @returns Whether the scanning is currently paused.
   */
  public isScanningPaused(): boolean {
    return this.scanningPaused;
  }

  /**
   * Pause the recognition of codes in the input image.
   *
   * By default video from the camera is still shown, if the *pauseCamera* option is enabled the camera stream
   * is paused (camera access is fully interrupted) and will be resumed when calling [[resumeScanning]] or
   * [[accessCamera]], possibly requesting user permissions if needed.
   *
   * In "single image mode" the input for submitting a picture is disabled.
   *
   * @param pauseCamera Whether to also pause the camera stream.
   * @returns The updated [[BarcodePicker]] object.
   */
  public pauseScanning(pauseCamera: boolean = false): BarcodePicker {
    this.scanningPaused = true;

    if (pauseCamera) {
      this.cameraManager.stopStream();
    }

    if (this.scanner.isReady()) {
      this.barcodePickerGui.pauseScanning();
    }

    return this;
  }

  /**
   * Resume the recognition of codes in the input image.
   *
   * If the camera stream was stopped when calling [[pauseScanning]], the camera stream is also resumed and
   * user permissions are requested if needed to resume video input.
   *
   * In "single image mode" the input for submitting a picture is enabled.
   *
   * @returns The updated [[BarcodePicker]] object.
   */
  public async resumeScanning(): Promise<BarcodePicker> {
    this.scanningPaused = false;

    if (this.scanner.isReady()) {
      this.barcodePickerGui.resumeScanning();
    }

    if (this.cameraManager.activeCamera == null && this.cameraAccess) {
      await this.cameraManager.setupCameras();
    }

    return this;
  }

  /**
   * @returns The currently active camera.
   */
  public getActiveCamera(): Camera | undefined {
    return this.cameraManager.activeCamera;
  }

  /**
   * Select a camera to be used for video input, if no camera is passed, the default one is selected.
   *
   * If camera access is enabled, the camera is enabled and accessed.
   *
   * Depending on device features and user permissions for camera access, any of the following errors
   * could be the rejected result of the returned promise:
   * - `PermissionDeniedError`
   * - `NotAllowedError`
   * - `NotFoundError`
   * - `AbortError`
   * - `NotReadableError`
   * - `InternalError`
   * - `NoCameraAvailableError`
   *
   * In "single image mode" this method has no effect.
   *
   * @param camera The new camera to be used, by default the automatically detected back camera is used.
   * @param cameraSettings The camera options used when accessing the camera, by default HD resolution is used.
   * @returns A promise resolving to the updated [[BarcodePicker]] object when the camera is set
   * (and accessed, if camera access is currently enabled).
   */
  public async setActiveCamera(camera?: Camera, cameraSettings?: CameraSettings): Promise<BarcodePicker> {
    if (camera == null || !this.cameraAccess) {
      this.cameraManager.setSelectedCamera(camera);
      this.cameraManager.setSelectedCameraSettings(cameraSettings);

      if (this.cameraAccess) {
        await this.cameraManager.setupCameras();
      }
    } else {
      await this.cameraManager.initializeCameraWithSettings(camera, cameraSettings);
    }

    return this;
  }

  /**
   * Try to apply new settings to the currently used camera for video input,
   * if no settings are passed the default ones are set.
   *
   * If camera access is enabled, the camera is updated and accessed with the new settings.
   *
   * Depending on device features and user permissions for camera access, any of the following errors
   * could be the rejected result of the returned promise:
   * - `PermissionDeniedError`
   * - `NotAllowedError`
   * - `NotFoundError`
   * - `AbortError`
   * - `NotReadableError`
   * - `InternalError`
   * - `NoCameraAvailableError`
   *
   * In "single image mode" this method has no effect.
   *
   * @param cameraSettings The new camera options used when accessing the camera, by default HD resolution is used.
   * @returns A promise resolving to the updated [[BarcodePicker]] object when the camera is updated
   * (and accessed, if camera access is currently enabled).
   */
  public async applyCameraSettings(cameraSettings?: CameraSettings): Promise<BarcodePicker> {
    if (!this.cameraAccess) {
      this.cameraManager.setSelectedCameraSettings(cameraSettings);
    } else {
      await this.cameraManager.applyCameraSettings(cameraSettings);
    }

    return this;
  }

  /**
   * @returns Whether the picker is in a visible state or not.
   */
  public isVisible(): boolean {
    return this.barcodePickerGui.isVisible();
  }

  /**
   * Enable or disable picker visibility.
   *
   * Note that this does not affect camera access, frame processing or any other picker logic.
   *
   * @param visible Whether the picker is in a visible state or not.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setVisible(visible: boolean): BarcodePicker {
    this.barcodePickerGui.setVisible(visible);

    return this;
  }

  /**
   * @returns Whether the currently selected camera's video is mirrored along the vertical axis.
   */
  public isMirrorImageEnabled(): boolean {
    return this.barcodePickerGui.isMirrorImageEnabled();
  }

  /**
   * Enable or disable camera video mirroring along the vertical axis.
   * By default front cameras are automatically mirrored.
   * This setting is applied per camera and the method has no effect if no camera is currently selected.
   *
   * In "single image mode" this method has no effect.
   *
   * @param enabled Whether the camera video is mirrored along the vertical axis.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setMirrorImageEnabled(enabled: boolean): BarcodePicker {
    this.barcodePickerGui.setMirrorImageEnabled(enabled, true);

    return this;
  }

  /**
   * @returns Whether a sound should be played on barcode recognition (iOS requires user input).
   * Note that the sound is played if there's at least a barcode not rejected via [[ScanResult.rejectCode]].
   */
  public isPlaySoundOnScanEnabled(): boolean {
    return this.playSoundOnScan;
  }

  /**
   * Enable or disable playing a sound on barcode recognition (iOS requires user input).
   *
   * The sound is played if there's at least a barcode not rejected via [[ScanResult.rejectCode]].
   *
   * @param enabled Whether a sound should be played on barcode recognition.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setPlaySoundOnScanEnabled(enabled: boolean): BarcodePicker {
    this.playSoundOnScan = enabled;

    return this;
  }

  /**
   * @returns Whether the device should vibrate on barcode recognition (only Chrome & Firefox, requires user input).
   * Note that the vibration is triggered if there's at least a barcode not rejected via [[ScanResult.rejectCode]].
   */
  public isVibrateOnScanEnabled(): boolean {
    return this.vibrateOnScan;
  }

  /**
   * Enable or disable vibrating the device on barcode recognition (only Chrome & Firefox, requires user input).
   *
   * The vibration is triggered if there's at least a barcode not rejected via [[ScanResult.rejectCode]].
   *
   * @param enabled Whether the device should vibrate on barcode recognition.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setVibrateOnScanEnabled(enabled: boolean): BarcodePicker {
    this.vibrateOnScan = enabled;

    return this;
  }

  /**
   * @returns Whether a GUI button to switch between different cameras is shown (when available).
   */
  public isCameraSwitcherEnabled(): boolean {
    return this.cameraManager.isCameraSwitcherEnabled();
  }

  /**
   * Show or hide a GUI button to switch between different cameras (when available).
   *
   * In "single image mode" this method has no effect.
   *
   * @param enabled Whether to show a GUI button to switch between different cameras.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setCameraSwitcherEnabled(enabled: boolean): BarcodePicker {
    this.cameraManager.setCameraSwitcherEnabled(enabled).catch(
      /* istanbul ignore next */ () => {
        // Ignored
      }
    );

    return this;
  }

  /**
   * @returns Whether a GUI button to toggle device torch on/off is shown (when available, only Chrome).
   */
  public isTorchToggleEnabled(): boolean {
    return this.cameraManager.isTorchToggleEnabled();
  }

  /**
   * Show or hide a GUI button to toggle device torch on/off (when available, only Chrome).
   *
   * In "single image mode" this method has no effect.
   *
   * @param enabled Whether to show a GUI button to toggle device torch on/off.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setTorchToggleEnabled(enabled: boolean): BarcodePicker {
    this.cameraManager.setTorchToggleEnabled(enabled);

    return this;
  }

  /**
   * @returns Whether manual camera focus when clicking/tapping on the video is enabled (when available, only Chrome).
   */
  public isTapToFocusEnabled(): boolean {
    return this.cameraManager.isTapToFocusEnabled();
  }

  /**
   * Enable or disable manual camera focus when clicking/tapping on the video (when available, only Chrome).
   *
   * In "single image mode" this method has no effect.
   *
   * @param enabled Whether to enable manual camera focus when clicking/tapping on the video.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setTapToFocusEnabled(enabled: boolean): BarcodePicker {
    this.cameraManager.setTapToFocusEnabled(enabled);

    return this;
  }

  /**
   * @returns Whether camera zoom control via pinching gesture on the video is enabled (when available, only Chrome).
   */
  public isPinchToZoomEnabled(): boolean {
    return this.cameraManager.isPinchToZoomEnabled();
  }

  /**
   * Enable or disable camera zoom control via pinching gesture on the video (when available, only Chrome).
   *
   * In "single image mode" this method has no effect.
   *
   * @param enabled Whether to enable camera zoom control via pinching gesture on the video.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setPinchToZoomEnabled(enabled: boolean): BarcodePicker {
    this.cameraManager.setPinchToZoomEnabled(enabled);

    return this;
  }

  /**
   * Enable or disable the torch/flashlight of the device (when available, only Chrome).
   * Changing active camera or camera settings will cause the torch to become disabled.
   *
   * A button on the [[BarcodePicker]] GUI to let the user toggle this functionality can also be set
   * on creation via the *enableTorchToggle* option (enabled by default, when available).
   *
   * In "single image mode" this method has no effect.
   *
   * @param enabled Whether the torch should be enabled or disabled.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setTorchEnabled(enabled: boolean): BarcodePicker {
    this.cameraManager.setTorchEnabled(enabled);

    return this;
  }

  /**
   * Set the zoom level of the device (when available, only Chrome).
   * Changing active camera or camera settings will cause the zoom to be reset.
   *
   * In "single image mode" this method has no effect.
   *
   * @param zoomPercentage The percentage of the max zoom (between 0 and 1).
   * @returns The updated [[BarcodePicker]] object.
   */
  public setZoom(zoomPercentage: number): BarcodePicker {
    this.cameraManager.setZoom(zoomPercentage);

    return this;
  }

  /**
   * @returns Whether the barcode picker has loaded the external Scandit Engine library and is ready to scan.
   */
  public isReady(): boolean {
    return this.isReadyToWork;
  }

  /**
   * Add the listener function to the listeners array for an event.
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @param eventName The name of the event to listen to.
   * @param listener The listener function.
   * @param once <div class="tsd-signature-symbol">Default =&nbsp;false</div>
   * Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:bool-param-default
  public on(eventName: EventName, listener: ListenerFn, once?: boolean): BarcodePicker;
  /**
   * Add the listener function to the listeners array for the [[ready]] event, fired when the external
   * Scandit Engine library has been loaded and the barcode picker can thus start to scan barcodes.
   * If the library has already been loaded the listener is called immediately.
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @param eventName The name of the event to listen to.
   * @param listener The listener function.
   * @returns The updated [[BarcodePicker]] object.
   */
  public on(eventName: "ready", listener: () => void): BarcodePicker;
  /**
   * Add the listener function to the listeners array for the [[submitFrame]] event, fired when a new frame is submitted
   * to the engine to be processed. As the frame is not processed yet, the [[ScanResult.barcodes]] property will
   * always be empty (no results yet).
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @param eventName The name of the event to listen to.
   * @param listener The listener function, which will be invoked with a [[ScanResult]] object.
   * @param once <div class="tsd-signature-symbol">Default =&nbsp;false</div>
   * Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  public on(
    eventName: "submitFrame",
    listener: (scanResult: ScanResult) => void,
    // tslint:disable-next-line:bool-param-default
    once?: boolean
  ): BarcodePicker;
  /**
   * Add the listener function to the listeners array for the [[processFrame]] event, fired when a new frame is
   * processed. This event is fired on every frame, independently from the number of recognized barcodes (can be none).
   * The returned barcodes are affected by [[ScanSettings]]'s *codeDuplicateFilter* option.
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @param eventName The name of the event to listen to.
   * @param listener The listener function, which will be invoked with a [[ScanResult]] object.
   * @param once <div class="tsd-signature-symbol">Default =&nbsp;false</div>
   * Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  public on(
    // tslint:disable-next-line:unified-signatures
    eventName: "processFrame",
    listener: (scanResult: ScanResult) => void,
    // tslint:disable-next-line:bool-param-default
    once?: boolean
  ): BarcodePicker;
  /**
   * Add the listener function to the listeners array for the [[scan]] event, fired when new barcodes
   * are recognized in the image frame. The returned barcodes are affected by [[ScanSettings]]'s *codeDuplicateFilter*
   * option.
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @param eventName The name of the event to listen to.
   * @param listener The listener function, which will be invoked with a [[ScanResult]] object.
   * @param once <div class="tsd-signature-symbol">Default =&nbsp;false</div>
   * Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  public on(
    // tslint:disable-next-line:unified-signatures
    eventName: "scan",
    listener: (scanResult: ScanResult) => void,
    // tslint:disable-next-line:bool-param-default
    once?: boolean
  ): BarcodePicker;
  /**
   * Add the listener function to the listeners array for the [[scanError]] event, fired when an error occurs
   * during scanning initialization and execution. The barcode picker will be automatically paused when this happens.
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @param eventName The name of the event to listen to.
   * @param listener The listener function, which will be invoked with an `ScanditEngineError` object.
   * @param once <div class="tsd-signature-symbol">Default =&nbsp;false</div>
   * Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:bool-param-default
  public on(eventName: "scanError", listener: (error: Error) => void, once?: boolean): BarcodePicker;
  public on(eventName: EventName, listener: ListenerFn, once: boolean = false): BarcodePicker {
    if (eventName === "ready") {
      if (this.isReadyToWork) {
        listener();
      } else {
        this.eventEmitter.once(eventName, listener, this);
      }
    } else {
      if (once === true) {
        this.eventEmitter.once(eventName, listener, this);
      } else {
        this.eventEmitter.on(eventName, listener, this);
      }
    }

    return this;
  }

  /**
   * Remove the specified listener from the given event's listener array.
   *
   * @param eventName The name of the event from which to remove the listener.
   * @param listener The listener function to be removed.
   * @returns The updated [[BarcodePicker]] object.
   */
  public removeListener(eventName: EventName, listener: ListenerFn): BarcodePicker {
    this.eventEmitter.removeListener(eventName, listener);

    return this;
  }

  /**
   * Remove all listeners from the given event's listener array.
   *
   * @param eventName The name of the event from which to remove all listeners.
   * @returns The updated [[BarcodePicker]] object.
   */
  public removeAllListeners(eventName: EventName): BarcodePicker {
    this.eventEmitter.removeAllListeners(eventName);

    return this;
  }

  /**
   * *See the [[on]] method.*
   *
   * @param eventName The name of the event to listen to.
   * @param listener The listener function.
   * @param once <div class="tsd-signature-symbol">Default =&nbsp;false</div>
   * Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:bool-param-default
  public addListener(eventName: EventName, listener: ListenerFn, once?: boolean): BarcodePicker {
    return this.on(eventName, listener, once);
  }

  /**
   * Add the listener function to the listeners array for the [[ready]] event, fired when the external
   * Scandit Engine library has been loaded and the barcode picker can thus start to scan barcodes.
   * If the library has already been loaded the listener is called immediately.
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @deprecated Use the [[on]] method instead.
   *
   * @param listener The listener function.
   * @returns The updated [[BarcodePicker]] object.
   */
  public onReady(listener: () => void): BarcodePicker {
    console.warn(
      "The onReady(<listener>) method is deprecated and will be removed in the next" +
        ' major library version. Please use on("ready", <listener>) instead.'
    );

    return this.on("ready", listener);
  }

  /**
   * Add the listener function to the listeners array for the [[scan]] event, fired when new barcodes
   * are recognized in the image frame. The returned barcodes are affected
   * by the [[ScanSettings.setCodeDuplicateFilter]] option.
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @deprecated Use the [[on]] method instead.
   *
   * @param listener The listener function, which will be invoked with a [[ScanResult]] object.
   * @param once Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  public onScan(listener: (scanResult: ScanResult) => void, once: boolean = false): BarcodePicker {
    console.warn(
      "The onScan(<listener>) method is deprecated and will be removed in the next" +
        ' major library version. Please use on("scan", <listener>) instead.'
    );

    return this.on("scan", listener, once);
  }

  /**
   * Remove the specified listener from the [[scan]] event's listener array.
   *
   * @deprecated Use the [[removeListener]] method instead.
   *
   * @param listener The listener function to be removed.
   * @returns The updated [[BarcodePicker]] object.
   */
  public removeScanListener(listener: (scanResult: ScanResult) => void): BarcodePicker {
    console.warn(
      "The removeScanListener(<listener>) method is deprecated and will be removed in the next" +
        ' major library version. Please use removeListener("scan", <listener>) instead.'
    );

    return this.removeListener("scan", listener);
  }

  /**
   * Remove all listeners from the [[scan]] event's listener array.
   *
   * @deprecated Use the [[removeAllListeners]] method instead.
   *
   * @returns The updated [[BarcodePicker]] object.
   */
  public removeScanListeners(): BarcodePicker {
    console.warn(
      "The removeScanListeners() method is deprecated and will be removed in the next" +
        ' major library version. Please use removeAllListeners("scan") instead.'
    );

    return this.removeAllListeners("scan");
  }

  /**
   * Add the listener function to the listeners array for the [[scanError]] event, fired when an error occurs
   * during scanning initialization and execution. The barcode picker will be automatically paused when this happens.
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @deprecated Use the [[on]] method instead.
   *
   * @param listener The listener function, which will be invoked with an `ScanditEngineError` object.
   * @param once Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:no-identical-functions
  public onScanError(listener: (error: Error) => void, once: boolean = false): BarcodePicker {
    console.warn(
      "The onScanError(<listener>) method is deprecated and will be removed in the next" +
        ' major library version. Please use on("scanError", <listener>) instead.'
    );

    return this.on("scanError", listener, once);
  }

  /**
   * Remove the specified listener from the [[scanError]] event's listener array.
   *
   * @deprecated Use the [[removeListener]] method instead.
   *
   * @param listener The listener function to be removed.
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:no-identical-functions
  public removeScanErrorListener(listener: (error: Error) => void): BarcodePicker {
    console.warn(
      "The removeScanErrorListener(<listener>) method is deprecated and will be removed in the next" +
        ' major library version. Please use removeListener("scanError", <listener>) instead.'
    );

    return this.removeListener("scanError", listener);
  }

  /**
   * Remove all listeners from the [[scanError]] event's listener array.
   *
   * @deprecated Use the [[removeAllListeners]] method instead.
   *
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:no-identical-functions
  public removeScanErrorListeners(): BarcodePicker {
    console.warn(
      "The removeScanErrorListeners() method is deprecated and will be removed in the next" +
        ' major library version. Please use removeAllListeners("scanError") instead.'
    );

    return this.removeAllListeners("scanError");
  }

  /**
   * Add the listener function to the listeners array for the [[submitFrame]] event, fired when a new frame is submitted
   * to the engine to be processed. As the frame is not processed yet, the [[ScanResult.barcodes]] property will
   * always be empty (no results yet).
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @deprecated Use the [[on]] method instead.
   *
   * @param listener The listener function, which will be invoked with a [[ScanResult]] object.
   * @param once Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:no-identical-functions
  public onSubmitFrame(listener: (scanResult: ScanResult) => void, once: boolean = false): BarcodePicker {
    console.warn(
      "The onSubmitFrame(<listener>) method is deprecated and will be removed in the next" +
        ' major library version. Please use on("submitFrame", <listener>) instead.'
    );

    return this.on("submitFrame", listener, once);
  }

  /**
   * Remove the specified listener from the [[submitFrame]] event's listener array.
   *
   * @deprecated Use the [[removeListener]] method instead.
   *
   * @param listener The listener function to be removed.
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:no-identical-functions
  public removeSubmitFrameListener(listener: (scanResult: ScanResult) => void): BarcodePicker {
    console.warn(
      "The removeSubmitFrameListener(<listener>) method is deprecated and will be removed in the next" +
        ' major library version. Please use removeListener("submitFrame", <listener>) instead.'
    );

    return this.removeListener("submitFrame", listener);
  }

  /**
   * Remove all listeners from the [[submitFrame]] event's listener array.
   *
   * @deprecated Use the [[removeAllListeners]] method instead.
   *
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:no-identical-functions
  public removeSubmitFrameListeners(): BarcodePicker {
    console.warn(
      "The removeSubmitFrameListeners() method is deprecated and will be removed in the next" +
        ' major library version. Please use removeAllListeners("submitFrame") instead.'
    );

    return this.removeAllListeners("submitFrame");
  }

  /**
   * Add the listener function to the listeners array for the [[processFrame]] event, fired when a new frame is
   * processed. This event is fired on every frame, independently from the number of recognized barcodes (can be none).
   * The returned barcodes are affected by the [[ScanSettings.setCodeDuplicateFilter]] option.
   *
   * No checks are made to see if the listener has already been added.
   * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
   *
   * @deprecated Use the [[on]] method instead.
   *
   * @param listener The listener function, which will be invoked with a [[ScanResult]] object.
   * @param once Whether the listener should just be triggered only once and then discarded.
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:no-identical-functions
  public onProcessFrame(listener: (scanResult: ScanResult) => void, once: boolean = false): BarcodePicker {
    console.warn(
      "The onProcessFrame(<listener>) method is deprecated and will be removed in the next" +
        ' major library version. Please use on("processFrame", <listener>) instead.'
    );

    return this.on("processFrame", listener, once);
  }

  /**
   * Remove the specified listener from the [[processFrame]] event's listener array.
   *
   * @deprecated Use the [[removeListener]] method instead.
   *
   * @param listener The listener function to be removed.
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:no-identical-functions
  public removeProcessFrameListener(listener: (scanResult: ScanResult) => void): BarcodePicker {
    console.warn(
      "The removeProcessFrameListener(<listener>) method is deprecated and will be removed in the next" +
        ' major library version. Please use removeListener("processFrame", <listener>) instead.'
    );

    return this.removeListener("processFrame", listener);
  }

  /**
   * Remove all listeners from the [[processFrame]] event's listener array.
   *
   * @deprecated Use the [[removeAllListeners]] method instead.
   *
   * @returns The updated [[BarcodePicker]] object.
   */
  // tslint:disable-next-line:no-identical-functions
  public removeProcessFrameListeners(): BarcodePicker {
    console.warn(
      "The removeProcessFrameListeners() method is deprecated and will be removed in the next" +
        ' major library version. Please use removeAllListeners("processFrame") instead.'
    );

    return this.removeAllListeners("processFrame");
  }

  /**
   * Set the GUI style for the picker.
   *
   * In "single image mode" this method has no effect.
   *
   * When the GUI style is set to *laser* or *viewfinder*, the GUI will flash on barcode recognition.
   * Note that the GUI will flash if there's at least a barcode not rejected via [[ScanResult.rejectCode]].
   *
   * @param guiStyle The new GUI style to be applied.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setGuiStyle(guiStyle: BarcodePicker.GuiStyle): BarcodePicker {
    this.barcodePickerGui.setGuiStyle(guiStyle);

    return this;
  }

  /**
   * Set the fit type for the video element of the picker.
   *
   * If the "cover" type is selected the maximum available search area for barcode detection is (continuously) adjusted
   * automatically according to the visible area of the picker.
   *
   * In "single image mode" this method has no effect.
   *
   * @param objectFit The new fit type to be applied.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setVideoFit(objectFit: BarcodePicker.ObjectFit): BarcodePicker {
    this.barcodePickerGui.setVideoFit(objectFit);

    return this;
  }

  /**
   * Access the currently set or default camera, requesting user permissions if needed.
   * This method is meant to be used after the picker has been initialized with disabled camera access
   * (*accessCamera*=false) or after [[pauseScanning]] has been called with the pause camera stream option.
   * Calling this doesn't do anything if the camera is already being accessed.
   *
   * Depending on device features and user permissions for camera access, any of the following errors
   * could be the rejected result of the returned promise:
   * - `PermissionDeniedError`
   * - `NotAllowedError`
   * - `NotFoundError`
   * - `AbortError`
   * - `NotReadableError`
   * - `InternalError`
   * - `NoCameraAvailableError`
   *
   * In "single image mode" this method has no effect.
   *
   * @returns A promise resolving to the updated [[BarcodePicker]] object when the camera is accessed.
   */
  public async accessCamera(): Promise<BarcodePicker> {
    if (!this.cameraAccess || this.cameraManager.activeCamera == null) {
      await this.cameraManager.setupCameras();
      this.cameraAccess = true;
    }

    return this;
  }

  /**
   * Create a new parser object.
   *
   * @param dataFormat The format of the input data for the parser.
   * @returns The newly created parser.
   */
  public createParserForFormat(dataFormat: Parser.DataFormat): Parser {
    return this.scanner.createParserForFormat(dataFormat);
  }

  /**
   * Reassign the barcode picker to a different HTML element.
   *
   * All the barcode picker elements inside the current origin element will be moved to the new given one.
   *
   * If an invalid element is given, a `NoOriginElementError` error is thrown.
   *
   * @param originElement The HTMLElement into which all the necessary elements for the picker will be moved.
   * @returns The updated [[BarcodePicker]] object.
   */
  public reassignOriginElement(originElement: HTMLElement): BarcodePicker {
    if (!BrowserHelper.isValidHTMLElement(originElement)) {
      throw new CustomError({
        name: "NoOriginElementError",
        message: "A valid origin HTML element must be given"
      });
    }

    this.barcodePickerGui.reassignOriginElement(originElement);

    return this;
  }

  /**
   * Set the target frames per second to be processed by the scanning engine.
   *
   * The final speed is limited by the camera framerate (usually 30 FPS) and the frame processing time of the device.
   * By setting this to lower numbers devices can save power by performing less work during scanning operations,
   * depending on device speed (faster devices can "sleep" for longer periods).
   *
   * In "single image mode" this method has no effect.
   *
   * @param targetScanningFPS The target frames per second to be processed.
   * Must be a number bigger than 0, by default set to 30.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setTargetScanningFPS(targetScanningFPS: number): BarcodePicker {
    if (targetScanningFPS <= 0) {
      targetScanningFPS = 30;
    }
    this.targetScanningFPS = targetScanningFPS;

    return this;
  }

  /**
   * @returns The internally used initialized (and possibly configured) [[Scanner]] object instance.
   */
  public getScanner(): Scanner {
    return this.scanner;
  }

  /**
   * Clear the internal scanner session.
   *
   * This removes all recognized barcodes from the scanner session and allows them to be scanned again in case a custom
   * *codeDuplicateFilter* option was set in the [[ScanSettings]].
   *
   * @returns The updated [[BarcodePicker]] object.
   */
  public clearSession(): BarcodePicker {
    this.scanner.clearSession();

    return this;
  }

  /**
   * Set the area of the laser displayed when the GUI style is set to *laser* (the laser will match the width and be
   * vertically centered).
   * Note that this functionality affects UI only and doesn't change the actual *searchArea* option set via
   * [[ScanSettings]]. If no area is passed, the default automatic size behaviour is set, where the laser will match
   * the current area of the image in which barcodes are searched, controlled via the *searchArea* option in
   * [[ScanSettings]].
   *
   * @param area The new search area, by default the area will match [[ScanSettings]]'s *searchArea* option.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setLaserArea(area?: SearchArea): BarcodePicker {
    this.barcodePickerGui.setLaserArea(area);

    return this;
  }

  /**
   * Set the area of the viewfinder displayed when the GUI style is set to *viewfinder*.
   * Note that this functionality affects UI only and doesn't change the actual search area set via [[ScanSettings]].
   * If no area is passed, the default automatic size behaviour is set, where the viewfinder will match the current area
   * of the image in which barcodes are searched, controlled via the *searchArea* option in [[ScanSettings]].
   *
   * @param area The new search area, by default the area will match the [[ScanSettings]]'s *searchArea*.
   * @returns The updated [[BarcodePicker]] object.
   */
  public setViewfinderArea(area?: SearchArea): BarcodePicker {
    this.barcodePickerGui.setViewfinderArea(area);

    return this;
  }

  private triggerFatalError(error: Error): void {
    this.fatalError = error;
    console.error(error);
  }

  private handleScanResult(scanResult: ScanResult): void {
    this.eventEmitter.emit("processFrame", scanResult);

    if (scanResult.barcodes.length !== 0) {
      // This will get executed only after the other existing listeners for "processFrame" and "scan" are executed
      this.eventEmitter.once("scan", () => {
        if (
          scanResult.barcodes.some(barcode => {
            return !scanResult.rejectedCodes.has(barcode);
          })
        ) {
          this.barcodePickerGui.flashGUI();
          if (this.playSoundOnScan) {
            this.beepSound.play();
          }
          if (this.vibrateOnScan && this.vibrateFunction != null) {
            this.vibrateFunction.call(navigator, 300);
          }
        }
      });
      this.eventEmitter.emit("scan", scanResult);
    }
  }

  private scheduleVideoProcessing(timeout: number = 0): void {
    window.setTimeout(async () => {
      await this.videoProcessing();
    }, timeout); // Leave some breathing room for other operations
  }

  private async scheduleNextVideoProcessing(processingStartTime: number): Promise<void> {
    if (this.targetScanningFPS < 60) {
      if (this.averageProcessingTime == null) {
        this.averageProcessingTime = performance.now() - processingStartTime;
      } else {
        this.averageProcessingTime = this.averageProcessingTime * 0.9 + (performance.now() - processingStartTime) * 0.1;
      }
      const nextProcessingCallDelay: number = Math.max(0, 1000 / this.targetScanningFPS - this.averageProcessingTime);
      if (Math.round(nextProcessingCallDelay) <= 16) {
        await this.videoProcessing();
      } else {
        this.scheduleVideoProcessing(nextProcessingCallDelay);
      }
    } else {
      await this.videoProcessing();
    }
  }

  private async processVideoFrame(highQualitySingleFrameMode: boolean): Promise<void> {
    const imageData: Uint8ClampedArray | undefined = this.barcodePickerGui.getVideoImageData();

    // This could happen in very weird situations and should be temporary
    // istanbul ignore if
    if (imageData == null) {
      return;
    }

    if (!this.scanningPaused) {
      if (this.eventEmitter.listenerCount("submitFrame") > 0) {
        this.eventEmitter.emit(
          "submitFrame",
          new ScanResult([], imageData.slice(), <ImageSettings>this.scanner.getImageSettings())
        );
      }

      try {
        const scanResult: ScanResult = await this.scanner.processImage(imageData, highQualitySingleFrameMode);
        // Paused status could have changed in the meantime
        if (!this.scanningPaused) {
          this.handleScanResult(scanResult);
        }
      } catch (error) {
        this.pauseScanning();
        this.eventEmitter.emit("scanError", error);
      }
    }
  }

  private async videoProcessing(): Promise<void> {
    if (this.destroyed) {
      return;
    }

    if (
      this.cameraManager.activeCamera == null ||
      this.cameraManager.activeCamera.currentResolution == null ||
      this.fatalError != null ||
      this.scanningPaused ||
      !this.scanner.isReady() ||
      this.scanner.isBusyProcessing() ||
      this.latestVideoTimeProcessed === this.barcodePickerGui.getVideoCurrentTime()
    ) {
      this.scheduleVideoProcessing();

      return;
    }

    if (this.latestVideoTimeProcessed == null) {
      // Show active GUI if needed, as now it's the moment the scanner is ready and used for the first time
      await this.resumeScanning();
    }

    const processingStartTime: number = performance.now();
    this.latestVideoTimeProcessed = this.barcodePickerGui.getVideoCurrentTime();

    await this.processVideoFrame(false);
    await this.scheduleNextVideoProcessing(processingStartTime);
  }

  private handleScannerReady(): void {
    this.isReadyToWork = true;
    this.eventEmitter.emit("ready");
  }
}

// istanbul ignore next
export namespace BarcodePicker {
  /**
   * Fired when the external Scandit Engine library has been loaded and the barcode picker can thus start to scan
   * barcodes.
   *
   * @asMemberOf BarcodePicker
   * @event
   */
  // @ts-ignore
  declare function ready(): void;
  /**
   * Fired when a new frame is submitted to the engine to be processed. As the frame is not processed yet, the
   * [[ScanResult.barcodes]] property will always be empty (no results yet).
   *
   * @asMemberOf BarcodePicker
   * @event
   * @param scanResult The result of the scanning operation on the image.
   */
  // @ts-ignore
  declare function submitFrame(scanResult: ScanResult): void;
  /**
   * Fired when a new frame is processed by the engine. This event is fired on every frame, independently from the
   * number of recognized barcodes (can be none). The returned barcodes are affected by [[ScanSettings]]'s
   * *codeDuplicateFilter* option.
   *
   * @asMemberOf BarcodePicker
   * @event
   * @param scanResult The result of the scanning operation on the image.
   */
  // @ts-ignore
  declare function processFrame(scanResult: ScanResult): void;
  /**
   * Fired when new barcodes are recognized in the image frame. The returned barcodes are affected by [[ScanSettings]]'s
   * *codeDuplicateFilter* option.
   *
   * @asMemberOf BarcodePicker
   * @event
   * @param scanResult The result of the scanning operation on the image.
   */
  // @ts-ignore
  declare function scan(scanResult: ScanResult): void;
  /**
   * Fired when an error occurs during scanning initialization and execution. The barcode picker will be automatically
   * paused when this happens.
   *
   * @asMemberOf BarcodePicker
   * @event
   * @param error The ScanditEngineError that was triggered.
   */
  // @ts-ignore
  declare function scanError(error: Error): void;

  /**
   * GUI style to be used by a barcode picker, used to hint barcode placement in the frame.
   */
  export enum GuiStyle {
    /**
     * No GUI is shown to indicate where the barcode should be placed.
     * Be aware that the Scandit logo continues to be displayed as showing it is part of the license agreement.
     */
    NONE = "none",
    /**
     * A laser line is shown.
     */
    LASER = "laser",
    /**
     * A rectangular viewfinder with rounded corners is shown.
     */
    VIEWFINDER = "viewfinder"
  }

  /**
   * Fit type used to control the resizing (scale) of the barcode picker to fit in its container *originElement*.
   */
  export enum ObjectFit {
    /**
     * Scale to maintain aspect ratio while fitting within the *originElement*'s content box.
     * Aspect ratio is preserved, so the barcode picker will be "letterboxed" if its aspect ratio
     * does not match the aspect ratio of the box.
     */
    CONTAIN = "contain",
    /**
     * Scale to maintain aspect ratio while filling the *originElement*'s entire content box.
     * Aspect ratio is preserved, so the barcode picker will be clipped to fit if its aspect ratio
     * does not match the aspect ratio of the box.
     */
    COVER = "cover"
  }
}
