Chapter counter for Map and Next Chapter choice

Counts and displays how many chapters each branch has inside on the map screen and when choosing your next chapter

// ==UserScript==
// @name         Chapter counter for Map and Next Chapter choice
// @version      3.0.2
// @description  Counts and displays how many chapters each branch has inside on the map screen and when choosing your next chapter
// @author       sllypper
// @namespace    https://greasyfork.org/en/users/55535-sllypper
// @match        *://chyoa.com/story/*
// @match        *://chyoa.com/chapter/*
// @icon         https://chyoa.com/favicon.png
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @grant        GM.listValues
// @grant        GM.registerMenuCommand
// ==/UserScript==

/* Load the entire map by moving to the bottom, then click the Regenerate button. */

// runs automatically when opening a new map
let runAutomatically = true

// shows the floating buttons on the bottom-right of the page
let showFloatingButton = true
let showTogglerButton = true

// Style for the counter on the chapter pages. Use one of the below between ""
// alternative | simple | crazy | default
const cssStyle = "alternative"

/************************/

let chapterDataArray;
let chapterData;
let storyStorageId;
let pageType = getPageType()
console.log('The current page is a ' + pageType);

/************************/
(async() => {
    GM.registerMenuCommand("Get map & regenerate (experimental)", getMapAndRegenerateViaAjax, "k" )

    if (pageType == "map") {
        GM.registerMenuCommand("Regenerate map data", generateDraw, "r");
        GM.registerMenuCommand("Redraw map counters", drawChildrenCounter, "d");
        GM.registerMenuCommand("Fold read chapters", foldTopmostReadChapters, "f");
        GM.registerMenuCommand("Fold unread chapters", foldTopmostUnreadChapters, "u");

        await loadMapData()

        if (showFloatingButton) { document.body.appendChild(btnLoad()) }

        addStyleHead(".title-wrapper > .children { padding-right: 8px; }")

        if (showTogglerButton) { collapsibator() }
    }

    // /chapter/* and /story/* pages
    if (pageType == "chapter") {
        await showChapterCountOnChapterChoices()
        showChapterDate()

        // previous chapters map tree on clicking the link
        await prevMapInit()
    }

    GM.registerMenuCommand("Delete this from cache", deleteThisFromCache, "c");
    GM.registerMenuCommand("Delete entire cache", deleteEntireCache, "a");

    GM.registerMenuCommand("Get superParent Group sorted by New", () => console.log(getSuperParentGroupSortedByNew()) )

    unsafeWindow.computeParents = getSuperParentGroupSortedByNew
})();
return
/************************/

async function loadMapData() {
  if (! await attemptLoadChapterArrayFromCache() && runAutomatically) {
    // no map data on cache
    generateMap()
    drawChildrenCounter()
  } else {
    // got map data from cache
    drawChildrenCounter()
  }
}

function generateDraw() {
    generateMap()
    drawChildrenCounter()
}

function createChildrenElement(chapterCount) {
    var child = document.createElement('span')
    child.setAttribute('class', 'control-item children')
    let icon = document.createElement('i')
    icon.setAttribute('class', 'btb bt-folder')
    child.appendChild(icon);
    let count = document.createTextNode(" "+chapterCount)
    child.appendChild(count)
    return child
}

function addStyleHead(css) {
    var style = document.createElement('style');
    document.head.appendChild(style);
    style.textContent = css;
};

async function attemptLoadChapterArrayFromCache() {
  if (!storyStorageId) loadStoryPathName()
  if (! await storyIsInTheCache(storyStorageId)) {
    console.log('story not found in cache', GM.listValues(), storyStorageId);
    return false
  }

  let chapterDataJson = await GM.getValue(storyStorageId)

  chapterDataArray = JSON.parse(chapterDataJson);
  console.log("!! got map from cache");
  unsafeWindow.chapterDataArray = chapterDataArray
  console.log("Map available as a javascript array on `chapterDataArray`");

  return true
}

function deleteThisFromCache() {
    if (pageType == "map") {
        GM.deleteValue(getStoryStorageId())
    }
    let storyStorageId = getStoryStorageId()
    GM.deleteValue(storyStorageId)
}

