<?php
/**
 * @author Jan Haller <jan.haller@math.uzh.ch>
 */

use IMATHUZH\Qfq\Core\Evaluate;
use IMATHUZH\Qfq\Core\Helper\OnString;
use IMATHUZH\Qfq\Core\Report\Link;
use IMATHUZH\Qfq\Core\Store\Store;
use IMATHUZH\Qfq\Core\Database\Database;

/**
 * Creates a DOMNodeList from the HTML content and iterates over each element
 * If the content is empty, a link gets created to add content to the wiki
 * The first element gets preceded by a link to edit the content as a whole
 * <h1> to <h6> tags get preceded by a link to edit the section including its child elements
 * Macros ({{toc}}, {{childPages}}, [[wikiPage#Heading]], [[pageSlug/wikiPage#Heading]], {{collapse()}}, {{collapse}}) will be replaced
 * Output to the report is generated by the echo commands
 *
 * @param $paramArray
 * @param $qfq
 * @return void
 * @throws CodeException
 * @throws DbException
 * @throws UserFormException
 * @throws UserReportException
 */
function renderWiki($paramArray, $qfq) {
    // Variables used in SQL queries
    $constId = WIKI_PAGE_COLUMN_ID;
    $constContent = WIKI_PAGE_COLUMN_CONTENT;
    $constPageSlug = WIKI_PAGE_COLUMN_PAGE_SLUG;
    $constWikiPage = WIKI_PAGE_TABLE_WIKI_PAGE;
    $constWikiClassContent = WIKI_CSS_CLASS_QFQ_WIKI_CONTENT;
    $constWikiClassNavigation = WIKI_CSS_CLASS_QFQ_WIKI_NAVIGATION;
    $constWikiIdSection = WIKI_HTML_ID_QFQ_WIKI_SECTION;

    $headingTags = array(HTML_TAG_H1, HTML_TAG_H2, HTML_TAG_H3, HTML_TAG_H4, HTML_TAG_H5, HTML_TAG_H6);
    $unwantedTags = array(HTML_TAG_HTML, HTML_TAG_BODY);

    // id of the wiki page record
    $id = $paramArray[WIKI_PAGE_COLUMN_WP_ID];

    // Will be either '|r:0' or '|r:5'
    $renderMode = '|r:' . $paramArray[WIKI_RENDER_MODE];

    // Will be either true or false
    $printMode = (isset($paramArray[WIKI_PRINT_MODE])) ? $paramArray[WIKI_PRINT_MODE] : false;

    $sql = "SELECT $constContent, $constPageSlug FROM $constWikiPage WHERE $constId = $id";
    $resultArray = doSql($sql);

    $wikiPageContent = (isset($resultArray[0][WIKI_PAGE_COLUMN_CONTENT])) ? $resultArray[0][WIKI_PAGE_COLUMN_CONTENT] : '';
    $pageSlug = (isset($resultArray[0][WIKI_PAGE_COLUMN_PAGE_SLUG])) ? $resultArray[0][WIKI_PAGE_COLUMN_PAGE_SLUG] : '';

    // This is necessary in print mode to hide the button text
    $buttonText = ($printMode) ? '' : '|t:Edit';

    // No wiki page has been created yet
    if (empty($wikiPageContent) && empty($pageSlug)) {
        return;

    // Wiki page has been created but is without content
    // Creates an 'edit' link and exits the function
    } elseif (empty($wikiPageContent)) {
        echo "<div class='$constWikiClassNavigation' style='display:inline-block; margin-bottom:10px'>" . doNavigation($id) . "</div>";
        echo "<div style='clear: both;'></div>";
        echo "<div class='$constWikiClassContent'>";
        echo "<div style='float:right;'>";
        echo doQfqLink("p:", $pageSlug, "&form=wikiEditor&r=0&wpIdPrevious=$id&action=new$renderMode|s|b|E$buttonText");
        echo "</div>";
        echo "<div style='clear: both;'></div>";

        return;
    }

    // Replace Protected image src with QFQ SIP
    $wikiPageContent = OnString::replaceImageSourceWithSip($wikiPageContent);

    // Navigation
    echo ($printMode) ? "" : "<div class='$constWikiClassNavigation'>" . doNavigation($id) . "</div>";
    echo "<div style='clear: both;'></div>";

    // Will be passed as a parameter in the link and enables the edit of the content as a whole
    // Always used for the first element
    $action = WIKI_TOKEN_EDIT_ALL;

    $xpath = doDOMXPath($wikiPageContent);

    // Get all elements
    $content = $xpath->query('//*');

    $counter = 1;
    $firstElement = true;

    // Iterate over each element
    foreach ($content as $element) {
        $nodeName = $element->nodeName;

        // The first two elements are always <html> and <body>
        if (!in_array($nodeName, $unwantedTags)) {
            $nodeValue = $element->nodeValue;

            // Looks for <h1> - <h6> elements or the first element
            // Creates a <div> containing the edit link and an <a>-tag
            // In printMode this will be skipped
            if ((in_array($nodeName, $headingTags) || $firstElement) && !$printMode) {

                // If the first element is not <h1> - <h6>, then $sectionId = 0
                $sectionId = (in_array($nodeName, $headingTags)) ? $counter : 0;

                // <div>
                $divHtml = "<div title='Edit this section' id='$constWikiIdSection-$sectionId' style='float:right;'>";
                $divHtml .= doQfqLink("p:", $pageSlug, "&form=wikiEditor&r=0&sectionId=$sectionId&wpIdPrevious=$id&action=$action$renderMode|s|b|E$buttonText|A:id=" . slugify($nodeValue));
                $divHtml .= "</div>";
                $divHtml .= "<div style='clear: both;'></div>";

                // Creates a DOMElement from HTML and inserts it before the current element into the DOM
                $divElement = doDOMElement($divHtml);
                $divElement = $element->ownerDocument->importNode($divElement, true);
                $parentNode = $element->parentNode;
                $parentNode = $parentNode->insertBefore($divElement, $element);

                $counter = ($sectionId === 0) ? $counter : $counter + 1;
                unset($divHtml, $divElement, $parentNode, $sectionId);
            }

            // Reset values
            $buttonText = '';
            $action = WIKI_TOKEN_EDIT_SECTION;
            $firstElement = false;

            // Check for macro '{{toc}}'
            if (trim($nodeValue) === WIKI_TOKEN_TABLE_OF_CONTENTS) {

                // HTML for the list table of contents
                $tocHtml = doTableOfContents($content);

                // Check if list table of contents has been successfully created
                if ($tocHtml !== $nodeValue) {

                    // Creates a DOMElement from HTML and inserts it before the current element into the DOM
                    $ulElement = doDOMElement($tocHtml);
                    $ulElement = $element->ownerDocument->importNode($ulElement, true);
                    $parentNode = $element->parentNode;
                    $parentNode = $parentNode->replaceChild($ulElement, $element);

                    unset($tocHtml, $ulElement, $parentNode);
                }
            }

            // Check for macro '{{childPages}}'
            if (trim($nodeValue) === WIKI_TOKEN_CHILD_PAGES) {

                // HTML for the list child pages
                $cpHtml = doChildPages($id);

                // Check if list child pages has been successfully created
                if ($cpHtml !== $nodeValue) {

                    // Creates a DOMElement from HTML and inserts it before the current element into the DOM
                    $ulElement = doDOMElement($cpHtml);
                    $ulElement = $element->ownerDocument->importNode($ulElement, true);
                    $parentNode = $element->parentNode;
                    $parentNode = $parentNode->replaceChild($ulElement, $element);

                    unset($cpHtml, $ulElement, $parentNode);
                }
            }

            // Check for and replace macro '[[wikiPage#Heading]]' and '[[pageSlug/wikiPage#Heading]]'
            if (preg_match('<(\[\[[a-zA-Z0-9- :,;/\-]+#[a-zA-Z0-9. :,;\-]*\]\])>', $nodeValue)) {
                $linkHtml = preg_replace_callback(
                    '<(\[\[[a-zA-Z0-9- :,;/\-]+#[a-zA-Z0-9. :,;\-]*\]\])>',

                    /**
                     * Creates a link to a wiki page within the same wiki or in another wiki
                     * Gets invoked by the macros '[[wikiPage#Heading]]' and/or '[[pageSlug/wikiPage#Heading]]'
                     * Anonymous function makes 'use' keyword possible
                     *
                     * @param $match
                     * @return string
                     */
                    function ($match) use ($pageSlug) {
                        // e.g. $match = '[[wikiPage#Heading]]'
                        // e.g. $match = '[[pageSlug/wikiPage#Heading]]'

                        // Variables used in SQL queries
                        $constId = WIKI_PAGE_COLUMN_ID;
                        $constPageSlug = WIKI_PAGE_COLUMN_PAGE_SLUG;
                        $constName = WIKI_PAGE_COLUMN_NAME;
                        $constWpIdCurrent = WIKI_PAGE_COLUMN_WP_ID_CURRENT;
                        $constWikiPage = WIKI_PAGE_TABLE_WIKI_PAGE;
                        $constWikiClassLink = WIKI_CSS_CLASS_QFQ_WIKI_LINK;

                        // Removes '[[' and ']]'
                        $match = substr($match[0], 2, -2);

                        // Separates 'wikiPage'/'pageSlug/wikiPage' and 'Heading'
                        $arr = explode("#", $match);
                        $wikiPage = $arr[0];
                        $heading = $arr[1];

                        // Looks for the name of the wiki page: '[[wikiPage#Heading]]'
                        // Page slug has to match (belongs to the same wiki)
                        $sql = "SELECT $constId, $constPageSlug FROM $constWikiPage WHERE $constName = '$wikiPage' AND ISNULL($constWpIdCurrent) AND $constPageSlug = '$pageSlug'";
                        $resultArray = doSql($sql);

                        // Looks for the combination of page slug and name of the wiki page: '[[pageSlug/wikiPage#Heading]]'
                        if (empty($resultArray)) {
                            $sql = "SELECT $constId, $constPageSlug FROM $constWikiPage WHERE CONCAT($constPageSlug, '/', $constName) = '$wikiPage' AND ISNULL($constWpIdCurrent)";
                            $resultArray = doSql($sql);
                        }

                        // If no wiki page was found the macro will be returned as is
                        if (empty($resultArray)) {
                            return '[[' . $match . ']]';
                        }

                        // id of the wiki page
                        $id = $resultArray[0][WIKI_PAGE_COLUMN_ID];

                        // page slug of the wiki page
                        $pageSlug = $resultArray[0][WIKI_PAGE_COLUMN_PAGE_SLUG];

                        // Link to the wiki page with anchor to the heading
                        $link = doQfqLink("p:", $pageSlug, "&wpId=$id#". slugify($heading) . "|t:$heading|c:$constWikiClassLink");
                        return $link;
                    },
                    $nodeValue
                );

                // Check if link has been successfully created
                if ($linkHtml !== $nodeValue) {

                    // Creates a DOMElement from HTML and inserts it before the current element into the DOM
                    $aElement = doDOMElement($linkHtml);
                    $aElement = $element->ownerDocument->importNode($aElement, true);
                    $parentNode = $element->parentNode;
                    $parentNode = $parentNode->replaceChild($aElement, $element);

                    unset($linkHtml, $aElement, $parentNode);
                }
            }
        }
    }

    // Wrap wiki page content
    $divHtml = "<div class='$constWikiClassContent'>";
    $divElement = doDOMElement($divHtml);
    $document = $element->ownerDocument;
    $divElement = $document->importNode($divElement, true);

    // The first two elements are always <html> and <body>
    $parentNode = $document->firstElementChild->firstElementChild;

    // Move all children from $parentNode to $divElement
    while ($parentNode->hasChildNodes()) {
        $child = $parentNode->firstChild;
        $child = $document->importNode($child, true);
        $divElement->appendChild($child);
    }

    // Raw HTML
    $wikiHtml = $document->saveHTML($divElement);

    // HTML is different in print mode
    if ($printMode) {

        // Remove {{collapse()}} and {{collapse}}
        // This keeps the content visible on the PDF
        $wikiHtml = preg_replace('<<[a-zA-Z]+.*>{{collapse\((.*?)\)}}<[a-zA-Z/]+>>', '', $wikiHtml);
        $wikiHtml = preg_replace('<(<[a-zA-Z]+.*>{{collapse}}<[a-zA-Z/]+>)>', '', $wikiHtml);
    } else {

        // Search and replace opening collapse macro
        // Opening <div>-tag gets inserted which acts as a parent for the collapsed content
        // Search: <p>{{collapse(optional text)}}</p>
        // Replace: <a><span></span>optional text</a></br><div>
        $wikiHtml = preg_replace_callback('<<[a-zA-Z]+.*>{{collapse\((.*?)\)}}<[a-zA-Z/]+>>', 'doCollapse', $wikiHtml);

        // Search and replace closing collapse macro
        // Close previously opened <div>-tag
        // Search: <p>{{collapse}}</p>
        // Replace: </div>
        $wikiHtml = preg_replace('<(<[a-zA-Z]+.*>{{collapse}}<[a-zA-Z/]+>)>', '</div>', $wikiHtml);
    }

    echo $wikiHtml;
}

