<script lang="ts">
export enum TabType {
  Year,
  Month,
  Day,
  Hour,
  Minute
}
</script>

<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import {
  MAX_DATETIME,
  MIN_DATETIME,
  MIN_YEAR,
  MAX_YEAR,
  MONTH_DAY_COUNT,
  dayOfWeek,
  isLeapYear
} from '@/utils/datetime';
import { MONTH_NAMES, WEEK_NAMES } from '@/utils/i18n';

const DIAL_PLATE_WIDTH: number = 200;
const DIAL_PLATE_RADIUS: number = DIAL_PLATE_WIDTH / 2;
const DIAL_PLATE_HOUR_INNER_PERCENT: number = 0.6;
const DIAL_PLATE_HOUR_OUTTER_PERCENT: number = 0.8;
const DIAL_PLATE_HOUR_DISTINGUISH_PERCENT: number = 0.7;
const DIAL_PLATE_MINUTE_PERCENT: number = 0.8;
const DIAL_PLATE_HOUR_RESOLUTION: number = Math.PI * 2 / 12;
const DIAL_PLATE_MINUTE_RESOLUTION: number = Math.PI * 2 / 60;

const NBSP: string = '\u00A0';

const isVisible = ref(false);
const currentTab = ref<TabType>(TabType.Year);
const mode = ref<TabType>(TabType.Day);
const svgFontScale = ref<number>(1);

let enableHourDrag = false;
let enableMinuteDrag = false;

const internalDateTime = ref<Date>(new Date());
const displayCacheDateTime = ref<Date>(new Date());

const pickerContainerRef = ref<HTMLElement>();
const hourSvgRef = ref<SVGSVGElement>();
const minuteSvgRef = ref<SVGSVGElement>();

// region: Exported API and Signals

const emit = defineEmits<{
  (e: 'confirm', date: Date): void
  (e: 'cancel'): void
}>();

const clampDateTime = (d: Date): void => {
  if (d < MIN_DATETIME) d.setTime(MIN_DATETIME.getTime());
  if (d >= MAX_DATETIME) d.setTime(MAX_DATETIME.getTime());
};

const switchTab = (newTab: TabType): void => {
  displayCacheDateTime.value = new Date(internalDateTime.value);
  currentTab.value = newTab;
};

const modal = (initialDate: Date, _mode: TabType): void => {
  mode.value = _mode;
  const d = new Date(initialDate);
  clampDateTime(d);
  internalDateTime.value = new Date(d);
  displayCacheDateTime.value = new Date(d);
  isVisible.value = true;
  switchTab(_mode);
};

const confirm = (): void => {
  isVisible.value = false;
  emit('confirm', new Date(internalDateTime.value));
};

const cancel = (): void => {
  isVisible.value = false;
  emit('cancel');
};

defineExpose({ modal });

// endregion

// region: Mutator helpers
// Vue does not track in-place Date mutation, so every change clones, mutates,
// clamps, and reassigns the ref to trigger reactivity.

const updateDisplay = (mutator: (d: Date) => void): void => {
  const d = new Date(displayCacheDateTime.value);
  mutator(d);
  clampDateTime(d);
  displayCacheDateTime.value = d;
};

const updateInternal = (mutator: (d: Date) => void): void => {
  const d = new Date(internalDateTime.value);
  mutator(d);
  clampDateTime(d);
  internalDateTime.value = d;
};

// endregion

// region: Computed views (replaces legacy imperative RefreshDisplay)

const visibleTabs = computed<Set<TabType>>(() => {
  const s = new Set<TabType>();
  if (mode.value >= TabType.Year) s.add(TabType.Year);
  if (mode.value >= TabType.Month) s.add(TabType.Month);
  if (mode.value >= TabType.Day) s.add(TabType.Day);
  if (mode.value >= TabType.Hour) s.add(TabType.Hour);
  if (mode.value >= TabType.Minute) s.add(TabType.Minute);
  return s;
});

const headerParts = computed(() => ({
  year: internalDateTime.value.getFullYear(),
  month: internalDateTime.value.getMonth() + 1,
  day: internalDateTime.value.getDate(),
  hour: internalDateTime.value.getHours(),
  minute: internalDateTime.value.getMinutes(),
}));

