<script setup>

import { nextTick, useTemplateRef, computed, ref } from 'vue';

import MenuItem from './MenuItem.vue';
import MenuItemLink from './MenuItemLink.vue';
import TextInput from './TextInput.vue';

function getViewport() {
  const win = window,
    d = document,
    e = d.documentElement,
    g = d.getElementsByTagName('body')[0],
    w = win.innerWidth || e.clientWidth || g.clientWidth,
    h = win.innerHeight || e.clientHeight || g.clientHeight;

  return {
    width: w,
    height: h
  };
}

function getHiddenElementSize(element) {
  if (element) {
    const originalVisibility = element.style.visibility;
    const originalDisplay = element.style.display;

    element.style.visibility = 'hidden';
    element.style.display = 'block';

    element.clientHeight; // force reflow

    const height = element.offsetHeight;
    const width = element.offsetWidth;

    element.style.display = originalDisplay;
    element.style.visibility = originalVisibility;

    return { height, width };
  }
  return { height: 0, width: 0 };
}


function getActiveElement(children) {
  for (const child of children) {
    if (child === document.activeElement) return child;
  }
  return null;
}

// outsideClickListener: null,
const container = useTemplateRef('container');
const itemElements = useTemplateRef('itemElements');

const emit = defineEmits([ 'close' ]);

const props = defineProps({
  icon: String,
  model: Array,
  closeOnActivation: {
    type: Boolean,
    default: true,
  },
  searchThreshold: {
    type: Number,
    default: Infinity,
  }
});

const openEventTimeStamp = ref(0);
let forElement = null;
let prevFocusElement = null;
const isOpen = ref(false);
let pageX = 0;
let targetBottom = 0;
let targetTop = 0;
const offsetY = ref(0);
const rollUp = ref(false);
const searchString = ref('');
const keyboardNav = ref(false);
const emptyItem = ref({
  label: '',
  disabled: true,
});

const hasIcons = computed(() => {
  return !!props.model.find((item) => !!item.icon);
});

const visibleItems = computed(() => {
  if (!searchString.value) return props.model;
  const s = searchString.value.toLowerCase();
  let results = [];

  let group = null;
  let filteredGroupItems = [];

  for (let item of props.model) {
    if (item.separator) {
      if (group && filteredGroupItems.length) {
        results.push(group);
        results = results.concat(filteredGroupItems);
      }

      group = item;
      filteredGroupItems = [];

      continue;
    }

    if (group && group.label?.toLowerCase().indexOf(s) !== -1) filteredGroupItems.push(item);
    else if (item.label.toLowerCase().indexOf(s) !== -1) filteredGroupItems.push(item);
  }

  // add remaining group and filtered items (also matches if no group ever set)
  if (group && filteredGroupItems.length) results.push(group);
  if (filteredGroupItems.length) results = results.concat(filteredGroupItems);

  return results;
  // return props.model.filter((item) => item.label.toLowerCase().indexOf(s) !== -1);
});

// select menu item by .label
function select(elem) {
  let next = container.value.children[0];
  while (true) {
    if (!next) return;

    if (next.innerText !== elem.label) {
      next = next.nextElementSibling;
      continue;
    }

    next.focus();
    break;
  }
}

let search = '';
let debounceTimer = null;
function onKeyDown(event) {
  // only handle alphanumerics
  if (!/^[a-zA-Z0-9]$/.test(event.key)) return;

  const key = event.key.toLowerCase();

  if (debounceTimer) clearTimeout(debounceTimer);
  debounceTimer = setTimeout(() => search = '', 500);

  search += key;

  const found = visibleItems.value.find(elem => {
    if (!elem.label) return false;
    return elem.label.toLowerCase().indexOf(search) === 0;
  });

  if (found) select(found)
}

function onItemActivated(item) {
  if (item.action instanceof Function) item.action(item);
  if (props.closeOnActivation) close();
}

function blurEventHandler() {
  close();
}

async function open(event, element = null) {
  isOpen.value = true;
  pageX = element ? element.getBoundingClientRect().left : (event.touches && event.touches[0] ? event.touches[0].pageX : event.pageX);
  targetBottom = element ? element.getBoundingClientRect().bottom : (event.touches && event.touches[0] ? event.touches[0].pageY : event.pageY);
  targetTop = element ? element.getBoundingClientRect().top : (event.touches && event.touches[0] ? event.touches[0].pageY : event.pageY);
  offsetY.value = element ? (element.getBoundingClientRect().height + 2) : 0; // offset in case we roll up
  forElement = element;
  prevFocusElement = document.activeElement;

  if (!container.value) return;

  if (element) container.value.style.minWidth = element.getBoundingClientRect().width + 'px';

  await nextTick();

  position();

  openEventTimeStamp.value = event.timeStamp;

  event.preventDefault();

  // if opened from a key action, immediately select first
  if (event.type === 'keydown' && event.key === 'ArrowUp') selectUp();
  else if (event.type === 'keydown') selectDown();
  else container.value.focus();
}