/**
 * Creates a DOMNodeList from the HTML content and iterates over each sibling of the edited heading
 * Output will only contain the sections that will be loaded into the editor
 *
 * @param $paramArray
 * @param $qfq
 * @return void
 */
function loadWikiContent($paramArray, $qfq) {
    // Variables used in SQL queries
    $constId = WIKI_PAGE_COLUMN_ID;
    $constContent = WIKI_PAGE_COLUMN_CONTENT;
    $constWikiPage = WIKI_PAGE_TABLE_WIKI_PAGE;

    $headingTags = array(HTML_TAG_H1, HTML_TAG_H2, HTML_TAG_H3, HTML_TAG_H4, HTML_TAG_H5, HTML_TAG_H6);

    // id of the previous wiki page record
    $wpIdPrevious = $paramArray[WIKI_TOKEN_WIKI_PAGE_ID_PREVIOUS];

    // id of the section that will be edited
    $sectionId = $sectionIdEnd = intval($paramArray[WIKI_PAGE_SECTION_ID]);

    // Will be either 'editAll' or 'editSection'
    $action = $paramArray[WIKI_TOKEN_ACTION];

    $sql = "SELECT $constContent FROM $constWikiPage WHERE $constId =  $wpIdPrevious";
    $resultArray = doSql($sql);

    $wikiPageContent = $resultArray[0][WIKI_PAGE_COLUMN_CONTENT];
    $content = '';

    // Parse content when editing section
    if ($action === WIKI_TOKEN_EDIT_SECTION) {

        $xpath = doDOMXPath($wikiPageContent);
        $heading = getHeadingBySectionId($headingTags, $xpath, $sectionId);
        $current = $heading;
        $headingTag = $heading->nodeName;
        $headingLevel = intval(substr($headingTag, 1));

        // Heading title is later used for form redirect
        $headingTitle = slugify($heading->nodeValue);
        setVar(WIKI_HEADING_TITLE, $headingTitle, STORE_SIP);

        while ($current) {
            $currentTag = $current->nodeName;
            if (in_array($currentTag, $headingTags)) {
                $currentLevel = intval(substr($currentTag, 1));
                if ($current !== $heading && $currentLevel <= $headingLevel) {
                    break;
                }
                $sectionIdEnd++;
            }
            $content .= $current->ownerDocument->saveHTML($current);
            $current = $current->nextSibling;
        }

        // Set section id end to S-Store
        setVar(WIKI_PAGE_SECTION_ID_END, $sectionIdEnd, STORE_SIP);

    // Editing entire content
    } else {
        $content = $wikiPageContent;
    }

    // Set content to V-Store
    return [WIKI_PAGE_CONTENT => $content];
}

