import { extend } from '../../vue';
import { NAME_CALENDAR } from '../../constants/components';
import { DATE_FORMAT_NUMERIC, CALENDAR_LONG, CALENDAR_SHORT, CALENDAR_NARROW, CALENDAR_GREGORY, DATE_FORMAT_2_DIGIT } from '../../constants/date';
import { EVENT_NAME_CONTEXT, EVENT_NAME_SELECTED } from '../../constants/events';
import { CODE_PAGEUP, CODE_PAGEDOWN, CODE_END, CODE_HOME, CODE_LEFT, CODE_UP, CODE_RIGHT, CODE_DOWN, CODE_ENTER, CODE_SPACE } from '../../constants/key-codes';
import { PROP_TYPE_DATE_STRING, PROP_TYPE_STRING, PROP_TYPE_BOOLEAN, PROP_TYPE_FUNCTION, PROP_TYPE_OBJECT, PROP_TYPE_ARRAY_STRING, PROP_TYPE_NUMBER_STRING } from '../../constants/props';
import { SLOT_NAME_NAV_PEV_DECADE, SLOT_NAME_NAV_PEV_YEAR, SLOT_NAME_NAV_PEV_MONTH, SLOT_NAME_NAV_THIS_MONTH, SLOT_NAME_NAV_NEXT_MONTH, SLOT_NAME_NAV_NEXT_YEAR, SLOT_NAME_NAV_NEXT_DECADE } from '../../constants/slots';
import { arrayIncludes, concat } from '../../utils/array';
import { formatYMD, constrainDate, parseYMD, resolveLocale, createDate, createDateFormatter, lastDateOfMonth, oneDecadeAgo, oneYearAgo, oneMonthAgo, firstDateOfMonth, oneMonthAhead, oneYearAhead, oneDecadeAhead, datesEqual } from '../../utils/date';
import { attemptFocus, attemptBlur, requestAF } from '../../utils/dom';
import { stopEvent } from '../../utils/events';
import { identity } from '../../utils/identity';
import { isString, isArray, isPlainObject } from '../../utils/inspect';
import { isLocaleRTL } from '../../utils/locale';
import { looseEqual } from '../../utils/loose-equal';
import { mathMax } from '../../utils/math';
import { makeModelMixin } from '../../utils/model';
import { toInteger } from '../../utils/number';
import { sortKeys } from '../../utils/object';
import { makePropsConfigurable, makeProp, hasPropFunction } from '../../utils/props';
import { toString } from '../../utils/string';
import { attrsMixin } from '../../mixins/attrs';
import { props as props$1, idMixin } from '../../mixins/id';
import { normalizeSlotMixin } from '../../mixins/normalize-slot';
import { BIconChevronBarLeft, BIconChevronDoubleLeft, BIconChevronLeft, BIconCircleFill } from '../../icons/icons';

// --- Constants ---

const {
  mixin: modelMixin,
  props: modelProps,
  prop: MODEL_PROP_NAME,
  event: MODEL_EVENT_NAME
} = makeModelMixin('value', {
  type: PROP_TYPE_DATE_STRING
});

// --- Props ---

