import { SLIDES_SELECTOR } from "../utils/constants.js";
import { queryAll, createStyleSheet } from "../utils/util.js";

/**
 * Setups up our presentation for printing/exporting to PDF.
 */
export default class PrintView {
    constructor(Reveal) {
        this.Reveal = Reveal;
    }

    /**
     * Configures the presentation for printing to a static
     * PDF.
     */
    async activate() {
        const config = this.Reveal.getConfig();
        const slides = queryAll(
            this.Reveal.getRevealElement(),
            SLIDES_SELECTOR,
        );

        // Compute slide numbers now, before we start duplicating slides
        const injectPageNumbers =
            config.slideNumber && /all|print/i.test(config.showSlideNumber);

        const slideSize = this.Reveal.getComputedSlideSize(
            window.innerWidth,
            window.innerHeight,
        );

        // Dimensions of the PDF pages
        const pageWidth = Math.floor(slideSize.width * (1 + config.margin)),
            pageHeight = Math.floor(slideSize.height * (1 + config.margin));

        // Dimensions of slides within the pages
        const slideWidth = slideSize.width,
            slideHeight = slideSize.height;

        await new Promise(requestAnimationFrame);

        // Let the browser know what page size we want to print
        createStyleSheet(
            "@page{size:" +
                pageWidth +
                "px " +
                pageHeight +
                "px; margin: 0px;}",
        );

        // Limit the size of certain elements to the dimensions of the slide
        createStyleSheet(
            ".reveal section>img, .reveal section>video, .reveal section>iframe{max-width: " +
                slideWidth +
                "px; max-height:" +
                slideHeight +
                "px}",
        );

        document.documentElement.classList.add("reveal-print", "print-pdf");
        document.body.style.width = pageWidth + "px";
        document.body.style.height = pageHeight + "px";

        const viewportElement = this.Reveal.getViewportElement();
        let presentationBackground;
        if (viewportElement) {
            const viewportStyles = window.getComputedStyle(viewportElement);
            if (viewportStyles && viewportStyles.background) {
                presentationBackground = viewportStyles.background;
            }
        }

        // Make sure stretch elements fit on slide
        await new Promise(requestAnimationFrame);
        this.Reveal.layoutSlideContents(slideWidth, slideHeight);

        // Batch scrollHeight access to prevent layout thrashing
        await new Promise(requestAnimationFrame);

        const slideScrollHeights = slides.map((slide) => slide.scrollHeight);

        const pages = [];
        const pageContainer = slides[0].parentNode;
        let slideNumber = 1;

        // Slide and slide background layout
        slides.forEach(function (slide, index) {
            // Vertical stacks are not centred since their section
            // children will be
            if (slide.classList.contains("stack") === false) {
                // Center the slide inside of the page, giving the slide some margin
                let left = (pageWidth - slideWidth) / 2;
                let top = (pageHeight - slideHeight) / 2;

                const contentHeight = slideScrollHeights[index];
                let numberOfPages = Math.max(
                    Math.ceil(contentHeight / pageHeight),
                    1,
                );

                // Adhere to configured pages per slide limit
                numberOfPages = Math.min(
                    numberOfPages,
                    config.pdfMaxPagesPerSlide,
                );

                // Center slides vertically
                if (
                    (numberOfPages === 1 && config.center) ||
                    slide.classList.contains("center")
                ) {
                    top = Math.max((pageHeight - contentHeight) / 2, 0);
                }

                // Wrap the slide in a page element and hide its overflow
                // so that no page ever flows onto another
                const page = document.createElement("div");
                pages.push(page);

                page.className = "pdf-page";
                page.style.height =
                    (pageHeight + config.pdfPageHeightOffset) * numberOfPages +
                    "px";

                // Copy the presentation-wide background to each individual
                // page when printing
                if (presentationBackground) {
                    page.style.background = presentationBackground;
                }

                page.appendChild(slide);

                // Position the slide inside of the page
                slide.style.left = left + "px";
                slide.style.top = top + "px";
                slide.style.width = slideWidth + "px";

                this.Reveal.slideContent.layout(slide);

                if (slide.slideBackgroundElement) {
                    page.insertBefore(slide.slideBackgroundElement, slide);
                }

                // Inject notes if `showNotes` is enabled
                if (config.showNotes) {
                    // Are there notes for this slide?
                    const notes = this.Reveal.getSlideNotes(slide);
                    if (notes) {
                        const notesSpacing = 8;
                        const notesLayout =
                            typeof config.showNotes === "string"
                                ? config.showNotes
                                : "inline";
                        const notesElement = document.createElement("div");
                        notesElement.classList.add("speaker-notes");
                        notesElement.classList.add("speaker-notes-pdf");
                        notesElement.setAttribute("data-layout", notesLayout);
                        notesElement.innerHTML = notes;

                        if (notesLayout === "separate-page") {
                            pages.push(notesElement);
                        } else {
                            notesElement.style.left = notesSpacing + "px";
                            notesElement.style.bottom = notesSpacing + "px";
                            notesElement.style.width =
                                pageWidth - notesSpacing * 2 + "px";
                            page.appendChild(notesElement);
                        }
                    }
                }

                // Inject page numbers if `slideNumbers` are enabled
                if (injectPageNumbers) {
                    const numberElement = document.createElement("div");
                    numberElement.classList.add("slide-number");
                    numberElement.classList.add("slide-number-pdf");
                    numberElement.innerHTML = slideNumber++;
                    page.appendChild(numberElement);
                }

                // Copy page and show fragments one after another
                if (config.pdfSeparateFragments) {
                    // Each fragment 'group' is an array containing one or more
                    // fragments. Multiple fragments that appear at the same time
                    // are part of the same group.
                    const fragmentGroups = this.Reveal.fragments.sort(
                        page.querySelectorAll(".fragment"),
                        true,
                    );

                    let previousFragmentStep;

                    fragmentGroups.forEach(function (fragments, index) {
                        // Remove 'current-fragment' from the previous group
                        if (previousFragmentStep) {
                            previousFragmentStep.forEach(function (fragment) {
                                fragment.classList.remove("current-fragment");
                            });
                        }

                        // Show the fragments for the current index
                        fragments.forEach(function (fragment) {
                            fragment.classList.add(
                                "visible",
                                "current-fragment",
                            );
                        }, this);

                        // Create a separate page for the current fragment state
                        const clonedPage = page.cloneNode(true);

                        // Inject unique page numbers for fragments
                        if (injectPageNumbers) {
                            const numberElement =
                                clonedPage.querySelector(".slide-number-pdf");
                            const fragmentNumber = index + 1;
                            numberElement.innerHTML += "." + fragmentNumber;
                        }

                        pages.push(clonedPage);

                        previousFragmentStep = fragments;
                    }, this);

                    // Reset the first/original page so that all fragments are hidden
                    fragmentGroups.forEach(function (fragments) {
                        fragments.forEach(function (fragment) {
                            fragment.classList.remove(
                                "visible",
                                "current-fragment",
                            );
                        });
                    });
                }
                // Show all fragments
                else {
                    queryAll(page, ".fragment:not(.fade-out)").forEach(
                        function (fragment) {
                            fragment.classList.add("visible");
                        },
                    );
                }
            }
        }, this);

        await new Promise(requestAnimationFrame);

        pages.forEach((page) => pageContainer.appendChild(page));

        // Re-run JS-based content layout after the slide is added to page DOM
        this.Reveal.slideContent.layout(this.Reveal.getSlidesElement());

        // Notify subscribers that the PDF layout is good to go
        this.Reveal.dispatchEvent({ type: "pdf-ready" });

        viewportElement.classList.remove("loading-scroll-mode");
    }

    /**
     * Checks if the print mode is/should be activated.
     */
    isActive() {
        return this.Reveal.getConfig().view === "print";
    }
}