/**
 * Creates a DOMNodeList from the HTML content and iterates over each sibling of the edited heading that was not loaded into the editor
 * Updates the newly created record with the full content and remaining values from the previous version
 * Updates previous versions with the current id
 *
 * @param $paramArray
 * @param $qfq
 * @return null[]|string[]
 */
function updateWikiPage($paramArray, $qfq) {
    // Variables used in SQL queries
    $constId = WIKI_PAGE_COLUMN_ID;
    $constContent = WIKI_PAGE_COLUMN_CONTENT;
    $constPageSlug = WIKI_PAGE_COLUMN_PAGE_SLUG;
    $constName = WIKI_PAGE_COLUMN_NAME;
    $constWpId = WIKI_PAGE_COLUMN_WP_ID;
    $constWpIdParent = WIKI_PAGE_COLUMN_WP_ID_PARENT;
    $constWpIdCurrent = WIKI_PAGE_COLUMN_WP_ID_CURRENT;
    $constRoUser = WIKI_PAGE_COLUMN_RO_USER;
    $constRoGroup = WIKI_PAGE_COLUMN_RO_GROUP;
    $constRoPublic = WIKI_PAGE_COLUMN_RO_PUBLIC;
    $constRwUser = WIKI_PAGE_COLUMN_RW_USER;
    $constRwGroup = WIKI_PAGE_COLUMN_RW_GROUP;
    $constRwPublic = WIKI_PAGE_COLUMN_RW_PUBLIC;
    $constPageLock = WIKI_PAGE_COLUMN_PAGE_LOCK;
    $constAuthor = WIKI_PAGE_COLUMN_AUTHOR;
    $constImageBorder = WIKI_PAGE_COLUMN_IMAGE_BORDER;
    $constWikiPage = WIKI_PAGE_TABLE_WIKI_PAGE;
    $constWikiAttachment = WIKI_PAGE_TABLE_WIKI_ATTACHMENT;

    $headingTags = array(HTML_TAG_H1, HTML_TAG_H2, HTML_TAG_H3, HTML_TAG_H4, HTML_TAG_H5, HTML_TAG_H6);

    // id of the wiki page record
    $wpIdPrevious = $paramArray[WIKI_TOKEN_WIKI_PAGE_ID_PREVIOUS];
    $wpIdCurrent = $paramArray[WIKI_PAGE_COLUMN_WP_ID_CURRENT];
    $action = $paramArray[WIKI_TOKEN_ACTION];
    $feUser = $paramArray[WIKI_TOKEN_FE_USER];

    if ($action === WIKI_TOKEN_EDIT_SECTION) {
        $sectionIdStart = intval($paramArray[WIKI_PAGE_SECTION_ID_START]);
        $sectionIdEnd = intval($paramArray[WIKI_PAGE_SECTION_ID_END]);

        // Content from previous version of wiki page
        $sql = "SELECT $constContent FROM $constWikiPage WHERE $constId = $wpIdPrevious";
        $resultArray = doSql($sql);

        $wikiPageContent = $resultArray[0][WIKI_PAGE_COLUMN_CONTENT];

        $xpath = doDOMXPath($wikiPageContent);

        // Get start and end heading nodes
        $headingStart = getHeadingBySectionId($headingTags, $xpath, $sectionIdStart);
        $headingEnd = getHeadingBySectionId($headingTags, $xpath, $sectionIdEnd);

        $firstSection = $lastSection = '';

        // Collect nodes before the start heading
        $current = $headingStart->previousSibling;

        while ($current) {
            $firstSection = $current->ownerDocument->saveHTML($current) . $firstSection;
            $current = $current->previousSibling;
        }

        // Collect nodes after the end heading
        $current = $headingEnd;

        while ($current) {
            $lastSection .= $current->ownerDocument->saveHTML($current);
            $current = $current->nextSibling;
        }

        // Only used as parameter for function
        $boolFalse = false;

        // Escape single and double ticks
        $firstSection = OnString::escape('sd', $firstSection, $boolFalse);
        $lastSection = OnString::escape('sd', $lastSection, $boolFalse);

        // Update wiki content
        $sql = "UPDATE $constWikiPage SET $constContent = CONCAT('$firstSection', $constContent, '$lastSection') WHERE $constId = $wpIdCurrent";
        doSql($sql);
    }

    // Update wiki settings
    // Make sure that there is a trailing space at the end of every line!
    $sql = <<<EOD
    UPDATE $constWikiPage AS wp1 
    JOIN $constWikiPage AS wp2 
      ON wp2.$constId = $wpIdPrevious 
    SET wp1.$constWpIdParent =  wp2.$constWpIdParent 
    , wp1.$constPageSlug =  wp2.$constPageSlug 
    , wp1.$constName =  wp2.$constName 
    , wp1.$constAuthor = '$feUser' 
    , wp1.$constRoUser =  wp2.$constRoUser 
    , wp1.$constRoGroup =  wp2.$constRoGroup 
    , wp1.$constRoPublic =  wp2.$constRoPublic 
    , wp1.$constRwUser =  wp2.$constRwUser 
    , wp1.$constRwGroup =  wp2.$constRwGroup 
    , wp1.$constRwPublic =  wp2.$constRwPublic 
    , wp1.$constPageLock =  wp2.$constPageLock 
    , wp1.$constImageBorder =  wp2.$constImageBorder 
    WHERE wp1.$constId = $wpIdCurrent
    EOD;

    $sql = str_replace(PHP_EOL, '', $sql);
    doSql($sql);

    // Update all previous wiki pages with current id
    $sql = "UPDATE $constWikiPage SET $constWpIdCurrent = $wpIdCurrent WHERE IFNULL($constWpIdCurrent, $constId) = $wpIdPrevious";
    doSql($sql);

    // Update all child wiki pages with current id
    $sql = "UPDATE $constWikiPage SET $constWpIdParent = $wpIdCurrent WHERE $constWpIdParent = $wpIdPrevious";
    doSql($sql);

    // Update all wiki attachments with current id
    $sql = "UPDATE $constWikiAttachment SET $constWpId = $wpIdCurrent WHERE $constWpId = $wpIdPrevious";
    doSql($sql);

    // Prevents form from executing this function twice
    setVar(WIKI_TOKEN_ACTION, '', STORE_SIP);
}

