import { ButtonClass } from '@descope-ui/descope-button/class';
import { injectStyle } from '@descope-ui/common/components-helpers';
import { createBaseInputClass } from '../../../baseClasses/createBaseInputClass';
import { compose } from '../../../helpers';
import { forwardAttrs, getComponentName } from '../../../helpers/componentHelpers';
import { inputFloatingLabelStyle } from '../../../helpers/themeHelpers/resetHelpers';
import { componentNameValidationMixin, createStyleMixin, draggableMixin } from '../../../mixins';
import {
  getCurrentDay,
  getPartsFromEpoch,
  isValidEpoch,
  formatEpoch,
  getCurrentTime,
  getFullYear,
  getMonth,
} from '../helpers';
import { arrowLeftIcon, arrowRightIcon } from '../icons';
import {
  BUTTON_LABEL_CANCEL,
  BUTTON_LABEL_DONE,
  CALENDAR_LABEL_TODAY,
  NATIVE_FORMAT,
  YEARS_RANGE,
} from '../consts';
import {
  createMonthView,
  createInputs,
  getMonthsOptions,
  getYearOptions,
  initComboBox,
  ensureMonthNamesArr,
  isViewVisible,
  ensureWeekdayNamesArr,
  truncateWeekdays,
  prevMonth,
  nextMonth,
} from './helpers';
import { newDate } from '../date-utils';

export const componentName = getComponentName('calendar');

const observedAttrs = [
  'initial-value',
  'initial-year',
  'initial-month',
  'years-range',
  'calendar-months',
  'calendar-weekdays',
  'calendar-label-today',
  'st-host-direction',
  'disabled',
  'full-width',
];

const calendarUiAttrs = ['calendar-label-submit', 'calendar-label-cancel'];

const BaseInputClass = createBaseInputClass({ componentName, baseSelector: 'div' });

class RawCalendar extends BaseInputClass {
  static get observedAttributes() {
    return [].concat(BaseInputClass.observedAttributes || [], observedAttrs, calendarUiAttrs);
  }

  // preview state epoch
  preview;

  // value epoch
  epoch;

  constructor() {
    super();

    this.attachShadow({ mode: 'open' }).innerHTML = `
    <div>
      <nav class="nav top-nav">
        <div class="nav-prev">
        </div>
        <div class="selectors"></div>
        <div class="nav-next">
        </div>
      </nav>
      <div class="calendar"></div>
      <nav class="nav bottom-nav">
        <descope-button class="cancel-button" variant="link" mode="primary">Cancel</descope-button>
        <descope-button class="submit-button" variant="link" mode="primary" disabled="true">Done</descope-button>
      </nav>
    </div>
  `;

    injectStyle(
      `
      :host {
        display: inline-block;
        box-sizing: border-box;
        max-width: 100%;
        user-select: none;
        -webkit-user-select: none;
      }

      :host ::slotted {
        padding: 0;
      }

      [data-toggle-calendar] {
        cursor: pointer;
        display: flex;
        align-self: center;
        z-index: 1;
      }

      [data-disabled="true"] {
        pointer-events: none;
      }

      .calendar .day {
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .nav {
        display: flex;
        align-items: center;
        box-sizing: border-box;
      }

      .selectors,
      .selectors span {
        display: flex;
        justify-content: center;
      }

      descope-button {
        padding: 0;
        ${ButtonClass.cssVarList.horizontalPadding}: 0;
        ${ButtonClass.cssVarList.verticalPadding}: 0;
        ${ButtonClass.cssVarList.outlineWidth}: 0;
      }

      .combo-box-item {
        display:flex;
        flex-direction: column;
      }

      ${inputFloatingLabelStyle()}
      `,
      this
    );
  }

  set value(val) {
    if (!val) return;

    const epoch = newDate(val).getTime();

    if (!isValidEpoch(epoch) || epoch === this.epoch) {
      return;
    }

    this.epoch = epoch;

    this.renderCalendar();
  }

  get value() {
    return this.epoch ? formatEpoch(this.epoch, NATIVE_FORMAT) : '';
  }

