Source: HT.js

import window from 'global/window';
import document from 'global/document';
import EventEmitter from 'events';
import ObjectAssign from 'object-assign';
import Cookies from 'js-cookie';
import Promise from 'es6-promise';
import qs from 'query-string';

import 'classlist-polyfill';

import { version } from '../package.json';

import { createLogger, getDeviceInfo } from './utils';

import TextManager from './managers/TextManager';
import VideoManager from './managers/VideoManager';
import PageSpeechManager from './managers/PageSpeechManager';
import PageTranslatorManager from './managers/PageTranslatorManager';

import { ytEmbedReplace, videojsReplace } from './tools';
import DesktopDrawer from './components/DesktopDrawer/DesktopDrawer';
import MobileDrawer from './components/MobileDrawer/MobileDrawer';

/**
 * Classe principal do plugin da Hand Talk.
 */
class HT extends EventEmitter {
  /**
   * Versão do plugin
   */
  static version = version;

  static defaultConfig = {
    bottom: '0px',
    side: 'right',
    zIndex: 1000000,
    maxTextSize: 500,
    debug: false,
    doNotTrack: false,
    parentElement: document.body,
    exceptions: [],
    textEnabled: true,
    videoEnabled: false,
    ytEmbedReplace: false,
    videojsReplace: false,
    align: 'default',
    mobileEnabled: true,
    pageSpeech: false
  };

  /**
   * Tags que devem ser ignoradas pelo tradutor de texto
   * @type {string[]}
   */
  static skipTags = ['script', 'head', 'style', 'body', 'document', 'table', 'thead', 'tbody'];

  static deviceInfo = getDeviceInfo();

  static customCorrespondingObject = {
    pele: ['UNT-BRACOS', 'UNT-CABECA', 'UNT-CABELO'],
    calca: ['UNT-CALCA', 'UNT-CINTO', 'UNT-SAPATO'],
    manga_curta: ['UNT-CAMISA_MCURTA'],
    manga_longa: ['UNT-CAMISA_MLONGA'],
    manga_longa_sem_gravata: ['UNT-CAMISA_MLONGA_SGRAVATA'],
    chapeu_natal: ['UNT-CHAPEU_NATAL'],
    chapeu_palha: ['UNT-CHAPEU_PALHA'],
    kimono: ['UNT-KIMONO'],
  };

  /**
   * Objeto responsável pelo log (.error, .warn, .info, .debug)
   * @type {object}
   */
  logger = null;

  /**
   * Objeto de configuração
   * @type {{side: string, zIndex: number, maxTextSize: number, debug: boolean, doNotTrack: boolean, parentElement: HTMLElement, exceptions: Array, textEnabled: boolean, videoEnabled: boolean, ytEmbedReplace: boolean, align: string, mobileEnabled: boolean, mobileConfig{bottom, side, videoEnabled, ytEmbedReplace, videojsReplace}, pageSpeech}}
   */
  config = {};

  /**
   * Instancia do gerenciador de texto
   * @type {TextManager}
   */
  textManager = null;

  /**
   * Instancia do gerenciador de vídeos
   * @type {VideoManager}
   */
  videoManager = null;

  /**
   * Instancia do gerenciador de texto para voz
   * @type {SpeechSynthesizerManager}
   */
  pageSpeechManager = null;

  /**
   * Instancia do gerenciador de tradução de texto para linguagem de sinais.
   */
  pageTranslatorManager = null;

  _token = null;

  hugoLoaded = false;