const props = makePropsConfigurable(sortKeys({
  ...props$1,
  ...modelProps,
  ariaControls: makeProp(PROP_TYPE_STRING),
  // Makes calendar the full width of its parent container
  block: makeProp(PROP_TYPE_BOOLEAN, false),
  dateDisabledFn: makeProp(PROP_TYPE_FUNCTION),
  // `Intl.DateTimeFormat` object
  dateFormatOptions: makeProp(PROP_TYPE_OBJECT, {
    year: DATE_FORMAT_NUMERIC,
    month: CALENDAR_LONG,
    day: DATE_FORMAT_NUMERIC,
    weekday: CALENDAR_LONG
  }),
  // Function to set a class of (classes) on the date cell
  // if passed a string or an array
  // TODO:
  //   If the function returns an object, look for class prop for classes,
  //   and other props for handling events/details/descriptions
  dateInfoFn: makeProp(PROP_TYPE_FUNCTION),
  // 'ltr', 'rtl', or `null` (for auto detect)
  direction: makeProp(PROP_TYPE_STRING),
  disabled: makeProp(PROP_TYPE_BOOLEAN, false),
  headerTag: makeProp(PROP_TYPE_STRING, 'header'),
  // When `true`, renders a comment node, but keeps the component instance active
  // Mainly for <b-form-date>, so that we can get the component's value and locale
  // But we might just use separate date formatters, using the resolved locale
  // (adjusted for the gregorian calendar)
  hidden: makeProp(PROP_TYPE_BOOLEAN, false),
  // When `true` makes the selected date header `sr-only`
  hideHeader: makeProp(PROP_TYPE_BOOLEAN, false),
  // This specifies the calendar year/month/day that will be shown when
  // first opening the datepicker if no v-model value is provided
  // Default is the current date (or `min`/`max`)
  initialDate: makeProp(PROP_TYPE_DATE_STRING),
  // Labels for buttons and keyboard shortcuts
  labelCalendar: makeProp(PROP_TYPE_STRING, 'Calendar'),
  labelCurrentMonth: makeProp(PROP_TYPE_STRING, 'Current month'),
  labelHelp: makeProp(PROP_TYPE_STRING, 'Use cursor keys to navigate calendar dates'),
  labelNav: makeProp(PROP_TYPE_STRING, 'Calendar navigation'),
  labelNextDecade: makeProp(PROP_TYPE_STRING, 'Next decade'),
  labelNextMonth: makeProp(PROP_TYPE_STRING, 'Next month'),
  labelNextYear: makeProp(PROP_TYPE_STRING, 'Next year'),
  labelNoDateSelected: makeProp(PROP_TYPE_STRING, 'No date selected'),
  labelPrevDecade: makeProp(PROP_TYPE_STRING, 'Previous decade'),
  labelPrevMonth: makeProp(PROP_TYPE_STRING, 'Previous month'),
  labelPrevYear: makeProp(PROP_TYPE_STRING, 'Previous year'),
  labelSelected: makeProp(PROP_TYPE_STRING, 'Selected date'),
  labelToday: makeProp(PROP_TYPE_STRING, 'Today'),
  // Locale(s) to use
  // Default is to use page/browser default setting
  locale: makeProp(PROP_TYPE_ARRAY_STRING),
  max: makeProp(PROP_TYPE_DATE_STRING),
  min: makeProp(PROP_TYPE_DATE_STRING),
  // Variant color to use for the navigation buttons
  navButtonVariant: makeProp(PROP_TYPE_STRING, 'secondary'),
  // Disable highlighting today's date
  noHighlightToday: makeProp(PROP_TYPE_BOOLEAN, false),
  noKeyNav: makeProp(PROP_TYPE_BOOLEAN, false),
  readonly: makeProp(PROP_TYPE_BOOLEAN, false),
  roleDescription: makeProp(PROP_TYPE_STRING),
  // Variant color to use for the selected date
  selectedVariant: makeProp(PROP_TYPE_STRING, 'primary'),
  // When `true` enables the decade navigation buttons
  showDecadeNav: makeProp(PROP_TYPE_BOOLEAN, false),
  // Day of week to start calendar on
  // `0` (Sunday), `1` (Monday), ... `6` (Saturday)
  startWeekday: makeProp(PROP_TYPE_NUMBER_STRING, 0),
  // Variant color to use for today's date (defaults to `selectedVariant`)
  todayVariant: makeProp(PROP_TYPE_STRING),
  // Always return the `v-model` value as a date object
  valueAsDate: makeProp(PROP_TYPE_BOOLEAN, false),
  // Format of the weekday names at the top of the calendar
  // `short` is typically a 3 letter abbreviation,
  // `narrow` is typically a single letter
  // `long` is the full week day name
  // Although some locales may override this (i.e `ar`, etc.)
  weekdayHeaderFormat: makeProp(PROP_TYPE_STRING, CALENDAR_SHORT, value => {
    return arrayIncludes([CALENDAR_LONG, CALENDAR_SHORT, CALENDAR_NARROW], value);
  }),
  // Has no effect if prop `block` is set
  width: makeProp(PROP_TYPE_STRING, '270px')
}), NAME_CALENDAR);

// --- Main component ---