const yearPageStart = computed<number>(() => {
  const y = displayCacheDateTime.value.getFullYear();
  return Math.floor((y - MIN_YEAR) / 12) * 12 + MIN_YEAR;
});

const yearCells = computed<{ value: number | undefined; picked: boolean }[]>(() => {
  const start = yearPageStart.value;
  const internalYear = internalDateTime.value.getFullYear();
  const cells: { value: number | undefined; picked: boolean }[] = [];
  for (let i = 0; i < 12; i++) {
    const counter = start + i;
    if (counter < MAX_YEAR) {
      cells.push({ value: counter, picked: counter === internalYear });
    } else {
      cells.push({ value: undefined, picked: false });
    }
  }
  return cells;
});

const yearRows = computed<{ value: number | undefined; picked: boolean }[][]>(() => {
  const cells = yearCells.value;
  const rows: { value: number | undefined; picked: boolean }[][] = [];
  for (let i = 0; i < 3; i++) rows.push(cells.slice(i * 4, i * 4 + 4));
  return rows;
});

const yearTitle = computed<string>(() => {
  const start = yearPageStart.value;
  return `${start} - ${Math.min(start + 12, MAX_YEAR)}`;
});

const pickedMonthIndex = computed<number>(() => {
  if (internalDateTime.value.getFullYear() === displayCacheDateTime.value.getFullYear()) {
    return internalDateTime.value.getMonth();
  }
  return -1;
});

const monthCells = computed<{ index: number; name: string; picked: boolean }[]>(() => {
  const picked = pickedMonthIndex.value;
  const cells: { index: number; name: string; picked: boolean }[] = [];
  for (let i = 0; i < 12; i++) {
    cells.push({ index: i, name: MONTH_NAMES[i]!, picked: i === picked });
  }
  return cells;
});

const monthRows = computed<{ index: number; name: string; picked: boolean }[][]>(() => {
  const cells = monthCells.value;
  const rows: { index: number; name: string; picked: boolean }[][] = [];
  for (let i = 0; i < 3; i++) rows.push(cells.slice(i * 4, i * 4 + 4));
  return rows;
});

const monthTitle = computed<number>(() => displayCacheDateTime.value.getFullYear());

const dayGrid = computed<{ value: number | undefined; picked: boolean }[][]>(() => {
  const y = displayCacheDateTime.value.getFullYear();
  const m = displayCacheDateTime.value.getMonth() + 1;
  const firstDow = dayOfWeek(y, m, 1);
  const daysInMonth = MONTH_DAY_COUNT[m - 1]! + (m === 2 && isLeapYear(y) ? 1 : 0);
  const internalDay = internalDateTime.value.getDate();
  const rows: { value: number | undefined; picked: boolean }[][] = [];
  let counter = -firstDow;
  for (let i = 0; i < 6; i++) {
    const row: { value: number | undefined; picked: boolean }[] = [];
    for (let j = 0; j < 7; j++, counter++) {
      if (counter < 0 || counter >= daysInMonth) {
        row.push({ value: undefined, picked: false });
      } else {
        const dayNum = counter + 1;
        row.push({ value: dayNum, picked: dayNum === internalDay });
      }
    }
    rows.push(row);
  }
  return rows;
});

const dayTitle = computed<string>(() => {
  const y = displayCacheDateTime.value.getFullYear();
  const m = displayCacheDateTime.value.getMonth();
  return `${y} - ${MONTH_NAMES[m]!}`;
});

const hourHand = computed(() => {
  const h = displayCacheDateTime.value.getHours();
  const angle = (3 - h) * DIAL_PLATE_HOUR_RESOLUTION;
  const radius = DIAL_PLATE_RADIUS * (h < 12 ? DIAL_PLATE_HOUR_OUTTER_PERCENT : DIAL_PLATE_HOUR_INNER_PERCENT);
  const x = Math.cos(angle) * radius + DIAL_PLATE_RADIUS;
  const y = (-Math.sin(angle) * radius) + DIAL_PLATE_RADIUS;
  return { x2: x, y2: y, cx: x, cy: y };
});

