<template>
    <div :class="bem({ debug: debug, fitting: fitting })">
        <div ref="outer" :class="bem('outer')" :style="{ opacity: contentOpacity }">
            <slot
                name="default"
                v-bind="{
                    fit: fitting ? fit : selectedFit,
                    fitting: fitting,
                }"
            />
        </div>
    </div>
</template>

<script>
import PxBaseResize from '../bases/PxBaseResize.vue';
import { asyncTimeout } from '../utils/utils.js';

/**
 * A flexibile container that allows you to choose different possible renderings based on the size
 * of the container. You provide a collection of "fits" (essentially just tags). Each tag is
 * provided to the default slot template and the template can determine how to render based on that
 * tag. Once rendered, the content is measured and compared against the size of the parent and the
 * first that "fits" within the bounds of the parent is selected.
 *
 * ```html
 * <PxFit>
 *     <template #default="{ fit }">
 *         <div >
 *             <div v-if="fit == 'lg'" style="width: 600px">lg (600px)</div>
 *             <div v-if="fit == 'md'" style="width: 400px">md (400px)</div>
 *             <div v-if="fit == 'sm'" style="width: 200px">sm (200px)</div>
 *         </div>
 *     </template>
 * </PxFit>
 * ```
 */

export default {
    name: 'px-fit',
    mixins: [PxBaseResize],
    props: {
        /**
         * A list of named "fits" that will be applied during resize. These have no intrinsic size
         * or meaning. Instead it is up to the template to use these to modify the size of the
         * content. PxFit will apply each named fit from largest to smallest (by default in reverse
         * order of what is listed) and keep the first that fits within the bounds of the element.
         *
         */
        fits: { type: Array, default: () => ['sm', 'md', 'lg'] },

        /**
         * By default, fits will be tested in reverse order since it's natural to list them from
         * smallest to largest but they oftend need to be tested largest first (assuming we want to
         * show as much content as possible). To disable this behavior, set this to false.
         */
        reverseFitTestOrder: { type: Boolean, default: true },

        /**
         * By default the last fit attempted will be kept when no fit is found, but you can set this
         * to specific a different fallback. Note that fallback does not need to be a valid fit from
         * the fits collection, so you could set it to something like 'none' and use that to flag
         * that no fit was found and you need to render an alternative.
         */
        fallback: { type: String, default: null },

        /**
         * Whether to evaluate width, height or both when looking for a fit
         */
        mode: { type: String, default: 'both' },

        /**
         * Set this to something other than 0 to debounce resize events.
         */
        resizeDebounce: { type: Number, default: 0 },

        /**
         * If true, we set the opacity of the content to 0 while fitting is in progress. Doesn't
         * seem like this is necessary is most cases.
         */
        hideFitTests: { type: Boolean, default: true },

        /**
         * Use this to override the function that evaluates whether a particular fit is valid. Takes
         * the current mode, available size, content size (with fit applied) and fit as arguments
         * and should return a boolean.
         */
        fitTestFn: { default: null },

        /**
         * Disables resize events (and fitting) when true
         */
        disabled: { type: Boolean, default: false },

        /**
         * Whether to show the debug version of the component (allows you to see the component in
         * each fit state as it is evaluated).
         */
        debug: { type: Boolean, default: false },
    },

    data() {
        return {
            fit: null,
            selectedFit: null,
            fitting: false,
            resizeId: 0,
        };
    },

    computed: {
        resizeDebounceValue() {
            return this.resizeDebounce;
        },

        contentOpacity() {
            if (this.hideFitTests && this.fitting) {
                return this.debug ? 0.75 : 0;
            }
            return null;
        },
    },

    watch: {
        selectedFit(nv) {
            this.$emit('fit-changed', nv);
        },
    },

    methods: {
        async wait() {
            await this.$nextTick();
            if (this.debug) {
                await asyncTimeout(1000);
            }
        },

        testFitDefault(mode, availableSize, contentSize, fit) {
            if (!availableSize || !contentSize || availableSize.w == 0 || availableSize.h == 0) {
                return false;
            }

            let doesFit = true;

            if (mode == 'width' || mode == 'both') {
                doesFit = doesFit && availableSize.w >= contentSize.w;
            }

            if (mode == 'height' || mode == 'both') {
                doesFit = doesFit && availableSize.h >= contentSize.h;
            }

            return doesFit;
        },

        async tryFits(id) {
            const availableSize = {
                w: this.$el.clientWidth,
                h: this.$el.clientHeight,
            };

            this.fitting = true;

            const $outer = this.$refs.outer;
            const orderedFits = this.reverseFitTestOrder ? this.fits.slice().reverse() : this.fits;
            const testFn = this.fitTestFn || this.testFitDefault;

            let fit;
            let selectedFit;

            for (let i = 0; i < orderedFits.length; i++) {
                // we can abort the test if another has been started
                if (id != this.resizeId) {
                    return;
                }

                fit = orderedFits[i];
                this.fit = fit;

                await this.wait();

                // need to test his again after the wait
                if (id != this.resizeId) {
                    return;
                }

                const contentSize = {
                    w: $outer.offsetWidth,
                    h: $outer.offsetHeight,
                };

                const doesFit = testFn(this.mode, availableSize, contentSize, fit);

                if (this.debug) {
                    console.log(`----- ${id} Test ${this.fit} -----`);
                    console.log(`availableSize: ${availableSize.w} x ${availableSize.h}`);
                    console.log(`contentSize: ${contentSize.w} x ${contentSize.h}`);
                    console.log(`doesFit: ${doesFit}`);
                }

                if (doesFit) {
                    selectedFit = fit;
                    break;
                }
            }

            this.selectedFit = selectedFit ? selectedFit : this.fallback || fit;
            this.fitting = false;
        },

        async resize() {
            if (!this.disabled) {
                this.$emit('resize-start');

                this.resizeId++;

                if (this.debug) {
                    console.log(`===== RESIZE ${this.resizeId} =====`);
                }

                await this.tryFits(this.resizeId);

                this.$emit('resize-end');
            } else {
                this.selectedFit = this.fallback;
            }
        },
    },
};
</script>

<style lang="scss">
@use '../styles/px.scss' as *;
.px-fit {
    position: relative;
    width: 100%;
    height: 100%;
    display: grid;
    grid-template-columns: 100%;
    grid-template-rows: 100%;

    &__outer {
        position: absolute;
        left: 0;
        width: 0;
        height: 100%;
        width: 100%;
        display: grid;
        grid-template-columns: 100%;
        grid-template-rows: 100%;
        > * {
            grid-area: 1 / 1;
        }

        & {
            @include transition(opacity, $dur: 100ms);
        }
    }

    &--fitting & {
        width: 100vh;
        &__outer {
            position: fixed;
            overflow: visible;
            top: 0;
            left: 0;
            width: auto;
            height: auto;
            pointer-events: none;
            //opacity: 0;
        }
    }

    &--debug.px-fit--fitting & {
        &__outer {
            opacity: 1;
            background-color: white;
        }
    }
}
</style>