function deleteEntireCache() {
    (GM.listValues()).forEach((value) => {
        GM.deleteValue(value)
    })
}

async function readOrGenerateMap() {
    if (! await attemptLoadChapterArrayFromCache()) generateMap()
}

function generateMap() {
    console.log("!! generating map");
    chapterDataArray = createStoryMap()
    console.log("Chapter Count = ", chapterDataArray.length);

    unsafeWindow.chapterDataArray = chapterDataArray
    console.log("Map available as a javascript array on `chapterDataArray`");

    console.log("!! assembling hierarchy tree");
    countMapChapters();
    console.log("!! done counting the children");

    // cache it
    GM.setValue(getStoryStorageId(), JSON.stringify(chapterDataArray))
}

/**
* counts and assigns the children count of all elements
*/
function countMapChapters() {
    for (const chapter of chapterDataArray) {
        // find the leaves of the tree
        if (chapter.children.length !== 0) continue;
        chapter.chapterCount = 0;

        let next = chapter.parent != null ? chapter.parent : -1;

        // rise through the branch until it can't
        while (next != -1) {
            //console.log("from", chapter.id, "to", next);
            next = countChildChaptersOf(next);
        }
        // then continue to the next chapter childless chapter
    }
    // done
}

/**
* Counts and assigns the chapterCount of the node at that index
* Aborts returning -1 if it already has been counted
* or can't because one of its children hasn't been counted yet
*
* @param {number} currIndex - The index of the node
* @returns {number} The index of the next node. -1 to abort
*/
function countChildChaptersOf(currIndex) {
    let nextIndex = -1;
    // if (currIndex > chapterDataArray.length) console.log('currIndex > chapterDataArray.length', currIndex, chapterDataArray.length);
    const currentNode = chapterDataArray[currIndex];
    if (currentNode.chapterCount != undefined) {
        // this node was already been processed
        // abort
        return nextIndex;
    }
    // sum the counts of its children
    const chapterCountSum = sumChildrenChaptersCount(currentNode);
    if (chapterCountSum === -1) {
        // one or more children haven't been processed yet
        // abort
        return nextIndex;
    }

    // on successful sum
    currentNode.chapterCount = chapterCountSum + currentNode.children.length;
    nextIndex = currentNode.parent !== null ? currentNode.parent : -1;

    return nextIndex;
}

/**
* Sums the chapterCount of all children of the attribute node
*/
function sumChildrenChaptersCount(chapterObj) {
    return chapterObj.children.reduce((acc, curr) => {
        const currentNode = chapterDataArray[curr];
        if (currentNode.chapterCount == undefined) {
            return -1;
        }
        return acc !== -1 ? acc + currentNode.chapterCount : acc;
    }, 0);
}

function drawChildrenCounter() {
    let list = document.querySelectorAll(".title-wrapper");
    if (list.length !== chapterDataArray.length) {
      console.log('Outdated data. Please regenerate the map');
      if (list.length >= chapterDataArray.length) { return }
    }

    list.forEach((elem, i) => {
        let existingChildren = elem.querySelector('.children')
        if (existingChildren) {
            // redraw
            existingChildren.remove()
        }

        let child = createChildrenElement(chapterDataArray[i].chapterCount)
        let page = elem.querySelector('.page')

        elem.insertBefore(child, page)
    })
}

// receives html dom element
// returns parsed element as an object
function createChapterObj(el) {
    const pel = {};

    let tempEl = el.querySelector(".title");

    pel.title = tempEl.textContent;
    pel.url = tempEl.href;

    // sometimes the author is empty and there's no <a> inside it
    tempEl = el.querySelector(".username > a");
    pel.author = tempEl ? tempEl.textContent : "";

    // page is completely unreliable for chapters loaded afterwards. It resets on loading
    // pel.page = el.querySelector(".page").textContent;

    // Sometimes the date is empty, but there's no issue here
    // console.log(el);
    pel.date = el.querySelector(".date").textContent;

    pel.parent = null;
    pel.children = [];
    // pel.linksTo = null;

    pel.margin = parseInt(el.style["margin-left"]);

    // link chapters don't have views, likes, or comments
    // so find out if the chapter is a link chapter or not

    const viewsEl = el.querySelector(".views");
    if (viewsEl == null) {
        pel.views = null;
        pel.likes = null;
        pel.comments = null;
        pel.isLinkChapter = 1;

        return pel;
    }

    pel.views = parseInt(viewsEl.textContent.split(",").join("")) || 0;
    pel.likes = parseInt(el.querySelector(".likes").textContent) || 0;
    pel.comments = parseInt(el.querySelector(".comments").textContent) || 0;
    pel.isLinkChapter = 0;

    return pel;
}

