class RessourceList extends List {
  constructor(idOrElement, subscriberList, type, options={}) {
    if (!['corpus', 'job'].includes(type)) {
      console.error("Unknown Type!");
      return;
    }
    super(idOrElement, {...RessourceList.options['common'],
                        ...RessourceList.options[type],
                        ...options});
    this.type = type;
    subscriberList.push(this);
  }


  _init(ressources) {
    this.clear();
    this.addRessources(Object.values(ressources));
    this.sort("creation_date", {order: "desc"});
  }


  _update(patch) {
    let item, pathArray;

    for (let operation of patch) {
      /* "/{ressourceName}/{ressourceId}/..." -> ["{ressourceId}", "..."] */
      pathArray = operation.path.split("/").slice(2);
      switch(operation.op) {
        case "add":
          if (pathArray.includes("results")) {break;}
          this.addRessources([operation.value]);
          break;
        case "remove":
          this.remove("id", pathArray[0]);
          break;
        case "replace":
          item = this.get("id", pathArray[0])[0];
          switch(pathArray[1]) {
            case "status":
              item.values({status: operation.value,
                           "analyse-link": ["analysing", "prepared", "start analysis"].includes(operation.value) ? `/corpora/${pathArray[0]}/analyse` : ""});
              break;
            default:
              break;
          }
        default:
          break;
      }
    }
  }

  addRessources(ressources) {
    this.add(ressources.map(x => RessourceList.dataMapper[this.type](x)));
  }
}
RessourceList.dataMapper = {
  corpus: corpus => ({creation_date: corpus.creation_date,
                      description: corpus.description,
                      id: corpus.id,
                      "analyse-link": ["analysing", "prepared", "start analysis"].includes(corpus.status) ? `/corpora/${corpus.id}/analyse` : "",
                      "edit-link": `/corpora/${corpus.id}`,
                      status: corpus.status,
                      title: corpus.title}),
  job: job => ({creation_date: job.creation_date,
                description: job.description,
                id: job.id,
                link: `/jobs/${job.id}`,
                service: job.service,
                status: job.status,
                title: job.title})
};
RessourceList.options = {
  common: {page: 4, pagination: {innerWindow: 8, outerWindow: 1}},
  corpus: {item: `<tr>
                    <td>
                      <a class="btn-floating disabled">
                        <i class="material-icons service">book</i>
                      </a>
                    </td>
                    <td>
                      <b class="title"></b><br>
                      <i class="description"></i>
                    </td>
                    <td>
                      <span class="badge new status" data-badge-caption="">
                      </span>
                    </td>
                    <td class="right-align">
                      <a class="btn-small edit-link waves-effect waves-light">
                        <i class="material-icons">edit</i>
                      </a>
                      <a class="btn-small analyse-link waves-effect waves-light">
                        Analyse<i class="material-icons right">search</i>
                      </a>
                    </td>
                  </tr>`,
           valueNames: ["creation_date", "description", "title",
                        {data: ["id"]},
                        {name: "analyse-link", attr: "href"},
                        {name: "edit-link", attr: "href"},
                        {name: "status", attr: "data-status"}]},
  job: {item: `<tr>
                 <td>
                   <a class="btn-floating disabled">
                     <i class="material-icons service"></i>
                   </a>
                 </td>
                 <td>
                   <b class="title"></b><br>
                   <i class="description"></i>
                 </td>
                 <td>
                   <span class="badge new status" data-badge-caption=""></span>
                 </td>
                 <td class="right-align">
                   <a class="btn-small link waves-effect waves-light">
                    View<i class="material-icons right">send</i>
                  </a>
                 </td>
               </tr>`,
        valueNames: ["creation_date", "description", "title",
                     {data: ["id"]},
                     {name: "link", attr: "href"},
                     {name: "service", attr: "data-service"},
                     {name: "status", attr: "data-status"}]}
};


class ResultsList extends List {
  constructor(idOrElement, options={}) {
  super(idOrElement, options);
  this.eventTokens = {};  // all span tokens which are holdeing events if expert
  // mode is on. Collected here to delete later on
  this.currentExpertTokenElements = {}; // all token elements which have added
  // classes like chip and hoverable for expert view. Collected
  //here to delete later on
  }


