<template>
    <div :class="[bem({ fit }), isEmpty ? emptyClass : '', isError ? errorClass : '']">
        <transition :name="transitionName">
            <img
                v-if="imageA"
                :key="'A'"
                :class="
                    bem('img', {
                        [imageA.state]:
                            imageA.state === IMAGE_STATES.LOADING ||
                            imageA.state === IMAGE_STATES.UNLOADING
                                ? loadingVisible
                                : true,
                    })
                "
                :src="imageA.src"
                @load="imageLoad(imageA)"
                @error="imageError($event, imageA)"
            />
        </transition>
        <transition :name="transitionName">
            <img
                v-if="imageB"
                :key="'B'"
                :class="
                    bem('img', {
                        [imageB.state]:
                            imageB.state === IMAGE_STATES.LOADING ||
                            imageB.state === IMAGE_STATES.UNLOADING
                                ? loadingVisible
                                : true,
                    })
                "
                :src="imageB.src"
                @load="imageLoad(imageB)"
                @error="imageError($event, imageB)"
            />
        </transition>

        <slot name="placeholder" v-if="isEmpty"></slot>

        <slot name="error" v-if="isError">
            <PxIcon :class="bem('error-icon')" size="1.5em" :src="AlertTriangleSvgUrl" />
        </slot>

        <slot name="loading" v-if="loadingVisible">
            <PxSpinner v-if="showLoadingSpinner" :visible="true" />
        </slot>
    </div>
</template>

<script>
import PxSpinner from './PxSpinner.vue';
import PxIcon from './PxIcon.vue';
import AlertTriangleSvgUrl from '../assets/feather/alert-triangle.svg?url';

/**

An image component that can be used to easily access advanced functionality for displaying images.

Current functionality includes:

- Monitory image load state and displaying a loading indicator
- Cusotmization (via css) of a background for the empty state
- Fade in on load
- Crossfade between images when source changes
- Customizable error state
- Handles background-like layout scenarios with cover and contain natively

Future functionality:

- Customization of previous (unloaded) image state when source changes (via class)
- srcset (with img tag) / image-set (with background)
- Prioritized / depriotized loading (relative to other images)
- Only load on active state (via active property) or when in viewport
- Deal with natural block layout (that allows the container to grow with the image data)

IMPLEMENTATION NOTE:

We ended up with the two image solution after trying a number of different approaches. The
primary advantage of this approach is that it keeps image data and loading attached to an img
elmement in the DOM which seems to simplify a lot of corner conditions like cancelling loads if the
src changes partway through loading, etc. and issues with flickering when loading large image files.
Mentioning here so that future me can avoid trying to "optimize" this later and making the same
mistakes.

 */

const IMAGE_STATES = {
    // inactive so it won't be rendered
    INACTIVE: 'inactive',

    // src has set but image isn't loaded
    LOADING: 'loading',

    // image has loaded with valid source
    LOADED: 'loaded',

    // image is unloading (previous image)
    UNLOADING: 'unloading',
};

