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

/**
 * Handles all logic related to the overview mode
 * (birds-eye view of all slides).
 */
export default class Overview {
    constructor(Reveal) {
        this.Reveal = Reveal;

        this.active = false;

        this.onSlideClicked = this.onSlideClicked.bind(this);
    }

    /**
     * Displays the overview of slides (quick nav) by scaling
     * down and arranging all slide elements.
     */
    activate() {
        // Only proceed if enabled in config
        if (
            this.Reveal.getConfig().overview &&
            !this.Reveal.isScrollView() &&
            !this.isActive()
        ) {
            this.active = true;

            this.Reveal.getRevealElement().classList.add("overview");

            // Don't auto-slide while in overview mode
            this.Reveal.cancelAutoSlide();

            // Move the backgrounds element into the slide container to
            // that the same scaling is applied
            this.Reveal.getSlidesElement().appendChild(
                this.Reveal.getBackgroundsElement(),
            );

            // Clicking on an overview slide navigates to it
            queryAll(this.Reveal.getRevealElement(), SLIDES_SELECTOR).forEach(
                (slide) => {
                    if (!slide.classList.contains("stack")) {
                        slide.addEventListener(
                            "click",
                            this.onSlideClicked,
                            true,
                        );
                    }
                },
            );

            // Calculate slide sizes
            const margin = 70;
            const slideSize = this.Reveal.getComputedSlideSize();
            this.overviewSlideWidth = slideSize.width + margin;
            this.overviewSlideHeight = slideSize.height + margin;

            // Reverse in RTL mode
            if (this.Reveal.getConfig().rtl) {
                this.overviewSlideWidth = -this.overviewSlideWidth;
            }

            this.Reveal.updateSlidesVisibility();

            this.layout();
            this.update();

            this.Reveal.layout();

            const indices = this.Reveal.getIndices();

            // Notify observers of the overview showing
            this.Reveal.dispatchEvent({
                type: "overviewshown",
                data: {
                    indexh: indices.h,
                    indexv: indices.v,
                    currentSlide: this.Reveal.getCurrentSlide(),
                },
            });
        }
    }

    /**
     * Uses CSS transforms to position all slides in a grid for
     * display inside of the overview mode.
     */
    layout() {
        // Layout slides
        this.Reveal.getHorizontalSlides().forEach((hslide, h) => {
            hslide.setAttribute("data-index-h", h);
            transformElement(
                hslide,
                "translate3d(" + h * this.overviewSlideWidth + "px, 0, 0)",
            );

            if (hslide.classList.contains("stack")) {
                queryAll(hslide, "section").forEach((vslide, v) => {
                    vslide.setAttribute("data-index-h", h);
                    vslide.setAttribute("data-index-v", v);

                    transformElement(
                        vslide,
                        "translate3d(0, " +
                            v * this.overviewSlideHeight +
                            "px, 0)",
                    );
                });
            }
        });

        // Layout slide backgrounds
        Array.from(this.Reveal.getBackgroundsElement().childNodes).forEach(
            (hbackground, h) => {
                transformElement(
                    hbackground,
                    "translate3d(" + h * this.overviewSlideWidth + "px, 0, 0)",
                );

                queryAll(hbackground, ".slide-background").forEach(
                    (vbackground, v) => {
                        transformElement(
                            vbackground,
                            "translate3d(0, " +
                                v * this.overviewSlideHeight +
                                "px, 0)",
                        );
                    },
                );
            },
        );
    }

    /**
     * Moves the overview viewport to the current slides.
     * Called each time the current slide changes.
     */
    update() {
        const vmin = Math.min(window.innerWidth, window.innerHeight);
        const scale = Math.max(vmin / 5, 150) / vmin;
        const indices = this.Reveal.getIndices();

        this.Reveal.transformSlides({
            overview: [
                "scale(" + scale + ")",
                "translateX(" + -indices.h * this.overviewSlideWidth + "px)",
                "translateY(" + -indices.v * this.overviewSlideHeight + "px)",
            ].join(" "),
        });
    }

    /**
     * Exits the slide overview and enters the currently
     * active slide.
     */
    deactivate() {
        // Only proceed if enabled in config
        if (this.Reveal.getConfig().overview) {
            this.active = false;

            this.Reveal.getRevealElement().classList.remove("overview");

            // Temporarily add a class so that transitions can do different things
            // depending on whether they are exiting/entering overview, or just
            // moving from slide to slide
            this.Reveal.getRevealElement().classList.add(
                "overview-deactivating",
            );

            setTimeout(() => {
                this.Reveal.getRevealElement().classList.remove(
                    "overview-deactivating",
                );
            }, 1);

            // Move the background element back out
            this.Reveal.getRevealElement().appendChild(
                this.Reveal.getBackgroundsElement(),
            );

            // Clean up changes made to slides
            queryAll(this.Reveal.getRevealElement(), SLIDES_SELECTOR).forEach(
                (slide) => {
                    transformElement(slide, "");

                    slide.removeEventListener(
                        "click",
                        this.onSlideClicked,
                        true,
                    );
                },
            );

            // Clean up changes made to backgrounds
            queryAll(
                this.Reveal.getBackgroundsElement(),
                ".slide-background",
            ).forEach((background) => {
                transformElement(background, "");
            });

            this.Reveal.transformSlides({ overview: "" });

            const indices = this.Reveal.getIndices();

            this.Reveal.slide(indices.h, indices.v);
            this.Reveal.layout();
            this.Reveal.cueAutoSlide();

            // Notify observers of the overview hiding
            this.Reveal.dispatchEvent({
                type: "overviewhidden",
                data: {
                    indexh: indices.h,
                    indexv: indices.v,
                    currentSlide: this.Reveal.getCurrentSlide(),
                },
            });
        }
    }

    /**
     * Toggles the slide overview mode on and off.
     *
     * @param {Boolean} [override] Flag which overrides the
     * toggle logic and forcibly sets the desired state. True means
     * overview is open, false means it's closed.
     */
    toggle(override) {
        if (typeof override === "boolean") {
            override ? this.activate() : this.deactivate();
        } else {
            this.isActive() ? this.deactivate() : this.activate();
        }
    }

    /**
     * Checks if the overview is currently active.
     *
     * @return {Boolean} true if the overview is active,
     * false otherwise
     */
    isActive() {
        return this.active;
    }

    /**
     * Invoked when a slide is and we're in the overview.
     *
     * @param {object} event
     */
    onSlideClicked(event) {
        if (this.isActive()) {
            event.preventDefault();

            let element = event.target;

            while (element && !element.nodeName.match(/section/gi)) {
                element = element.parentNode;
            }

            if (element && !element.classList.contains("disabled")) {
                this.deactivate();

                if (element.nodeName.match(/section/gi)) {
                    let h = parseInt(element.getAttribute("data-index-h"), 10),
                        v = parseInt(element.getAttribute("data-index-v"), 10);

                    this.Reveal.slide(h, v);
                }
            }
        }
    }
}