// final list like [ chapterObj, (...) ]
// where every element has its parent and children noted
function createStoryMap() {
    // temporary list, to get the DOM element from the page
    // let list = document.getElementsByClassName("story-map-content");
    // if (list == null || !list) return;
    // list = Array.from(list[0].children);
    let chapterElementArray = Array.from(document.querySelectorAll(".story-map-chapter"))

    let prevParentI = -1;
    const finalList = [];

    chapterElementArray.forEach((el, i) => {
    // for (const i in chapterElementArray) {
        // console.log("- Processing Chapter", i);
        // const el = chapterElementArray[i];

        // parse el and add it to the final list
        const chapterObj = createChapterObj(el);
        finalList[i] = chapterObj;
        // console.log(chapterObj);

        // now we find the parent of el

        // before checking margin
        // check if it's the first element of the list
        if (i == 0) {
            prevParentI = 0;
            // continue; // when using a for loop
            return;
        }

        // check margins

        const currElMargin = chapterObj.margin
        const prevElMargin = finalList[i-1].margin

        // check if el is child of prev el
        if (prevElMargin < currElMargin) {
            // prev el is parent
            chapterObj.parent = parseInt(i - 1);
            // add this el as child of prev element
            finalList[i - 1].children.push(parseInt(i));
            // set prev parent to prev element
            prevParentI = i - 1;
            // continue; // when using a for loop
            return;
        }

        // check if el is sibling of prev el
        if (prevElMargin == currElMargin) {
            // they share the same parent

            // prevParent is parent
            chapterObj.parent = prevParentI;
            // add this el as child of prevParent
            finalList[prevParentI].children.push(i);
            // continue; // when using a for loop
            return;
        }

        // then el must be the "uncle" of prev el
        // prevElMargin > currElMargin

        // use a loop go back through the parents from the previous node
        // to find the first element with margin smaller than self
        const selfMargin = chapterObj.margin;
        for (let j = i - 1; j >= 0; j = finalList[j].parent) {
            if (finalList[j].margin < selfMargin) {
                // found the parent: j
                const actualParentI = j;
                chapterObj.parent = actualParentI;
                // add this el as child of actual parent
                // finalList[actualParentI].children.push(chapterObj.id);
                finalList[actualParentI].children.push(i);
                // set prev parent to actual parent
                prevParentI = actualParentI;
                break;
            }
        }
    // } // when using a for loop
    })

    return finalList;
}

// button stuff

function createButton(text, action, styleStr) {
    let button = document.createElement('button');
    button.textContent = text;
    button.onclick = action;
    button.setAttribute('style', styleStr || '');
    return button;
};
function toStyleStr(obj, selector) {
    let stack = [],
        key;
    for (key in obj) {
        if (obj.hasOwnProperty(key)) {
            stack.push(key + ':' + obj[key]);
        }
    }
    if (selector) {
        return selector + '{' + stack.join(';') + '}';
    }
    return stack.join(';');
};
function btnLoadCss() {
    return toStyleStr({
        'position': 'fixed',
        'bottom': 0,
        'right': 0,
        'padding': '2px',
        'margin': '0 10px 10px 0',
        'color': '#333',
        'background-color': 'rgb(246, 245, 244)',
        'z-index': '9999999999'
    })
}
function btnLoad() {
    return createButton('Regenerate', function() {
        generateDraw()
        // this.remove();
    }, btnLoadCss());
}

/* Depth Toggler */

