/*

Script for the left iframe.

*/

// CSS must be loaded before finding the geography of indexes // document.addEventListener(‘DOMContentLoaded’, function () { window.addEventListener(‘load’, function () {

// --- class for index info

// the info about an index block
// index blocks are like this:

// div#file-index
//   div.title
//     span.text
//       Files
//   div.entries
//     <p><a href="files/standard_library_rdoc.html">doc/standard_library.rdoc</a></p>
//     ...

// div#class-index
//   div.title
//     span.text
//       Classes
//     input.search-field
//   div.entries
//     p.class
//       span.type
//         C
//       <a href="classes/ACL.html">ACL</a>
//     ...

class Index {

  constructor(name) {
    this.name = name;
    this.div = document.getElementById(`${name}-index`);
    if (this.div) {
      // the height fitting the whole content
      this.fullHeight = this.div.getBoundingClientRect().height;
      this.title = this.div.querySelector('div.title');
      this.text = this.title.querySelector('span.text');
      // null for files:
      this.searchBox = this.title.querySelector('input');
      if (this.searchBox)
        this.fullBoxWidth = this.searchBox.getBoundingClientRect().width;
      else
        this.fullBoxWidth = 0;
      this.list = this.div.querySelector('div.entries');
      const style = window.getComputedStyle(this.list);
      const listHeightPadding = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom);
      this.entries = this.list.getElementsByTagName('p');
      this.links = this.list.getElementsByTagName('a');
      this.entryCount = this.entries.length;
      // height of one entry
      const listRect = this.list.getBoundingClientRect();
      this.entryHeight = (listRect.height - listHeightPadding) / this.entries.length;
      // amount of vertical space in an index other than the entries themselves
      // (title, paddings, etc.)
      this.fixedHeight = this.fullHeight - listRect.height + listHeightPadding;
    }
    else {
      this.fullHeight = 0;
      this.title = null;
      this.searchBox = null;
      this.list = null;
      this.entries = [];
      this.links = [];
      this.entryCount = 0;
      this.entryHeight = 0;
      this.fixedHeight = 0;
    }
    // height of this index before the current resize
    this.prevHeight = null;
  }

  get currentHeight() {
    if (this.div)
      return this.div.getBoundingClientRect().height;
    else
      return 0;
  }

  // enough room for 2 entries
  get minHeight() {
    return this.fixedHeight + 2 * this.entryHeight;
  }

  setHeight(height, { resetPrev = false } = {}) {
    if (!this.div) return;
    const h = height - this.fixedHeight;
    if (this.prevHeight === null || resetPrev) {
      // no previous or reset asked: set prev = current
      this.list.style.height = `${h}px`;
      this.prevHeight = this.currentHeight;
    }
    else {
      // save previous value before setting the current height
      this.prevHeight = this.currentHeight;
      this.list.style.height = `${h}px`;
    }
  }

  // save the layout of this index, including the search text
  saveInfo(storage) {
    if (!this.div) return;
    storage.setItem(`${this.name}Index.prevHeight`, this.prevHeight);
    storage.setItem(`${this.name}Index.styleHeight`, this.list.style.height);
    storage.setItem(`${this.name}Index.scrollTop`, this.list.scrollTop);
    if (this.searchBox) {
      storage.setItem(`${this.name}Index.searchWidth`, this.searchBox.style.width);
      storage.setItem(`${this.name}Index.searchText`, this.searchBox.value || '');
    }
  }

  // restore a previously saved layout & search text
  restoreInfo(storage) {
    if (!this.div) return;
    this.prevHeight = storage.getItem(`${this.name}Index.prevHeight`);
    this.list.style.height = storage.getItem(`${this.name}Index.styleHeight`);
    this.list.scrollTop = storage.getItem(`${this.name}Index.scrollTop`);
    if (this.searchBox) {
      this.searchBox.style.width = storage.getItem(`${this.name}Index.searchWidth`);
      this.searchBox.value = storage.getItem(`${this.name}Index.searchText`);
    }
  }

  // highlight the entry for <a> aNode and make sure it is visible
  setCurrent(aNode) {
    for (const a of this.links)
      if (a === aNode) {
        a.classList.add('current-main');
        this.ensureVisible(a);
      }
      else
        a.classList.remove('current-main');
  }

  ensureVisible(a) {
    const aTopOffset = a.offsetTop - this.links[0].offsetTop;
    const aHeight = a.getBoundingClientRect().height;
    const offScreen =
      // not (completely) visible because above
      aTopOffset < this.list.scrollTop ||
      // not (completely) visible because below
      aTopOffset + aHeight > this.list.scrollTop + this.list.clientHeight
    ;
    // position in the middle of the list
    if (offScreen)
      this.list.scrollTop = aTopOffset - this.list.clientHeight / 2;
  }

}