/**
 * Creates a DOMXPath object from given HTML
 *
 * @param $html
 * @return mixed DOMElement
 */
function doDOMXPath($html) {

    // Ensures that "Umlaute" (äöü) are displayed correctly
    $utf8Encoding = '<?xml encoding="UTF-8">';

    $dom = new DOMDocument();
    $dom->loadHTML($utf8Encoding . $html, LIBXML_NOERROR);
    $xpath = new DOMXPath($dom);

    return $xpath;
}

/**
 * Creates a DOMElement from given HTML
 *
 * @param $html
 * @return mixed DOMElement
 */
function doDOMElement($html) {

    // Ensures that "Umlaute" (äöü) are displayed correctly
    $utf8Encoding = '<?xml encoding="UTF-8">';

    $dom = new DOMDocument();
    $dom->loadHTML($utf8Encoding . $html, LIBXML_NOERROR);
    $element = $dom->getElementsByTagName(HTML_TAG_BODY)->item(0)->firstChild;

    return $element;
}

/**
 * Recursive function to generate a navigation with links from the current page to its parent page and so on.
 * Finished version looks like this: grandparent page > parent page > current page
 *
 * @param $id
 * @return string
 * @throws CodeException
 * @throws DbException
 * @throws UserFormException
 * @throws UserReportException
 */
