<template>
  <div class="formcontrol date-input-control" :class="{ 'formcontrol--error': !isValid }">
    <div class="formcontrol-label">{{ label }}</div>
    <div class="input-wrapper" @click="openPicker">
      <input
        class="date-input"
        :class="{ 'date-input--invalid': !isValid }"
        type="text"
        :placeholder="placeholder"
        v-model="textValue"
        @change="parseNewText"
        @focus="openPicker"
      />
    </div>
    <div class="datepicker-container" :class="[{ show: isPickerOpen }, position]">
      <div class="datepicker" :class="zoom">
        <div class="datepicker-header">
          <div class="datepicker-arrow" @click="prev" tabindex="-1">chevron_left</div>
          <div class="datepicker-current" @click="goUp">{{ pickerTitle }}</div>
          <div class="datepicker-arrow" @click="next" tabindex="-1">chevron_right</div>
        </div>
        <div class="datepicker-weekday" v-if="zoom == 'month'">
          <div v-for="(d, i) in weekDays" :key="i">{{ d }}</div>
        </div>
        <div class="datepicker-items" :class="zoom">
          <div
            v-for="(d, i) in pickerContent"
            @click="selectItem(i)"
            tabindex="-1"
            :key="i"
            :class="{
              disable: d.isBefore(min) || d.isAfter(max),
              out: isOut(d),
              selected: d.isSame(valueObject, sameKey),
              today: d.isSame(today, sameKey),
            }"
          >
            {{ d.format(pickerFormat) }}
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from "vue";
import dayjs, { Dayjs, OpUnitType } from "dayjs";
// eslint-disable-next-line
const dateValidator = (d: any) => dayjs(d).isValid();
const decadeSpan = 10;
type ZoomLevel = "day" | "month" | "year" | "decade";
type ContentParameter = { getFirstDate: (d: Dayjs) => Dayjs; delta: OpUnitType; maxItem: number };
type ZoomParameter = {
  pickerTitle: (d: Dayjs) => string;
  content: ContentParameter;
  pickerFormat: string;
  outOfCurrentZoom: (activeDate: Dayjs, other: Dayjs) => boolean;
  sameKey: OpUnitType;
  nextKey: OpUnitType;
};
type PickerParameter = {
  [k in ZoomLevel]: ZoomParameter;
};
const datepickerParameter: PickerParameter = {
  day: {
    pickerTitle: (d) => d.format("ddd D MMMM YYYY"),
    content: {
      getFirstDate: (d) => d.startOf("d").startOf("h"),
      delta: "hour",
      maxItem: 24,
    },
    pickerFormat: "HH:mm",
    outOfCurrentZoom: () => false,
    sameKey: "hour",
    nextKey: "day",
  },
  month: {
    pickerTitle: (d) => d.format("MMMM YYYY"),
    content: {
      getFirstDate: (d) => d.startOf("month").startOf("week"),
      delta: "day",
      maxItem: 6 * 7,
    },
    pickerFormat: "D",
    outOfCurrentZoom: (d, other) => !d.isSame(other, "month"),
    sameKey: "day",
    nextKey: "month",
  },
  year: {
    pickerTitle: (d) => d.format("YYYY"),
    content: {
      getFirstDate: (d) => d.startOf("year"),
      delta: "month",
      maxItem: 12,
    },
    pickerFormat: "MMM",
    outOfCurrentZoom: (d, other) => !d.isSame(other, "year"),
    sameKey: "month",
    nextKey: "year",
  },
  decade: {
    pickerTitle: (d) => {
      let tmp = d.startOf("y");
      tmp = tmp.year(Math.floor(tmp.year() / decadeSpan) * decadeSpan);
      return tmp.format("YYYY") + "-" + tmp.add(decadeSpan - 1, "y").format("YYYY");
    },
    content: {
      getFirstDate: (d) =>
        d
          .startOf("y")
          .year(Math.floor(d.year() / decadeSpan) * decadeSpan)
          .subtract(1, "y"),
      delta: "year",
      maxItem: 12,
    },
    pickerFormat: "YYYY",
    outOfCurrentZoom: (d, other) =>
      Math.floor(d.year() / decadeSpan) !== Math.floor(other.year() / decadeSpan),
    sameKey: "year",
    nextKey: "year",
  },
};
export default defineComponent({
  name: "DatePicker",
  emits: ["update:modelValue"],
  props: {
    label: {
      type: String,
      required: false,
      default: () => "Select date",
    },
    modelValue: {
      type: [Number, Object, String, Date],
      required: false,
      default: () => "",
    },
    min: {
      type: [Number, Object, String, Date],
      required: false,
      default: () => {
        return dayjs().year(0);
      },
      validator: dateValidator,
    },
    max: {
      type: [Number, Object, String, Date],
      required: false,
      default: () => {
        return dayjs().year(9999);
      },
      validator: dateValidator,
    },
    lang: {
      type: String,
      required: false,
      default: "en",
    },
    position: {
      type: String,
      required: false,
      default: "",
    },
    target: {
      type: String as PropType<"day" | "hour">,
      default: "day",
    },
  },
  data: function () {
    const now = dayjs();
    return {
      isValid: true,
      today: now,
      currentMonth: now.startOf("month"),
      zoom: "month" as "day" | "month" | "year" | "decade",
      isPickerOpen: false,
      valueObject: null as Dayjs | null,
      textValue: "",
      textRegex: /^(?<year>\d{4})\/(?<month>(?:0[1-9])|(?:1[0-2]))\/(?<day>(?:0[1-9])|(?:[12][0-9])|(?:3[01]))$/,
    };
  },
  computed: {
    placeholder(): string {
      return "yyyy/mm/dd" + (this.target == "hour" ? " HH:mm" : "");
    },
    currentZoomParameter(): ZoomParameter {
      return datepickerParameter[this.zoom];
    },
    pickerTitle(): string {
      var title: string = this.currentZoomParameter.pickerTitle(this.currentMonth);
      return title[0].toUpperCase() + title.slice(1);
    },
    pickerContent(): Array<Dayjs> {
      const output: Array<Dayjs> = [];
      let firstDay = this.currentZoomParameter.content.getFirstDate(this.currentMonth);
      let delta: OpUnitType = this.currentZoomParameter.content.delta;
      let maxItem: number = this.currentZoomParameter.content.maxItem;
      for (let i = 0; i < maxItem; i++) {
        output.push(firstDay.add(i, delta));
      }
      return output;
    },
    pickerFormat(): string {
      return this.currentZoomParameter.pickerFormat;
    },
    sameKey(): OpUnitType {
      return this.currentZoomParameter.sameKey;
    },
    weekDays: function (): Array<string> {
      const start = dayjs().startOf("week");
      return Array(7)
        .fill(0)
        .map((_, i) => start.add(i, "d").format("dd"));
    },
  },
  watch: {
    modelValue: function (val) {
      if (["object", "string", "number"].includes(typeof val)) {
        const d = dayjs(val);
        if (!this.valueObject?.isSame(d)) {
          this.valueObject = dayjs(val);
        }
      }
    },
    valueObject(v: Dayjs) {
      if (v.isValid()) {
        this.currentMonth = v;
        if (this.target == "day") {
          this.textValue = v.format("YYYY/MM/DD");
          this.zoom = "month";
        } else {
          this.textValue = v.format("YYYY/MM/DD HH:mm");
          this.zoom = "day";
        }
        this.$emit("update:modelValue", v);
      } else {
        this.textValue = "";
      }
    },
  },
  methods: {
    prev(): void {
      this.changeCurrentDate(-1);
    },
    next(): void {
      this.changeCurrentDate(1);
    },
    changeCurrentDate(direction: number): void {
      const amount = direction * (this.zoom == "decade" ? decadeSpan : 1);
      this.currentMonth = this.currentMonth.add(amount, this.currentZoomParameter.nextKey);
    },
    isOut(d: Dayjs): boolean {
      return this.currentZoomParameter.outOfCurrentZoom(this.currentMonth, d);
    },
    inputEventLintener(event: InputEvent) {
      this.parseNewText((event.target as HTMLInputElement).value);
    },
    parseNewText(newValue: string) {
      let candidate: Dayjs | null = null;
      if (this.target == "day") {
        candidate = dayjs(newValue, "YYYY/M/D");
      } else {
        candidate = dayjs(newValue, "YYYY/M/D HH:mm");
      }
      if (candidate && candidate.isValid() && !this.valueObject?.isSame(candidate)) {
        this.valueObject = candidate;
        this.isValid = true;
      }
      if (candidate && !candidate.isValid()) {
        this.isValid = false;
      } else {
        this.isValid = true;
      }
    },
    selectItem: function (i: number): void {
      const d = this.pickerContent[i];
      if (
        (this.zoom == "day" && this.target == "hour") ||
        (this.zoom == "month" && this.target == "day")
      ) {
        if (d.isAfter(this.min) && d.isBefore(this.max)) {
          this.valueObject = d;
        }
      } else {
        this.currentMonth = d;
        this.zoom = this.zoom == "decade" ? "year" : this.zoom == "year" ? "month" : "day";
      }
    },
    goUp() {
      if (this.zoom === "year") {
        this.zoom = "decade";
      }
      if (this.zoom === "month") {
        this.zoom = "year";
      }
      if (this.zoom === "day") {
        this.zoom = "month";
      }
    },
    openPicker: function () {
      if (!this.isPickerOpen) {
        this.isPickerOpen = true;
        window.addEventListener("focusin", this.loseFocusListener);
        window.addEventListener("click", this.loseFocusListener);
      }
    },
    loseFocusListener: function (e: Event) {
      if (
        !this.$el.contains(e.target) ||
        (e.target as HTMLElement).classList.contains("date-input-title")
      ) {
        window.removeEventListener("focusin", this.loseFocusListener);
        window.removeEventListener("click", this.loseFocusListener);
        this.isPickerOpen = false;
        this.parseNewText(this.textValue);
      }
    },
  },
  mounted: function () {
    if (this.modelValue) {
      if (["object", "string", "number"].includes(typeof this.modelValue)) {
        this.valueObject = dayjs(this.modelValue);
      }
    }
    this.zoom = "month";
  },
});
</script>
<style>
.date-input-control {
  width: 8rem;
  margin-right: 4px;
  position: relative;
}
.date-input-title {
  color: #505d74;
  font-weight: bold;
  padding-bottom: 4px;
}
.input-wrapper {
  position: relative;
}
.input-wrapper:after {
  font-family: "Material Icons";
  font-weight: normal;
  font-style: normal;
  font-size: 20px;
  content: "calendar_today";
  position: absolute;
  right: 8px;
  top: 3px;
  color: var(--color-accent-400);
  cursor: pointer;
}