  /**
   * @param {object} config - Objeto de configuração
   */
  constructor(config) {
    super();

    const mobileConfig = config.mobileConfig;

    // Mescla as configurações
    this.config = ObjectAssign(HT.defaultConfig, config);
    if (HT.deviceInfo.isMobile) this.config = ObjectAssign(this.config, mobileConfig);

    // Define console como debug quando depuração está ativada
    this.logger = this.config.debug ? createLogger(3) : createLogger(1);

    // Verifica se o plugin foi inicializado mais de uma vez
    if (window.hasHtInitialized) return this.logger.error('O plugin da Hand Talk já foi inicializado nesta página');

    this._token = TOKEN || this.config.token;

    // Marca como plugin inicializado
    window.hasHtInitialized = true;

    // Preferência de lado (side)
    const preferredSide = Cookies.get(PREFERRED_SIDE_COOKIE_NAME);
    if (preferredSide) this.config.side = preferredSide;
    if (this.config.side !== 'left' && this.config.side !== 'right') this.side = 'right';

    // Condicionais para instanciar o leitor/tradutor de sites
    let pageSpeechReady = this.config.pageSpeech && !HT.deviceInfo.isMobile;
    let textTranslatorReady = (this.config.textEnabled && !HT.deviceInfo.isMobile) || (this.config.mobileEnabled && HT.deviceInfo.isMobile);

    // Cria o drawer
    if (textTranslatorReady || pageSpeechReady) {
      this.drawer = HT.deviceInfo.isMobile ? new MobileDrawer(this) : new DesktopDrawer(this);
      this.config.parentElement.appendChild(this.drawer.elem);
    }

    // Instancia o manipulador de elementos
    if (textTranslatorReady || pageSpeechReady) this.textManager = new TextManager(this);

    // Instancia o gerenciador de vídeos
    if (this.config.videoEnabled) this.videoManager = new VideoManager(this);

    // Instancia o leitor de sites
    if (pageSpeechReady) this.pageSpeechManager = new PageSpeechManager(this);

    // Instancia do tradutor de sites
    if (textTranslatorReady) this.pageTranslatorManager = new PageTranslatorManager(this);


    this.logger.info('Plugin da Hand Talk iniciado com as seguintes configurações', this.config);

    videojsReplace(this);
    ytEmbedReplace(this);

    if (this.config.ytEmbedReplace) this.once('videoManagerReady', this.replaceYtEmbedAll);
    if (this.config.videojsReplace) this.once('videoManagerReady', this.replaceVideoJsAll);
  }

  /** 
   * Adiciona os listeners de click em um iframe existente
   * @param iframe - Iframe que receberá os listeners
   * */
  addListenersToIframe = (iframe) => {
    if (this.textManager) {
      try {
        this.textManager.addIframeListeners(iframe);
      } catch (e) {
        this.logger.warn('Falha ao adicionar EventListener no iframe ' + e);
      }
    }
  }

  /**
   * Busca por iframes e adiciona os listeners de click
   */
  addIframesListenersAll = () => {
    if (this.textManager) this.textManager.addIframesListenersAll();
  }
  get side() { return this.config.side; }
  set side(value) {
    this.config.side = value;
    if (this.textManager && this.drawer) this.drawer.side = value;
  }

  get align() { return this.config.align; }
  set align(value) {
    if (this.textManager && this.drawer) this.drawer.align = value;
  }

  _getHugoPromise = null;
  _hugo = null;
  get hugo() {
    if (this._getHugoPromise) return this._getHugoPromise;
    return this._getHugoPromise = Promise.resolve()
      .then(this._auth)
      .then(this._loadHugo);
  }