function doNavigation($id) {
    // Variables used in SQL queries
    $constId = WIKI_PAGE_COLUMN_ID;
    $constPageSlug = WIKI_PAGE_COLUMN_PAGE_SLUG;
    $constName = WIKI_PAGE_COLUMN_NAME;
    $constWpIdParent = WIKI_PAGE_COLUMN_WP_ID_PARENT;
    $constWikiPage = WIKI_PAGE_TABLE_WIKI_PAGE;

    $sql = "SELECT $constWpIdParent, $constPageSlug, $constName FROM $constWikiPage WHERE $constId = $id";
    $resultArray = doSql($sql);

    // If no $wpIdParent is found this equals to null
    $wpIdParent = $resultArray[0][WIKI_PAGE_COLUMN_WP_ID_PARENT] ?? null;
    $pageSlug = $resultArray[0][WIKI_PAGE_COLUMN_PAGE_SLUG] ?? null;
    $name = $resultArray[0][WIKI_PAGE_COLUMN_NAME] ?? null;

    $link = doQfqLink('p:', "$pageSlug?wpId=$id", '|t:' . $name);
    $separator = $output = '';

    // Check if the root page has been reached
    if (!is_null($wpIdParent)) {
        $output .= doNavigation($wpIdParent);
        $separator = '<span>&nbsp;»&nbsp;</span>';
    }

    $output .= $separator . $link;
    return $output;
}