  // get display options from display options form element
  static getDisplayOptions(displayOptionsFormElement) {
    // gets display options parameters
    let displayOptionsFormData
    let displayOptionsData;
    displayOptionsFormData = new FormData(displayOptionsFormElement);
    displayOptionsData =
      {
        "resultsPerPage": displayOptionsFormData.get("display-options-form-results_per_page"),
        "resultsContex": displayOptionsFormData.get("display-options-form-result_context"),
        "expertMode": displayOptionsFormData.get("display-options-form-expert_mode")
      };
    return displayOptionsData
  }

  // ###### Functions to inspect one match, to show more details ######
  // activate inspect buttons if progress is 100
  activateInspect() {
    let inspectBtnElements;
    if (progress === 100) {
      inspectBtnElements = document.getElementsByClassName("inspect");
      for (let inspectBtn of inspectBtnElements) {
        inspectBtn.classList.remove("disabled");
      }
    } else {
      return
    }
  }

  //gets result cpos infos for one dataIndex to send back to the server
  inspect(dataIndex) {
    this.contextId = dataIndex;
    let contextResultsElement;
    contextResultsElement = document.getElementById("context-results");
    contextResultsElement.innerHTML = "";  // clear it from old inspects
    contextModal.open();
    nopaque.socket.emit("corpus_analysis_inspect_match",
            {
              payload: {
                       first_cpos: results.data.matches[dataIndex].c[0],
                       last_cpos: results.data.matches[dataIndex].c[1],
                      }
            }
          );
  }

  HTMLTStrToElement(htmlStr) {
    // https://stackoverflow.com/questions/494143/creating-a-new-dom-element-from-an-html-string-using-built-in-dom-methods-or-pro/35385518#35385518
    let template = document.createElement("template");
    htmlStr = htmlStr.trim();
    template.innerHTML = htmlStr;
    return template.content.firstChild;
    }