const minuteHand = computed(() => {
  const mi = displayCacheDateTime.value.getMinutes();
  const angle = (15 - mi) * DIAL_PLATE_MINUTE_RESOLUTION;
  const radius = DIAL_PLATE_RADIUS * DIAL_PLATE_MINUTE_PERCENT;
  const x = Math.cos(angle) * radius + DIAL_PLATE_RADIUS;
  const y = (-Math.sin(angle) * radius) + DIAL_PLATE_RADIUS;
  return { x2: x, y2: y, cx: x, cy: y };
});

// endregion

// region: Navigation and click handlers

const prevNextYear = (isPrev: boolean): void => {
  updateDisplay((d) => d.setFullYear(d.getFullYear() + (isPrev ? -12 : 12)));
};

// NOTE: legacy PrevNextMonth actually shifts the YEAR (the month panel browses years).
const prevNextMonth = (isPrev: boolean): void => {
  updateDisplay((d) => d.setFullYear(d.getFullYear() + (isPrev ? -1 : 1)));
};

// NOTE: legacy PrevNextDay actually shifts the MONTH (the day panel browses months).
const prevNextDay = (isPrev: boolean): void => {
  updateDisplay((d) => d.setMonth(d.getMonth() + (isPrev ? -1 : 1)));
};

const clickYear = (value: number | undefined): void => {
  if (value === undefined) return;
  updateInternal((d) => d.setFullYear(value));
  if (mode.value !== TabType.Year) switchTab(TabType.Month);
};

const clickMonth = (monthIndex: number): void => {
  updateInternal((d) => {
    d.setFullYear(displayCacheDateTime.value.getFullYear());
    d.setMonth(monthIndex);
  });
  if (mode.value !== TabType.Month) switchTab(TabType.Day);
};

const clickDay = (day: number | undefined): void => {
  if (day === undefined) return;
  updateInternal((d) => {
    d.setFullYear(displayCacheDateTime.value.getFullYear());
    d.setMonth(displayCacheDateTime.value.getMonth());
    d.setDate(day);
  });
  if (mode.value !== TabType.Day) switchTab(TabType.Hour);
};

// endregion

// region: Dial drag interaction

const getUniformedXY = (e: MouseEvent | TouchEvent, el: Element): { x: number; y: number } => {
  const rect = el.getBoundingClientRect();
  const halfWidth = rect.width / 2;
  const halfHeight = rect.height / 2;
  const halfSquare = Math.min(rect.width, rect.height) / 2;
  let clientX = 0;
  let clientY = 0;
  const te = e as TouchEvent;
  const me = e as MouseEvent;
  if (te.targetTouches && te.targetTouches.length >= 1) {
    clientX = te.targetTouches[0]!.clientX;
    clientY = te.targetTouches[0]!.clientY;
  } else {
    clientX = me.clientX;
    clientY = me.clientY;
  }
  const x = ((clientX - rect.left - halfWidth) / halfSquare) * DIAL_PLATE_RADIUS;
  const y = -(((clientY - rect.top - halfHeight) / halfSquare) * DIAL_PLATE_RADIUS);
  return { x, y };
};

const startDragHour = (): void => { enableHourDrag = true; };
const draggingHour = (e: MouseEvent | TouchEvent): void => {
  if (!enableHourDrag || !hourSvgRef.value) return;
  const { x, y } = getUniformedXY(e, hourSvgRef.value);
  const distance = Math.sqrt(x * x + y * y);
  let angle = Math.acos(x / distance);
  if (y < 0) angle = Math.PI * 2 - angle;
  angle += DIAL_PLATE_HOUR_RESOLUTION / 2;
  if (angle > Math.PI * 2) angle -= Math.PI * 2;
  let number = Math.floor(angle / DIAL_PLATE_HOUR_RESOLUTION);
  if (number >= 12) number = 11;
  number = (15 - number) % 12;
  if (distance < DIAL_PLATE_RADIUS * DIAL_PLATE_HOUR_DISTINGUISH_PERCENT) number += 12;
  if (displayCacheDateTime.value.getHours() !== number) {
    updateDisplay((d) => d.setHours(number));
  }
  e.preventDefault();
};
const stopDragHour = (): void => {
  enableHourDrag = false;
  updateInternal((d) => d.setHours(displayCacheDateTime.value.getHours()));
  if (mode.value !== TabType.Hour) switchTab(TabType.Minute);
};