/**
 * Creates a table of contents from all headings of the wiki page
 * Gets invoked by the macro '{{toc}}'
 * Every level/element has its own indentation
 *
 * @param $content
 * @return string
 */
function doTableOfContents($content) {
    $headingTags = array(HTML_TAG_H1, HTML_TAG_H2, HTML_TAG_H3, HTML_TAG_H4, HTML_TAG_H5, HTML_TAG_H6);
    $headings = array();

    // <h6>
    $highestLevel = 6;

    // <h1>, lowest level
    $previousLevel = 1;
    $toc = WIKI_TOKEN_TABLE_OF_CONTENTS;
    $constWikiClassToc = WIKI_CSS_CLASS_QFQ_WIKI_TOC;

    // Iterate over each element
    foreach ($content as $element) {
        $nodeName = $element->nodeName;

        // Looks for <h1> - <h6> elements
        if (in_array($nodeName, $headingTags)) {
            $nodeValue = $element->nodeValue;

            // Heading level and name get added to the array
            $headings[] = intval(substr($nodeName, 1)) . "=" . $nodeValue;
        }
    }

    // If no heading elements were found the macro '{{toc}}' will be returned as is
    if (empty($headings)) {
        return $toc;
    }

    // List for the table of contents
    $toc = "<ul class='$constWikiClassToc'><li><strong>Table of contents</strong></li>";

    // Iterate over each heading
    foreach ($headings as $key => $value) {
        $headingLevel = explode("=", $value);
        $currentLevel = $headingLevel[0];
        $heading = $headingLevel[1];

        // Check if current level is higher than the highest level, update if true
        $highestLevel = ($currentLevel < $highestLevel) ? $currentLevel : $highestLevel;

        if ($currentLevel > $previousLevel) {

            // 'Indentation' of the <ul> element inside the table of contents
            $toc .= str_repeat('<ul>', $currentLevel - $previousLevel);

        } elseif ($currentLevel === $previousLevel) {
            $toc .= '</li>';
        } else {

            // Closing the 'indented' <ul> element inside the table of contents
            $toc .= str_repeat('</ul>', $previousLevel - $currentLevel);
        }

        $toc .= '<li>';

        // Anchor tag that links to the heading
        $toc .= '<a href="#' . slugify($heading) . '">' . $heading . '</a>';
        $previousLevel = $currentLevel;
    }

    // Complete the list
    $toc .= str_repeat('</li></ul>', $previousLevel);
    return $toc;
}

/**
 * Creates a list of child pages of the current wiki page
 * Gets invoked by the macro '{{childPages}}'
 *
 * @param $id
 * @return string
 */
function doChildPages($id) {
    // Variables used in SQL queries
    $constId = WIKI_PAGE_COLUMN_ID;
    $constPageSlug = WIKI_PAGE_COLUMN_PAGE_SLUG;
    $constName = WIKI_PAGE_COLUMN_NAME;
    $constWpIdParent = WIKI_PAGE_COLUMN_WP_ID_PARENT;
    $constWpIdCurrent = WIKI_PAGE_COLUMN_WP_ID_CURRENT;
    $constWikiPage = WIKI_PAGE_TABLE_WIKI_PAGE;
    $constWikiClassChildPages = WIKI_CSS_CLASS_QFQ_WIKI_CHILD_PAGES;

    $sql = "SELECT $constId, $constName, $constPageSlug FROM $constWikiPage WHERE $constWpIdParent = $id AND ISNULL($constWpIdCurrent) ORDER BY $constName ASC";
    $resultArray = doSql($sql);

    $childPages = '{{childPages}}';

    // If no child pages were found the macro gets returned as is
    if (empty($resultArray)) {
        return $childPages;
    }

    // List of the child pages
    $childPages = "<ul class='$constWikiClassChildPages'>";

    // Iterate over each row
    foreach ($resultArray as $row) {
        $id = $row[WIKI_PAGE_COLUMN_ID];
        $name = $row[WIKI_PAGE_COLUMN_NAME];
        $pageSlug = $row[WIKI_PAGE_COLUMN_PAGE_SLUG];

        // Link to the child page
        $childPages .= '<li>' . doQfqLink('p:', $pageSlug, '?wpId=' . $id . '|t:' . $name) . '</li>';
    }

    // Complete the list
    $childPages .= '</ul>';
    return $childPages;
}