export default {
    name: 'px-image',
    components: { PxSpinner, PxIcon },
    // mixins: [ Mixin ],
    props: {
        /**
         * The image source.
         */
        src: { type: String, default: null },

        /**
         * The fit property specifies how the image should be sized to fit its container.
         */
        fit: { type: String, default: 'contain' },

        /**
         * Class that gets applied to the component if there is no current src (typically the
         * initial state). Default behaviorOverridden is to set the background to a semi-transparent
         * gray.
         */
        emptyClass: { type: [String, Array, Object], default: 'px-image--empty' },

        /**
         * Class that gets applied to the component if there is an error state. Default
         * behaviorOverridden is to set the background to a semi-transparent gray like the empty
         * state (and show the default error slot but that is controlled by the slot, not this
         * prop).
         */
        errorClass: { type: [String, Array, Object], default: 'px-image--error' },

        /**
         * When true, we'll restore an "unloading" image when possible if the new image fails to
         * load. In this case, we'll continue to raise an event but we won't show the error state or
         * set the error class.
         */
        retorePreviousOnError: { type: Boolean, default: false },

        // /**
        //  * Class that gets applied to the component when an image is loading. No default.
        //  */
        // loadingClass: { type: [String, Array, Object], default: null },

        // /**
        //  * Class that gets applied to all image elements. The default sets the opacity transition.
        //  */
        // imageClass: { type: [String, Array, Object], default: 'px-image__image' },

        // /**
        //  * Class that gets applied to all image elements while loading. Default set opacity to 0.
        //  */
        // imageLoadingClass: { type: [String, Array, Object], default: 'px-image__image--loading' },

        // /**
        //  * Class that gets applied to all image elements while unloading. Default sets opacity to 0.5.
        //  */
        // imageUnloadingClass: {
        //     type: [String, Array, Object],
        //     default: 'px-image__image--unloading',
        // },

        /**
         * A delay (in seconds) before showing loading state to avoid flickering when loading is fast.
         */
        loadingVisibleDelay: { type: Number, default: 0.5 },

        /**
         * Whether to show a loading spinner when loading in the default loading slot.
         */
        showLoadingSpinner: { type: Boolean, default: true },

        /**
         * Transition that gets applied when the src changes (we will generate multiple img elements
         * in this case and apply this transition between them, leave as null to use the default
         * which will fade.)
         */
        transitionName: { type: String, default: 'px-image__fade' },

        /**
         * Duration of the transition in seconds.
         */
        transitionDuration: { type: Number, default: 0.5 },
    },
    data() {
        const image = {
            state: IMAGE_STATES.INACTIVE,
            src: null,
        };

        return {
            // we manage the data for two images of these (one for the loaded image and one for the
            // next image so we can fade on load) but only render as needed
            images: [{ ...image }, { ...image }],

            // reference to the last loading error if there is one (otherwise null)
            error: null,

            // whether to show the loading state (baed on loadingVisibleDelay)
            loadingVisible: false,

            // so we can access from the template
            IMAGE_STATES,
            AlertTriangleSvgUrl,
        };
    },
    computed: {
        imageA() {
            return this.images[0]?.state !== IMAGE_STATES.INACTIVE ? this.images[0] : null;
        },
        imageB() {
            return this.images[1]?.state !== IMAGE_STATES.INACTIVE ? this.images[1] : null;
        },
        isError() {
            return !!this.error;
        },
        isEmpty() {
            return !this.isError && !this.imageA?.src && !this.imageB?.src;
        },
        isLoading() {
            return !this.isError && this.images.some(i => i.state === IMAGE_STATES.LOADING);
        },
        cssTransitionDuration() {
            return this.transitionDuration + 's';
        },
    },
    watch: {
        src: {
            immediate: true,
            handler(nv) {
                // clear previous errors
                this.error = null;

                // trim src (just in case but also to prevent false positives on empty string)
                nv = typeof nv === 'string' ? nv.trim() : null;

                if (!nv) {
                    // if we have no src, just mark both images as inactive
                    this.images.forEach(i => (i.state = IMAGE_STATES.INACTIVE));
                    return;
                }

                // find an an inactive, unloaded or loading image to update (priotize loading so we don't end
                // up with two loading)

                const nextImage =
                    this.images.find(i => i.state === IMAGE_STATES.LOADING) ||
                    this.images.find(i => i.state === IMAGE_STATES.INACTIVE);

                if (nextImage) {
                    nextImage.src = nv;
                    nextImage.state = IMAGE_STATES.LOADING;
                } else {
                    console.warn('No image available to update');
                    return;
                }

                // if we have a loaded image, mark it as unloading
                const prevImage = this.images.find(i => i.state === IMAGE_STATES.LOADED);

                if (prevImage) {
                    prevImage.state = IMAGE_STATES.UNLOADING;
                }

                //image.state = IMAGE_STATES.LOADING;
            },
        },
        isLoading(nv) {
            clearTimeout(this.loadingTimeout);

            if (!nv) {
                this.loadingVisible = false;
                return;
            }

            this.loadingTimeout = setTimeout(() => {
                this.loadingVisible = this.isLoading;
            }, this.loadingVisibleDelay * 1000);
        },
    },
    mounted() {
        this.nextId = 0;
        this.invalidIds = [];
    },
    methods: {
        resetImages() {
            this.images[0].state = IMAGE_STATES.INACTIVE;
            this.images[0].src = null;
            this.images[1].state = IMAGE_STATES.INACTIVE;
            this.images[1].src = null;
        },
        imageLoad(image) {
            this.$emit('load', image.src);

            // mark the current image as loaded
            image.state = IMAGE_STATES.LOADED;

            // mark the previous image as inactive so we can hide remove it from the DOM
            const prevImage = this.images.find(i => i.state === IMAGE_STATES.UNLOADING);
            if (prevImage) {
                prevImage.state = IMAGE_STATES.INACTIVE;
            }

            // we shouldn't have only have loaded or inactive images at this point
            const loadingImage = this.images.find(
                i => !(i.state == IMAGE_STATES.LOADED || i.state == IMAGE_STATES.INACTIVE),
            );
            if (loadingImage) {
                console.warn('Loading image not properly cleared');
            }
        },
        imageError(e, image) {
            const error = { event: e, image };
            this.$emit('error', error);

            image.state = IMAGE_STATES.INACTIVE;

            // try to restore a previous image if we have one and retorePreviousOnError is true
            if (this.retorePreviousOnError) {
                const prevImage = this.images.find(i => i.state === IMAGE_STATES.UNLOADING);
                if (prevImage) {
                    prevImage.state = IMAGE_STATES.LOADED;
                    prevImage.src = image.src;
                    return;
                }
            }

            // otherwise, set the error and reset images
            this.error = error;
            this.resetImages();
        },
    },
};
</script>

<style lang="scss">
@use '../styles/px.scss' as *;

$ease: $ease-out-cubic;
$dur: v-bind(cssTransitionDuration);
$name: px-image__fade;

.px-image {
    width: 100%;
    height: 100%;
    position: relative;

    // so that placeholder and loading are centered
    display: flex;
    align-items: center;
    justify-content: center;

    // for the error and empty states
    @include transition(background-color);

    img {
        @include abs-fill();
        object-position: center center;
    }

    &--fit-cover {
        img {
            object-fit: cover;
        }
    }

    &--fit-contain {
        img {
            object-fit: contain;
        }
    }

    &--error,
    &--empty {
        background-color: rgba(#444, 0.1);
    }

    &__img {
        transition: opacity $dur $ease;

        // default transition is to fade

        &.#{$name}-enter,
        &.#{$name}-enter-from,
        &.#{$name}-leave-to {
            opacity: 0;
        }

        &--loading {
            visibility: hidden;
            opacity: 0 !important;
        }

        &--unloading {
            opacity: 0.5;
        }
    }
}
</style>