function collapsibator() {
    const input = document.createElement('input')
    input.defaultValue = 1
    input.setAttribute('id', 'toggler-input')
    input.setAttribute('style', toStyleStr({
        // 'padding': '2px',
        'color': '#333',
        'width': '30px'
        // 'display': 'inline-block'
    }))

    const button = document.createElement('button')
    button.setAttribute('id', 'toggler-btn')
    button.textContent = 'Toggle'
    button.setAttribute('style', toStyleStr({
        'padding': '2px',
        'color': '#333',
        'background-color': 'rgb(246, 245, 244)',
        'display': 'inline-block',
        'font-size': '12px',
        'margin-left': '4px',
    }))
    button.onclick = () => {
        const level = document.getElementById('toggler-input').value
        toggleCollapsibleLevel(level)
    }

    const cont = document.createElement('div')
    cont.setAttribute('id', 'toggler-container')
    cont.setAttribute('style', toStyleStr({
        'position': 'fixed',
        'bottom': '50px',
        'right': '10px',
        'padding': '2px',
        'z-index': '9999999999'
    }))
    cont.appendChild(input)
    cont.appendChild(button)
    document.body.appendChild(cont)
}

// toggle all collapsibles from depht level "level"
function toggleCollapsibleLevel(level) {
    const chapters = Array.from(document.getElementsByClassName("story-map-chapter"));
    if (chapters == null) return;
    // if (!chapterDataArray.length) { console.error('chapterDataArray is undefined'); return; }
    if (!chapterDataArray) { console.error('chapterDataArray is undefined'); return; }

    const firstMargin = parseInt(chapters[0].style['margin-left'])
    const marginGap = parseInt(chapters[1].style['margin-left']) - firstMargin;

    for (let i = 0; i < chapterDataArray.length; i++) {
        // if (st.margin == (level*marginGap + firstMargin)) {
        if (chapterDataArray[i].margin == (level*marginGap + firstMargin)) {
            // toggle it
            const btn = chapters[i].querySelector('.btn.btn-link.collapsable.js-collapsable')
            if (btn) btn.click()

            // maybe will use this in the future for better performance???
            // wasn't able to figure it out

            // expand
            // let clazz = Array.from(chapters[i].getAttribute('class')).split(' ').filter(c=>c!="hidden")
            // chapters[i].setAttribute('class', clazz)
            // clazz = Array.from(chapters[i].querySelector(".js-collapsable > i").getAttribute('class')).split(' ').filter(c=>c!="bt-minus"&&c!="bt-plus")
            // clazz.push('bt-minus')
            // chapters[i].querySelector(".js-collapsable > i").setAttribute('class', clazz)

            // collapse

            // if (chapterDataArray[i].margin == (level*marginGap + firstMargin)) {
            //     let el = chapters[i].querySelector(".js-collapsable > i")
            //     let clazz = el.getAttribute('class').split(' ').filter(c=>c!="bt-minus").join(' ') + " bt-plus"
            //     el.setAttribute('class', clazz)
            // if (chapterDataArray[i].margin > (level*marginGap + firstMargin)) {
            //     let el = chapters[i]
            //     let clazz = el.getAttribute('class') + " hidden"
            //     el.setAttribute('class', clazz)
        }
    }
    // })
}