const startDragMinute = (): void => { enableMinuteDrag = true; };
const draggingMinute = (e: MouseEvent | TouchEvent): void => {
  if (!enableMinuteDrag || !minuteSvgRef.value) return;
  const { x, y } = getUniformedXY(e, minuteSvgRef.value);
  const distance = Math.sqrt(x * x + y * y);
  let angle = Math.acos(x / distance);
  if (y < 0) angle = Math.PI * 2 - angle;
  angle += DIAL_PLATE_MINUTE_RESOLUTION / 2;
  if (angle > Math.PI * 2) angle -= Math.PI * 2;
  let number = Math.floor(angle / DIAL_PLATE_MINUTE_RESOLUTION);
  if (number >= 60) number = 59;
  number = (75 - number) % 60;
  if (displayCacheDateTime.value.getMinutes() !== number) {
    updateDisplay((d) => d.setMinutes(number));
  }
  e.preventDefault();
};
const stopDragMinute = (): void => {
  enableMinuteDrag = false;
  updateInternal((d) => d.setMinutes(displayCacheDateTime.value.getMinutes()));
};

// endregion

// region: Lifecycle - SVG font-size scaling

let resizeObserver: ResizeObserver | null = null;

onMounted(() => {
  resizeObserver = new ResizeObserver((entries) => {
    for (const entry of entries) {
      const w = entry.contentRect.width;
      const h = entry.contentRect.height;
      if (w > 0 && h > 0) svgFontScale.value = 200 / Math.min(w, h);
    }
  });
  if (pickerContainerRef.value) resizeObserver.observe(pickerContainerRef.value);
});

onUnmounted(() => {
  resizeObserver?.disconnect();
  resizeObserver = null;
});

// endregion
</script>