.date-input[type="text"] {
  all: initial;
  color: var(--color-neutral-100);
  width: 100%;
  border: none;
  height: 2rem;
  margin: 0;
  outline: 0;
  padding: 0.2rem 0.5rem;
  font-size: 0.875rem;
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  box-shadow: 0 -1px 0 0 var(--color-accent-700) inset;
  box-sizing: border-box;
  -webkit-transition: 0.1s ease-in-out;
  transition: 0.1s ease-in-out;
  font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif;
  line-height: 1.25rem;
  background-color: var(--color-accent-950);
  -webkit-transition-property: box-shadow, border;
  transition-property: box-shadow, border;
  position: relative;
}
.date-input[type="text"]:focus {
  border-bottom: solid 2px var(--color-accent-400);
}
.date-input[type="text"]:focus + .datepicker-container,
.date-input[type="text"]:hover + .datepicker-container {
  display: block;
}
.date-input--invalid {
  box-shadow: 0 -1px 0 0 #e93255 inset !important;
}
[type="text"].date-input--invalid:focus {
  box-shadow: 0 0 0 2px #e93255 !important;
  border-bottom: 0;
}
.datepicker-container {
  position: absolute;
  width: 315px;
  height: 350px;
  display: none;
  z-index: 10;
}
.datepicker-container.show {
  display: block;
}
.datepicker-container.same-size {
  width: 100%;
  height: auto;
  padding-top: 114%;
}
.datepicker-container.right {
  right: 0px;
}
.datepicker {
  position: absolute;
  top: 0;
  left: 0;
  bottom: 0;
  right: 0;
  background: white;
  box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.14);
  padding: 8px;
  z-index: 10;
  display: grid;
  grid-template: 1fr 7fr/ 1fr;
  row-gap: 4px;
}
.datepicker.month {
  grid-template-rows: 1fr 1fr 6fr;
}
.datepicker-header,
.datepicker-weekday,
.datepicker-items {
  color: var(--color-neutral-400);
  display: grid;
  column-gap: 4px;
  row-gap: 4px;
  grid-template-rows: 1fr;
  grid-template-columns: repeat(7, 1fr);
}
.datepicker-items.day {
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(6, 1fr);
}
.datepicker-items.month {
  grid-template-rows: repeat(6, 1fr);
}
.datepicker-items.decade,
.datepicker-items.year {
  grid-template-columns: repeat(4, 1fr);
  grid-template-rows: repeat(3, 1fr);
}
.datepicker-header > div,
.datepicker-weekday > div,
.datepicker-items > div {
  border: solid 2px transparent;
  border-radius: 3px;
  display: flex;
  justify-content: center;
  align-items: center;
  text-align: center;
}
.datepicker-arrow {
  font-family: "Material Icons";
  font-weight: normal;
  font-style: normal;
  font-size: 35px;
  text-align: center;
  color: var(--color-accent-400);
}

.datepicker-current {
  grid-column: 2/7;
  font-weight: bold;
}
.datepicker-header > div,
.datepicker-items > div {
  cursor: pointer;
}
.datepicker-header > div:hover,
.datepicker-items > div:not(.disable):hover {
  background: #f0f4f6;
  color: var(--color-neutral-100);
}
.datepicker-header > div:focus,
.datepicker-items > div:not(.disable):focus {
  border-color: var(--color-accent-500);
  border-radius: 2px;
}
.datepicker-items > div.selected:not(.disable) {
  background: var(--color-accent-500);
  color: #fff;
  font-weight: bold;
}
.datepicker-weekday > div {
  color: var(--color-neutral-600);
}
.datepicker-items .today {
  border: solid 2px var(--color-accent-500);
  font-weight: bold;
  color: var(--color-accent-500);
}
.datepicker-items .disable,
.datepicker-items .out {
  color: var(--color-neutral-800);
}
.datepicker-items .disable {
  cursor: not-allowed;
}
</style>
