<template>
    <div class="date-picker d-flex align-items-center">
        <button
            class="date-picker__nav-btn"
            :disabled="!canGoBack"
            aria-label="Previous dates"
            @click="goBack">
            <img
                :src="navIconSrc(canGoBack)"
                class="date-picker__nav-icon date-picker__nav-icon--flipped"
                alt="" />
        </button>
        <div class="date-picker__track d-flex flex-grow-1">
            <button
                v-for="date in visibleDates"
                :key="date.value"
                class="date-picker__day-card d-flex flex-column align-items-center justify-content-center"
                :class="{
                    'date-picker__day-card--selected': isSelected(date.value),
                    'date-picker__day-card--disabled': !date.isAvailable,
                }"
                :disabled="!date.isAvailable"
                :aria-pressed="isSelected(date.value)"
                :aria-label="`${date.dayAbbr} ${date.monthAbbr} ${date.day}`"
                @click="selectDate(date)">
                <span class="date-picker__day-abbr">{{ date.dayAbbr }}</span>
                <span class="date-picker__day-date">{{ date.monthAbbr }} {{ date.day }}</span>
            </button>
        </div>
        <button
            class="date-picker__nav-btn"
            :disabled="!canGoForward"
            aria-label="Next dates"
            @click="goForward">
            <img :src="navIconSrc(canGoForward)" class="date-picker__nav-icon" alt="" />
        </button>
    </div>
</template>

<script>
import { Breakpoints } from "@/constants/breakpoints";

const DAY_ABBRS = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
const MONTH_ABBRS = [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec",
];
const MOBILE_WINDOW_SIZE = 3;
const DESKTOP_WINDOW_SIZE = 5;
const DESKTOP_BREAKPOINT_PX = Breakpoints.MD;
const MAX_DATE_RANGE = 180;

function toLocalDateString(date) {
    const y = date.getFullYear();
    const m = String(date.getMonth() + 1).padStart(2, "0");
    const d = String(date.getDate()).padStart(2, "0");
    return `${y}-${m}-${d}`;
}

function parseLocalDate(str) {
    const [year, month, day] = str.split("-").map(Number);
    return new Date(year, month - 1, day);
}

function getWindowSize() {
    return window.innerWidth >= DESKTOP_BREAKPOINT_PX ? DESKTOP_WINDOW_SIZE : MOBILE_WINDOW_SIZE;
}