// --- global setup

const fileIndex = new Index('file');
const classIndex = new Index('class');
const methodIndex = new Index('method');

setupResizing();

// --- setup the vertical resizing of indexes by dragging

const fileClassResizer = document.getElementById('file-class-resizer');   // may be null
const classMethodResizer = document.getElementById('class-method-resizer');

if (!restoreLayoutInfo())
  frameResized(true);
setupSearches();

if (fileClassResizer)
  fileClassResizer.addEventListener('mousedown', function(e) {startDrag(e, fileIndex, classIndex)});
classMethodResizer.addEventListener('mousedown', function(e) {startDrag(e, classIndex, methodIndex)});

// --- highlight of the current file/class/method

highlightCurrentIndexEntries();

function highlightCurrentIndexEntries() {

  // /C:/docs/ruby/32/core/files/toc_core_md.html
  // /C:/docs/ruby/32/core/classes/Process.html
  const currentPath = window.parent.location.pathname;
  const index = (currentPath.indexOf('/classes/') < 0) ? fileIndex : classIndex;

  for (const a of index.links) {
    const href = a.getAttribute('href');
    if (currentPath.endsWith(href)) {
      index.setCurrent(a);
      break;
    }
  }

  if (index === fileIndex)
    return;

  setCurrentMethod(currentPath);
}

function setCurrentMethod(currentPath) {
  const hash = window.parent.location.hash;
  if (!hash) return;

  const currentMethod = `${currentPath}${hash}`;
  for (const a of methodIndex.links) {
    const href = a.getAttribute('href');
    if (currentMethod.endsWith(href)) {
      methodIndex.setCurrent(a);
      break;
    }
  }
}

// --- when clicking on a link, remember the scroll positions & search state of each index

for (const a of fileIndex.links)
  a.addEventListener('click', saveLayoutInfo);
for (const a of classIndex.links)
  a.addEventListener('click', saveLayoutInfo);
for (const a of methodIndex.links)
  a.addEventListener('click', saveLayoutInfo);

function saveLayoutInfo(e) {
  const s = window.parent.sessionStorage;
  s.setItem('infoSaved', 'true');

  // the width of the left frame
  const width = window.parent.document.getElementById('left-container').style.width;
  s.setItem('left.frameWidth', width);

  // position of the left frame resizer
  const left = window.parent.document.getElementById('resizer').style.left;
  s.setItem('left.resizerLeft', left);

  // the height & position of each index
  fileIndex.saveInfo(s);
  classIndex.saveInfo(s);
  methodIndex.saveInfo(s);
}

// restores the saved layout and returns true, or returns false if no saved layout
function restoreLayoutInfo() {
  // restore previous scroll positions & search states if any
  const s = window.parent.sessionStorage;
  const saved = s.getItem(`infoSaved`);
  if (saved) {
    window.parent.document.getElementById('left-container').style.width = s.getItem('left.frameWidth');
    window.parent.document.getElementById('resizer').style.left = s.getItem('left.resizerLeft');
    fileIndex.restoreInfo(s);
    classIndex.restoreInfo(s);
    methodIndex.restoreInfo(s);
    placeResizers();
  }
  return saved === 'true';
}

// --- when navigating inside the same main document, handle current method highlighting