  get isSelectedView() {
    return isViewVisible(this.preview, this.epoch);
  }

  get isTodayView() {
    return isViewVisible(getCurrentTime(), this.preview);
  }

  get monthNames() {
    return ensureMonthNamesArr(this.getAttribute('calendar-months'));
  }

  get weekdayNames() {
    return ensureWeekdayNamesArr(this.getAttribute('calendar-weekdays'));
  }

  get hasYearValue() {
    return !!this.yearInput.value;
  }

  get hasMonthValue() {
    return !!this.monthInput.value;
  }

  get yearsRange() {
    const range = this.getAttribute('years-range')?.split('-');

    if (range?.length === 2) {
      const [start, end] = range.map(Number);
      if (start <= end) return [start, end];
    }

    const currentYear = newDate().getFullYear();

    return [currentYear, currentYear + YEARS_RANGE];
  }

  init() {
    super.init?.();
    this.initCalendar();
    this.renderCalendar();
  }

  initCalendar() {
    this.calendar = this.shadowRoot.querySelector('.calendar');

    this.navPrevEle = this.shadowRoot.querySelector('.nav-prev');
    this.navNextEle = this.shadowRoot.querySelector('.nav-next');
    this.submitButton = this.shadowRoot.querySelector('.submit-button');
    this.cancelButton = this.shadowRoot.querySelector('.cancel-button');

    this.initInputs();
    this.initNav();

    this.addEventListener('day-changed', () => this.submitButton.removeAttribute('disabled'));
    // This is a workaround for descope-components inside Vaadin's Popover component.
    // When we sync attributes, the `disabled` attribute is being compared to the attribute
    // on Vaadin component; since the Vaadin component has no `disabled` attribute, we sync
    // that back to our component. This happens when using Popover, and for specific attributes,
    // namely `disabled`. This is pending a more generic fix in the way we sync attibutes.
    this.submitButton.shadowRoot.querySelector('vaadin-button').setAttribute('disabled', 'true');
  }

  initInputs() {
    const ele = createInputs(this.monthNames, this.yearsRange);
    this.shadowRoot.querySelector('.selectors').appendChild(ele);

    this.monthInput = initComboBox(
      this.shadowRoot.querySelector('.month-input'),
      this.onMonthChange.bind(this),
      this.onMonthInputBlur.bind(this)
    );

    this.yearInput = initComboBox(
      this.shadowRoot.querySelector('.year-input'),
      this.onYearChange.bind(this),
      this.onYearInputBlur.bind(this)
    );

    [this.monthInput, this.yearInput].forEach((selector) =>
      forwardAttrs(this, selector, {
        includeAttrs: ['disabled', 'size', 'st-host-direction'],
      })
    );
  }

  initNav() {
    this.navPrevEle.addEventListener('click', this.navPrevMonth.bind(this));
    this.navNextEle.addEventListener('click', this.navNextMonth.bind(this));
    this.cancelButton.addEventListener('click', this.onCancel.bind(this));
    this.submitButton.addEventListener('click', this.onSubmit.bind(this));

    [this.navPrevEle, this.navNextEle].forEach((btn) =>
      forwardAttrs(this, btn, {
        includeAttrs: ['size'],
      })
    );

    this.shadowRoot.querySelector('.nav-next').innerHTML = arrowRightIcon;
    this.shadowRoot.querySelector('.nav-prev').innerHTML = arrowLeftIcon;
  }

  onSubmit() {
    if (this.isDisabled) return;
    if (!this.value) return;
    this.dispatchEvent(new CustomEvent('date-submit', { detail: newDate(this.preview) }));
  }

  onCancel() {
    if (this.isDisabled) return;
    this.dispatchEvent(new CustomEvent('cancel'));
    this.dispatchEvent(new CustomEvent('date-cancel'));
    this.clearSelectedDay();
  }

  clearSelectedDay() {
    this.getSelectedDayEle()?.removeAttribute('data-selected');
    this.submitButton.setAttribute('disabled', 'true');
    this.epoch = '';
  }

