<!DOCTYPE html>
<html>
<head>
  <meta http-equiv="content-type" content="text/html; charset=utf-8">
  <style type="text/css"><%- include('page.css', {style}) %></style>
  <style type="text/css"><%- include('fontello-embedded.css') %></style>
  <style type="text/css">
  <% if (process.platform == 'win32') { %>
    <%- include('light-mode.css', {style}) %>
    <%- include('../../node_modules/highlight.js/styles/github.css') %>
  <% } else { %>
    @media (prefers-color-scheme: light) {
      <%- include('light-mode.css', {style}) %>
      <%- include('../../node_modules/highlight.js/styles/github.css') %>
    }
    @media (prefers-color-scheme: dark) {
      <%- include('dark-mode.css', {style}) %>
      <%- include('../../node_modules/highlight.js/styles/github-dark.css') %>
    }
  <% } %>
  </style>
</head>
<body>

<div id="messages">
  <% for (const message of messages) { %>
    <%- include('message.html', {message, response: {pending: false}}) %>
  <% } %>
</div>

<script type="text/javascript" charset="utf-8">
  // Global variables ======================================================

  // Which element is on top.
  var topElement = null;
  // Offset from the top element.
  var scrollOffset = 0;
  // Mark page is resizing.
  var ignoreNextScroll = false;

  // Initialization ========================================================

  var isIE = /Trident|MSIE/.test(navigator.userAgent);
  if (isIE) {
    // Add endsWith polyfill.
    if (!String.prototype.endsWith) {
      String.prototype.endsWith = function(suffix) {
        return this.indexOf(suffix, this.length - suffix.length) !== -1;
      };
    }
  } else {
    // For IE we need to scroll after ready, as the window body's size may be
    // incorrect before showing the browser.
    // For morden browsers scrolling before showing has the best visual effect.
    scrollToBottom();
  }

  // External APIs =========================================================

  // External API for invoking callbacks from outside world.
  var callbacks = {};
  window.executeCallback = function(id, arg) {
    if (!callbacks[id])
      return;
    callbacks[id](arg);
    delete callbacks[id];
  };
  // External API to add a new message.
  window.appendMessage = function(html) {
    removeSuggestedReplies();
    var children = HTMLToDOM(html);
    for (var i = 0; i < children.length; i++)
      messages.appendChild(children[i]);
    // Scroll to bottom for new messages.
    scrollToBottom();
  };
  // External API to update pending message.
  window.appendHtmlToPendingMessage = function(delta) {
    var text = findPending('text');
    if (!text)
      return;
    removeLoader();
    var wasAtBottom = isPageBottom();
    if (delta.type == 'append') {
      text.insertAdjacentHTML('beforeend', delta.html);
    } else if (delta.type == 'insert') {
      // Find the last child.
      var target = text;
      try {
        for (var i = 0; i < delta.insertDepth; ++i)
          target = target.lastElementChild;
        if (!target)
          throw new Error('Unexpected null child');
      } catch (error) {
        console.error('Error inserting', delta, text.innerHTML);
        throw error;
      }
      // Remove text from the target element.
      if (delta.deleteText) {
        var textNodes = target.childNodes;
        if (textNodes.length <= 0)
          throw new Error('Trying to remove text while there is no text node.');
        // Iterate text nodes and remove them until enough text is deleted.
        for (var i = textNodes.length - 1; i >= 0; --i) {
          var node = textNodes[i];
          if (node.nodeType != Node.TEXT_NODE)
            throw new Error('See non text node when trying to remove text.');
          if (node.textContent.length >= delta.deleteText) {
            node.textContent = node.textContent.slice(0, -delta.deleteText);
            delta.deleteText = 0;
            break;
          }
          delta.deleteText -= node.textContent.length;
          target.removeChild(node);
        }
        if (delta.deleteText != 0)
          throw new Error('Unable to delete enough text requested');
      }
      // Insert html to the ending of the element.
      target.insertAdjacentHTML('beforeend', delta.html);
    } else if (delta.type == 'reset') {
      text.innerHTML = delta.html;
    } else {
      throw new Error('Invalid html delta: ' + delta + '.');
    }
    if (wasAtBottom)
      scrollToBottom();
  };
  window.endPending = function() {
    var wasAtBottom = isPageBottom();
    var pending = findPending();
    if (pending)
      pending.removeAttribute('id');
    removeLoader();
    highlightCodes();
    if (wasAtBottom)
      scrollToBottom();
  };
  window.removeMessagesAfter = function(index) {
    var wasAtBottom = isPageBottom();
    for (var i = messages.children.length - 1; i >= 0; --i) {
      var msg = messages.children[i];
      var msgIndex = parseInt(msg.getAttribute('message-index'));
      if (msgIndex < index)
        break;
      messages.removeChild(msg);
    }
    if (wasAtBottom)
      scrollToBottom();
  };
  window.updateMessage = function(index, html) {
    var message = document.querySelectorAll('div[message-index="' + index + '"]')[0];
    if (!message) {
      console.error('No message for updating.');
      return;
    }
    message.outerHTML = html;
  };
  window.changeAll = function(query, content) {
    var elements = document.querySelectorAll(query);
    for (var i = 0; i < elements.length; ++i)
      elements[i].innerHTML = content;
  };
  window.abortPending = function() {
    var text = findPending('text');
    var wasAtBottom = isPageBottom();
    if (text) {
      var inlineText = text;
      // If the last child of text is an element, append into it.
      if (text.lastElementChild &&
          text.lastElementChild == this.lastChild &&
          text.lastElementChild.tagName.toLowerCase() == 'p')
        inlineText = text.lastElementChild;
      inlineText.insertAdjacentHTML('beforeend', ' <span class="error label">(aborted)</span>');
    }
    endPending();
    if (wasAtBottom)
      scrollToBottom();
  };
  window.appendError = function(error) {
    var text = findPending('text');
    if (!text)
      throw new Error('Can not append error when there is no pending message.');
    var wasAtBottom = isPageBottom();
    text.innerHTML += '<div class="error">' + error + '</div>';
    var actionMenu = text.parentNode.getElementsByClassName('action-menu');
    if (actionMenu)
      text.parentNode.removeChild(actionMenu[0]);
    endPending();
    if (wasAtBottom)
      scrollToBottom();
  };
  window.appendSteps = function(steps) {
    var element = findPending('steps');
    if (!element)
      throw new Error('Can not append steps when there is no pending message.');
    var wasAtBottom = isPageBottom();
    for (var i in steps) {
      var p = document.createElement('p');
      p.appendChild(document.createTextNode(steps[i]));
      element.appendChild(p);
    }
    if (wasAtBottom)
      scrollToBottom();
  }
  window.appendLinks = function(index, links) {
    var text = findPending('text');
    if (!text)
      throw new Error('Can not append links when there is no pending message.');
    var as = [].slice.call(text.getElementsByClassName('pending-ref'));
    for (var i = 0; i < as.length; ++i) {
      var a = as[i];
      var aIndex = a.firstElementChild.textContent - 1;
      if (aIndex >= index && aIndex < index + links.length) {
        var link = links[aIndex - index];
        a.classList.remove('pending-ref');
        a.title = link.name;
        a.href = link.url;
      }
    }
  }
  window.setSuggestdReplies = function(replies) {
    var messages = document.getElementsByClassName('msg');
    if (messages.length === 0)
      throw new Error('Can not set replies when there is no message.');
    var wasAtBottom = isPageBottom();
    removeSuggestedReplies();
    var content = messages[messages.length - 1].getElementsByClassName('content')[0];
    content.insertAdjacentHTML('beforeend', replies);
    if (wasAtBottom)
      scrollToBottom();
  }

  // DOM event handlers ====================================================

  // Capture clicks on <a>.
  window.onclick = function(event) {
    var target = findParent('a', event.target);
    if (!target || !target.href)
      return false;
    // Open links in actual browsers.
    if (target.href.indexOf('#') == -1 && !target.onclick) {
      chie.openLink(target.href);
      return false;
    }
    // Copy code.
    if (target.href.endsWith('#copy-code')) {
      chie.copyText(target.parentNode.parentNode.children[1].textContent);
      showOk(target);
      return false;
    }
    var msg = target.parentNode.parentNode.parentNode;
    var index = parseInt(msg.getAttribute('message-index'));
    var text = msg.getElementsByClassName('text')[0];
    // Show popup window to display message.
    if (target.href.endsWith('#show-text-at')) {
      chie.showTextAt(index, isIE ? text.offsetWidth : text.getBoundingClientRect().width);
      return false;
    }
    // Request regeneration
    if (target.href.endsWith('#regenerate-from')) {
      chie.regenerateFrom(index);
      return false;
    }
    // Copy text.
    if (target.href.endsWith('#copy-text-at')) {
      chie.copyTextAt(index);
      showOk(target);
      return false;
    }
  };
  // Move focus to entry on Tab.
  window.onkeyup = function(event) {
    if (event.keyCode == 9 &&  // Tab key
        !event.ctrlKey && !event.altKey && !event.shiftKey) {
      event.preventDefault();
      chie.focusEntry();
    }
  };
  // Update states when scrolling.
  window.onscroll = function() {
    if (ignoreNextScroll) {
      ignoreNextScroll = false;
      return;
    }
    topElement = null;
    scrollOffset = 0;
    var messages = document.getElementsByClassName('msg');
    if (messages.length === 0)
      return;
    var setTopElement = !isPageBottom();
    // Search backwards.
    for (var i = messages.length - 1; i >= 0;  --i) {
      var rect = messages[i].getBoundingClientRect();
      // Make sure the action menu is always visible.
      var actionMenu = messages[i].getElementsByClassName('action-menu')[0];
      actionMenu.style.position = rect.top > 0 ? 'absolute' : 'fixed';
      // Find out which message is the oldest visible one.
      if (setTopElement) {
        topElement = messages[i];
        scrollOffset = rect.top;
      }
      if (rect.top < 0)
        break;
    }
  };
  // Make sure the view is always scrolled at current message.
  window.onresize = function() {
    ignoreNextScroll = true;
    if (topElement)
      window.scrollTo(0, topElement.getBoundingClientRect().top + window.pageYOffset - scrollOffset);
    else
      scrollToBottom();
  };
  // Catch errors.
  window.onerror = function(message, url, line, column, error) {
    chie.catchDomError(message, url, line, column, error);
  };

  // Ready =================================================================

  // See comment above on IE.
  if (isIE)
    scrollToBottom();

  // Helpers ===============================================================

  // Find all unhilighted code blocks and highlight them.
  function highlightCodes() {
    var pres = [].slice.call(document.getElementsByClassName('unhilighted'));
    for (var i = 0; i < pres.length; ++i) {
      var pre = pres[i];
      pre.classList.remove('unhilighted');
      highlightCode(pre, pre.getAttribute('lang'));
    }
  }
  // Highlight a single code block.
  var nextCallbackId = 0;
  function highlightCode(element, lang) {
    callbacks[++nextCallbackId] = function(code) {
      element.innerHTML = code.html;
      if (code.lang)
        element.parentNode.firstElementChild.firstElementChild.textContent = code.lang;
    }
    chie.highlightCode(element.textContent, lang, nextCallbackId);
  }
  // Show ok when clicked on icon.
  function showOk(target) {
    var i = target.firstElementChild;
    var oldClassName = i.className;
    i.className = 'icon-ok';
    setTimeout(function () { i.className = oldClassName }, 1000);
  }
  // Remove the load indicator.
  function removeLoader() {
    var loader = document.getElementById('loader');
    if (loader)
      loader.parentNode.removeChild(loader);
  }
  // Remove suggested replies.
  function removeSuggestedReplies() {
    var replies = document.getElementById('replies');
    if (replies)
      replies.parentNode.removeChild(replies);
  }
  // Helper to convert HTML to DOM.
  function HTMLToDOM(html) {
    // Note: Don't use DOMParser, it has problems with applying CSS styles.
    var wrapper = document.createElement('div');
    wrapper.innerHTML = html;
    return wrapper.childNodes;
  }
  // Find the pending message.
  function findPending(child) {
    var pending = document.getElementById('pending');
    if (!pending) {
      console.error('Unable to find the pending message.');
      return;
    }
    if (!child)
      return pending;
    var text = pending.getElementsByClassName(child)[0];
    if (!text) {
      console.error('The pending message has no '+ child + ' element.');
      return;
    }
    return text;
  }
  // Scroll to the bottom of page.
  function scrollToBottom() {
    window.scrollTo(0, document.body.scrollHeight);
  }
  // Check scrolled at page bottom.
  function isPageBottom() {
    // IE has one pixel offset problem so we can not do precise comparison, and
    // we want to treat as at bottom if only slightly scrolled above to provide
    // more natural experience.
    return document.body.scrollHeight - (window.pageYOffset + window.innerHeight) < 5;
  }
  // Helper function to find parent element.
  function findParent(tagName, target) {
    var loops = 0;
    while (target && ++loops < 4) {
      if (target.tagName && target.tagName.toLowerCase() === tagName)
        return target;
      target = target.parentNode;
    }
    return null;
  }
</script>
</body>
</html>