window.parent.addEventListener('hashchange', mainHashChanged);

// highlight the new method in the main frame, and in the method index
function mainHashChanged(e) {
  const id = e.newURL.split('#')[1];
  highlightElement(id);
  setCurrentMethod(window.parent.location.pathname);
}

// highlight the passed id in the main frame
function highlightElement(id) {
  const doc = window.parent.document;
  for (const h of doc.querySelectorAll('.highlighted'))
    h.classList.remove('highlighted');
  if (id === 'header')
    return;
  const e = doc.getElementById(id);
  if (e)
    e.classList.add('highlighted');
}

// --- search boxes

function setupSearches() {
  const helpText = 'filter...';
  setupSearch(classIndex.searchBox, classIndex.entries, helpText);
  setupSearch(methodIndex.searchBox, methodIndex.entries, helpText);
}

function setupSearch(searchBox, entries, helpText) {
  // hook quicksearch
  setupQuickSearch(searchBox, entries);
  // set helper text
  searchBox.setAttribute('placeholder', helpText);
}

// --- vertical manual resizing of the indexes
// div#file-index
// div#file-class-resizer  (absent if the above div is not there)
// div#class-index
// div#class-method-resizer
// div#method-index

let startY;             // where drag begins
let startTopIndex;      // index object above when drag begins
let startBottomIndex;   // index object below when drag begins
let startTopHeight;     // height of the index above when drag begins
let startBottomHeight;  // height of the index below when drag begins

function startDrag(e, topIndex, bottomIndex) {
  startY = e.clientY;
  startTopIndex = topIndex;
  startBottomIndex = bottomIndex;
  startTopHeight = topIndex.currentHeight;
  startBottomHeight = bottomIndex.currentHeight;
  document.documentElement.addEventListener('mousemove', doDrag);
  document.documentElement.addEventListener('mouseup', stopDrag);
}

function stopDrag(e) {
  document.documentElement.removeEventListener('mousemove', doDrag);
  document.documentElement.removeEventListener('mouseup', stopDrag);
}

function doDrag(e) {
  const dY = e.clientY - startY;
  const newTopHeight = startTopHeight + dY;
  const newBottomHeight = startBottomHeight - dY;
  if (newTopHeight < startTopIndex.minHeight || newBottomHeight < startBottomIndex.minHeight)
    return;
  startTopIndex.setHeight(newTopHeight, { resetPrev: true });
  startBottomIndex.setHeight(newBottomHeight, { resetPrev: true });
  placeResizers();
}

// place the vertical resizers between their indexes
function placeResizers() {
  if (fileClassResizer)
    fileClassResizer.style.top = `${fileIndex.currentHeight}px`;
  if (classMethodResizer)
    classMethodResizer.style.top = `${fileIndex.currentHeight + classIndex.currentHeight}px`;
}

// --- resizing of the window by the user or by the left/main resizer in the main frame

function setupResizing() {
  window.addEventListener('resize', function(e) { frameResized(false) });
}

// resize the left index blocks
// if initial is true, this is the first resize, called above when loading
function frameResized(initial) {

  resizeSearchField(classIndex);
  resizeSearchField(methodIndex);

  const heights = initial ? initialHeights() : updatedHeights();
  fileIndex.setHeight(heights.files);
  classIndex.setHeight(heights.classes);
  methodIndex.setHeight(heights.methods);

  placeResizers();
}

// resize a search field to avoid overlapping text (if possible)
function resizeSearchField(indexBlock) {
  const container = indexBlock.title;
  const box = indexBlock.searchBox;
  const text = indexBlock.text;

  const frameWidth = window.visualViewport.width;

  const textRect = text.getBoundingClientRect();
  const textWidth = textRect.width;
  const textRight = textRect.left + textWidth;

  const boxRect = box.getBoundingClientRect();
  const boxLeft = boxRect.left;
  const boxWidth = boxRect.width;

  const boxRight = boxLeft + boxWidth;
  const offset = frameWidth - boxRight;

  // try to ensure offset between the text & the box
  // if the box becomes too narrow, stop resizing it

  const boxSpace = frameWidth - 2 * offset - textRight;

  if (boxSpace >= indexBlock.fullBoxWidth)
    box.style.width = `${indexBlock.fullBoxWidth}px`;   // plenty of room
  else if (boxSpace > textWidth)
    box.style.width = `${boxSpace}px`;       // shrink it
  else
    box.style.width = `${textWidth}px`;      // min width

}