  updateInputs() {
    if (this.yearInput && this.monthInput) {
      const epoch = this.preview || getCurrentTime();
      const year = getFullYear(epoch);
      const month = getMonth(epoch);
      this.monthInput.value = month;
      // For the yearInput we update the base element directly to properly trigger the change event
      // since this can be a custom value
      setTimeout(() => {
        this.yearInput.baseElement.value = year;
      });
    }
  }

  renderCalendar() {
    const date = newDate(this.preview || getCurrentTime());
    const epoch = date.getTime();

    const [year, month] = getPartsFromEpoch(epoch);

    if (this.calendar) {
      this.calendar.innerHTML = createMonthView(year, month, truncateWeekdays(this.weekdayNames));
    }

    Array.from(this.calendar?.querySelectorAll('.day-entry .day') || []).forEach((ele) =>
      ele.addEventListener('click', this.onDayChange.bind(this))
    );

    this.updateInputs();
    this.setSelectedDay();
    this.setCurrentDay();
  }

  onDayChange(e) {
    if (this.isDisabled) return;
    const date = newDate(this.preview);
    date.setDate(Number(e.target.getAttribute('data-date-day')));
    this.value = formatEpoch(date.getTime(), NATIVE_FORMAT);
    this.dispatchEvent(new CustomEvent('day-changed', { detail: date }));
  }

  onYearChange(e) {
    if (!e.target.value) return;
    const date = newDate(this.preview);
    date.setFullYear(Number(e.target.value.trim()));
    this.preview = date.getTime();
    this.renderCalendar();
    this.dispatchEvent(new CustomEvent('year-changed', { detail: date }));
  }

  onMonthChange(e) {
    if (!e.target.value) return;
    const date = newDate(this.preview);
    date.setMonth(Number(e.target.value) - 1);
    this.preview = date.getTime();
    this.renderCalendar();
    this.dispatchEvent(new CustomEvent('month-changed', { detail: date }));
  }

  onYearInputBlur(e) {
    this.yearInput.baseElement.value = e.target.value || getFullYear(this.preview);
  }

  onMonthInputBlur(e) {
    this.monthInput.baseElement.value = e.target.value || getMonth(this.preview);
  }

  getSelectedDayEle() {
    const day = newDate(this.epoch).getDate();
    return this.calendar?.querySelector(`[data-date-day="${day}"]`);
  }

  getCurrentDayEle() {
    return this.calendar?.querySelector(`[data-date-day="${getCurrentDay()}"]`);
  }

  clearSelectedDays() {
    const cells = this.calendar?.querySelectorAll(`[data-selected]`);
    if (!cells) return;
    cells.forEach((c) => c.removeAttribute('data-selected'));
  }

  setSelectedDay() {
    this.clearSelectedDays();
    if (this.value && this.isSelectedView) {
      this.getSelectedDayEle().setAttribute('data-selected', 'true');
    }
  }

  setCurrentDay() {
    if (this.isTodayView) {
      const ele = this.getCurrentDayEle();
      const title = this.getAttribute('calendar-label-today') || CALENDAR_LABEL_TODAY;
      ele?.setAttribute('data-current-day', 'true');
      ele?.setAttribute('title', title);
    }
  }

  navPrevMonth() {
    if (this.isDisabled) {
      return;
    }
    const date = prevMonth(this.preview);
    this.preview = date.getTime();
    this.renderCalendar();
  }

  navNextMonth() {
    if (this.isDisabled) {
      return;
    }
    const date = nextMonth(this.preview);
    this.preview = date.getTime();
    this.renderCalendar();
  }

  clearValue() {
    this.epoch = '';
    this.value = '';
    this.removeAttribute('preview');
    this.submitButton.setAttribute('disabled', 'true');
  }

  onInitialValueChange(newValue) {
    // if component already has a value don't re-set value
    if (this.value) {
      return;
    }

    const date = newDate(newValue);
    const epoch = date.getTime();

    if (isValidEpoch(epoch)) {
      this.epoch = epoch;
      this.preview = epoch;
    } else {
      this.clearValue();
    }
  }