export default {
    name: "datePicker",
    props: {
        availableDates: {
            default: null,
            validator: (v) => v === null || Array.isArray(v),
        },
        startDate: {
            type: String,
            default: null,
        },
        endDate: {
            type: String,
            default: null,
        },
        modelValue: {
            type: String,
            default: null,
        },
        isLoadingDates: {
            type: Boolean,
            default: false,
        },
    },
    emits: ["update:modelValue", "requestMoreDates"],
    data() {
        return {
            windowStart: 0,
            windowSize: getWindowSize(),
            pendingAutoSelect: false,
            initialized: false,
        };
    },
    computed: {
        allDates() {
            if (this.availableDates === null) return [];

            const availableSet = new Set(this.availableDates);

            if (!this.startDate || !this.endDate) return [];

            const first = parseLocalDate(this.startDate);
            const last = parseLocalDate(this.endDate);

            const dates = [];
            const cursor = new Date(first);
            while (cursor <= last) {
                const value = toLocalDateString(cursor);
                dates.push({
                    value,
                    isAvailable: availableSet.has(value),
                    dayAbbr: DAY_ABBRS[cursor.getDay()],
                    monthAbbr: MONTH_ABBRS[cursor.getMonth()],
                    day: cursor.getDate(),
                });
                cursor.setDate(cursor.getDate() + 1);
            }
            return dates;
        },
        visibleDates() {
            return this.allDates.slice(this.windowStart, this.windowStart + this.windowSize);
        },
        canGoBack() {
            return this.windowStart > 0;
        },
        canGoForward() {
            if (this.isLoadingDates || !this.allDates.length) return false;
            const atEnd = this.windowStart + this.windowSize >= this.allDates.length;
            return !(atEnd && this.allDates.length >= MAX_DATE_RANGE);
        },
    },
    watch: {
        allDates(newDates) {
            if (!newDates.length) return;

            if (!this.initialized && !this.modelValue) {
                this.initialize();
                return;
            }
            // Pending auto select is set when the user is at the end of the dates and the next page of dates is loaded.
            if (this.pendingAutoSelect) {
                this.pendingAutoSelect = false;
                this.advanceWindowAndAutoSelect();
            }
        },
    },
    mounted() {
        this.updateWindowSize();
        window.addEventListener("resize", this.updateWindowSize);
        if (!this.allDates.length) return;
        // If a model value is set, we need to set the window start to the index of the model value.
        // Otherwise, we need to initialize the window.
        if (this.modelValue) {
            const index = this.allDates.findIndex((d) => d.value === this.modelValue);
            if (index !== -1) this.windowStart = this.getWindowStartFromIndex(index);
        } else {
            this.initialize();
        }
        this.initialized = true;
    },
    beforeUnmount() {
        window.removeEventListener("resize", this.updateWindowSize);
    },
    methods: {
        updateWindowSize() {
            this.windowSize = getWindowSize();
        },
        navIconSrc(enabled) {
            return enabled
                ? require("@/assets/img/icons/chevron-right-blue.svg")
                : require("@/assets/img/icons/chevron-right-grey.svg");
        },
        getWindowStartFromIndex(index) {
            return Math.floor(index / this.windowSize) * this.windowSize;
        },
        goBack() {
            this.windowStart = Math.max(0, this.windowStart - this.windowSize);
            this.autoSelectFirstAvailable();
        },
        goForward() {
            if (this.isLoadingDates) return;
            const isAtEnd = this.windowStart + this.windowSize >= this.allDates.length;
            if (isAtEnd) {
                this.$emit("requestMoreDates");
                this.pendingAutoSelect = true;
            } else {
                this.advanceWindowAndAutoSelect();
            }
        },
        advanceWindowAndAutoSelect() {
            const maxStart = Math.max(0, this.allDates.length - this.windowSize);
            this.windowStart = Math.min(maxStart, this.windowStart + this.windowSize);
            this.autoSelectFirstAvailable();
        },
        autoSelectFirstAvailable() {
            // Finds the first available date in the visible dates and emits the value.
            const first = this.visibleDates.find((d) => d.isAvailable);
            this.$emit("update:modelValue", first ? first.value : null);
        },
        initialize() {
            this.initialized = true;
            if (this.modelValue !== null) return;

            const firstAvailable = this.allDates.find((d) => d.isAvailable);
            if (firstAvailable) {
                // If an available date is found, set the window start ensures the first available date is visible.
                const index = this.allDates.indexOf(firstAvailable);
                this.windowStart = this.getWindowStartFromIndex(index);
                this.$emit("update:modelValue", firstAvailable.value);
            } else {
                // If no available dates are found, set the window start to the last window.
                this.windowStart = Math.max(0, this.allDates.length - this.windowSize);
            }
        },
        selectDate(date) {
            if (!date.isAvailable) return;
            this.$emit("update:modelValue", date.value);
        },
        isSelected(value) {
            return this.modelValue === value;
        },
    },
};
</script>

<style lang="scss">
.date-picker {
    gap: 0.25rem;

    &__nav-btn {
        flex-shrink: 0;
        width: 2rem;
        height: 2rem;
        border: none;
        background: transparent;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0;
        border-radius: 50%;
        transition: background 150ms ease;

        &:hover:not(:disabled) {
            background: $blue-100;
        }

        &:disabled {
            cursor: default;
        }
    }

    &__track {
        gap: 0.25rem;
    }

    &__day-card {
        flex: 1;
        min-width: 0;
        border: 1px solid $blue;
        border-radius: $border-radius-lg;
        background: $white;
        padding: 0.625rem 0.25rem;
        cursor: pointer;
        transition:
            background 150ms ease,
            border-color 150ms ease;

        &:hover:not(:disabled):not(.date-picker__day-card--selected) {
            background: $blue-100;
        }

        &--selected {
            background: $blue;
            border-color: $blue;

            .date-picker__day-abbr,
            .date-picker__day-date {
                color: $white;
            }
        }

        &--disabled {
            background: $gray-100;
            border-color: $gray-200;
            cursor: default;

            .date-picker__day-abbr,
            .date-picker__day-date {
                color: $gray-300;
            }
        }
    }

    &__day-abbr,
    &__day-date {
        font-family: $font-family-sans-serif-bold;
        font-size: $font-size-14;
        color: $blue;
    }

    &__day-abbr {
        text-transform: uppercase;
        line-height: 1.2;
    }

    &__day-date {
        line-height: 1.4;
        white-space: nowrap;
    }

    &__nav-icon {
        width: 24px;
        height: 24px;

        &--flipped {
            transform: scaleX(-1);
        }
    }
}
</style>