// returns the initial heights of index blocks for the current window size
// if the combined height of the 3 indexes is more than the window height:
// - shrinks the file index to 5 entries if more
// - shrinks the method index to 33%, but leaving at least 5 entries visible
// - shrinks the class index to the available space, but leaving at least 5 entries visible
// so if very little height, the result may not fit
function initialHeights() {

  // returned information
  const heights = {
    files: fileIndex.fullHeight,
    classes: classIndex.fullHeight,
    methods: methodIndex.fullHeight
  };

  let frameHeight = window.visualViewport.height;
  const totalHeight = heights.files + heights.classes + heights.methods;

  // if everything fits, we're done
  if (totalHeight <= frameHeight)
    return heights;

  let excess = totalHeight - frameHeight;

  // first try to reduce the file index to 5 files if more
  if (fileIndex.entryCount > 5) {
    // the most we can gain:
    const gain = (fileIndex.entryCount - 5) * fileIndex.entryHeight;
    if (gain >= excess) {
      heights.files -= excess;
      return heights;
    }
    // we will shrink something else: minimize the height for files
    heights.files -= gain;
    excess -= gain;
  }

  // if the method list is more than 33% high,
  // try first to reduce it to 33%
  if (methodIndex.entryCount > 5) {
    const maxHeight = frameHeight / 3;
    if (methodIndex.fullHeight > maxHeight) {
      let gain = methodIndex.fullHeight - maxHeight;
      // leave at least 5 methods visible
      const maxGain = (methodIndex.entryCount - 5) * methodIndex.entryHeight;
      if (gain > maxGain)
        gain = maxGain;
      if (gain >= excess) {
        // just shrinking the methods will be fine
        heights.methods -= excess;
        return heights;
      }
      // we will also shrink the classes: gain what we can
      heights.methods -= gain;
      excess -= gain;
    }
  }

  // shrink the classes if possible, leaving at least 5 classes visible
  if (classIndex.entryCount > 5) {
    let gain = excess;
    const maxGain = (classIndex.entryCount - 5) * classIndex.entryHeight;
    if (gain > maxGain)
      gain = maxGain;
    heights.classes -= gain;
    excess -= gain;
  }

  return heights;
}

// returns the updated heights of index blocks for the current window size
// resizes each index proportionally
function updatedHeights() {

  let frameHeight = window.visualViewport.height;
  const totalHeight = fileIndex.fullHeight + classIndex.fullHeight + methodIndex.fullHeight;

  // if everything fits, we're done
  if (totalHeight <= frameHeight)
    return {
      files: fileIndex.fullHeight,
      classes: classIndex.fullHeight,
      methods: methodIndex.fullHeight
    };

  // try to maintain proportionality
  const heights = {
    files: fileIndex.currentHeight,
    classes: classIndex.currentHeight,
    methods: methodIndex.currentHeight
  };

  // already sized as appropriate: nothing to do
  if (frameHeight === fileIndex.currentHeight + classIndex.currentHeight + methodIndex.currentHeight)
    return heights;

  // proportional resize
  const prevHeight = fileIndex.prevHeight + classIndex.prevHeight + methodIndex.prevHeight;
  const factor = frameHeight / prevHeight;

  if (fileIndex.prevHeight * factor >= fileIndex.minHeight)
    heights.files = fileIndex.prevHeight * factor;

  if (classIndex.prevHeight * factor >= classIndex.minHeight)
    heights.classes = classIndex.prevHeight * factor;

  if (frameHeight - heights.files - heights.classes >= methodIndex.minHeight)
    heights.methods = frameHeight - heights.files - heights.classes;

  return heights;
}

});