  onYearsRangeChange() {
    setTimeout(() => {
      if (this.yearInput) {
        this.yearInput.innerHTML = getYearOptions(this.yearsRange);
      }
    });
  }

  onMonthNamesChange() {
    setTimeout(() => {
      if (this.monthInput) {
        this.monthInput.innerHTML = getMonthsOptions(this.monthNames);
      }
    });
  }

  onSubmitLabelChange(newValue) {
    if (this.submitButton) {
      this.submitButton.textContent = newValue || BUTTON_LABEL_DONE;
    }
  }

  onCancelLabelChange(newValue) {
    if (this.cancelButton) {
      this.cancelButton.textContent = newValue || BUTTON_LABEL_CANCEL;
    }
  }

  onInitialMonthChange(val) {
    const currYear = getFullYear(this.preview);
    this.preview = newDate(`${currYear}/${val}/1`).getTime();
  }

  onInitialYearChange(val) {
    const currMonth = getMonth(this.preview);
    this.preview = newDate(`${val}/${currMonth}/1`).getTime();
  }

  attributeChangedCallback(attrName, oldValue, newValue) {
    super.attributeChangedCallback(attrName, oldValue, newValue);
    if (oldValue !== newValue) {
      // attrs for which affect static ui parts doesn't require calendar re-render
      if (calendarUiAttrs.includes(attrName)) {
        if (attrName === 'calendar-label-submit') {
          this.onSubmitLabelChange(newValue);
        }
        if (attrName === 'calendar-label-cancel') {
          this.onCancelLabelChange(newValue);
        }
      } else {
        // we want to render the calendar for each of these attr change

        if (newValue && attrName === 'initial-value') {
          this.onInitialValueChange(newValue);
        }
        if (newValue && attrName === 'initial-month') {
          this.onInitialMonthChange(newValue);
        }
        if (newValue && attrName === 'initial-year') {
          this.onInitialYearChange(newValue);
        }
        if (attrName === 'years-range') {
          this.onYearsRangeChange(Number(newValue));
        }
        if (attrName === 'calendar-months') {
          this.onMonthNamesChange();
        }
        if (attrName === 'calendar-label-today') {
          this.setCurrentDay(newValue);
        }
        if (attrName === 'disabled') {
          setTimeout(() => {
            this.cancelButton?.setAttribute('disabled', newValue === 'true');
          });
        }

        this.renderCalendar();
      }
    }
  }

  getValidity() {
    if (this.isRequired && !this.value) {
      return { valueMissing: true };
    }
    return {};
  }
}

const {
  calendar,
  day,
  dayEntry,
  currentDay,
  selectedDay,
  disabledDay,
  weekday,
  topNav,
  topNavSelectors,
  bottomNav,
  navPrev,
  navNext,
  navPrevRTL,
  navNextRTL,
  yearInput,
  monthInput,
} = {
  calendar: { selector: () => '.calendar' },
  topNav: { selector: () => '.nav.top-nav' },
  topNavSelectors: { selector: () => '.nav.top-nav .selectors > span' },
  bottomNav: { selector: () => '.nav.bottom-nav' },
  day: { selector: () => '.day' },
  dayEntry: { selector: () => '.day-entry' },
  currentDay: { selector: () => '.day-entry[data-current-day] .day' },
  selectedDay: { selector: () => '.day-entry[data-selected] .day' },
  disabledDay: { selector: () => '.day-entry[data-disabled="true"] .day' },
  weekday: { selector: () => '.weekday' },
  navPrev: { selector: () => '.nav-prev' },
  navNext: { selector: () => '.nav-next' },
  navPrevRTL: { selector: () => ':host([st-host-direction="rtl"]) .nav-prev' },
  navNextRTL: { selector: () => ':host([st-host-direction="rtl"]) .nav-next' },
  yearInput: { selector: () => '.year-input' },
  monthInput: { selector: () => '.month-input' },
};