  showMatchContext(response) {
    this.contextData;
    let c;
    let contextModalLoading;
    let contextModalReady;
    let contextResultsElement;
    let highlightSentencesSwitchElement;
    let htmlTokenStr;
    let lc;
    let modalExpertModeSwitchElement;
    let modalTokenElements;
    let nrOfContextSentences;
    let partElement;
    let rc;
    let token;
    let tokenHTMLArray;
    let tokenHTMlElement;
    let uniqueContextS;
    let uniqueS;

    this.contextData = response.payload;
    this.contextData["query"] = results.data.query;
    this.contextData["context_id"] = this.contextId;
    Object.assign(this.contextData, results.metaData);
    contextResultsElement = document.getElementById("context-results");
    modalExpertModeSwitchElement = document.getElementById("inspect-display-options-form-expert_mode_inspect");
    highlightSentencesSwitchElement = document.getElementById("inspect-display-options-form-highlight_sentences");
    nrOfContextSentences = document.getElementById("context-sentences");
    uniqueS = new Set();
    uniqueContextS = new Set();
    // check if cpos ranges are used or not
    if (this.contextData.cpos_ranges == true) {
      // python range like function from MDN
      // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Sequence_generator_(range)
      const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step));
      lc = range(this.contextData.match.lc[0], this.contextData.match.lc[1], 1)
      c = range(this.contextData.match.c[0], this.contextData.match.c[1], 1)
      rc = range(this.contextData.match.rc[0], this.contextData.match.rc[1], 1)
    } else {
      lc = this.contextData.match.lc;
      c = this.contextData.match.c;
      rc = this.contextData.match.rc;
    }
    // create sentence strings as tokens
    tokenHTMLArray = [];
    for (let cpos of lc) {
      token = this.contextData.cpos_lookup[cpos];
      uniqueS.add(token.s)
      htmlTokenStr = `<span class="token"` +
                           `data-sid="${token.s}"` +
                           `data-cpos="${cpos}">` +
                       `${token.word}` +
                     `</span>`;
      tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr)
      tokenHTMLArray.push(tokenHTMlElement);
    }
    for (let cpos of c) {
      token = this.contextData.cpos_lookup[cpos];
      uniqueContextS.add(token.s);
      uniqueS.add(token.s);
      htmlTokenStr = `<span class="token bold light-green"` +
                           `data-sid="${token.s}"` +
                           `data-cpos="${cpos}"` +
                           `style="text-decoration-line: underline;">` +
                       `${token.word}` +
                     `</span>`;
      tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr)
      tokenHTMLArray.push(tokenHTMlElement);
    }
    this.contextData["context_s_ids"] = Array.from(uniqueContextS);
    for (let cpos of rc) {
      token = this.contextData.cpos_lookup[cpos];
      uniqueS.add(token.s)
      htmlTokenStr = `<span class="token"` +
                           `data-sid="${token.s}"` +
                           `data-cpos="${cpos}">` +
                       `${token.word}` +
                     `</span>`;
      tokenHTMlElement = this.HTMLTStrToElement(htmlTokenStr)
      tokenHTMLArray.push(tokenHTMlElement);
    }
    // console.log(tokenHTMLArray);
    // console.log(uniqueS);

    for (let sId of uniqueS) {
      let htmlSentence = `<span class="sentence" data-sid="${sId}"></span>`;
      let sentenceElement = this.HTMLTStrToElement(htmlSentence);
      for (let tokenElement of tokenHTMLArray) {
        if (tokenElement.dataset.sid == sId) {
          sentenceElement.appendChild(tokenElement);
          sentenceElement.insertAdjacentHTML("beforeend", ` `);
        } else {
          continue;
        }
      }
      contextResultsElement.appendChild(sentenceElement);
    }


    // add inspect display options events
    modalExpertModeSwitchElement.onchange = (event) => {
      if (event.target.checked) {
        this.expertModeOn("context-results");
      } else {
        this.expertModeOff("context-results")
      }
    };

    highlightSentencesSwitchElement.onchange = (event) => {
      if (event.target.checked) {
        this.higlightContextSentences();
      } else {
      this.unhighlightContextSentences();
      }
    };

    nrOfContextSentences.onchange = (event) => {
      // console.log(event.target.value);
      this.changeSentenceContext(event.target.value);
    }

    // checks on new modal opening if switches are checked
    // if switches are checked functions are executed
    if (modalExpertModeSwitchElement.checked) {
      this.expertModeOn("context-results");
    }

    if (highlightSentencesSwitchElement.checked) {
      this.higlightContextSentences();
    }

    // checks the value of the number of sentences to show on modal opening
    // sets context sentences accordingly
    this.changeSentenceContext(nrOfContextSentences.value)
  }

  higlightContextSentences() {
    let sentences;
    sentences = document.getElementById("context-results").getElementsByClassName("sentence");
      for (let s of sentences) {
        s.insertAdjacentHTML("beforeend", `<span><br><br></span>`)
      }
  }

  unhighlightContextSentences() {
    let sentences;
    let br;
    sentences = document.getElementById("context-results").getElementsByClassName("sentence");
      for (let s of sentences) {
        br = s.lastChild;
        br.remove();
      }
  }

  changeSentenceContext(sValue, maxSValue=10) {
    let array;
    let sentences;
    let toHideArray;
    let toShowArray;
    sValue = maxSValue - sValue;
    // console.log(sValue);
    sentences = document.getElementById("context-results").getElementsByClassName("sentence");
    array = Array.from(sentences);
    if (sValue != 0) {
      toHideArray = array.slice(0, sValue).concat(array.slice(-(sValue)));
      toShowArray = array.slice(sValue, 9).concat(array.slice(9, -(sValue)))
    } else {
      toHideArray = [];
      toShowArray = array;
    }
    // console.log(array);
    // console.log("#######");
    // console.log(toHideArray);
    for (let s of toHideArray) {
      s.classList.add("hide");
    }
    for (let s of toShowArray) {
      s.classList.remove("hide");
    }
  }

  // ###### Display options changing live how the matches are being displayed ######

  // Event function that changes the shown hits per page.
  // Just alters the resultsList.page property
  changeHitsPerPage(event) {
    try {
      // console.log(this);
      this.page = event.target.value;
      this.update();
      this.activateInspect();
      if (expertModeSwitchElement.checked) {
        this.expertModeOn("query-display");  // page holds new result rows, so add new tooltips
      }
      nopaque.flash("Updated matches per page.", "corpus")
    } catch (e) {
      // console.log(e);
      // console.log("resultsList has no results right now.");
    }
  }

  // Event function triggered on context select change
  // also if pagination is clicked
  changeContext(event) {
    let array;
    let lc;
    let newContextValue;
    let rc;
    try {
        if (event.type === "change") {
            nopaque.flash("Updated context per match!", "corpus");
        }
    } catch (e) {
    } finally {
        newContextValue = document.getElementById("display-options-form-result_context").value;
        lc = document.getElementsByClassName("left-context");
        rc = document.getElementsByClassName("right-context");
        for (let element of lc) {
          array = Array.from(element.childNodes);
          for (let element of array.reverse().slice(newContextValue)) {
            element.classList.add("hide");
          }
          for (let element of array.slice(0, newContextValue)) {
            element.classList.remove("hide");
          }
        }
        for (let element of rc) {
          array = Array.from(element.childNodes);
          for (let element of array.slice(newContextValue)) {
            element.classList.add("hide");
          }
          for (let element of array.slice(0, newContextValue)) {
            element.classList.remove("hide");
          }
        }
    }
  }

  // ###### Expert view event functions ######

  // Event function to check if pagination is used and then look if
  // expertModeSwitchElement is checked
  // if checked than expertModeOn is executed
  // if unchecked expertModeOff is executed
  eventHandlerCheck(event) {
    if (expertModeSwitchElement.checked) {
      this.expertModeOn("query-display");
    } else if (!expertModeSwitchElement.checked) {
      event.preventDefault();
      this.expertModeOff("query-display");
    }
  }

  // function to create a tooltip for the current hovered token
  tooltipEventCreate(event) {
    // console.log("Create Tooltip on mouseover.");
    let token;
    token = results.data.cpos_lookup[event.target.dataset.cpos];
    if (!token) {
      token = this.contextData.cpos_lookup[event.target.dataset.cpos];
    }
    this.addToolTipToTokenElement(event.target, token);
  }

  // Function to destroy the current Tooltip for the current hovered tooltip
  // on mouse leave
  tooltipEventDestroy(event) {
    // console.log("Tooltip destroy on leave.");
    this.currentTooltipElement.destroy();
  }

  expertModeOn(htmlId) {
    // torn the expert mode on for all tokens in the DOM element identified by its htmlID
    this.currentExpertTokenElements[htmlId] = document.getElementById(htmlId).getElementsByClassName("token");
    this.tooltipEventCreateBind = this.tooltipEventCreate.bind(this);
    this.tooltipEventDestroyBind = this.tooltipEventDestroy.bind(this);
    this.eventTokens[htmlId] = [];
    for (let tokenElement of this.currentExpertTokenElements[htmlId]) {
      tokenElement.classList.add("chip", "hoverable", "expert-view");
      tokenElement.onmouseover = this.tooltipEventCreateBind;
      tokenElement.onmouseout = this.tooltipEventDestroyBind;
      this.eventTokens[htmlId].push(tokenElement);
    }
  }

  // fuction that creates Tooltip for one token and extracts the corresponding
  // infos from the result JSON
  addToolTipToTokenElement(tokenElement, token) {
    this.currentTooltipElement;
    this.currentTooltipElement = M.Tooltip.init(tokenElement,
     {"html": `<table>
                 <tr>
                   <th>Token information</th>
                   <th>Source information</th>
                 </tr>
                 <tr>
                   <td class="left-align">
                     Word: ${token.word}<br>
                     Lemma: ${token.lemma}<br>
                     POS: ${token.pos}<br>
                     Simple POS: ${token.simple_pos}<br>
                     NER: ${token.ner}
                   </td>
                   <td class="left-align">
                     Title: ${results.data.text_lookup[token.text].title}
                     <br>
                     Author: ${results.data.text_lookup[token.text].author}
                     <br>
                     Publishing year: ${results.data.text_lookup[token.text].publishing_year}
                   </td>
                 </tr>
               </table>`}
      );
  }

  // function to remove extra informations and animations from tokens
  expertModeOff(htmlId) {
    // console.log("Expert mode is off.");
    for (let tokenElement of this.currentExpertTokenElements[htmlId]) {
      tokenElement.classList.remove("chip", "hoverable", "expert-view");
    }
    this.currentExpertTokenElements[htmlId] = [];

    for (let eventToken of this.eventTokens[htmlId]) {
      eventToken.onmouseover = "";
      eventToken.onmouseout = "";
    }
  this.eventTokens[htmlId] = [];
  }

  createResultRowElement(item, chunk) {
    let c;
    let cCellElement;
    let cpos;
    let inspectBtn
    let lc;
    let lcCellElement;
    let matchNrElement;
    let matchRowElement;
    let rc;
    let rcCellElement;
    let textTitles;
    let textTitlesCellElement;
    let token;
    let values;
    // gather values from item
    values = item.values();
    if (chunk.cpos_ranges == true) {
      // python range like function from MDN
      // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Sequence_generator_(range)
      const range = (start, stop, step) => Array.from({ length: (stop - start) / step + 1}, (_, i) => start + (i * step));
      lc = range(values.lc[0], values.lc[1], 1)
      c = range(values.c[0], values.c[1], 1)
      rc = range(values.rc[0], values.rc[1], 1)
    } else {
      lc = values.lc;
      c = values.c;
      rc = values.rc;
    }
    // get infos for full match row
    matchRowElement = document.createElement("tr");
    matchRowElement.setAttribute("data-index", values.index)
    lcCellElement = document.createElement("td");
    lcCellElement.classList.add("left-context");
    matchRowElement.appendChild(lcCellElement);
    for (cpos of lc) {
      token = chunk.cpos_lookup[cpos];
      lcCellElement.insertAdjacentHTML("beforeend",
        `<span class="token" data-cpos="${cpos}">${token.word} </span>`);
    }

    // get infos for hit of match
    textTitles = new Set();
    cCellElement = document.createElement("td");
    cCellElement.classList.add("match-hit");
    textTitlesCellElement = document.createElement("td");
    textTitlesCellElement.classList.add("titles");
    matchNrElement = document.createElement("td");
    matchNrElement.classList.add("match-nr");
    matchRowElement.appendChild(cCellElement);
    for (cpos of c) {
      token = chunk.cpos_lookup[cpos];
      cCellElement.insertAdjacentHTML("beforeend",
        `<span class="token" data-cpos="${cpos}">${token.word} </span>`);
      // get text titles of every hit cpos token
      textTitles.add(chunk.text_lookup[token.text].title);
      // add button to trigger more context to every match td
      inspectBtn = document.createElement("a");
      inspectBtn.setAttribute("class", `btn-floating btn-flat waves-effect` +
                                       `waves-light grey right inspect disabled`
                              );
      inspectBtn.innerHTML = '<i class="material-icons">search</i>';
      inspectBtn.onclick = () => {this.inspect(values.index)};
    }
    // add text titles at front as first td of one row
    cCellElement.appendChild(inspectBtn);
    textTitlesCellElement.innerText = [...textTitles].join(", ");
    matchRowElement.insertAdjacentHTML("afterbegin", textTitlesCellElement.outerHTML);
    matchNrElement.innerText = values.index + 1;
    matchRowElement.insertAdjacentHTML("afterbegin", matchNrElement.outerHTML);

    // get infos for right context of match
    rcCellElement = document.createElement("td");
    rcCellElement.classList.add("right-context");
    matchRowElement.appendChild(rcCellElement);
    for (cpos of rc) {
      token = chunk.cpos_lookup[cpos];
      rcCellElement.insertAdjacentHTML("beforeend",
        `<span class="token" data-cpos="${cpos}">${token.word} </span>`);
    }
    return matchRowElement
  }
}