<template>
  <div class="modal" :class="{ 'is-active': isVisible }"
       style="float: left; position: fixed; top: 0; bottom: 0; left: 0; right: 0;">
    <div class="modal-background"></div>
    <div class="modal-card" style="height: 70%;">
      <header class="modal-card-head pickerHeader">
        <div v-show="visibleTabs.has(TabType.Year)" @click="switchTab(TabType.Year)">
          <small>Year</small><span>{{ headerParts.year }}</span>
        </div>
        <div v-show="visibleTabs.has(TabType.Month)" @click="switchTab(TabType.Month)">
          <small>Month</small><span>{{ headerParts.month }}</span>
        </div>
        <div v-show="visibleTabs.has(TabType.Day)" @click="switchTab(TabType.Day)">
          <small>Day</small><span>{{ headerParts.day }}</span>
        </div>
        <div v-show="visibleTabs.has(TabType.Hour)" @click="switchTab(TabType.Hour)">
          <small>Hour</small><span>{{ headerParts.hour }}</span>
        </div>
        <div v-show="visibleTabs.has(TabType.Minute)" @click="switchTab(TabType.Minute)">
          <small>Minute</small><span>{{ headerParts.minute }}</span>
        </div>
      </header>
      <div class="modal-card-body pickerContainer" ref="pickerContainerRef">
        <div v-show="currentTab === TabType.Year">
          <nav class="level is-mobile">
            <div class="level-left">
              <div class="level-item control">
                <a class="button" @click="prevNextYear(true)">
                  <span class="icon is-small"><font-awesome-icon icon="fas fa-chevron-circle-left"></font-awesome-icon></span>
                </a>
              </div>
            </div>
            <div class="level-item">{{ yearTitle }}</div>
            <div class="level-right">
              <div class="level-item control">
                <a class="button" @click="prevNextYear(false)">
                  <span class="icon is-small"><font-awesome-icon icon="fas fa-chevron-circle-right"></font-awesome-icon></span>
                </a>
              </div>
            </div>
          </nav>

          <div class="perfectTable">
            <div v-for="(row, ri) in yearRows" :key="ri">
              <div v-for="(cell, ci) in row" :key="ci"
                   :class="{ picked: cell.picked }"
                   @click="clickYear(cell.value)">{{ cell.value ?? NBSP }}</div>
            </div>
          </div>
        </div>

        <div v-show="currentTab === TabType.Month">
          <nav class="level is-mobile">
            <div class="level-left">
              <div class="level-item control">
                <a class="button" @click="prevNextMonth(true)">
                  <span class="icon is-small"><font-awesome-icon icon="fas fa-chevron-circle-left"></font-awesome-icon></span>
                </a>
              </div>
            </div>
            <div class="level-item">{{ monthTitle }}</div>
            <div class="level-right">
              <div class="level-item control">
                <a class="button" @click="prevNextMonth(false)">
                  <span class="icon is-small"><font-awesome-icon icon="fas fa-chevron-circle-right"></font-awesome-icon></span>
                </a>
              </div>
            </div>
          </nav>

          <div class="perfectTable">
            <div v-for="(row, ri) in monthRows" :key="ri">
              <div v-for="(cell, ci) in row" :key="ci"
                   :class="{ picked: cell.picked }"
                   @click="clickMonth(cell.index)">{{ cell.name }}</div>
            </div>
          </div>
        </div>

        <div v-show="currentTab === TabType.Day">
          <nav class="level is-mobile">
            <div class="level-left">
              <div class="level-item control">
                <a class="button" @click="prevNextDay(true)">
                  <span class="icon is-small"><font-awesome-icon icon="fas fa-chevron-circle-left"></font-awesome-icon></span>
                </a>
              </div>
            </div>
            <div class="level-item">{{ dayTitle }}</div>
            <div class="level-right">
              <div class="level-item control">
                <a class="button" @click="prevNextDay(false)">
                  <span class="icon is-small"><font-awesome-icon icon="fas fa-chevron-circle-right"></font-awesome-icon></span>
                </a>
              </div>
            </div>
          </nav>

          <div class="perfectTable">
            <div>
              <div v-for="(name, wi) in WEEK_NAMES" :key="wi">{{ name }}</div>
            </div>
            <div v-for="(row, ri) in dayGrid" :key="ri">
              <div v-for="(cell, ci) in row" :key="ci"
                   :class="{ picked: cell.picked }"
                   @click="clickDay(cell.value)">{{ cell.value ?? NBSP }}</div>
            </div>
          </div>
        </div>

        <svg v-show="currentTab === TabType.Hour" ref="hourSvgRef"
             xmlns="http://www.w3.org/2000/svg" version="1.1"
             preserveAspectRatio="xMidYMid" viewBox="0 0 200 200"
             :style="{ fontSize: svgFontScale + 'em' }"
             @mousedown="startDragHour" @mousemove="draggingHour" @mouseup="stopDragHour"
             @touchstart="startDragHour" @touchmove="draggingHour" @touchend="stopDragHour">
          <circle cx="100.000000" cy="100.000000" r="100.000000" type="background"></circle>
          <line x1="100" y1="100" :x2="hourHand.x2" :y2="hourHand.y2"></line>
          <circle :cx="hourHand.cx" :cy="hourHand.cy" r="1em" type="symbol"></circle>

          <text x="100.000000" y="20.000000">0</text>
          <text x="140.000000" y="30.717968">1</text>
          <text x="169.282032" y="60.000000">2</text>
          <text x="180.000000" y="100.000000">3</text>
          <text x="169.282032" y="140.000000">4</text>
          <text x="140.000000" y="169.282032">5</text>
          <text x="100.000000" y="180.000000">6</text>
          <text x="60.000000" y="169.282032">7</text>
          <text x="30.717968" y="140.000000">8</text>
          <text x="20.000000" y="100.000000">9</text>
          <text x="30.717968" y="60.000000">10</text>
          <text x="60.000000" y="30.717968">11</text>
          <text x="100.000000" y="40.000000">12</text>
          <text x="130.000000" y="48.038476">13</text>
          <text x="151.961524" y="70.000000">14</text>
          <text x="160.000000" y="100.000000">15</text>
          <text x="151.961524" y="130.000000">16</text>
          <text x="130.000000" y="151.961524">17</text>
          <text x="100.000000" y="160.000000">18</text>
          <text x="70.000000" y="151.961524">19</text>
          <text x="48.038476" y="130.000000">20</text>
          <text x="40.000000" y="100.000000">21</text>
          <text x="48.038476" y="70.000000">22</text>
          <text x="70.000000" y="48.038476">23</text>
        </svg>

        <svg v-show="currentTab === TabType.Minute" ref="minuteSvgRef"
             xmlns="http://www.w3.org/2000/svg" version="1.1"
             preserveAspectRatio="xMidYMid" viewBox="0 0 200 200"
             :style="{ fontSize: svgFontScale + 'em' }"
             @mousedown="startDragMinute" @mousemove="draggingMinute" @mouseup="stopDragMinute"
             @touchstart="startDragMinute" @touchmove="draggingMinute" @touchend="stopDragMinute">
          <circle cx="100.000000" cy="100.000000" r="100.000000" type="background"></circle>
          <line x1="100" y1="100" :x2="minuteHand.x2" :y2="minuteHand.y2"></line>
          <circle :cx="minuteHand.cx" :cy="minuteHand.cy" r="1em" type="symbol"></circle>

          <text x="100.000000" y="20.000000">0</text>
          <text x="140.000000" y="30.717968">5</text>
          <text x="169.282032" y="60.000000">10</text>
          <text x="180.000000" y="100.000000">15</text>
          <text x="169.282032" y="140.000000">20</text>
          <text x="140.000000" y="169.282032">25</text>
          <text x="100.000000" y="180.000000">30</text>
          <text x="60.000000" y="169.282032">35</text>
          <text x="30.717968" y="140.000000">40</text>
          <text x="20.000000" y="100.000000">45</text>
          <text x="30.717968" y="60.000000">50</text>
          <text x="60.000000" y="30.717968">55</text>
        </svg>

      </div>
      <footer class="modal-card-foot">
        <a class="button is-success" @click="confirm">
          <span>OK</span>
        </a>
        <a class="button" @click="cancel">
          <span>Cancel</span>
        </a>
      </footer>
    </div>
  </div>