export const CalendarClass = compose(
  createStyleMixin({ componentNameOverride: getComponentName('input-wrapper') }),
  createStyleMixin({
    mappings: {
      fontSize: {},
      fontFamily: {},
      hostWidth: [
        { selector: () => ':host', property: 'width' },
        { selector: 'table', property: 'width' },
      ],
      hostDirection: { property: 'direction' },

      topNavVerticalPadding: [
        { ...topNav, property: 'padding-top' },
        { ...topNav, property: 'padding-bottom' },
      ],
      topNavHorizointalPadding: [
        { ...topNav, property: 'padding-right' },
        { ...topNav, property: 'padding-left' },
      ],
      topNavAlignment: { ...topNav, property: 'justify-content' },
      topNavGap: { ...topNav, property: 'gap' },
      topNavSelectorsGap: { ...topNavSelectors, property: 'gap' },

      bottomNavVerticalPadding: [
        { ...bottomNav, property: 'padding-top' },
        { ...bottomNav, property: 'padding-bottom' },
      ],
      bottomNavHorizontalPadding: [
        { ...bottomNav, property: 'padding-right' },
        { ...bottomNav, property: 'padding-left' },
      ],
      bottomNavAlignment: { ...bottomNav, property: 'justify-content' },
      bottomNavGap: { ...bottomNav, property: 'gap' },

      navMarginBottom: { ...topNav, property: 'margin-bottom' },

      calendarPadding: [
        { ...calendar, property: 'padding-bottom' },
        { ...calendar, property: 'padding-right' },
        { ...calendar, property: 'padding-left' },
      ],

      navBorderBottomWidth: { ...topNav, property: 'border-bottom-width' },
      navBorderBottomColor: { ...topNav, property: 'border-bottom-color' },
      navBorderBottomStyle: { ...topNav, property: 'border-bottom-style' },

      weekdayFontSize: { ...weekday, property: 'font-size' },
      weekdayFontWeight: { ...weekday, property: 'font-weight' },

      currentDayBorderColor: {
        ...currentDay,
        property: 'border-color',
      },
      currentDayBorderWidth: {
        ...currentDay,
        property: 'border-width',
      },
      currentDayBorderStyle: {
        ...currentDay,
        property: 'border-style',
      },

      dayHeight: { ...dayEntry, property: 'height' },
      daySize: [
        { ...day, property: 'height' },
        { ...day, property: 'width' },
      ],
      dayPadding: { ...day, property: 'padding' },
      dayRadius: { ...day, property: 'border-radius' },
      dayTextAlign: { ...day, property: 'text-align' },
      dayBlockAlign: { ...day, property: 'margin' },
      dayTextColor: { ...day, property: 'color' },
      dayFontWeight: { ...day, property: 'font-weight' },
      dayBackgroundColor: { ...day, property: 'background-color' },
      dayCursor: { ...day, property: 'cursor' },
      dayFontSize: { ...day, property: 'font-size' },

      dayBackgroundColorHover: {
        selector: () => '.day:hover',
        property: 'background-color',
      },

      daySelectedTextdColor: {
        ...selectedDay,
        property: 'color',
      },
      daySelectedBackgroundColor: {
        ...selectedDay,
        property: 'background-color',
      },
      dayDisabledTextdColor: {
        ...disabledDay,
        property: 'color',
      },

      yearInputWidth: { ...yearInput, property: 'width' },
      monthInputWidth: { ...monthInput, property: 'width' },

      navButtonSize: [
        { ...navPrev, property: 'width' },
        { ...navPrev, property: 'height' },
        { ...navNext, property: 'width' },
        { ...navNext, property: 'height' },
      ],
      navButtonCursor: [
        { ...navPrev, property: 'cursor' },
        { ...navNext, property: 'cursor' },
      ],
      navButtonRotation: [
        { ...navPrevRTL, property: 'transform' },
        { ...navNextRTL, property: 'transform' },
      ],
      navButtonOpacity: [
        { ...navPrev, property: 'opacity' },
        { ...navNext, property: 'opacity' },
      ],
    },
  }),
  draggableMixin,
  componentNameValidationMixin
)(RawCalendar);
