Sleazy Fork is available in English.

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;
    }
}