// @vue/component
const BCalendar = extend({
  name: NAME_CALENDAR,
  // Mixin order is important!
  mixins: [attrsMixin, idMixin, modelMixin, normalizeSlotMixin],
  props,
  data() {
    const selected = formatYMD(this[MODEL_PROP_NAME]) || '';
    return {
      // Selected date
      selectedYMD: selected,
      // Date in calendar grid that has `tabindex` of `0`
      activeYMD: selected || formatYMD(constrainDate(this.initialDate || this.getToday()), this.min, this.max),
      // Will be true if the calendar grid has/contains focus
      gridHasFocus: false,
      // Flag to enable the `aria-live` region(s) after mount
      // to prevent screen reader "outbursts" when mounting
      isLive: false
    };
  },
  computed: {
    valueId() {
      return this.safeId();
    },
    widgetId() {
      return this.safeId('_calendar-wrapper_');
    },
    navId() {
      return this.safeId('_calendar-nav_');
    },
    gridId() {
      return this.safeId('_calendar-grid_');
    },
    gridCaptionId() {
      return this.safeId('_calendar-grid-caption_');
    },
    gridHelpId() {
      return this.safeId('_calendar-grid-help_');
    },
    activeId() {
      return this.activeYMD ? this.safeId(`_cell-${this.activeYMD}_`) : null;
    },
    // TODO: Use computed props to convert `YYYY-MM-DD` to `Date` object
    selectedDate() {
      // Selected as a `Date` object
      return parseYMD(this.selectedYMD);
    },
    activeDate() {
      // Active as a `Date` object
      return parseYMD(this.activeYMD);
    },
    computedMin() {
      return parseYMD(this.min);
    },
    computedMax() {
      return parseYMD(this.max);
    },
    computedWeekStarts() {
      // `startWeekday` is a prop (constrained to `0` through `6`)
      return mathMax(toInteger(this.startWeekday, 0), 0) % 7;
    },
    computedLocale() {
      // Returns the resolved locale used by the calendar
      return resolveLocale(concat(this.locale).filter(identity), CALENDAR_GREGORY);
    },
    computedDateDisabledFn() {
      const {
        dateDisabledFn
      } = this;
      return hasPropFunction(dateDisabledFn) ? dateDisabledFn : () => false;
    },
    // TODO: Change `dateInfoFn` to handle events and notes as well as classes
    computedDateInfoFn() {
      const {
        dateInfoFn
      } = this;
      return hasPropFunction(dateInfoFn) ? dateInfoFn : () => ({});
    },
    calendarLocale() {
      // This locale enforces the gregorian calendar (for use in formatter functions)
      // Needed because IE 11 resolves `ar-IR` as islamic-civil calendar
      // and IE 11 (and some other browsers) do not support the `calendar` option
      // And we currently only support the gregorian calendar
      const fmt = new Intl.DateTimeFormat(this.computedLocale, {
        calendar: CALENDAR_GREGORY
      });
      const calendar = fmt.resolvedOptions().calendar;
      let locale = fmt.resolvedOptions().locale;
      /* istanbul ignore if: mainly for IE 11 and a few other browsers, hard to test in JSDOM */
      if (calendar !== CALENDAR_GREGORY) {
        // Ensure the locale requests the gregorian calendar
        // Mainly for IE 11, and currently we can't handle non-gregorian calendars
        // TODO: Should we always return this value?
        locale = locale.replace(/-u-.+$/i, '').concat('-u-ca-gregory');
      }
      return locale;
    },
    calendarYear() {
      return this.activeDate.getFullYear();
    },
    calendarMonth() {
      return this.activeDate.getMonth();
    },
    calendarFirstDay() {
      // We set the time for this date to 12pm to work around
      // date formatting issues in Firefox and Safari
      // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/5818
      return createDate(this.calendarYear, this.calendarMonth, 1, 12);
    },
    calendarDaysInMonth() {
      // We create a new date as to not mutate the original
      const date = createDate(this.calendarFirstDay);
      date.setMonth(date.getMonth() + 1, 0);
      return date.getDate();
    },
    computedVariant() {
      return `btn-${this.selectedVariant || 'primary'}`;
    },
    computedTodayVariant() {
      return `btn-outline-${this.todayVariant || this.selectedVariant || 'primary'}`;
    },
    computedNavButtonVariant() {
      return `btn-outline-${this.navButtonVariant || 'primary'}`;
    },
    isRTL() {
      // `true` if the language requested is RTL
      const dir = toString(this.direction).toLowerCase();
      if (dir === 'rtl') {
        /* istanbul ignore next */
        return true;
      } else if (dir === 'ltr') {
        /* istanbul ignore next */
        return false;
      }
      return isLocaleRTL(this.computedLocale);
    },
    context() {
      const {
        selectedYMD,
        activeYMD
      } = this;
      const selectedDate = parseYMD(selectedYMD);
      const activeDate = parseYMD(activeYMD);
      return {
        // The current value of the `v-model`
        selectedYMD,
        selectedDate,
        selectedFormatted: selectedDate ? this.formatDateString(selectedDate) : this.labelNoDateSelected,
        // Which date cell is considered active due to navigation
        activeYMD,
        activeDate,
        activeFormatted: activeDate ? this.formatDateString(activeDate) : '',
        // `true` if the date is disabled (when using keyboard navigation)
        disabled: this.dateDisabled(activeDate),
        // Locales used in formatting dates
        locale: this.computedLocale,
        calendarLocale: this.calendarLocale,
        rtl: this.isRTL
      };
    },
    // Computed props that return a function reference
    dateOutOfRange() {
      // Check whether a date is within the min/max range
      // Returns a new function ref if the pops change
      // We do this as we need to trigger the calendar computed prop
      // to update when these props update
      const {
        computedMin: min,
        computedMax: max
      } = this;
      return date => {
        // Handle both `YYYY-MM-DD` and `Date` objects
        date = parseYMD(date);
        return min && date < min || max && date > max;
      };
    },
    dateDisabled() {
      // Returns a function for validating if a date is within range
      // We grab this variables first to ensure a new function ref
      // is generated when the props value changes
      // We do this as we need to trigger the calendar computed prop
      // to update when these props update
      const rangeFn = this.dateOutOfRange;
      // Return the function ref
      return date => {
        // Handle both `YYYY-MM-DD` and `Date` objects
        date = parseYMD(date);
        const ymd = formatYMD(date);
        return !!(rangeFn(date) || this.computedDateDisabledFn(ymd, date));
      };
    },
    // Computed props that return date formatter functions
    formatDateString() {
      // Returns a date formatter function
      return createDateFormatter(this.calendarLocale, {
        // Ensure we have year, month, day shown for screen readers/ARIA
        // If users really want to leave one of these out, they can
        // pass `undefined` for the property value
        year: DATE_FORMAT_NUMERIC,
        month: DATE_FORMAT_2_DIGIT,
        day: DATE_FORMAT_2_DIGIT,
        // Merge in user supplied options
        ...this.dateFormatOptions,
        // Ensure hours/minutes/seconds are not shown
        // As we do not support the time portion (yet)
        hour: undefined,
        minute: undefined,
        second: undefined,
        // Ensure calendar is gregorian
        calendar: CALENDAR_GREGORY
      });
    },
    formatYearMonth() {
      // Returns a date formatter function
      return createDateFormatter(this.calendarLocale, {
        year: DATE_FORMAT_NUMERIC,
        month: CALENDAR_LONG,
        calendar: CALENDAR_GREGORY
      });
    },
    formatWeekdayName() {
      // Long weekday name for weekday header aria-label
      return createDateFormatter(this.calendarLocale, {
        weekday: CALENDAR_LONG,
        calendar: CALENDAR_GREGORY
      });
    },
    formatWeekdayNameShort() {
      // Weekday header cell format
      // defaults to 'short' 3 letter days, where possible
      return createDateFormatter(this.calendarLocale, {
        weekday: this.weekdayHeaderFormat || CALENDAR_SHORT,
        calendar: CALENDAR_GREGORY
      });
    },
    formatDay() {
      // Calendar grid day number formatter
      // We don't use DateTimeFormatter here as it can place extra
      // character(s) after the number (i.e the `zh` locale)
      const nf = new Intl.NumberFormat([this.computedLocale], {
        style: 'decimal',
        minimumIntegerDigits: 1,
        minimumFractionDigits: 0,
        maximumFractionDigits: 0,
        notation: 'standard'
      });
      // Return a formatter function instance
      return date => nf.format(date.getDate());
    },
    // Disabled states for the nav buttons
    prevDecadeDisabled() {
      const min = this.computedMin;
      return this.disabled || min && lastDateOfMonth(oneDecadeAgo(this.activeDate)) < min;
    },
    prevYearDisabled() {
      const min = this.computedMin;
      return this.disabled || min && lastDateOfMonth(oneYearAgo(this.activeDate)) < min;
    },
    prevMonthDisabled() {
      const min = this.computedMin;
      return this.disabled || min && lastDateOfMonth(oneMonthAgo(this.activeDate)) < min;
    },
    thisMonthDisabled() {
      // TODO: We could/should check if today is out of range
      return this.disabled;
    },
    nextMonthDisabled() {
      const max = this.computedMax;
      return this.disabled || max && firstDateOfMonth(oneMonthAhead(this.activeDate)) > max;
    },
    nextYearDisabled() {
      const max = this.computedMax;
      return this.disabled || max && firstDateOfMonth(oneYearAhead(this.activeDate)) > max;
    },
    nextDecadeDisabled() {
      const max = this.computedMax;
      return this.disabled || max && firstDateOfMonth(oneDecadeAhead(this.activeDate)) > max;
    },
    // Calendar dates generation
    calendar() {
      const matrix = [];
      const firstDay = this.calendarFirstDay;
      const calendarYear = firstDay.getFullYear();
      const calendarMonth = firstDay.getMonth();
      const daysInMonth = this.calendarDaysInMonth;
      const startIndex = firstDay.getDay(); // `0`..`6`
      const weekOffset = (this.computedWeekStarts > startIndex ? 7 : 0) - this.computedWeekStarts;
      // Build the calendar matrix
      let currentDay = 0 - weekOffset - startIndex;
      for (let week = 0; week < 6 && currentDay < daysInMonth; week++) {
        // For each week
        matrix[week] = [];
        // The following could be a map function
        for (let j = 0; j < 7; j++) {
          // For each day in week
          currentDay++;
          const date = createDate(calendarYear, calendarMonth, currentDay);
          const month = date.getMonth();
          const dayYMD = formatYMD(date);
          const dayDisabled = this.dateDisabled(date);
          // TODO: This could be a normalizer method
          let dateInfo = this.computedDateInfoFn(dayYMD, parseYMD(dayYMD));
          dateInfo = isString(dateInfo) || isArray(dateInfo) ? /* istanbul ignore next */{
            class: dateInfo
          } : isPlainObject(dateInfo) ? {
            class: '',
            ...dateInfo
          } : /* istanbul ignore next */{
            class: ''
          };
          matrix[week].push({
            ymd: dayYMD,
            // Cell content
            day: this.formatDay(date),
            label: this.formatDateString(date),
            // Flags for styling
            isThisMonth: month === calendarMonth,
            isDisabled: dayDisabled,
            // TODO: Handle other dateInfo properties such as notes/events
            info: dateInfo
          });
        }
      }
      return matrix;
    },
    calendarHeadings() {
      return this.calendar[0].map(d => {
        return {
          text: this.formatWeekdayNameShort(parseYMD(d.ymd)),
          label: this.formatWeekdayName(parseYMD(d.ymd))
        };
      });
    }
  },
  watch: {
    [MODEL_PROP_NAME](newValue, oldValue) {
      const selected = formatYMD(newValue) || '';
      const old = formatYMD(oldValue) || '';
      if (!datesEqual(selected, old)) {
        this.activeYMD = selected || this.activeYMD;
        this.selectedYMD = selected;
      }
    },
    selectedYMD(newYMD, oldYMD) {
      // TODO:
      //   Should we compare to `formatYMD(this.value)` and emit
      //   only if they are different?
      if (newYMD !== oldYMD) {
        this.$emit(MODEL_EVENT_NAME, this.valueAsDate ? parseYMD(newYMD) || null : newYMD || '');
      }
    },
    context(newValue, oldValue) {
      if (!looseEqual(newValue, oldValue)) {
        this.$emit(EVENT_NAME_CONTEXT, newValue);
      }
    },
    hidden(newValue) {
      // Reset the active focused day when hidden
      this.activeYMD = this.selectedYMD || formatYMD(this[MODEL_PROP_NAME] || this.constrainDate(this.initialDate || this.getToday()));
      // Enable/disable the live regions
      this.setLive(!newValue);
    }
  },
  created() {
    this.$nextTick(() => {
      this.$emit(EVENT_NAME_CONTEXT, this.context);
    });
  },
  mounted() {
    this.setLive(true);
  },
  /* istanbul ignore next */
  activated() {
    this.setLive(true);
  },
  /* istanbul ignore next */
  deactivated() {
    this.setLive(false);
  },
  beforeDestroy() {
    this.setLive(false);
  },
  methods: {
    // Public method(s)
    focus() {
      if (!this.disabled) {
        attemptFocus(this.$refs.grid);
      }
    },
    blur() {
      if (!this.disabled) {
        attemptBlur(this.$refs.grid);
      }
    },
    // Private methods
    setLive(on) {
      if (on) {
        this.$nextTick(() => {
          requestAF(() => {
            this.isLive = true;
          });
        });
      } else {
        this.isLive = false;
      }
    },
    getToday() {
      return parseYMD(createDate());
    },
    constrainDate(date) {
      // Constrains a date between min and max
      // returns a new `Date` object instance
      return constrainDate(date, this.computedMin, this.computedMax);
    },
    emitSelected(date) {
      // Performed in a `$nextTick()` to (probably) ensure
      // the input event has emitted first
      this.$nextTick(() => {
        this.$emit(EVENT_NAME_SELECTED, formatYMD(date) || '', parseYMD(date) || null);
      });
    },
    // Event handlers
    setGridFocusFlag(event) {
      // Sets the gridHasFocus flag to make date "button" look focused
      this.gridHasFocus = !this.disabled && event.type === 'focus';
    },
    onKeydownWrapper(event) {
      // Calendar keyboard navigation
      // Handles PAGEUP/PAGEDOWN/END/HOME/LEFT/UP/RIGHT/DOWN
      // Focuses grid after updating
      if (this.noKeyNav) {
        /* istanbul ignore next */
        return;
      }
      const {
        altKey,
        ctrlKey,
        keyCode
      } = event;
      if (!arrayIncludes([CODE_PAGEUP, CODE_PAGEDOWN, CODE_END, CODE_HOME, CODE_LEFT, CODE_UP, CODE_RIGHT, CODE_DOWN], keyCode)) {
        /* istanbul ignore next */
        return;
      }
      stopEvent(event);
      let activeDate = createDate(this.activeDate);
      let checkDate = createDate(this.activeDate);
      const day = activeDate.getDate();
      const constrainedToday = this.constrainDate(this.getToday());
      const isRTL = this.isRTL;
      if (keyCode === CODE_PAGEUP) {
        // PAGEUP - Previous month/year
        activeDate = (altKey ? ctrlKey ? oneDecadeAgo : oneYearAgo : oneMonthAgo)(activeDate);
        // We check the first day of month to be in rage
        checkDate = createDate(activeDate);
        checkDate.setDate(1);
      } else if (keyCode === CODE_PAGEDOWN) {
        // PAGEDOWN - Next month/year
        activeDate = (altKey ? ctrlKey ? oneDecadeAhead : oneYearAhead : oneMonthAhead)(activeDate);
        // We check the last day of month to be in rage
        checkDate = createDate(activeDate);
        checkDate.setMonth(checkDate.getMonth() + 1);
        checkDate.setDate(0);
      } else if (keyCode === CODE_LEFT) {
        // LEFT - Previous day (or next day for RTL)
        activeDate.setDate(day + (isRTL ? 1 : -1));
        activeDate = this.constrainDate(activeDate);
        checkDate = activeDate;
      } else if (keyCode === CODE_RIGHT) {
        // RIGHT - Next day (or previous day for RTL)
        activeDate.setDate(day + (isRTL ? -1 : 1));
        activeDate = this.constrainDate(activeDate);
        checkDate = activeDate;
      } else if (keyCode === CODE_UP) {
        // UP - Previous week
        activeDate.setDate(day - 7);
        activeDate = this.constrainDate(activeDate);
        checkDate = activeDate;
      } else if (keyCode === CODE_DOWN) {
        // DOWN - Next week
        activeDate.setDate(day + 7);
        activeDate = this.constrainDate(activeDate);
        checkDate = activeDate;
      } else if (keyCode === CODE_HOME) {
        // HOME - Today
        activeDate = constrainedToday;
        checkDate = activeDate;
      } else if (keyCode === CODE_END) {
        // END - Selected date, or today if no selected date
        activeDate = parseYMD(this.selectedDate) || constrainedToday;
        checkDate = activeDate;
      }
      if (!this.dateOutOfRange(checkDate) && !datesEqual(activeDate, this.activeDate)) {
        // We only jump to date if within min/max
        // We don't check for individual disabled dates though (via user function)
        this.activeYMD = formatYMD(activeDate);
      }
      // Ensure grid is focused
      this.focus();
    },
    onKeydownGrid(event) {
      // Pressing enter/space on grid to select active date
      const keyCode = event.keyCode;
      const activeDate = this.activeDate;
      if (keyCode === CODE_ENTER || keyCode === CODE_SPACE) {
        stopEvent(event);
        if (!this.disabled && !this.readonly && !this.dateDisabled(activeDate)) {
          this.selectedYMD = formatYMD(activeDate);
          this.emitSelected(activeDate);
        }
        // Ensure grid is focused
        this.focus();
      }
    },
    onClickDay(day) {
      // Clicking on a date "button" to select it
      const {
        selectedDate,
        activeDate
      } = this;
      const clickedDate = parseYMD(day.ymd);
      if (!this.disabled && !day.isDisabled && !this.dateDisabled(clickedDate)) {
        if (!this.readonly) {
          // If readonly mode, we don't set the selected date, just the active date
          // If the clicked date is equal to the already selected date, we don't update the model
          this.selectedYMD = formatYMD(datesEqual(clickedDate, selectedDate) ? selectedDate : clickedDate);
          this.emitSelected(clickedDate);
        }
        this.activeYMD = formatYMD(datesEqual(clickedDate, activeDate) ? activeDate : createDate(clickedDate));
        // Ensure grid is focused
        this.focus();
      }
    },
    gotoPrevDecade() {
      this.activeYMD = formatYMD(this.constrainDate(oneDecadeAgo(this.activeDate)));
    },
    gotoPrevYear() {
      this.activeYMD = formatYMD(this.constrainDate(oneYearAgo(this.activeDate)));
    },
    gotoPrevMonth() {
      this.activeYMD = formatYMD(this.constrainDate(oneMonthAgo(this.activeDate)));
    },
    gotoCurrentMonth() {
      // TODO: Maybe this goto date should be configurable?
      this.activeYMD = formatYMD(this.constrainDate(this.getToday()));
    },
    gotoNextMonth() {
      this.activeYMD = formatYMD(this.constrainDate(oneMonthAhead(this.activeDate)));
    },
    gotoNextYear() {
      this.activeYMD = formatYMD(this.constrainDate(oneYearAhead(this.activeDate)));
    },
    gotoNextDecade() {
      this.activeYMD = formatYMD(this.constrainDate(oneDecadeAhead(this.activeDate)));
    },
    onHeaderClick() {
      if (!this.disabled) {
        this.activeYMD = this.selectedYMD || formatYMD(this.getToday());
        this.focus();
      }
    }
  },
  render(h) {
    // If `hidden` prop is set, render just a placeholder node
    if (this.hidden) {
      return h();
    }
    const {
      valueId,
      widgetId,
      navId,
      gridId,
      gridCaptionId,
      gridHelpId,
      activeId,
      disabled,
      noKeyNav,
      isLive,
      isRTL,
      activeYMD,
      selectedYMD,
      safeId
    } = this;
    const hideDecadeNav = !this.showDecadeNav;
    const todayYMD = formatYMD(this.getToday());
    const highlightToday = !this.noHighlightToday;

    // Header showing current selected date
    let $header = h('output', {
      staticClass: 'form-control form-control-sm text-center',
      class: {
        'text-muted': disabled,
        readonly: this.readonly || disabled
      },
      attrs: {
        id: valueId,
        for: gridId,
        role: 'status',
        tabindex: disabled ? null : '-1',
        // Mainly for testing purposes, as we do not know
        // the exact format `Intl` will format the date string
        'data-selected': toString(selectedYMD),
        // We wait until after mount to enable `aria-live`
        // to prevent initial announcement on page render
        'aria-live': isLive ? 'polite' : 'off',
        'aria-atomic': isLive ? 'true' : null
      },
      on: {
        // Transfer focus/click to focus grid
        // and focus active date (or today if no selection)
        click: this.onHeaderClick,
        focus: this.onHeaderClick
      }
    }, this.selectedDate ? [
    // We use `bdi` elements here in case the label doesn't match the locale
    // Although IE 11 does not deal with <BDI> at all (equivalent to a span)
    h('bdi', {
      staticClass: 'sr-only'
    }, ` (${toString(this.labelSelected)}) `), h('bdi', this.formatDateString(this.selectedDate))] : this.labelNoDateSelected || '\u00a0' // '&nbsp;'
    );
    $header = h(this.headerTag, {
      staticClass: 'b-calendar-header',
      class: {
        'sr-only': this.hideHeader
      },
      attrs: {
        title: this.selectedDate ? this.labelSelected || null : null
      }
    }, [$header]);

    // Content for the date navigation buttons
    const navScope = {
      isRTL
    };
    const navProps = {
      shiftV: 0.5
    };
    const navPrevProps = {
      ...navProps,
      flipH: isRTL
    };
    const navNextProps = {
      ...navProps,
      flipH: !isRTL
    };
    const $prevDecadeIcon = this.normalizeSlot(SLOT_NAME_NAV_PEV_DECADE, navScope) || h(BIconChevronBarLeft, {
      props: navPrevProps
    });
    const $prevYearIcon = this.normalizeSlot(SLOT_NAME_NAV_PEV_YEAR, navScope) || h(BIconChevronDoubleLeft, {
      props: navPrevProps
    });
    const $prevMonthIcon = this.normalizeSlot(SLOT_NAME_NAV_PEV_MONTH, navScope) || h(BIconChevronLeft, {
      props: navPrevProps
    });
    const $thisMonthIcon = this.normalizeSlot(SLOT_NAME_NAV_THIS_MONTH, navScope) || h(BIconCircleFill, {
      props: navProps
    });
    const $nextMonthIcon = this.normalizeSlot(SLOT_NAME_NAV_NEXT_MONTH, navScope) || h(BIconChevronLeft, {
      props: navNextProps
    });
    const $nextYearIcon = this.normalizeSlot(SLOT_NAME_NAV_NEXT_YEAR, navScope) || h(BIconChevronDoubleLeft, {
      props: navNextProps
    });
    const $nextDecadeIcon = this.normalizeSlot(SLOT_NAME_NAV_NEXT_DECADE, navScope) || h(BIconChevronBarLeft, {
      props: navNextProps
    });

    // Utility to create the date navigation buttons
    const makeNavBtn = (content, label, handler, btnDisabled, shortcut) => {
      return h('button', {
        staticClass: 'btn btn-sm border-0 flex-fill',
        class: [this.computedNavButtonVariant, {
          disabled: btnDisabled
        }],
        attrs: {
          title: label || null,
          type: 'button',
          tabindex: noKeyNav ? '-1' : null,
          'aria-label': label || null,
          'aria-disabled': btnDisabled ? 'true' : null,
          'aria-keyshortcuts': shortcut || null
        },
        on: btnDisabled ? {} : {
          click: handler
        }
      }, [h('div', {
        attrs: {
          'aria-hidden': 'true'
        }
      }, [content])]);
    };

    // Generate the date navigation buttons
    const $nav = h('div', {
      staticClass: 'b-calendar-nav d-flex',
      attrs: {
        id: navId,
        role: 'group',
        tabindex: noKeyNav ? '-1' : null,
        'aria-hidden': disabled ? 'true' : null,
        'aria-label': this.labelNav || null,
        'aria-controls': gridId
      }
    }, [hideDecadeNav ? h() : makeNavBtn($prevDecadeIcon, this.labelPrevDecade, this.gotoPrevDecade, this.prevDecadeDisabled, 'Ctrl+Alt+PageDown'), makeNavBtn($prevYearIcon, this.labelPrevYear, this.gotoPrevYear, this.prevYearDisabled, 'Alt+PageDown'), makeNavBtn($prevMonthIcon, this.labelPrevMonth, this.gotoPrevMonth, this.prevMonthDisabled, 'PageDown'), makeNavBtn($thisMonthIcon, this.labelCurrentMonth, this.gotoCurrentMonth, this.thisMonthDisabled, 'Home'), makeNavBtn($nextMonthIcon, this.labelNextMonth, this.gotoNextMonth, this.nextMonthDisabled, 'PageUp'), makeNavBtn($nextYearIcon, this.labelNextYear, this.gotoNextYear, this.nextYearDisabled, 'Alt+PageUp'), hideDecadeNav ? h() : makeNavBtn($nextDecadeIcon, this.labelNextDecade, this.gotoNextDecade, this.nextDecadeDisabled, 'Ctrl+Alt+PageUp')]);

    // Caption for calendar grid
    const $gridCaption = h('div', {
      staticClass: 'b-calendar-grid-caption text-center font-weight-bold',
      class: {
        'text-muted': disabled
      },
      attrs: {
        id: gridCaptionId,
        'aria-live': isLive ? 'polite' : null,
        'aria-atomic': isLive ? 'true' : null
      },
      key: 'grid-caption'
    }, this.formatYearMonth(this.calendarFirstDay));

    // Calendar weekday headings
    const $gridWeekDays = h('div', {
      staticClass: 'b-calendar-grid-weekdays row no-gutters border-bottom',
      attrs: {
        'aria-hidden': 'true'
      }
    }, this.calendarHeadings.map((d, idx) => {
      return h('small', {
        staticClass: 'col text-truncate',
        class: {
          'text-muted': disabled
        },
        attrs: {
          title: d.label === d.text ? null : d.label,
          'aria-label': d.label
        },
        key: idx
      }, d.text);
    }));

    // Calendar day grid
    let $gridBody = this.calendar.map(week => {
      const $cells = week.map((day, dIndex) => {
        const isSelected = day.ymd === selectedYMD;
        const isActive = day.ymd === activeYMD;
        const isToday = day.ymd === todayYMD;
        const idCell = safeId(`_cell-${day.ymd}_`);
        // "fake" button
        const $btn = h('span', {
          staticClass: 'btn border-0 rounded-circle text-nowrap',
          // Should we add some classes to signify if today/selected/etc?
          class: {
            // Give the fake button a focus ring
            focus: isActive && this.gridHasFocus,
            // Styling
            disabled: day.isDisabled || disabled,
            active: isSelected,
            // makes the button look "pressed"
            // Selected date style (need to computed from variant)
            [this.computedVariant]: isSelected,
            // Today day style (if not selected), same variant color as selected date
            [this.computedTodayVariant]: isToday && highlightToday && !isSelected && day.isThisMonth,
            // Non selected/today styling
            'btn-outline-light': !(isToday && highlightToday) && !isSelected && !isActive,
            'btn-light': !(isToday && highlightToday) && !isSelected && isActive,
            // Text styling
            'text-muted': !day.isThisMonth && !isSelected,
            'text-dark': !(isToday && highlightToday) && !isSelected && !isActive && day.isThisMonth,
            'font-weight-bold': (isSelected || day.isThisMonth) && !day.isDisabled
          },
          on: {
            click: () => this.onClickDay(day)
          }
        }, day.day);
        return h('div',
        // Cell with button
        {
          staticClass: 'col p-0',
          class: day.isDisabled ? 'bg-light' : day.info.class || '',
          attrs: {
            id: idCell,
            role: 'button',
            'data-date': day.ymd,
            // Primarily for testing purposes
            // Only days in the month are presented as buttons to screen readers
            'aria-hidden': day.isThisMonth ? null : 'true',
            'aria-disabled': day.isDisabled || disabled ? 'true' : null,
            'aria-label': [day.label, isSelected ? `(${this.labelSelected})` : null, isToday ? `(${this.labelToday})` : null].filter(identity).join(' '),
            // NVDA doesn't convey `aria-selected`, but does `aria-current`,
            // ChromeVox doesn't convey `aria-current`, but does `aria-selected`,
            // so we set both attributes for robustness
            'aria-selected': isSelected ? 'true' : null,
            'aria-current': isSelected ? 'date' : null
          },
          key: dIndex
        }, [$btn]);
      });
      // Return the week "row"
      // We use the first day of the weeks YMD value as a
      // key for efficient DOM patching / element re-use
      return h('div', {
        staticClass: 'row no-gutters',
        key: week[0].ymd
      }, $cells);
    });
    $gridBody = h('div', {
      // A key is only required on the body if we add in transition support
      staticClass: 'b-calendar-grid-body',
      style: disabled ? {
        pointerEvents: 'none'
      } : {}
      // key: this.activeYMD.slice(0, -3)
    }, $gridBody);
    const $gridHelp = h('div', {
      staticClass: 'b-calendar-grid-help border-top small text-muted text-center bg-light',
      attrs: {
        id: gridHelpId
      }
    }, [h('div', {
      staticClass: 'small'
    }, this.labelHelp)]);
    const $grid = h('div', {
      staticClass: 'b-calendar-grid form-control h-auto text-center',
      attrs: {
        id: gridId,
        role: 'application',
        tabindex: noKeyNav ? '-1' : disabled ? null : '0',
        'data-month': activeYMD.slice(0, -3),
        // `YYYY-MM`, mainly for testing
        'aria-roledescription': this.labelCalendar || null,
        'aria-labelledby': gridCaptionId,
        'aria-describedby': gridHelpId,
        // `aria-readonly` is not considered valid on `role="application"`
        // https://www.w3.org/TR/wai-aria-1.1/#aria-readonly
        // 'aria-readonly': this.readonly && !disabled ? 'true' : null,
        'aria-disabled': disabled ? 'true' : null,
        'aria-activedescendant': activeId
      },
      on: {
        keydown: this.onKeydownGrid,
        focus: this.setGridFocusFlag,
        blur: this.setGridFocusFlag
      },
      ref: 'grid'
    }, [$gridCaption, $gridWeekDays, $gridBody, $gridHelp]);

    // Optional bottom slot
    let $slot = this.normalizeSlot();
    $slot = $slot ? h('footer', {
      staticClass: 'b-calendar-footer'
    }, $slot) : h();
    const $widget = h('div', {
      staticClass: 'b-calendar-inner',
      style: this.block ? {} : {
        width: this.width
      },
      attrs: {
        id: widgetId,
        dir: isRTL ? 'rtl' : 'ltr',
        lang: this.computedLocale || null,
        role: 'group',
        'aria-disabled': disabled ? 'true' : null,
        // If datepicker controls an input, this will specify the ID of the input
        'aria-controls': this.ariaControls || null,
        // This should be a prop (so it can be changed to Date picker, etc, localized
        'aria-roledescription': this.roleDescription || null,
        'aria-describedby': [
        // Should the attr (if present) go last?
        // Or should this attr be a prop?
        this.bvAttrs['aria-describedby'], valueId, gridHelpId].filter(identity).join(' ')
      },
      on: {
        keydown: this.onKeydownWrapper
      }
    }, [$header, $nav, $grid, $slot]);

    // Wrap in an outer div that can be styled
    return h('div', {
      staticClass: 'b-calendar',
      class: {
        'd-block': this.block
      }
    }, [$widget]);
  }
});

export { BCalendar, props };