function onMouseMove(event) {
  keyboardNav.value = false;
  const item = event.target.closest('.pankow-menu-item');
  if (item && !item.classList.contains('pankow-menu-item-disabled') && item.getAttribute('separator') !== 'true') {
    item.focus();
  }
}

function onBackdrop(event) {
  close();
  event.preventDefault();
}

function selectUp() {
  keyboardNav.value = true;

  if (!container.value.children[0]) return;

  const active = getActiveElement(container.value.children);

  if (!active) return container.value.lastElementChild.focus();

  let prev = active;
  while (true) {
    prev = prev.previousElementSibling;
    if (!prev) return;

    if (prev.getAttribute('separator') === 'true') continue;
    else if (prev.classList.contains('pankow-menu-item-disabled')) continue;

    prev.focus();
    break;
  }
}

function selectDown() {
  keyboardNav.value = true;

  if (!container.value.children[0]) return;

  const active = getActiveElement(container.value.children);

  if (!active) return container.value.firstElementChild.focus();

  let next = active;
  while (true) {
    next = next.nextElementSibling;
    if (!next) return;

    if (next.getAttribute('separator') === 'true') continue;
    else if (next.classList.contains('pankow-menu-item-disabled')) continue;

    next.focus();
    break;
  }
}

function close() {
  isOpen.value = false;
  container.value.style.maxHeight = 'unset';

  // restore focus
  if (prevFocusElement) prevFocusElement.focus();
  prevFocusElement = null;

  emit('close');
}

function position() {
  if (!container.value) return;

  const size = getHiddenElementSize(container.value);
  const viewport = getViewport();

  let left = pageX;
  let top = targetBottom + 1;
  let width = container.value.offsetParent ? container.value.offsetWidth : size.width;
  let height = container.value.offsetParent ? container.value.offsetHeight : size.height;
  let bottom = viewport.height - targetTop + 1;

  //flip
  if (left + width - document.body.scrollLeft > viewport.width) {
    // if this is like a dropdown right align instead of flip
    if (forElement) {
      left = forElement.getBoundingClientRect().left + (forElement.getBoundingClientRect().width - width);
    } else {
      left -= width;
    }
  }

  //flip
  if (top + height - document.body.scrollTop > viewport.height) {
    if (top - document.body.scrollTop > viewport.height/2) {
      rollUp.value = true;
    } else {
      rollUp.value = false;
    }
  } else {
    rollUp.value = false;
  }

  //fit
  if (left < document.body.scrollLeft) {
    left = document.body.scrollLeft;
  }

  container.value.style.left = left + 'px';
  if (rollUp.value) {
    container.value.style.top = 'unset';
    container.value.style.bottom = bottom + 'px';
    container.value.style.maxHeight = viewport.height - bottom + 'px';
    container.value.style.height = 'auto';
  } else {
    container.value.style.top = top + 'px';
    container.value.style.bottom = 'unset';
    container.value.style.maxHeight = viewport.height - top + 'px';
  }
}

defineExpose({
  isOpen,
  open,
  close,
});

</script>

<template>
  <teleport to="#app">
    <div class="pankow-menu-backdrop" @click="onBackdrop($event)" @contextmenu="onBackdrop($event)" v-show="isOpen"></div>
    <Transition :name="rollUp ? 'pankow-roll-up' : 'pankow-roll-down'">
      <div class="pankow-menu" :class="{ 'pankow-menu--keyboard-nav': keyboardNav }" v-show="isOpen" ref="container" tabindex="0" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.esc.stop="close()" @keydown="onKeyDown" @mousemove="onMouseMove">
        <TextInput placeholder="Filter ..." autocomplete="off" @keydown.up.stop="selectUp()" @keydown.down.stop="selectDown()" @keydown.stop @keydown.esc.stop="close()" @click.stop style="width: 100%; border: 0; padding: 8px 12px;" v-model="searchString" v-if="searchThreshold < model.length"/>
        <component v-for="item in visibleItems" ref="itemElements" :is="item.type || (item.href ? MenuItemLink : MenuItem)" @activated="onItemActivated(item)" :item="item" :has-icons="hasIcons" />
        <MenuItem v-if="model.length === 0" :item="emptyItem"/>
      </div>
    </Transition>
  </teleport>
</template>

<style>

.pankow-menu-backdrop {
  position: fixed;
  height: 100%;
  width: 100%;
  left: 0px;
  top: 0px;
  z-index: 3001;
  background-color: transparent;
}

.pankow-menu {
  position: fixed;
  box-shadow: var(--pankow-menu-shadow);
  border-radius: var(--pankow-border-radius);
  background-color: var(--pankow-input-background-color);
  min-width: 120px;
  z-index: 3001;
  color: var(--pankow-color-dark);
  overflow: auto;
  outline: none;
}

@media (prefers-color-scheme: dark) {
  .pankow-menu {
    color: var(--pankow-color-light-dark);
  }
}

</style>