function getPageType() {
    let url = window.location.pathname
    // // console.log(url);
    if (url.search(/\/story\/.*\/map/) >= 0) {
        return "map"
    }
    if (url.search(/\/chapter\//) >= 0) {
        return "chapter"
    }
    if (url.search(/\/story\/.*\.[0-9]+$\/?/) >= 0) {
        // first chapter of story
        return "chapter"
    }
}

async function storyIsInTheCache(storyStorageId) {
  return ((await GM.listValues()).indexOf(storyStorageId) >= 0)
}

// get story url
// find it on cache
// get chapter url
// compare with chapters listed

async function showChapterCountOnChapterChoices() {
  let storyStorageId = getStoryStorageId()
  // check if chapterData is undefined
  // if it is, try fetching it
  if (!chapterData) {
    //fetch story data array first
    if (!chapterDataArray) if (! await attemptLoadChapterArrayFromCache()) return false;

    // fetch story data
    chapterData = await getChapterData(storyStorageId)
    if (!chapterData) return console.error('Story found but Chapter not found. Please regenerate the map.');
  }

  let nextChapterIndexList = chapterData.children

  // get chapter choices
  let chapterChoices = Array.from(document.querySelectorAll(".question-content a"))
  // Remove "Add a new chapter" link from the list (if the story is not private)
  // its link ends with "/new"
  if (chapterChoices[chapterChoices.length-1].href.search(/\/new$/) > -1) {
    chapterChoices.pop()
  }

  // prepare Count Number css style
  if (cssStyle == "crazy") { applyCrazyCSS() } else
    if (cssStyle == "alternative") { applyAlternativeCSS() }

  chapterChoices.forEach((el, i) => {
    let mapIndex = nextChapterIndexList[i]
    if (mapIndex == undefined) { return console.log('Chapter "'+el.textContent+'" not found. Please regenerate the story map.'); }
    drawLinkChapterCount(el, chapterDataArray[mapIndex], cssStyle)
  })
}

async function getChapterData() {
  if (chapterData) return chapterData;
  if (!chapterDataArray) chapterDataArray = await attemptLoadChapterArrayFromCache()
  if (!chapterDataArray) {
      throw new Error("Failed to load chapterDataArray from cache")
      // return false
  }

  // story is on cache as chapterDataArray
  // let chapterDataArray = JSON.parse(await GM.getValue(storyStorageId))

  // getting the chapter url if we're in /story/
  let chapterUrl = window.location.href
  if (chapterUrl.search(/\/story\//) > -1) {
    let els = document.querySelectorAll('.controls-left a')
    let chapterNum = els[els.length-1].href.match(/\d+\/?$/)[0]
    chapterUrl = "https://chyoa.com/chapter/Introduction."+chapterNum;
  }

  // console.log(chapterDataArray, chapterDataArray[1].url, chapterUrl);
  return chapterDataArray.find(c=>c.url==chapterUrl)
}

// Show chapter date under its Title
// assuming pageType == "chapter"
function showChapterDate() {
  if (!chapterData) return false;

  let date = document.createElement('div')
  date.textContent = chapterData.date
  document.querySelector('.meta').append(date)
}

// crazy css
function applyCrazyCSS() {
    let style = toStyleStr({
        'display': 'flex',
        'width': '55px',
        'justify-content': 'space-between',
        'align-items': 'center',
        'margin': '0 8px 0 -55px !important',
        'position': 'inherit !important',
        'float': 'left',
        'border-right': '1px solid',
        'padding': '0 8px 0 0',
        'font-family': 'monospace',
        'font-size': '14px',
        'line-height': '27px',
    }, '.question-content .chapterCount')
    addStyleHead(style)
}

function applyAlternativeCSS() {
    let style = toStyleStr({
        'position': 'absolute',
        'left': '-45px',
        'text-align': 'right',
        'width': '40px',
        'padding': '11px 0',
        'top': '0',
    }, '.question-content .chapterCount')
        + toStyleStr({
        'position': 'relative',
    }, '.question-content a')
    addStyleHead(style)
}

function drawLinkChapterCount(el, chapterObj, cssStyle = 'default') {

    const chapterCount = chapterObj.chapterCount
    const isLinkChapter = chapterObj.isLinkChapter

    let span = document.createElement('SPAN')

    if (isLinkChapter) {
        let icon = document.createElement('i')

        icon.setAttribute('class', 'btb bt-external-link')
        el.insertAdjacentElement('afterbegin',icon)

        return
    }

    span.setAttribute('class', 'chapterCount')

    if (cssStyle == 'simple') {
        span.textContent = " ("+chapterCount+")"
        el.append(span)
        return
    }

    if (cssStyle == 'alternative') {
        let icon = document.createElement('i')

        icon.setAttribute('class', 'btb bt-folder')
        span.textContent = Number(chapterCount).toString()
        el.append(span)
        el.insertAdjacentElement('afterbegin', icon)

        return
    }

    // default & carzy markup

    let icon = document.createElement('i')
    icon.setAttribute('class', 'btb bt-folder')
    span.appendChild(icon);
    let count = document.createTextNode(" "+chapterCount)
    span.append(count)

    el.insertAdjacentElement('afterbegin',span)

    // skip styling if cssStyle is set to 'crazy'
    if (cssStyle != 'crazy') {
        let color = window.getComputedStyle(el).color
        span.setAttribute('style', 'position: absolute; margin: 0 0 0 -60px; color: '+color+' !important;')
        icon.setAttribute('style', 'margin: 0 2px 0 0;')
    }
}

function loadStoryPathName() {
  storyStorageId = getStoryStorageId()
}

function getStoryStorageId() {
  if (storyStorageId) return storyStorageId;

  return getMapUrl();
}

function getMapUrl() {
    if (pageType == "map") return window.location.pathname;
    if (pageType != "chapter") {
        const errorMsg = "Unrecognizable page type"
        console.error(errorMsg);
        throw new Error(errorMsg);
    }

    // pageType is chapter
    const mapElement = [...document.querySelectorAll('.controls-left a')].find((item) => item.text.includes('Map'))
    const location = new URL(mapElement.href)
    return location.pathname
}

function foldTopmostReadChapters() {
  let ignoreList = []
  const els = Array.from(document.querySelectorAll(".story-map-chapter"))

  if (chapterDataArray.length != els.length) return false;

  // iterate all chapters
  // fold all read chapters not in the ignore list
  // put its children in the ignore list
  // put every ignored chapter children in the ignored list
  // console.log(chapterDataArray);

  for (const i in chapterDataArray) {
    if (i == 0) continue
    // console.log('in for');
    // check if chapter read && i different from 0
    if (els[i].classList.length == 1) {
      // console.log('unread chapter found');
      // if (binarySearch(i, ignoreList) == -1 && i != 0) {
      if (binarySearch(i, ignoreList) == -1) {
        console.log('success at i = ', i);
        // console.log('first unread not in ignore list, fold');
        els[i].querySelector('.btn').click()
      }
      // console.log('add children to ignored list');
      ignoreList = ignoreList.concat(chapterDataArray[i].children)
    }
  }
}

function foldTopmostUnreadChapters() {
  let ignoreList = []
  const els = Array.from(document.querySelectorAll(".story-map-chapter"))

  if (chapterDataArray.length != els.length) return false;

  for (const i in chapterDataArray) {
    if (i == 0) continue
    if (els[i].classList.length != 1) {
      if (binarySearch(i, ignoreList) == -1) {
        console.log('success at i = ', i);
        els[i].querySelector('.btn').click()
      }
      ignoreList = ignoreList.concat(chapterDataArray[i].children)
    }
  }
  console.log('ignoreList leng',ignoreList.length);
}

function binarySearch(value, list) {
  return list.find(a=>a==value) ? 1 : -1;
  let first = 0; //left endpoint
  let last = list.length - 1; //right endpoint
  let position = -1;
  let found = false;
  let middle;

  if (value < 100) console.log('search', value, list);

  while (found === false && first <= last) {
    middle = Math.floor((first + last)/2);
    if (list[middle] == value) {
      found = true;
      position = middle;
    } else if (list[middle] > value) { //if in lower half
      last = middle - 1;
    } else { //in in upper half
      first = middle + 1;
    }
  }
  return position;
}

// TODO
// index to remain in cache forever containing a story index with the number of chapters and other story stats
//   update index if the generated data has more chapters than what's on the index
// detect branches (chapters where child chapters have many chapters) and note them down
//   on chapter pages, check if it belongs to any recognizable branch and print its name on the page
// fold all topmost read chapters

// prev chapters map
// let prevMapContainer;
function prevMapShow() {
  let container = document.querySelector('.prevMapContainer')
  // if (container === null) {
  //   container = prevMapRender()
    // container = document.querySelector('.prevMapContainer')
  // }
  container.classList.toggle('show');
  // prevMapContainer.classList.toggle('show');
}



async function prevMapRender() {
  let storyStorageId = getStoryStorageId()
  // check if chapterData is undefined
  // if it is, try fetching it
  if (!chapterData) {
    //fetch story data array first
    // if (!chapterDataArray) if (!attemptLoadChapterArrayFromCache()) {
    if (!chapterDataArray) {
      if (! await attemptLoadChapterArrayFromCache()) {
        document.querySelector('.question').insertAdjacentText('afterend', 'Map data not found. Please regenerate the map.')
        return false;
      }
    }

    // fetch story data
    chapterData = getChapterData(storyStorageId)
    if (!chapterData) {
      document.querySelector('.question').insertAdjacentText('afterend', 'Unable to find Chapter in the story data. Please regenerate the mapp.')
      // return console.error('Unable to find Chapter in the story data. Please regenerate the map.');
      return false;
    }
  }

  if (document.querySelector('.prevMapContainer') === null) {
  // if (prevMapContainer === null) {
    document.querySelector('.question').insertAdjacentHTML('beforeend', '<div class="prevMapContainer"></div>')
    // prevMapContainer = document.querySelector('.prevMapContainer')
  }

  // drawThisChapter()
  drawParentTree()
}

function drawThisChapter() {
  let container = document.querySelector('.prevMapContainer')
  container.insertAdjacentHTML('beforend', '<div class="prevMap_chapter prevMap_currentChapter"><a href="'+chapterData.url+'">'+chapterData.title+'</a></div>')
}

function drawParentTree() {
  let container = document.querySelector('.prevMapContainer')

  let chapter = chapterData
  // let chapter = chapterDataArray[chapterData.parent]
  while (chapter.parent != null) {
    container.insertAdjacentHTML('afterbegin', '<div class="prevMap_chapter"><a href="'+chapter.url+'">'+chapter.title+'</a></div>')
    chapter = chapterDataArray[chapter.parent]
  }
}

async function prevMapInit() {
  document.querySelector('.controls-left').insertAdjacentHTML('beforeend','<br class="visible-xs-inline"> <a href="#" class="prevMap_button"><i class="btb bt-sitemap"></i>Prev Chapters</a>')
  let prevMapButton = document.querySelector('.prevMap_button')
  prevMapButton.addEventListener('click', function(e) {
    e.preventDefault();
    prevMapShow()
  });

  await prevMapRender()

  let style = toStyleStr({
    'display': 'none',
  }, '.prevMapContainer')
  + toStyleStr({
    'display': 'block',
  }, '.prevMapContainer.show')
  addStyleHead(style)
}

function getSuperParentGroupSortedByNew() {
    if (unsafeWindow.parentChapters) return unsafeWindow.parentChapters;

    chapterDataArray.forEach((c, i) => { c.id = i; })

    let parents = chapterDataArray.filter(c => c.parent == 0)
    let parentIds = parents.map(c => c.id)

    function computeSuperParent(chapter) {
        if (chapter.superParent) return

        if (parentIds.includes(chapter.id)) {
            chapter.superParent = chapter.id;
            return
        }

        if (parentIds.includes(chapter.parent)) {
            chapter.superParent = chapter.parent
            return chapter.superParent
        }

        chapter.superParent = getSuperParent(chapterDataArray[chapter.parent])
    }

    function getSuperParent(chapter) {
        if (!chapter.superParent) computeSuperParent(chapter)

        return chapter.superParent
    }

    chapterDataArray.slice(1).forEach(computeSuperParent)

    unsafeWindow.parentChapters = parents.map(p => {
        return {
            p: p,
            c: chapterDataArray.filter(c => c.superParent === p.id)
        }
    })

    function dateComparison(a, b) {
        let aDate = Date.parse(a.date)
        if (! aDate) aDate = 1;

        let bDate = Date.parse(b.date)
        if (! bDate) bDate = 1;

        return bDate - aDate;
    }

    unsafeWindow.parentChapters.forEach(g => g.c.sort((a, b) => {
        return dateComparison(a, b);
    }))

/*         Date.parse(b.c[0].date) - Date.parse(a.c[0].date) */
    unsafeWindow.parentChapters.sort((a, b) => {
        return dateComparison(a.c[0], b.c[0]);
    })

    return unsafeWindow.parentChapters
}

function getMapAndRegenerateViaAjax() {
    console.log("!! generating map");

    const url = getMapUrl() + '.json'
    let ajaxChapters = []

    console.log("!! url: "+url);

    function recursiveGetChaptersFromAjax(page) {
        let currentUrl = url + (page > 1 ? `?page=${page}` : '');

        console.log("!! currentUrl: "+currentUrl);

        unsafeWindow.$.get({
            url: currentUrl,
            // method: "GET",
            dataType: "json",
            headers: {
                "Accept": "application/json, text/javascript, */*; q=0.01",
                "X-CSRF-TOKEN": unsafeWindow.Chyoa.csrf_token,
            }
        }).done(function (response) {
                // console.log(response);
                ajaxChapters = ajaxChapters.concat(Object.values(response.data.chapters))

                if (response.data.hasMorePages) {
                    recursiveGetChaptersFromAjax(page + 1)
                } else {
                    chapterDataArray = createStoryMapAjax(ajaxChapters)
                    console.log("Chapter Count = ", chapterDataArray.length);

                    unsafeWindow.chapterDataArray = chapterDataArray
                    console.log("Map available as a javascript array on `chapterDataArray`")

                    console.log("!! assembling hierarchy tree");
                    countMapChapters();
                    console.log("!! done counting the children");

                    // cache it
                    GM.setValue(getStoryStorageId(), JSON.stringify(chapterDataArray))
                }
            }
        )
    }

    // Recursivelly get all the chapters from page 1
    recursiveGetChaptersFromAjax(1);

    function createChapterObjFromAjax(obj) {
        const chapter = {
            parent: null,
            children: [],
        }

        chapter.title = obj.title
        chapter.url = obj.url
        chapter.author = obj.author
        chapter.date = obj.created_at
        chapter.margin = obj.indent

        if (chapter.views) {
            chapter.views = parseInt(obj.views.split(",").join("")) || 0;
            chapter.likes = parseInt(obj.likes) || 0;
            chapter.comments = parseInt(obj.comments) || 0;
            chapter.isLinkChapter = 0;
        } else {
            chapter.views = null;
            chapter.likes = null;
            chapter.comments = null;
            chapter.isLinkChapter = 1;
        }

        return chapter;
    }


    // final list like [ chapterObj, (...) ]
    // where every element has its parent and children noted
    function createStoryMapAjax(chapters) {

        let prevParentI = -1;
        const finalList = [];

        chapters.forEach((el, chapterIndex) => {
            // parse el and add it to the final list
            const chapterObj = createChapterObjFromAjax(el);
            finalList[chapterIndex] = chapterObj;
            // console.log(chapterObj)

            // now we find the parent of el

            // before checking margin
            // check if it's the first element of the list
            if (chapterIndex == 0) {
                prevParentI = 0;
                // continue; // when using a for loop
                return;
            }

            // check margins

            const currElMargin = chapterObj.margin
            const prevElMargin = finalList[chapterIndex-1].margin

            // check if el is child of prev el
            if (prevElMargin < currElMargin) {
                // prev el is parent
                chapterObj.parent = parseInt(chapterIndex - 1);
                // add this el as child of prev element
                finalList[chapterIndex - 1].children.push(parseInt(chapterIndex));
                // set prev parent to prev element
                prevParentI = chapterIndex - 1;
                // continue; // when using a for loop
                return;
            }

            // check if el is sibling of prev el
            if (prevElMargin == currElMargin) {
                // they share the same parent

                // prevParent is parent
                chapterObj.parent = prevParentI;
                // add this el as child of prevParent
                finalList[prevParentI].children.push(chapterIndex);
                // continue; // when using a for loop
                return;
            }

            // then el must be the "uncle" of prev el
            // prevElMargin > currElMargin

            // use a loop go back through the parents from the previous node
            // to find the first element with margin smaller than self
            const selfMargin = chapterObj.margin;
            for (let parentIndex = chapterIndex - 1; parentIndex >= 0; parentIndex = finalList[parentIndex].parent) {
                if (finalList[parentIndex].margin < selfMargin) {
                    // found the parent: parentIndex
                    const actualParentI = parentIndex;
                    chapterObj.parent = actualParentI;
                    // add this el as child of actual parent
                    // finalList[actualParentI].children.push(chapterObj.id);
                    finalList[actualParentI].children.push(chapterIndex);
                    // set prev parent to actual parent
                    prevParentI = actualParentI;
                    break;
                }
            }
        // } // when using a for loop
        })

        return finalList;
    }
}