nopaque.corpus_analysis.ReaderExtension = class ReaderExtension {
  name = 'Reader';

  constructor(app) {
    this.app = app;

    this.data = {};

    this.elements = {
      container: document.querySelector(`#corpus-analysis-reader-container`),
      corpus: document.querySelector(`#corpus-analysis-reader-corpus`),
      corpusPagination: document.querySelector(`#corpus-analysis-reader-corpus-pagination`),
      error: document.querySelector(`#corpus-analysis-reader-error`),
      progress: document.querySelector(`#corpus-analysis-reader-progress`),
      userInterfaceForm: document.querySelector(`#corpus-analysis-reader-user-interface-form`)
    };

    this.settings = {
      perPage: parseInt(this.elements.userInterfaceForm['per-page'].value),
      textStyle: parseInt(this.elements.userInterfaceForm['text-style'].value),
      tokenRepresentation: this.elements.userInterfaceForm['token-representation'].value,
      pagination: {
        innerWindow: 5,
        outerWindow: 1
      }
    }

    this.app.registerExtension(this);
  }

  async submitForm() {
    this.app.disableActionElements();
    this.elements.error.innerText = '';
    this.elements.error.classList.add('hide');
    this.elements.progress.classList.remove('hide');
    try {
      const paginatedCorpus = await this.data.corpus.o.paginate(1, this.settings.perPage);
      this.data.corpus.p = paginatedCorpus;
      this.renderCorpus();
      this.renderCorpusPagination();
      this.elements.progress.classList.add('hide');
    } catch (error) {
      let errorString = '';
      if ('code' in error) {errorString += `[${error.code}] `;}
      errorString += `${error.constructor.name}`;
      if ('description' in error) {errorString += `: ${error.description}`;}
      this.elements.error.innerText = errorString;
      this.elements.error.classList.remove('hide');
      app.flash(errorString, 'error');
      this.elements.progress.classList.add('hide');
    }
    this.app.enableActionElements();
  }

  async init() {
    // Init data
    this.data.corpus = this.app.data.corpus;
    // Add event listeners
    this.elements.userInterfaceForm.addEventListener('submit', (event) => {
      event.preventDefault();
      this.submitForm();
    });
    this.elements.userInterfaceForm.addEventListener('change', (event) => {
      if (event.target === this.elements.userInterfaceForm['per-page']) {
        this.settings.perPage = parseInt(this.elements.userInterfaceForm['per-page'].value);
        this.submitForm();
      }
      if (event.target === this.elements.userInterfaceForm['text-style']) {
        this.settings.textStyle = parseInt(this.elements.userInterfaceForm['text-style'].value);
        this.setTextStyle();
      }
      if (event.target === this.elements.userInterfaceForm['token-representation']) {
        this.settings.tokenRepresentation = this.elements.userInterfaceForm['token-representation'].value;
        this.setTokenRepresentation();
      }
    });
    // Load initial data
    await this.submitForm();
  }

  clearCorpus() {
    // Destroy with .p-attr elements associated Materialize tooltips
    let pAttrElements = this.elements.corpus.querySelectorAll('.p-attr.tooltipped');
    for (let pAttrElement of pAttrElements) {
      M.Tooltip.getInstance(pAttrElement)?.destroy();
    }
    this.elements.corpus.innerHTML = `
      <p class="show-if-only-child">
        <span class="card-title"><i class="left material-icons" style="font-size: inherit;">search</i>Nothing here...</span><br>
        No text available.
      </p>
    `.trim();
  }

  renderCorpus() {
    this.clearCorpus();
    let item = this.data.corpus.p.items[0];
    this.elements.corpus.innerHTML += `
        <p>${this.cposRange2HTML(item[0], item[item.length - 1])}</p>
    `.trim();
    this.setTextStyle();
    this.setTokenRepresentation();
  }

  clearCorpusPagination() {
    this.elements.corpusPagination.innerHTML = '';
    this.elements.corpusPagination.classList.add('hide');
  }

  renderCorpusPagination() {
    this.clearCorpusPagination();
    if (this.data.corpus.p.pages === 0) {return;}
    let pageElement;
    // First page button. Disables first page button if on first page
    pageElement = nopaque.Utils.HTMLToElement(
      `
        <li class="${this.data.corpus.p.page === 1 ? 'disabled' : 'waves-effect'}">
          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.page === 1 ? '' : 'data-target="1"'}>
            <i class="material-icons">first_page</i>
          </a>
        </li>
      `
    );
    this.elements.corpusPagination.appendChild(pageElement);
    // Previous page button. Disables previous page button if on first page
    pageElement = nopaque.Utils.HTMLToElement(
      `
        <li class="${this.data.corpus.p.has_prev ? 'waves-effect' : 'disabled'}">
          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.has_prev ? 'data-target="' + this.data.corpus.p.prev_num + '"' : ''}>
            <i class="material-icons">chevron_left</i>
          </a>
        </li>
      `
    );
    this.elements.corpusPagination.appendChild(pageElement);
    // First page as number. Hides first page button if on first page
    if (this.data.corpus.p.page > 6) {
      pageElement = nopaque.Utils.HTMLToElement(
        `
          <li class="waves-effect">
            <a class="corpus-analysis-action pagination-trigger" data-target="1">1</a>
          </li>
        `
      );
      this.elements.corpusPagination.appendChild(pageElement);
      pageElement = nopaque.Utils.HTMLToElement("<li style='margin-top: 5px;'>&hellip;</li>");
      this.elements.corpusPagination.appendChild(pageElement);
    }

    // render page buttons (5 before and 5 after current page)
    for (let i = this.data.corpus.p.page - this.settings.pagination.innerWindow; i <= this.data.corpus.p.page; i++) {
      if (i <= 0) {continue;}
      pageElement = nopaque.Utils.HTMLToElement(
        `
          <li class="${i === this.data.corpus.p.page ? 'active' : 'waves-effect'}">
          <a class="corpus-analysis-action pagination-trigger" ${i === this.data.corpus.p.page ? '' : 'data-target="' + i + '"'}>${i}</a>
          </li>
        `
      );
      this.elements.corpusPagination.appendChild(pageElement);
    };
    for (let i = this.data.corpus.p.page +1; i <= this.data.corpus.p.page + this.settings.pagination.innerWindow; i++) {
      if (i > this.data.corpus.p.pages) {break;}
      pageElement = nopaque.Utils.HTMLToElement(
        `
          <li class="${i === this.data.corpus.p.page ? 'active' : 'waves-effect'}">
          <a class="corpus-analysis-action pagination-trigger" ${i === this.data.corpus.p.page ? '' : 'data-target="' + i + '"'}>${i}</a>
          </li>
        `
      );
      this.elements.corpusPagination.appendChild(pageElement);
    };
    // Last page as number. Hides last page button if on last page
    if (this.data.corpus.p.page < this.data.corpus.p.pages - 6) {
      pageElement = nopaque.Utils.HTMLToElement("<li style='margin-top: 5px;'>&hellip;</li>");
      this.elements.corpusPagination.appendChild(pageElement);
      pageElement = nopaque.Utils.HTMLToElement(
        `
          <li class="waves-effect">
            <a class="corpus-analysis-action pagination-trigger" data-target="${this.data.corpus.p.pages}">${this.data.corpus.p.pages}</a>
          </li>
        `
      );
      this.elements.corpusPagination.appendChild(pageElement);
    }
    // Next page button. Disables next page button if on last page
    pageElement = nopaque.Utils.HTMLToElement(
      `
        <li class="${this.data.corpus.p.has_next ? 'waves-effect' : 'disabled'}">
          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.has_next ? 'data-target="' + this.data.corpus.p.next_num + '"' : ''}>
            <i class="material-icons">chevron_right</i>
          </a>
        </li>
      `
    );
    this.elements.corpusPagination.appendChild(pageElement);
    // Last page button. Disables last page button if on last page
    pageElement = nopaque.Utils.HTMLToElement(
      `
        <li class="${this.data.corpus.p.page === this.data.corpus.p.pages ? 'disabled' : 'waves-effect'}">
          <a class="corpus-analysis-action pagination-trigger" ${this.data.corpus.p.page === this.data.corpus.p.pages ? '' : 'data-target="' + this.data.corpus.p.pages + '"'}>
            <i class="material-icons">last_page</i>
          </a>
        </li>
      `
    );
    this.elements.corpusPagination.appendChild(pageElement);
    
    for (let paginateTriggerElement of this.elements.corpusPagination.querySelectorAll('.pagination-trigger[data-target]')) {
      paginateTriggerElement.addEventListener('click', (event) => {
        event.preventDefault();
        let page = parseInt(paginateTriggerElement.dataset.target);
        this.page(page);
      });
    }
    this.elements.corpusPagination.classList.remove('hide');
  }

  cposRange2HTML(firstCpos, lastCpos) {
    let html = '';
    for (let cpos = firstCpos; cpos <= lastCpos; cpos++) {
      let prevPAttr = cpos > firstCpos ? this.data.corpus.p.lookups.cpos_lookup[cpos - 1] : null;
      let pAttr = this.data.corpus.p.lookups.cpos_lookup[cpos];
      let nextPAttr = cpos < lastCpos ? this.data.corpus.p.lookups.cpos_lookup[cpos + 1] : null;
      let isEntityStart = 'ent' in pAttr && pAttr.ent !== prevPAttr?.ent;
      let isEntityEnd = 'ent' in pAttr && pAttr.ent !== nextPAttr?.ent;
      // Add a space before pAttr
      if (cpos !== firstCpos || pAttr.simple_pos !== 'PUNCT') {html += ' ';}
      // Add entity start
      if (isEntityStart) {
        html += `<span class="s-attr" data-cpos="${cpos}" data-id="${pAttr.ent}" data-s-attr-type="ent" data-s-attr-ent-type="${this.data.corpus.p.lookups.ent_lookup[pAttr.ent].type}">`;
      }
      // Add pAttr
      html += `<span class="p-attr" data-cpos="${cpos}"></span>`;
      // Add entity end
      if (isEntityEnd) {
        html += ` <span class="badge black-text hide new white ent-indicator" data-badge-caption="">${this.data.corpus.p.lookups.ent_lookup[pAttr.ent].type}</span>`;
        html += '</span>';
      }
    }
    return html;
  }

  page(pageNum, callback) {
    if (this.data.corpus.p.page === pageNum && typeof callback === 'function') {
      callback();
      return;
    }
    this.app.disableActionElements();
    window.scrollTo(top);
    this.elements.progress.classList.remove('hide');
    this.data.corpus.o.paginate(pageNum, this.settings.perPage)
      .then(
        (paginatedCorpus) => {
          this.data.corpus.p = paginatedCorpus;
          this.renderCorpus();
          this.renderCorpusPagination();
          this.elements.progress.classList.add('hide');
          this.app.enableActionElements();
          if (typeof callback === 'function') {callback();}
        }
      )
  }

  setTextStyle() {
    if (this.settings.textStyle >= 0) {
      // Destroy with .p-attr elements associated Materialize tooltips
      for (let pAttrElement of this.elements.corpus.querySelectorAll('.p-attr.tooltipped')) {
        M.Tooltip.getInstance(pAttrElement)?.destroy();
      }
      // Set basic styling on .p-attr elements
      for (let pAttrElement of this.elements.corpus.querySelectorAll('.p-attr')) {
        pAttrElement.setAttribute('class', 'p-attr');
      }
      // Set basic styling on .s-attr[data-type="ent"] elements
      for (let entElement of this.elements.corpus.querySelectorAll('.s-attr[data-s-attr-type="ent"]')) {
        entElement.querySelector('.ent-indicator').classList.add('hide');
        entElement.removeAttribute('style');
        entElement.setAttribute('class', 's-attr');
      }
    }
    if (this.settings.textStyle >= 1) {
      // Set advanced styling on .s-attr[data-type="ent"] elements
      for (let entElement of this.elements.corpus.querySelectorAll('.s-attr[data-s-attr-type="ent"]')) {
        entElement.classList.add('chip');
        entElement.querySelector('.ent-indicator').classList.remove('hide');
      }
    }
    if (this.settings.textStyle >= 2) {
      // Set advanced styling on .p-attr elements
      for (let pAttrElement of this.elements.corpus.querySelectorAll('.p-attr')) {
        pAttrElement.classList.add('chip', 'hoverable', 'tooltipped');
        let cpos = pAttrElement.dataset.cpos;
        let pAttr = this.data.corpus.p.lookups.cpos_lookup[cpos];
        let positionalPropertiesHTML = `
          <p class="left-align">
            <b>Positional properties</b><br>
            <span>Token: ${cpos}</span>
        `.trim();
        let structuralPropertiesHTML = `
          <p class="left-align">
            <b>Structural properties</b>
        `.trim();
        for (let [property, propertyValue] of Object.entries(pAttr)) {
          if (['lemma', 'ner', 'pos', 'simple_pos', 'word'].includes(property)) {
            if (propertyValue === 'None') {continue;}
            positionalPropertiesHTML += `<br><i class="material-icons" style="font-size: inherit;">subdirectory_arrow_right</i>${property}: ${propertyValue}`;
          } else {
            structuralPropertiesHTML += `<br><span>${property}: ${propertyValue}</span>`;
            if (!(`${property}_lookup` in this.data.corpus.p.lookups)) {continue;}
            for (let [subproperty, subpropertyValue] of Object.entries(this.data.corpus.p.lookups[`${property}_lookup`][propertyValue])) {
              if (subpropertyValue === 'NULL') {continue;}
              structuralPropertiesHTML += `<br><i class="material-icons" style="font-size: inherit;">subdirectory_arrow_right</i>${subproperty}: ${subpropertyValue}`
            }
          }
        }
        positionalPropertiesHTML += '</p>';
        structuralPropertiesHTML += '</p>';
        M.Tooltip.init(
          pAttrElement,
          {html: positionalPropertiesHTML + structuralPropertiesHTML}
        );
      }
    }
  }

  setTokenRepresentation() {
    for (let pAttrElement of this.elements.corpus.querySelectorAll('.p-attr')) {
      let pAttr = this.data.corpus.p.lookups.cpos_lookup[pAttrElement.dataset.cpos];
      pAttrElement.innerText = pAttr[this.settings.tokenRepresentation];
    }
  }
}