nopaque.App = class App {
  constructor() {
    this.data = {
      promises: {getUser: {}, subscribeUser: {}},
      users: {},
    };
    this.socket = io({transports: ['websocket'], upgrade: false});
    this.socket.on('PATCH', (patch) => {this.onPatch(patch);});
  }

  getUser(userId) {
    if (userId in this.data.promises.getUser) {
      return this.data.promises.getUser[userId];
    }

    this.data.promises.getUser[userId] = new Promise((resolve, reject) => {
      this.socket.emit('GET /users/<user_id>', userId, (response) => {
        if (response.status === 200) {
          this.data.users[userId] = response.body;
          resolve(this.data.users[userId]);
        } else {
          reject(`[${response.status}] ${response.statusText}`);
        }
      });
    });

    return this.data.promises.getUser[userId];
  }

  subscribeUser(userId) {
    if (userId in this.data.promises.subscribeUser) {
      return this.data.promises.subscribeUser[userId];
    }

    this.data.promises.subscribeUser[userId] = new Promise((resolve, reject) => {
      this.socket.emit('SUBSCRIBE /users/<user_id>', userId, (response) => {
        if (response.status !== 200) {
          reject(response);
          return;
        }
        resolve(response);
      });
    });

    return this.data.promises.subscribeUser[userId];
  }

  flash(message, category) {
    let iconPrefix = '';
    switch (category) {
      case 'corpus': {
        iconPrefix = '<i class="left material-icons">book</i>';
        break;
      }
      case 'error': {
        iconPrefix = '<i class="error-color-text left material-icons">error</i>';
        break;
      }
      case 'job': {
        iconPrefix = '<i class="left nopaque-icons">J</i>';
        break;
      }
      case 'settings': {
        iconPrefix = '<i class="left material-icons">settings</i>';
        break;
      }
      default: {
        iconPrefix = '<i class="left material-icons">notifications</i>';
        break;
      }
    }
    let toast = M.toast(
      {
        html: `
          <span>${iconPrefix}${message}</span>
          <button class="action-button btn-flat toast-action white-text" data-action="close">
            <i class="material-icons">close</i>
          </button>
        `.trim()
      }
    );
    let toastCloseActionElement = toast.el.querySelector('.action-button[data-action="close"]');
    toastCloseActionElement.addEventListener('click', () => {toast.dismiss();});
  }
  
  onPatch(patch) {
    // Filter Patch to only include operations on users that are initialized
    let regExp = new RegExp(`^/users/(${Object.keys(this.data.users).join('|')})`);
    let filteredPatch = patch.filter(operation => regExp.test(operation.path));

    // Handle job status updates
    let subRegExp = new RegExp(`^/users/([A-Za-z0-9]*)/jobs/([A-Za-z0-9]*)/status$`);
    let subFilteredPatch = filteredPatch
      .filter((operation) => {return operation.op === 'replace';})
      .filter((operation) => {return subRegExp.test(operation.path);});
    for (let operation of subFilteredPatch) {
      let [match, userId, jobId] = operation.path.match(subRegExp);
      this.flash(`[<a href="/jobs/${jobId}">${this.data.users[userId].jobs[jobId].title}</a>] New status: <span class="job-status-text" data-job-status="${operation.value}"></span>`, 'job');
    }

    // Apply Patch
    jsonpatch.applyPatch(this.data, filteredPatch);
  }

  init() {
    this.initUi();
  }

  initUi() {
    /* Pre-Initialization fixes */
    // #region

    // Flask-WTF sets the standard HTML maxlength Attribute on input/textarea
    // elements to specify their maximum length (in characters). Unfortunatly
    // Materialize won't recognize the maxlength Attribute, instead it uses
    // the data-length Attribute. It's conversion time :)
    for (let elem of document.querySelectorAll('input[maxlength], textarea[maxlength]')) {
      elem.dataset.length = elem.getAttribute('maxlength');
      elem.removeAttribute('maxlength');
    }

    // To work around some limitations with the Form setup of Flask-WTF.
    // HTML option elements with an empty value are considered as placeholder
    // elements. The user should not be able to actively select these options.
    // So they get the disabled attribute.
    for (let optionElement of document.querySelectorAll('option[value=""]')) {
      optionElement.disabled = true;
    }

    // TODO: Check why we are doing this.
    for (let optgroupElement of document.querySelectorAll('optgroup[label=""]')) {
      for (let c of optgroupElement.children) {
        optgroupElement.parentElement.insertAdjacentElement('afterbegin', c);
      }
      optgroupElement.remove();
    }
    // #endregion


    /* Initialize Materialize Components */
    // #region
  
    // Automatically initialize Materialize Components that do not require
    // additional configuration.
    M.AutoInit();

    // CharacterCounters
    // Materialize didn't include the CharacterCounter plugin within the
    // AutoInit method (maybe they forgot it?). Anyway... We do it here. :)
    M.CharacterCounter.init(document.querySelectorAll('input[data-length]:not(.no-autoinit), textarea[data-length]:not(.no-autoinit)'));

    // Header navigation account Dropdown.
    M.Dropdown.init(
      document.querySelector('#nav-account-dropdown-trigger'),
      {
        alignment: 'right',
        constrainWidth: false,
        coverTrigger: false
      }
    );

    // Terms of use modal
    M.Modal.init(
      document.querySelector('#terms-of-use-modal'),
      {
        dismissible: false,
        onCloseEnd: (modalElement) => {
          nopaque.requests.users.entity.acceptTermsOfUse();
        }
      }
    );
    // #endregion


    /* Initialize nopaque Components */
    // #region 
    nopaque.resource_displays.AutoInit();
    nopaque.resource_lists.AutoInit();
    nopaque.forms.AutoInit();
    // #endregion
  }
};