(function(storyContent) {
    // Create ink story from the content using inkjs
    var story = new inkjs.Story(storyContent);

    var savePoint = "";

    let fade_in = true;

    // Global tags - those at the top of the ink file
    // We support:
    //  # theme: dark
    //  # author: Your Name
    var globalTags = story.globalTags;
    if( globalTags ) {
        for(var i=0; i<story.globalTags.length; i++) {
            var globalTag = story.globalTags[i];
            var splitTag = splitPropertyTag(globalTag);

            // THEME: dark
            if( splitTag && splitTag.property == "title" ) {
                var title = document.querySelector('.title');
                title.innerHTML = splitTag.val;
            }

            // author: Your Name
            else if( splitTag && splitTag.property == "author" ) {
                var byline = document.querySelector('.byline');
                byline.innerHTML = "by "+splitTag.val;
            }
        }
    }

    var storyContainer = document.querySelector('#story');
    var choiceContainer = document.querySelector('#choices');
    var outerScrollContainer = document.querySelector('#book');

    function updateParagraphPreview(paragraph_data, indent_width, preview_width) {
        var old_preview = document.getElementById("preview");
        var preview = document.createElement("div");
        preview.id = "preview";
        preview.style.width = preview_width + 'px';
        if(old_preview) {
            old_preview.replaceWith(preview);
            // preview = old_preview;
        } else {
            document.body.appendChild(preview);
        }
        // p = typesetParagraph(paragraph_data, indent_width);
        // preview.appendChild(p);
    }

    let timeoutQueue = [];

    function scheduleTimeout(func, delay, ...args) {
        const timeoutObject = {
            execute: () => func(...args),
            timeoutId: null
        };

        timeoutObject.timeoutId = setTimeout(() => {
            timeoutObject.execute();
            timeoutQueue = timeoutQueue.filter(t => t !== timeoutObject);
        }, delay);

        timeoutQueue.push(timeoutObject);

        return timeoutObject.timeoutId;
    }

    function fastForward() {
        // Sort the queue based on timeoutId (assuming that smaller ids are scheduled earlier)
        timeoutQueue.sort((a, b) => a.timeoutId - b.timeoutId);
        // Clear and execute all timeouts
        timeoutQueue.forEach(timeoutObject => {
            clearTimeout(timeoutObject.timeoutId);
            timeoutObject.execute();
        });

        timeoutQueue = [];
        document.getElementById("page_right").scrollTo({top: document.getElementById("page_right").scrollHeight, behavior: 'smooth'});
    }

    // var numberOfPreviewLines = 0;

    function typesetParagraph(paragraph_data, indent_width, delay = 0) {
        console.log("Typesetting Paragraph with: ", paragraph_data, indent_width);
        var left = indent_width;
        var p = document.createElement("p");
        p.style.position = 'relative';
        var line_height = parseFloat(window.getComputedStyle(document.querySelector("#ruler")).lineHeight);
        // numberOfPreviewLines += paragraph_data.breaks.length - 1;
        // console.log("Calculated line height:", line_height);
        p.style.height = line_height * (paragraph_data.breaks.length - 1) + 'px';
        p.style.marginBlockEnd = 0;
        for(let i = 1; i < paragraph_data.breaks.length; i++) {
            if(i > 1)
                left = 0;
            for(let j = paragraph_data.breaks[i-1].position; j <= paragraph_data.breaks[i].position; j++) {
                // console.log("i =",i,"j =",j,"from =",paragraph_data.breaks[i-1].position,"to =",paragraph_data.breaks[i].position,"node_width =", paragraph_data.nodes[j].width, "left =", left, "type =", paragraph_data.nodes[j].type, "value =", paragraph_data.nodes[j].value);
                if(paragraph_data.nodes[j].type === 'box' && paragraph_data.nodes[j].value !== '' &&  j < paragraph_data.breaks[i].position) {
                    if(j > paragraph_data.breaks[i-1].position + 1 && paragraph_data.nodes[j-1].type === 'penalty' && p.lastChild) {
                        p.lastChild.textContent += paragraph_data.nodes[j].value;
                        left += paragraph_data.nodes[j].width;
                    } else {
                        let word = document.createElement("span");
                        word.style.position = 'absolute';
                        word.classList.add("fade-in");
                        word.style.top = line_height * (i - 1) + 'px';
                        word.style.left = left + 'px';    
                        word.innerHTML = paragraph_data.nodes[j].value;
                        insertAfter(delay, p, word);
                        delay += 100.0;
                        // p.appendChild(word);
                        if(j > 0)
                            left += paragraph_data.nodes[j].width;
                        else
                            left += paragraph_data.nodes[j].width - indent_width;
                    }
                } else if(j > paragraph_data.breaks[i-1].position && paragraph_data.nodes[j].type === 'glue' && paragraph_data.nodes[j].width !== 0 && j <= paragraph_data.breaks[i].position) {
                    // Insert space character
                    if(paragraph_data.breaks[i].ratio > 0) {
                        left += paragraph_data.nodes[j].width + paragraph_data.breaks[i].ratio * paragraph_data.nodes[j].stretch;
                    } else {
                        left += paragraph_data.nodes[j].width + paragraph_data.breaks[i].ratio * paragraph_data.nodes[j].shrink;
                    }
                } else if(paragraph_data.nodes[j].type === 'penalty' && paragraph_data.nodes[j].penalty === 100 && j === paragraph_data.breaks[i].position) {
                    let word = document.createElement("span");
                    word.style.position = 'absolute';
                    word.style.top = line_height * (i - 1) + 'px';
                    word.style.left = left + 'px';    
                    word.innerHTML = "-";
                    insertAfter(delay, p, word);
                    delay += 100;
                    // p.appendChild(word);
                    // left += paragraph_data.nodes[j].width;
                }
            }
        };
        return [p, delay];
    }

    function measureText(str) {
        if (str === ' ') {
          str = '\u00A0';
        }
        ruler.textContent = str;
        return ruler.getClientRects()[0].width;
    }

    function updateBookDimensions() {
        const vw = window.innerWidth;
        const vh = window.innerHeight;
        const viewportAspectRatio = vw / vh;
        const imageAspectRatio = 2727 / 1691;
    
        let bookWidth, bookHeight;
    
        if (viewportAspectRatio > imageAspectRatio) {
            bookWidth = vh * imageAspectRatio;
            bookHeight = vh;
        } else {
            bookWidth = vw;
            bookHeight = vw / imageAspectRatio;
        }
    
        document.documentElement.style.setProperty('--book-width', `${bookWidth}px`);
        document.documentElement.style.setProperty('--book-height', `${bookHeight}px`);
          
        // Setting a CSS variable that will be either vw or vh depending on the viewport aspect ratio
        document.documentElement.style.setProperty(
          "--viewport-dimension", 
          viewportAspectRatio > imageAspectRatio ? 'vw' : 'vh'
        );
         document.documentElement.style.setProperty('--viewport-aspect-ratio', viewportAspectRatio);
         let story = document.getElementById("story");
         let paddingTop = window.getComputedStyle(story).paddingTop;
         let paddingBottom = window.getComputedStyle(story).paddingBottom;
         document.documentElement.style.setProperty('--story-line-height', (story.clientHeight - paddingTop - paddingBottom) / 28)
    }
      
    // Update the aspect ratio when the page loads
    updateBookDimensions();
      
    // Update the aspect ratio whenever the window is resized
    window.addEventListener('resize', updateBookDimensions);

    window.addEventListener('keydown', (event) => {
        if (event.code === 'Space') {
            fade_in = false;
            fastForward();
        }
    });

    // page features setup
    var hasSave = loadSavePoint();
    setupButtons(hasSave);

    // Set initial save point
    savePoint = story.state.toJson();

    // Kick off the start of the story!
    continueStory(true);

    // Main story processing function. Each time this is called it generates
    // all the next content up as far as the next set of choices.
    function continueStory(firstTime) {

        var paragraphIndex = 0;
        var delay = 0.0;

        // Don't over-scroll past new content
        var previousBottomEdge = firstTime ? 0 : contentBottomEdgeY();

        var fade_in = true

        // Generate story text - loop through available content
        while(story.canContinue) {
            // Get ink to generate the next paragraph
            var paragraphText = story.Continue();
            var tags = story.currentTags;

            // Any special tags included with this line
            var customClasses = [];
            for(var i=0; i<tags.length; i++) {
                var tag = tags[i];

                // Detect tags of the form "X: Y". Currently used for IMAGE and CLASS but could be
                // customised to be used for other things too.
                var splitTag = splitPropertyTag(tag);

                // AUDIO: src
                if( splitTag && splitTag.property == "AUDIO" ) {
                  if('audio' in this) {
                    this.audio.pause();
                    this.audio.removeAttribute('src');
                    this.audio.load();
                  }
                  this.audio = new Audio(splitTag.val);
                  this.audio.play();
                }

                // AUDIOLOOP: src
                else if( splitTag && splitTag.property == "AUDIOLOOP" ) {
                  if('audioLoop' in this) {
                    this.audioLoop.pause();
                    this.audioLoop.removeAttribute('src');
                    this.audioLoop.load();
                  }
                  this.audioLoop = new Audio(splitTag.val);
                  this.audioLoop.play();
                  this.audioLoop.loop = true;
                }

                // IMAGE: src
                if( splitTag && splitTag.property == "IMAGE" ) {
                    var imageElement = document.createElement('img');
                    imageElement.src = splitTag.val;
                    storyContainer.appendChild(imageElement);

                    showAfter(delay, imageElement);
                    delay += 200.0;
                }

                // LINK: url
                else if( splitTag && splitTag.property == "LINK" ) {
                    window.location.href = splitTag.val;
                }

                // LINKOPEN: url
                else if( splitTag && splitTag.property == "LINKOPEN" ) {
                    window.open(splitTag.val);
                }

                // BACKGROUND: src
                else if( splitTag && splitTag.property == "BACKGROUND" ) {
                    outerScrollContainer.style.backgroundImage = 'url('+splitTag.val+')';
                }

                // CLASS: className
                else if( splitTag && splitTag.property == "CLASS" ) {
                    customClasses.push(splitTag.val);
                }

                // CLEAR - removes all existing content.
                // RESTART - clears everything and restarts the story from the beginning
                else if( tag == "CLEAR" || tag == "RESTART" ) {
                    removeAll("p");
                    removeAll("img");

                    // Comment out this line if you want to leave the header visible when clearing
                    // setVisible(".header", false);

                    if( tag == "RESTART" ) {
                        restart();
                        return;
                    }
                }
            }

            // Create paragraph element (initially hidden)
            (function(text) {
                if(text.trim().length === 0)
                    return;
                    console.log("Hyphenating:", text);
                    let hyphenator_promise = Hyphenopoly.hyphenators["en-us"].then((hyphenator_en) => {
                        var measure = parseFloat(window.getComputedStyle(document.getElementById("story")).width);
                        var indentWidth = parseFloat(window.getComputedStyle(document.querySelector("#indent")).textIndent);
                        var previewWidth = measure;
                        var preview_data = kap(hyphenator_en(text, '.hyphenatePipe'), measureText, 'align-justify', measure, true, indentWidth);
                        return { preview_data, indentWidth, previewWidth};
                });
                hyphenator_promise.then(({ preview_data, indentWidth, previewWidth }) => {
                    // updateParagraphPreview(preview_data, indentWidth, previewWidth);
                    var p, d;
                    [p, d] = typesetParagraph(preview_data, indentWidth, delay);
                    delay = d;
                    // Add any custom classes derived from ink tags
                    for(var i=0; i<customClasses.length; i++)
                        p.classList.add(customClasses[i]);
                    storyContainer.appendChild(p);
                    p.scrollIntoView({ behavior: 'smooth'});
                });
            })(paragraphText);

            // var paragraphElement = document.createElement('p');
            // var words = paragraphText.split(" ");
            // words.forEach(word => {
            //     var wordElement = document.createElement('span');
            //     Hyphenopoly.hyphenators["en-us"].then((hyphenator_en) => {
            //         wordElement.innerHTML = hyphenator_en(word);                
            //     });
            //     // showAfter(delay, wordElement);
            //     insertAfter(delay, paragraphElement, wordElement, fade_in);
            //     insertAfter(delay, paragraphElement, document.createTextNode(" "), false);
            //     delay +=100.0;
            //     // paragraphElement.appendChild(wordElement);
            //     // paragraphElement.appendChild(document.createTextNode(" "));
            // });
            // // paragraphElement.innerHTML = paragraphText;
            // storyContainer.appendChild(paragraphElement);


            // Fade in paragraph after a short delay
            // showAfter(delay, paragraphElement);
            // delay += 200.0;
        }

        // Create HTML choices from ink choices
        story.currentChoices.forEach(function(choice) {

            // Create paragraph with anchor element
            var choiceParagraphElement = document.createElement('p');
            choiceParagraphElement.classList.add("choice");
            choiceParagraphElement.innerHTML = `<a href='#'>${choice.text}</a>`
            // choiceContainer.appendChild(choiceParagraphElement);
            insertAfter(delay, choiceContainer, choiceParagraphElement, fade_in);
            // Fade choice in after a short delay
            // showAfter(delay, choiceParagraphElement);
            delay += 200.0;

            // Click on choice
            var choiceAnchorEl = choiceParagraphElement.querySelectorAll("a")[0];
            choiceAnchorEl.addEventListener("click", function(event) {

                // Don't follow <a> link
                event.preventDefault();

                // Remove all existing choices
                removeAll(".choice", true);

                // Tell the story where to go next
                story.ChooseChoiceIndex(choice.index);

                // This is where the save button will save from
                savePoint = story.state.toJson();

                // Aaand loop
                continueStory();
            });
        });

        // Extend height to fit
        // We do this manually so that removing elements and creating new ones doesn't
        // cause the height (and therefore scroll) to jump backwards temporarily.
        // storyContainer.style.height = contentBottomEdgeY()+"px";

        if( !firstTime )
            scrollDown(previousBottomEdge);

    }

    function restart() {
        story.ResetState();

        setVisible(".header", true);
        removeAll(".choice", true);

        // set save point to here
        savePoint = story.state.toJson();

        continueStory(true);

        outerScrollContainer.scrollTo({ top: 0, left: 0, behavior: 'smooth'});
    }

    // -----------------------------------
    // Various Helper functions
    // -----------------------------------

    // Fades in an element after a specified delay
    function showAfter(delay, el) {
        el.classList.add("hide");
        setTimeout(function() {
            setTimeout(function() { el.classList.remove("hide") }, delay);
        });
    }

    function insertAfter(delay, target, el, fade_in = true) {
        if(fade_in) {
            el.classList.add("fade-in");
            scheduleTimeout(function() {
                target.appendChild(el);
                el.scrollIntoView({ behavior: 'smooth'});
            }, delay);
        } else {
            scheduleTimeout(function() {
                target.appendChild(el);
            }, delay);
        }
    }

    // Scrolls the page down, but no further than the bottom edge of what you could
    // see previously, so it doesn't go too far.
    function scrollDown(previousBottomEdge) {
        return; // TODO: Fix or remove function
        // Line up top of screen with the bottom of where the previous content ended
        var target = previousBottomEdge;

        // Can't go further than the very bottom of the page
        var limit = outerScrollContainer.scrollHeight - outerScrollContainer.clientHeight;
        if( target > limit ) target = limit;

        var start = outerScrollContainer.scrollTop;

        var dist = target - start;
        var duration = 300 + 300*dist/100;
        var startTime = null;
        function step(time) {
            if( startTime == null ) startTime = time;
            var t = (time-startTime) / duration;
            var lerp = 3*t*t - 2*t*t*t; // ease in/out
            outerScrollContainer.scrollTo({ left: 0, top: (1.0-lerp)*start + lerp*target, behavior: 'smooth'});
            if( t < 1 ) requestAnimationFrame(step);
        }
        requestAnimationFrame(step);
    }

    // The Y coordinate of the bottom end of all the story content, used
    // for growing the container, and deciding how far to scroll.
    function contentBottomEdgeY() {
        var bottomElement = storyContainer.lastElementChild;
        return bottomElement ? bottomElement.offsetTop + bottomElement.offsetHeight : 0;
    }

    // Remove all elements that match the given selector. Used for removing choices after
    // you've picked one, as well as for the CLEAR and RESTART tags.
    function removeAll(selector, choices = false)
    {
        if(choices)
            var allElements = choiceContainer.querySelectorAll(selector);
        else
            var allElements = storyContainer.querySelectorAll(selector);
        for(var i=0; i<allElements.length; i++) {
            var el = allElements[i];
            el.parentNode.removeChild(el);
        }
    }

    // Used for hiding and showing the header when you CLEAR or RESTART the story respectively.
    function setVisible(selector, visible)
    {
        var allElements = storyContainer.querySelectorAll(selector);
        for(var i=0; i<allElements.length; i++) {
            var el = allElements[i];
            if( !visible )
                el.classList.add("invisible");
            else
                el.classList.remove("invisible");
        }
    }

    // Helper for parsing out tags of the form:
    //  # PROPERTY: value
    // e.g. IMAGE: source path
    function splitPropertyTag(tag) {
        var propertySplitIdx = tag.indexOf(":");
        if( propertySplitIdx != null ) {
            var property = tag.substr(0, propertySplitIdx).trim();
            var val = tag.substr(propertySplitIdx+1).trim();
            return {
                property: property,
                val: val
            };
        }

        return null;
    }

    // Loads save state if exists in the browser memory
    function loadSavePoint() {
        try {
            let savedState = window.localStorage.getItem('save-state');
            if (savedState) {
                story.state.LoadJson(savedState);
                return true;
            }
        } catch (e) {
            console.debug("Couldn't load save state");
        }
        return false;
    }

    // Used to hook up the functionality for global functionality buttons
    function setupButtons(hasSave) {

        let rewindEl = document.getElementById("rewind");
        if (rewindEl) rewindEl.addEventListener("click", function(event) {
            removeAll("p");
            removeAll("img");
            setVisible(".header", false);
            restart();
        });

        let saveEl = document.getElementById("save");
        if (saveEl) saveEl.addEventListener("click", function(event) {
            try {
                window.localStorage.setItem('save-state', savePoint);
                document.getElementById("reload").removeAttribute("disabled");
            } catch (e) {
                console.warn("Couldn't save state");
            }

        });

        let reloadEl = document.getElementById("reload");
        if (!hasSave) {
            reloadEl.setAttribute("disabled", "disabled");
        }
        reloadEl.addEventListener("click", function(event) {
            if (reloadEl.getAttribute("disabled"))
                return;

            removeAll("p");
            removeAll("img");
            removeAll(".choice", true);
            try {
                let savedState = window.localStorage.getItem('save-state');
                if (savedState) story.state.LoadJson(savedState);
            } catch (e) {
                console.debug("Couldn't load save state");
            }
            continueStory(true);
        });
    }

})(storyContent);
