
/*****************************************************************
 * @author: Martijn De Jongh (Martino), martijn.de.jongh@gmail.com
 * https://github.com/Martinomagnifico
 *
 * Simplemenu.js for Reveal.js 
 * Version 2.0.1
 * 
 * @license 
 * MIT licensed
 *
 * Thanks to:
 *  - Hakim El Hattab, Reveal.js 
 ******************************************************************/



const Plugin = () => {
  let options = {};
  const vars = {};
  const sections = {};
  const mainArray = [];
  let autoListItems = [];
  let manualListItems = [];

  const debugLog = text => {
    if (options.debug) console.log(text);
  };

  const isObject = item => {
    return item && typeof item === 'object' && !Array.isArray(item);
  };

  const mergeDeep = function (target) {
    for (var _len = arguments.length, sources = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
      sources[_key - 1] = arguments[_key];
    }

    if (!sources.length) return target;
    const source = sources.shift();

    if (isObject(target) && isObject(source)) {
      for (const key in source) {
        if (isObject(source[key])) {
          if (!target[key]) Object.assign(target, {
            [key]: {}
          });
          mergeDeep(target[key], source[key]);
        } else {
          Object.assign(target, {
            [key]: source[key]
          });
        }
      }
    }

    return mergeDeep(target, ...sources);
  };

  const selectionArray = (container, selectors) => {
    let selections = container.querySelectorAll(selectors);
    let selectionarray = Array.prototype.slice.call(selections);
    return selectionarray;
  };

  const pluginPath = filename => {
    let path;
    let pluginScript = document.querySelector(`script[src$="${filename}"]`);

    if (pluginScript) {
      path = pluginScript.getAttribute("src").slice(0, -1 * filename.length);
    } else {
      path = import.meta.url.slice(0, import.meta.url.lastIndexOf('/') + 1);
    }

    return path;
  };

  const isBefore = (a, b) => {
    var all = document.getElementsByTagName('*');

    for (var i = 0; i < all.length; ++i) {
      if (all[i] === a) return true;else if (all[i] === b) return false;
    }
  };

  const isStack = section => {
    let isStack = false;

    for (let i = 0; i < section.childNodes.length; i++) {
      if (section.childNodes[i].tagName == "SECTION") {
        isStack = true;
        break;
      }
    }

    return isStack;
  };

  const createNode = thehtml => {
    const fragment = document.createRange().createContextualFragment(thehtml);
    return fragment.firstElementChild;
  };

  const loadStyle = (url, type, callback) => {
    let head = document.querySelector('head');
    let style = document.createElement('link');
    style.rel = 'stylesheet';
    style.href = url;

    let finish = () => {
      if (typeof callback === 'function') {
        callback.call();
        callback = null;
      }
    };

    style.onload = finish;

    style.onreadystatechange = function () {
      if (this.readyState === 'loaded') {
        finish();
      }
    };

    head.appendChild(style);
  };

  const checkOccurrence = (array, element) => {
    let counter = 0;

    for (let i = 0; i <= array.length; i++) {
      if (array[i] == element) {
        counter++;
      }
    }

    return counter;
  };

  const menuArray = () => {
    const matchString = vars.matchString;
    let menulist = selectionArray(vars.viewport, `.${options.menuclass}`) ? selectionArray(vars.viewport, `.${options.menuclass}`) : [];
    let automenus = [];
    let manualmenus = [];

    if (menulist.length) {
      menulist.forEach(menu => {
        if (menu.getElementsByTagName('li').length < 1) {
          menu.setAttribute('data-simplemenu-auto', '');
          automenus.push(menu);
        } else {
          if (options.selectby == "data-name" || options.selectby == "name") {
            let existingListItems = selectionArray(menu, `.${options.menuclass} ${options.activeelement}`);
            existingListItems.forEach(listItem => {
              if (!listItem.dataset[matchString]) {
                let content = listItem.textContent || listItem.querySelector('a').textContent;
                listItem.setAttribute(`data-${matchString}`, content);
              }
            });
          }

          manualmenus.push(menu);
        }
      });
      return {
        automenus: automenus,
        manualmenus: manualmenus
      };
    } else {
      return false;
    }
  };

  const setScale = revealScale => {
    let totalScale = revealScale * vars.userScale;
    vars.viewport.style.setProperty('--simplemenu-scale', totalScale.toFixed(3));
  };

  const moveRevealUI = (curUiEl, newUiEl) => {
    let newUiElClassList = newUiEl.classList;
    newUiEl.parentNode.replaceChild(curUiEl, newUiEl);
    curUiEl.classList = newUiElClassList;
  };

  const getRevealUI = () => {
    let revealUIs = ['controls', 'slide-number'];
    revealUIs.forEach(uielement => {
      let curUiEl = vars.deck.getRevealElement().querySelector(`.reveal > .${uielement}`);
      let newUiEl = vars.deck.getRevealElement().querySelector(`.reveal > * .${uielement}`);

      if (curUiEl && newUiEl) {
        moveRevealUI(curUiEl, newUiEl);
      }
    });
  };

  function copyDataAttributes(source, target) {
    [...source.attributes].filter(attr => attr.nodeName.indexOf('data') > -1).forEach(attr => {
      target.setAttribute(attr.nodeName, attr.nodeValue);
    });
  }

  const prepareSlides = () => {
    debugLog("Preparing slides");
    sections.all = selectionArray(vars.viewport, "section");
    sections.all.forEach(section => {
      // In Markdown environments, setting a data-name of a stack is not directly possible. 
      // Satting a data-stack-name on the first child solves this.
      if (!section.parentNode.dataset.name && section.dataset && section.dataset.stackName) {
        section.parentNode.dataset.name = section.dataset.stackName;
      } // If a section has a data-sm='none', it will also remove the data-name.


      if (section.dataset && section.dataset[vars.matchString] && section.dataset[vars.matchString] == "false" && section.dataset.name) {
        delete section.dataset.name;
      }
    }); // Get all of the kinds of sections

    sections.top = sections.all.filter(section => section.parentNode.classList.contains('slides') && !(section.dataset[vars.matchString] && section.dataset[vars.matchString] == "false"));
    sections.named = sections.top.filter(section => section.dataset.name || section.getAttribute('name'));
    sections.namedvisible = sections.named.filter(section => section.dataset.visibility != "hidden"); // Go through all the named sections

    let namedsectionMatches = [];
    sections.named.forEach(namedsection => {
      // The 'name' attribute is also allowed. 
      let matchName = namedsection.dataset.name || namedsection.getAttribute('name'); // Named sections can have the same name, but should then be differentiated.

      namedsectionMatches.push(matchName);
      let dupsBefore = matchName && checkOccurrence(namedsectionMatches, matchName) > 1 ? `-${checkOccurrence(namedsectionMatches, matchName)}` : null;
      let match = dupsBefore ? matchName + dupsBefore : matchName; // We set the name of the match as a data-attribute

      namedsection.setAttribute(`data-${vars.matchString}`, match); // If the (named) section is not a stack and does not have an ID, we need to give it one.

      if (!isStack(namedsection) && !namedsection.id) {
        // Note: Quarto will already have assigned an ID, but it may also have been done manually.
        namedsection.id = match.toLowerCase().replace(/\W/g, '');
      } else if (isStack(namedsection)) {
        // Find the first (visible) section inside a stack.
        let allsects = selectionArray(namedsection, `section`);
        let allVisibleSects = allsects.filter(section => section.dataset.visibility != "hidden");
        let firstChildSection = allVisibleSects[0];

        if (firstChildSection && !firstChildSection.id) {
          firstChildSection.id = match.toLowerCase().replace(/\W/g, '');

          if (namedsection.id == firstChildSection.id) {
            namedsection.removeAttribute('id');
          }
        }
      }
    });
    let currentMatch = null;
    let currentid = null; // Get all the sections that are actually slides

    sections.regular = sections.all.filter(section => !isStack(section) && section.dataset.visibility != "hidden"); // Go through all the sections

    sections.regular.forEach((section, i) => {
      // Filling an array with the needed comparison information
      let isChildSection = isStack(section.parentNode) && section.parentNode.tagName == "SECTION";
      let theSection = isChildSection ? section.parentNode : section;
      let dataname = theSection.dataset.name;
      let name = theSection.getAttribute(`name`);
      let dataintl = theSection.getAttribute(vars.langattribute) ? theSection.getAttribute(vars.langattribute) : null;
      let parentid = section.parentNode.id ? section.parentNode.id : null;
      let id = section.id ? section.id : isChildSection ? parentid : null;
      let match = theSection.dataset[vars.matchString];

      if (match) {
        currentMatch = match;
      }

      if (id || match == "false") {
        currentid = i;
      }

      if (options.flat) {
        if (match != "false") {
          match = currentMatch;
        }
      }

      if (match == "false") {
        match = null;
      }

      if (dataname == "false") {
        dataname = null;
      }

      let sectionObject = {
        index: i,
        ...(section && {
          section
        }),
        ...(dataname && {
          dataname
        }),
        ...(name && {
          name
        }),
        ...(id && {
          id
        }),
        ...(match && {
          match
        }),
        ...(currentid && {
          currentid
        }),
        ...(dataintl && {
          dataintl
        })
      };
      mainArray.push(sectionObject);
    });
  };

  const prepareMenubars = () => {
    debugLog("Preparing menubars");
    let menubars = selectionArray(vars.viewport, `.${options.menubarclass}`) ? selectionArray(vars.viewport, `.${options.menubarclass}`) : [];

    if (options.barhtml.header) {
      // Generate header menubar
      let bar = createNode(options.barhtml.header);
      menubars.push(bar);
      vars.slides.before(bar);
    }

    if (options.barhtml.footer) {
      // Generate footer menubar
      let bar = createNode(options.barhtml.footer);
      menubars.push(bar);
      vars.slides.after(bar);
    }

    if (menubars.length) {
      // If menubar (pre-existing or just added):
      setScale(vars.deck.getScale());
      menubars.forEach((menubar, i) => {
        let barLocation = isBefore(menubar, vars.slides) ? "top" : "bottom";
        menubar.classList.add(barLocation);

        if (!menubar.id) {
          menubar.id = `${options.menubarclass}${barLocation}`;
        }

        menubar.classList.add("ready");
      });
      vars.menubars = menubars;
    } else {
      console.log("There are no menubars. You can still use Simplemenu to populate empty menus like in an Agenda or Table Of Contents.");
    }
  };

  const prepareMenus = () => {
    debugLog("Preparing menus");
    let menus = menuArray();

    if (!menus || menus && !menus.automenus) {
      console.log("There are no menus. Please add one or more menus manually or through the 'barhtml' option.");
      return;
    }

    if (menus.automenus.length >= 1 && sections.namedvisible.length >= 1) {
      // There are empty menus. Autofill them.
      let idArray = [];
      const autoMenuLinks = sections.namedvisible.map(section => {
        let match = section.dataset[vars.matchString];
        let name = section.dataset.name || section.getAttribute(`name`) || section.id;
        let id = section.id || name.toLowerCase().replace(/\W/g, '');
        idArray.push(id);

        if (vars.quarto) {
          id = mainArray.find(item => item.match === match).id;
        }

        let duplicatesBefore = checkOccurrence(idArray, id) > 1 ? `-${checkOccurrence(idArray, id)}` : '';
        let href = vars.quarto ? id : id + duplicatesBefore;
        let smmatchString = ` data-${vars.matchString}="${match}"`;
        let nameString = section.getAttribute(`name`) ? ` name="${section.getAttribute(`name`)}"` : '';
        let intlString = section.getAttribute(vars.langattribute) ? ` ${vars.langattribute}="${section.getAttribute(vars.langattribute)}"` : '';
        return `<li><a href="#/${href}"${intlString}${nameString}${smmatchString}>${name}</a></li>`;
      }).reduce((combinedHTML, itemHTML) => {
        let orderedHTML = vars.rtl ? itemHTML + combinedHTML : combinedHTML + itemHTML;
        return orderedHTML;
      });
      menus.automenus.forEach(automenu => {
        automenu.innerHTML = autoMenuLinks;
      });
      autoListItems = menus.automenus.map(menu => Array.from(menu.querySelectorAll(options.activeelement))).flat();
    }

    if (menus.manualmenus.length >= 1) {
      // There are pre-existing menus. Fix link to ID if needed.
      // Only get the listitems
      manualListItems = menus.manualmenus.map(menu => Array.from(menu.querySelectorAll(options.activeelement))).flat();
      manualListItems.forEach(listItem => {
        // Get the anchorlinks
        let linker = listItem.tagName == "a" ? listItem : listItem.querySelector('a');
        let linkhref = linker.getAttribute('href');

        if (linkhref === "#") {
          let newLink = listItem.dataset[vars.matchString].toLowerCase().replace(/\W/g, '');
          linker.href = `#/${newLink}`;
        }
      });
    }
  };

  const preparePrint = () => {
    const urlParams = new URLSearchParams(window.location.search);
    const hasPrintParam = urlParams.has('print-pdf');

    if (hasPrintParam) {
      mainArray.forEach(item => {
        let printSection = item.section;
        let datainfo = document.createElement("div");
        datainfo.classList.add("datainfo");
        copyDataAttributes(printSection, datainfo);
        let moreData = ['match', 'name', 'dataname', 'currentid', 'id', 'dataintl'];
        moreData.forEach(moreDataItem => {
          if (item[moreDataItem]) {
            datainfo.dataset[moreDataItem] = item[moreDataItem];
          }
        });
        printSection.appendChild(datainfo);
      });
    }
  };

  const prepare = resolve => {
    prepareSlides();
    prepareMenubars();
    prepareMenus();
    preparePrint();
    return setTimeout(resolve, 0);
  };

  const compare = (listItem, section) => {
    let menukind = listItem.parentNode.hasAttribute('data-simplemenu-auto') ? "auto" : "manual";
    let sectionmatch = section.match ? section.match : null;

    if (menukind == "manual") {
      if (options.selectby == "id") {
        sectionmatch = section.id ? section.id : section.currentid ? mainArray[section.currentid].id : null;
      } else if (options.selectby == "name") {
        sectionmatch = section.name;
      } else {
        sectionmatch = section.dataname;
      }
    }

    if (sectionmatch) {
      let menumatch = listItem.dataset[vars.matchString] || listItem.querySelector('a').dataset[vars.matchString];

      if (options.selectby == "id" && menukind == "manual") {
        let href = listItem.href || listItem.querySelector('a').href;
        let lastHref = href ? href.substring(href.lastIndexOf("/") + 1) : '';
        menumatch = lastHref;
      }

      if (options.selectby == "data-name" && menukind == "manual") {
        sectionmatch = section.dataname ? section.dataname : null;
      }

      if (menumatch && menumatch == sectionmatch) {
        listItem.classList.add(options.activeclass);
      } else {
        listItem.classList.remove(options.activeclass);
      }
    } else {
      listItem.classList.remove(options.activeclass);
    }
  };

  const checkSlidesNormal = event => {
    const index = sections.regular.indexOf(event.currentSlide);
    let section = mainArray[index];
    autoListItems.filter(listItem => {
      compare(listItem, section);
    });
    manualListItems.filter(listItem => {
      compare(listItem, section);
    });
  };

  const checkSlidesPDF = event => {
    let pdfPages = selectionArray(vars.viewport, '.slides .pdf-page'); // Check if any menubar has a slide number

    let anyMenubarHasSlidenumber = false;

    if (vars.menubars) {
      vars.menubars.forEach(menubar => {
        anyMenubarHasSlidenumber = !!menubar.getElementsByClassName("slide-number");
      });
    }

    pdfPages.forEach(pdfPage => {
      // The original section has gone, so we rebuild it with the saved data-attributes
      let datainfo = pdfPage.getElementsByClassName("datainfo")[0];
      let section = {};
      section.name = datainfo.dataset.name;
      section.dataname = datainfo.dataset.dataname;
      section.currentid = datainfo.dataset.currentid;
      section.match = datainfo.dataset.match;
      section.id = datainfo.dataset.id;

      if (datainfo.dataset.state) {
        let newClasses = datainfo.dataset.state.split(" ");
        newClasses.forEach(newClass => {
          pdfPage.classList.add(newClass);
          let vp = vars.deck.getRevealElement().closest(".reveal-viewport");
          vp.classList.remove(newClass);
        });
      } // If any menubar has a slide number, turn the original one on this slide off


      if (anyMenubarHasSlidenumber && pdfPage.getElementsByClassName("slide-number").length > 0) {
        pdfPage.getElementsByClassName("slide-number")[0].style.display = "none";
      }

      if (vars.menubars) {
        vars.menubars.forEach(menubar => {
          let bar = menubar.cloneNode(true);
          pdfPage.appendChild(bar);
          let listItems = selectionArray(bar, `.${options.menuclass} ${options.activeelement}`);
          listItems.forEach(listItem => {
            compare(listItem, section);
          }); // If there is a slidenumber in the menu,

          let newSN = pdfPage.querySelector(`.${options.menubarclass} .slide-number`);
          let oldSN = pdfPage.querySelector(`:scope > .slide-number`);

          if (newSN && oldSN) {
            // ...then fill it with the current (total) slidenumber.
            newSN.textContent = oldSN.textContent;
          }
        });
      }
    });

    if (vars.menubars) {
      vars.menubars.forEach(menubar => {
        menubar.parentNode.removeChild(menubar);
      });
    }
  };

  const chapterize = event => {
    if (event && event.type == "ready") {
      debugLog(mainArray);
      getRevealUI();
    }

    if (event && (event.type == "ready" || event.type == "slidechanged")) {
      checkSlidesNormal(event);
    }

    if (event && event.type == "pdf-ready") {
      checkSlidesPDF();
    }
  };

  const simpleMenu = (deck, options, es5Filename) => {
    deck.configure({
      hash: true
    });
    vars.deck = deck;
    vars.viewport = deck.getRevealElement().tagName == "BODY" ? document : deck.getRevealElement();
    vars.slides = deck.getSlidesElement();
    vars.langattribute = deck.getConfig().internation ? deck.getConfig().internation.langattribute ? deck.getConfig().internation.langattribute : "data-i18n" : false;
    vars.rtl = deck.getConfig().rtl;
    vars.quarto = document.querySelector('[name=generator]') && document.querySelector('[name=generator]').content.includes("quarto") ? true : false;
    vars.matchString = "sm";
    vars.userScale = options.scale;
    deck.addEventListener('ready', chapterize, false);
    deck.addEventListener('slidechanged', chapterize, false);
    deck.addEventListener('pdf-ready', chapterize, false);
    deck.addEventListener('resize', _ref => {
      let {
        scale
      } = _ref;
      return setScale(scale);
    }, false);
    const SimplemenuStylePath = options.csspath ? options.csspath : `${pluginPath(es5Filename)}simplemenu.css` || 'plugin/simplemenu/simplemenu.css';
    return new Promise(resolve => {
      if (options.csspath === false) {
        return prepare(resolve);
      } else {
        loadStyle(SimplemenuStylePath, 'stylesheet', async () => prepare(resolve));
      }
    });
  };

  const init = deck => {
    let defaultOptions = {
      menubarclass: 'menubar',
      menuclass: 'menu',
      activeclass: 'active',
      activeelement: 'li',
      selectby: 'id',
      barhtml: {
        header: '',
        footer: ''
      },
      flat: false,
      scale: 0.67,
      csspath: ''
    };
    options = deck.getConfig().simplemenu || {};
    options = mergeDeep(defaultOptions, options);
    let wronginputs = false;
    let warning = '';

    if (options.selectby !== "id" && options.selectby !== "data-name" && options.selectby !== "name") {
      wronginputs = true;
      warning = 'The selectby option can be only "id",  "data-name" or "name".';
    }

    if (wronginputs) {
      console.log('Simplemenu did not load:');
      console.log(warning);
      return false;
    }

    return simpleMenu(deck, options, "simplemenu.js");
  };

  return {
    id: 'simplemenu',
    init: init
  };
};

export { Plugin as default };