</template>

<style scoped>
div.perfectTable {
  display: flex;
  flex-flow: column;
}

/*
div.perfectTable > div > div:nth-child(1) {
    border-left: 1px solid black;
}

div.perfectTable > div:nth-child(1) > div {
    border-top: 1px solid black;
}
*/

div.perfectTable>div>div {
  /*border-top: 0 solid black;
    border-left: 0 solid black;
    border-right: 1px solid black;
    border-bottom: 1px solid black;

    padding: 0.75em;*/

  flex-grow: 1;
  flex-basis: 0;
  flex-shrink: 0;

  overflow: hidden;

  display: flex;
  justify-content: center;

  padding-top: 1rem;
  padding-bottom: 1rem;
}

div.perfectTable>div {
  display: flex;
  flex-flow: row;
}

div.perfectTable>div>div.picked {
  background: hsl(171, 100%, 41%);
  color: #fff;
}






div.pickerContainer {
  display: flex;
  align-items: center;
  flex-flow: row;
  justify-content: center;
}

div.pickerContainer>div {
  flex-grow: 1;
  flex-shrink: 1;
  flex-basis: 0;
}

div.pickerContainer>svg {
  width: 100%;
  height: 100%;
}

div.pickerContainer>svg>text {
  dominant-baseline: middle;
  text-anchor: middle;
  user-select: none;
  cursor: default;
}

div.pickerContainer>svg>circle[type=background] {
  stroke-width: 0;
  fill: #d0d0d0;
}

div.pickerContainer>svg>circle[type=symbol] {
  stroke-width: 0;
  fill: hsl(171, 100%, 41%);
  /* $primary */
}

div.pickerContainer>svg>line {
  stroke-width: 0.125em;
  stroke: hsl(171, 100%, 41%);
  /* $primary */
}




header.pickerHeader {
  display: flex;
  flex-flow: row;

  flex-grow: 0;
  flex-basis: 0;
  flex-shrink: 0;

  justify-content: space-between;
}

header.pickerHeader>div {
  display: flex;
  flex-flow: column;

  flex-grow: 1;
  flex-basis: 0;
  flex-shrink: 0;
}
</style>