/**
 * Checks the privileges of the user that wants to access the wiki page
 *
 * @param $paramArray
 * @param $qfq
 * @return string[]
 * @throws CodeException
 * @throws DbException
 * @throws UserFormException
 * @throws UserReportException
 */
function checkWikiAccess($paramArray, $qfq) {
    // Variables used in SQL queries
    $constId = WIKI_PAGE_COLUMN_ID;
    $constRoUser = WIKI_PAGE_COLUMN_RO_USER;
    $constRoGroup = WIKI_PAGE_COLUMN_RO_GROUP;
    $constRoPublic = WIKI_PAGE_COLUMN_RO_PUBLIC;
    $constRwUser = WIKI_PAGE_COLUMN_RW_USER;
    $constRwGroup = WIKI_PAGE_COLUMN_RW_GROUP;
    $constRwPublic = WIKI_PAGE_COLUMN_RW_PUBLIC;
    $constPageLock = WIKI_PAGE_COLUMN_PAGE_LOCK;
    $constAuthor = WIKI_PAGE_COLUMN_AUTHOR;
    $constWikiPage = WIKI_PAGE_TABLE_WIKI_PAGE;

    $store = Store::getInstance();

    // id of the wiki page
    $id = $paramArray[WIKI_PAGE_COLUMN_WP_ID];

    $sql = "SELECT $constAuthor, $constRoUser, $constRoGroup, $constRoPublic, $constRwUser, $constRwGroup, $constRwPublic, $constPageLock FROM $constWikiPage WHERE $constId = $id";
    $resultArray = doSql($sql);

    $author = (isset($resultArray[0][WIKI_PAGE_COLUMN_AUTHOR])) ? $resultArray[0][WIKI_PAGE_COLUMN_AUTHOR] : '';
    $roUser = (isset($resultArray[0][WIKI_PAGE_COLUMN_RO_USER])) ? explode(',', $resultArray[0][WIKI_PAGE_COLUMN_RO_USER]) : array();
    $roGroup = (isset($resultArray[0][WIKI_PAGE_COLUMN_RO_GROUP])) ? explode(',', $resultArray[0][WIKI_PAGE_COLUMN_RO_GROUP]) : array();
    $roPublic = (isset($resultArray[0][WIKI_PAGE_COLUMN_RO_PUBLIC])) ? $resultArray[0][WIKI_PAGE_COLUMN_RO_PUBLIC] : '';
    $rwUser = (isset($resultArray[0][WIKI_PAGE_COLUMN_RW_USER])) ? explode(',', $resultArray[0][WIKI_PAGE_COLUMN_RW_USER]) : array();
    $rwGroup = (isset($resultArray[0][WIKI_PAGE_COLUMN_RW_GROUP])) ? explode(',', $resultArray[0][WIKI_PAGE_COLUMN_RW_GROUP]) : array();
    $rwPublic = (isset($resultArray[0][WIKI_PAGE_COLUMN_RW_PUBLIC])) ? $resultArray[0][WIKI_PAGE_COLUMN_RW_PUBLIC] : '';
    $pageLock = (isset($resultArray[0][WIKI_PAGE_COLUMN_PAGE_LOCK])) ? $resultArray[0][WIKI_PAGE_COLUMN_PAGE_LOCK] : '';

    // Get logged-in user from T-store
    $feUser = $store::getVar(TYPO3_FE_USER, STORE_TYPO3);

    // Get groups of logged-in user from T-store
    $feUserGroup[] = explode(',', $store::getVar(TYPO3_FE_USER_GROUP, STORE_TYPO3));

    $feUserGroup = (is_array($feUserGroup)) ? $feUserGroup : array();
    $wikiAccess = WIKI_TOKEN_ACCESS_OFF;

    /* Checks if permissions are set to read-write for either the public, the logged-in user or a group of the logged-in user
       Also check if the logged-in user is the author of the wiki page */
    if ($rwPublic === WIKI_TOKEN_ACCESS_ON || (in_array($feUser, $rwUser) && !empty($feUser)) || (array_intersect($feUserGroup[0], $rwGroup) && !in_array('', $feUserGroup[0])) || $feUser === $author) {
        $wikiAccess = WIKI_TOKEN_ACCESS_READ_WRITE;

        // Checks if permissions are set to read-only for either the public, the logged-in user or a group of the logged-in user
    } elseif ($roPublic === WIKI_TOKEN_ACCESS_ON || (in_array($feUser, $roUser) && !empty($feUser)) || (array_intersect($feUserGroup[0], $roGroup) && !in_array('', $feUserGroup[0]))) {
        $wikiAccess = WIKI_TOKEN_ACCESS_READ_ONLY;
    }

    // If page lock is set but not by the logged-in user who has read-write permissions, they change to read-only
    $wikiAccess = (!empty($pageLock) && $wikiAccess === WIKI_TOKEN_ACCESS_READ_WRITE && $pageLock !== $feUser) ? WIKI_TOKEN_ACCESS_READ_ONLY : $wikiAccess;

    // Permissions get set in V-Store, so they can later be used to render the wiki accordingly
    return [WIKI_TOKEN_WIKI_ACCESS => $wikiAccess];
}