  _authPromise = null;
  _auth = () => {
    if (this._authPromise) return this._authPromise;
    return this._authPromise = new Promise((resolve, reject) => {
      this.emit('authenticating');

      const xmlHttp = new XMLHttpRequest();
      this.uid = Cookies.get(HT_PLUGIN_UID_COOKIE_NAME);

      const params = {
        token: this._token,
      };

      if (this.config.videoEnabled) params.videoEnabled = true;

      xmlHttp.open('GET', `${TRANSLATE_AUTH_URL}?${qs.stringify(params)}`);
      xmlHttp.setRequestHeader('Version', '3');
      if (this.uid) xmlHttp.setRequestHeader('UID', this.uid);

      const on = () => {
        let data;
        try {
          data = JSON.parse(xmlHttp.responseText);
        } catch (e) {
          data = {};
        }

        const { uid, custom, error, message } = data;
        this.logger.debug('auth uid', uid);
        this.logger.debug('auth custom', custom);
        this.logger.debug('auth error', error);
        this.logger.debug('auth message', message);

        if (xmlHttp.status === 406)
          this.logger.warn(`${error || 'A sua assinatura está desabilitada'}. Acesse ` +
            'https://account.handtalk.me para regularizar o serviço ou entre em contato via ' +
            'suporte@handtalk.com.br');
        if (xmlHttp.status !== 200) {
          this.emit('errorOnAuth', message || 'Opa! Um problema ocorreu :/ aguarde um instante e recarregue a página.');
          return reject(data.message || data);
        }

        if (!this.uid && uid)
          Cookies.set(HT_PLUGIN_UID_COOKIE_NAME, uid, { expires: 10 * 365 });

        this.uid = Cookies.get(HT_PLUGIN_UID_COOKIE_NAME);
        this.emit('authenticated');
        resolve(data);

        this.hugo.then((hugo) => {
          const customDict = custom || {};
          const finalCustom = {};

          for (let key in customDict) {
            const value = customDict[key];
            HT.customCorrespondingObject[key].forEach((corresponding) => {
              finalCustom[corresponding] = {
                visible: !(value === 'hide'),
                map: value === 'hide' ? undefined : (value.indexOf('http://') === 0 || value.indexOf('https://') === 0) && value ||
                  `https://api.handtalk.me/unity/textures/${value}`, // TODO: atualiza link das texturas
              };
            });
          }

          this.logger.info(finalCustom);

          this.emit('customizing');
          hugo.custom(finalCustom)
            .then(this.emit.bind(this, 'customized'));
        });
      };

      xmlHttp.onload = on;
      xmlHttp.onerror = on;
      xmlHttp.send();
    });
  };

  _hugoPromise = null;
  /**
   * Injeta biblioteca hugo.js e inicia objeto Hugo
   * @function
   */
  startHugo = () => {
    if (this._hugoPromise) return this._hugoPromise;
    return this._hugoPromise = new Promise((resolve, reject) => {
      const scriptElem = document.createElement('script');
      scriptElem.src = HUGO_JS_SCRIPT_URL;
      scriptElem.onload = () => {
        this._hugo = new Hugo(this._token);

        this._hugo.on('downloadDependenciesProgress', this.emit.bind(this, 'hugoDownloadDependenciesProgress'));
        this._hugo.on('downloadSceneDataProgress', this.emit.bind(this, 'hugoDownloadSceneDataProgress'));
        this._hugo.on('translating', this.emit.bind(this, 'translating'));
        this._hugo.on('errorOnTranslate', this.emit.bind(this, 'errorOnTranslate'));
        this._hugo.on('translated', this.emit.bind(this, 'translated'));
        this._hugo.on('notCompatible', this.emit.bind(this, 'notCompatible'));

        resolve(this._hugo);
      };
      scriptElem.onerror = reject;
      this.config.parentElement.appendChild(scriptElem);
    });
  };

  _loadHugo = () => (this.startHugo()
    .then(() => (this._hugo.load()))
    .then(() => (this.emit('hugoLoaded')))
    .then(() => (this.hugoLoaded = true))
    .then(() => (this._hugo)));

  /**
   * Muda o plugin de lado
   * @function
   */
  toggleSide = () => {
    if (this.side === 'right') this.side = 'left';
    else if (this.side === 'left') this.side = 'right';
  };

  /**
   * Remove e destroi plugin da página
   * @function
   */
  destroy = () => {
    this.open = false;
    if (this.textManager) this.textManager.destroy();

    // Desativa e destroi o pageTranslator (caso exista)
    if (this.pageTranslatorManager && this.pageTranslatorManager.activated)
      this.pageTranslatorManager.activated = false;

    // Desativa e destroi o pageSpeech (caso exista)
    if (this.pageSpeechManager && this.pageSpeechManager.activated)
      this.pageSpeechManager.activated = false;

    // Remove o drawer da pagina
    if (this.drawer) this.drawer.elem.remove();

    window.hasHtInitialized = false;
  };
}

export default HT;