<!--
    #VUE3 Todo

    Keeps a child element contained within boundaries.

    <px-contain #default="{width, height, left, top }" :aspect="16/9" :round="true" :to-px="true">
        <div :style="{position: 'absolute', left: left top: top,  width: width, height: height}"  />
    </px-contain>

-->

<template>
    <div style="width: 100%; height: 100%; position: relative">
        <slot
            name="default"
            :top="contain.top"
            :right="contain.right"
            :left="contain.left"
            :bottom="contain.bottom"
            :width="contain.width"
            :height="contain.height"
            :scale="contain.scale"
            :enabled="contain.enabled"
        ></slot>
    </div>
</template>

<script>
import { contain } from '../utils/fit';
import aspectFilter from '../plugins/filters/aspect.js';
import PxBaseResize from '../bases/PxBaseResize.vue';

export default {
    name: 'px-contain',
    mixins: [PxBaseResize],

    props: {
        /**
         * Whether the contain functionality should be enabled
         */
        enabled: { type: Boolean, default: true },

        /**
         * An optional aspect ratio that should be maintained when calculating size and position, if
         * not provided then width and height will always cover the full outer container (can be a
         * number or ratio string like '16:9')
         */
        aspect: { type: [Number, String], default: null },

        /**
         * The natural width of the slot content. If this is provided, then slot values will also
         * include a value for scale which is the relative scale of the container compared to the
         * natural size of the object.
         */
        naturalWidth: { type: Number, default: null },

        /**
         * The natural height of the slot content. If this is provided, then slot values will also
         * include a value for scale which is the relative scale of the container compared to the
         * natural size of the object.
         */
        naturalHeight: { type: Number, default: null },

        // these are adjustments to the available area inside of the container rectangle; the
        // difference between padding and border is that padding values will also affect the final
        // top/left values but border does not (to visualize this, imagine that you use border if
        // the content has a visible border around and the final values reflect that where padding
        // is empty space so palcement gets shfited); specify as either a number or an arry of up to
        // 4 numbers that follow the css box-model pattern
        padding: { default: null },
        border: { default: null },

        /** Maximum width of the container rectangle */
        maxWidth: { type: Number, default: null },

        /** Maximum height of the container rectangle */
        maxHeight: { type: Number, default: null },

        /**
         * The horizontal positioning when there is leftover space.
         * @values start, center, end, (any number between 0 and 1)
         */
        alignX: { default: 0.5 },

        /**
         * The vertical positioning when there is leftover space.
         * @values start, center, end, (any number between 0 and 1)
         */
        alignY: { default: 0.5 },

        /**
         * Whether slot values should be returned as strings with 'px' (this can make styling easier)
         */
        toPx: { type: Boolean, default: true },

        /**
         * Whether slot values should be rounded to screen pixels */
        round: { type: Boolean, default: true },
    },
    data() {
        return {
            // use setContainer to use a container source other than this.$el
            container: null,

            contain: {
                enabled: this.enabled,
                current: false,
                width: null,
                height: null,
                top: null,
                right: null,
                bottom: null,
                left: null,
                scale: 1,
            },
        };
    },
    computed: {
        aspectNumber() {
            return aspectFilter(this.aspect);
        },
        aspectRect() {
            let a = this.aspect ? this.aspectNumber : null;
            let w = this.naturalWidth;
            let h = this.naturalHeight;

            // width and height (ignore aspect)
            if (w && h) {
                return {
                    width: w,
                    height: h,
                };
            }

            // just aspect (no width or height)
            if (!w && !h && a) {
                return {
                    width: a,
                    height: 1,
                };
            }

            // width and aspect, infer height
            if (w && a && !h) {
                return {
                    width: w,
                    height: w * (1 / a),
                };
            }

            // height and aspect, infer width
            if (w && a && !h) {
                return {
                    width: h * a,
                    height: h,
                };
            }

            // no width or height or aspect
            return null;
        },
        containerValue() {
            return this.container || this.$el;
        },
    },

    watch: {
        enabled() {
            this.resize();
        },
        aspectNumber() {
            this.resize();
        },
        alignX() {
            this.resize();
        },
        alignY() {
            this.resize();
        },
        contain(nv) {
            this.$emit('update', nv);
        },
    },
    methods: {
        // Use a source for the outer container size other than this.$el. The container argument can
        // be an element or any object that implements getBoundingClientRect; setting a custom
        // container may also require a direct call to resize when the container size updates (since
        // the default resize observer behavior only observes this.$el)
        setContainer(container) {
            this.container = container;
            this.resize();
        },
        resize() {
            if (!this.enabled) {
                this.contain = {
                    enabled: false,
                    current: true,
                    width: null,
                    height: null,
                    top: null,
                    right: null,
                    bottom: null,
                    left: null,
                    scale: 1,
                };

                return;
            }
            let p = this.toBoxModelObject(this.padding);
            let b = this.toBoxModelObject(this.border);

            let outer = this.containerValue.getBoundingClientRect();
            let inner = this.aspectRect || outer;

            outer = {
                width: Math.max(0, outer.width - (b.w + p.w)),
                height: Math.max(0, outer.height - (b.h + p.h)),
            };

            // max width/height

            if (this.maxHeight && outer.height > this.maxHeight) {
                outer.y = (outer.height - this.maxHeight) * this.alignToDecimal(this.alignY);
                outer.height = this.maxHeight;
            }

            if (this.maxWidth && outer.width > this.maxWidth) {
                outer.x = (outer.width - this.maxWidth) * this.alignToDecimal(this.alignX);
                outer.width = this.maxWidth;
            }

            // get contain

            let r;

            if (this.aspect || (this.naturalWidth && this.naturalHeight)) {
                r = contain(
                    inner,
                    outer,
                    this.alignToDecimal(this.alignX),
                    this.alignToDecimal(this.alignY),
                );
            } else {
                r = {
                    x: 0,
                    y: 0,
                    width: outer.width,
                    height: outer.height,
                };
            }

            let c = {
                current: true,
                enabled: true,
                width: r.width + b.w,
                height: r.height + b.h,
                top: r.y + p.top,
                left: r.x + p.left,
                right: r.width - (r.x + b.w),
                bottom: r.height - (r.y + b.h),
                scale: this.naturalWidth || this.naturalHeight ? r.scale : 1,
            };

            c.width = this.prepValue(c.width);
            c.height = this.prepValue(c.height);
            c.left = this.prepValue(c.left);
            c.top = this.prepValue(c.top);
            c.right = this.prepValue(c.right);
            c.bottom = this.prepValue(c.bottom);

            this.contain = c;
        },

        prepValue(n) {
            if (this.round && typeof window != 'undefined') {
                n = Math.round(n * window.devicePixelRatio) / window.devicePixelRatio;
            }
            if (this.toPx) {
                n = this.cssPx(n);
            }
            return n;
        },

        toBoxModelObject(val) {
            const arr = this.toBoxModelArray(val);
            return {
                top: arr[0],
                right: arr[1],
                bottom: arr[2],
                left: arr[3],
                w: arr[1] + arr[3],
                h: arr[0] + arr[2],
            };
        },

        toBoxModelArray(val) {
            if (!val) {
                return [0, 0, 0, 0];
            }

            if (Array.isArray(val)) {
                if (val.length == 0) {
                    return [0, 0, 0, 0];
                }
                if (val.length == 1) {
                    return [val[0], val[0], val[0], val[0]];
                }
                if (val.length == 2) {
                    return [val[0], val[1], val[0], val[1]];
                }
                if (val.length == 3) {
                    return [val[0], val[1], val[2], val[1]];
                }
                return [val[0], val[1], val[2], val[3]];
            }

            return [val, val, val, val];
        },

        alignToDecimal(align) {
            if (typeof align == 'number') {
                return align;
            }
            if (align === 'center') {
                return 0.5;
            }
            if (align === 'start') {
                return 0;
            }
            if (align === 'end') {
                return 1;
            }
            const float = parseFloat(align);
            return !isNaN(float) ? float : 0.5;
        },
    },
};
</script>