/**
 * Replaces the macro '{{collapse()}}' and/or '{{collapse(string)}}'
 *
 * @param $matches
 * @return mixed|string
 */
function doCollapse($matches) {
    // $matches[0] = '<p>{{collapse(optional text)}}</p>
    // $matches[1] = 'optional text'
    $text = $matches[1];
    $randomInt = rand();

    // Constants for CSS classes and HTML ids
    $constWikiClassCollapseLink = WIKI_CSS_CLASS_QFQ_WIKI_COLLAPSE_LINK;
    $constWikiClassCollapseChevron = WIKI_CSS_CLASS_QFQ_WIKI_COLLAPSE_CHEVRON;
    $constWikiClassCollapseContainer = WIKI_CSS_CLASS_QFQ_WIKI_COLLAPSE_CONTAINER;
    $constWikiClassGlyphicon = WIKI_CSS_CLASS_GLYPHICON;
    $constWikiClassGlyphiconChevronDown = WIKI_CSS_CLASS_GLYPHICON_CHEVRON_DOWN;
    $constWikiClassGlyphiconChevronRight = WIKI_CSS_CLASS_GLYPHICON_CHEVRON_RIGHT;
    $constWikiIdCollapseChevron = WIKI_HTML_ID_QFQ_WIKI_COLLAPSE_CHEVRON;
    $constWikiIdCollapseContent = WIKI_HTML_ID_QFQ_WIKI_COLLAPSE_CONTENT;

    $str = "<a class='$constWikiClassCollapseLink' style='cursor:pointer;' onclick='$(`#$constWikiIdCollapseChevron-$randomInt`).toggleClass(`$constWikiClassGlyphiconChevronDown $constWikiClassGlyphiconChevronRight`); $(`#$constWikiIdCollapseContent-$randomInt`).fadeToggle(`fast`);'>
            <span class='$constWikiClassCollapseChevron $constWikiClassGlyphicon $constWikiClassGlyphiconChevronRight' id='$constWikiIdCollapseChevron-$randomInt'></span>$text</a></br>
            <div class='$constWikiClassCollapseContainer' style='display: none;' id='$constWikiIdCollapseContent-$randomInt'>";

    return $str;
}

/**
 * Executes SQL query
 *
 * @param $sql
 * @return array|bool|int|string
 * @throws CodeException
 * @throws DbException
 * @throws UserFormException
 */
function doSql($sql) {
    $db = new Database();
    $result = $db->sql($sql);
    return $result;
}

/**
 * Creates a QFQ-link
 *
 * @param $page
 * @param $pageSlug
 * @param $params
 * @return string
 * @throws CodeException
 * @throws DbException
 * @throws UserFormException
 * @throws UserReportException
 */
function doQfqLink($page, $pageSlug, $params) {
    $s = Store::getInstance();
    $sip = $s->getSipInstance();
    $link = new Link($sip);
    $str = $link->renderLink($page . $pageSlug . $params);
    return $str;
}

/**
 * Sets a variable to a store
 *
 * @param $key
 * @param $value
 * @param $store
 * @return void
 * @throws CodeException
 * @throws UserFormException
 * @throws UserReportException
 */
function setVar($key, $value, $store) {
    $s = Store::getInstance();
    $s::setVar($key, $value, $store);
}

/**
 * Slugify function
 * E.g. This is my custom heading -> This-is-my-custom-heading
 *
 * @param $text
 * @return array|string|string[]
 */
function slugify($text) {
    $divider = '-';

    // Replace non letter or digits by divider
    $text = preg_replace('~[^\pL\d]+~u', $divider, $text);

    // Transliterate
    $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);

    // Remove unwanted characters
    $text = preg_replace('~[^-\w]+~', '', $text);

    // Trim
    $text = trim($text, $divider);

    // Remove duplicate divider
    $text = preg_replace('~-+~', $divider, $text);

    return $text;
}

/**
 * Returns the index of given HTML element
 *
 * @param $headingTags
 * @param $xpath
 * @param $sectionId
 * @return mixed
 */
function getHeadingBySectionId($headingTags, $xpath, $sectionId) {
    return $xpath->query("//*[self::" . implode(" or self::", $headingTags) . "]")->item($sectionId - 1);
}