<template>
  <div>
    <input
        ref="textInput"
        type="text"
        autocomplete="off"

        :id="id"
        :name="name"
        :placeholder="placeholder"
        :value="rawValue"
        :class="finalInputClass"

        @click="clickHandler"
        @blur="blurHandler"
        @input="inputHandler"
        @keydown="keydownHandler"
    />
    <div v-show="isListOpen" class="list">
      <ul>
        <li v-for="(opt, idx) in options" :key="optionKey(opt)" :class="optionClass(idx)" @mousemove="optionMousemove(idx)" @click="optionClick(opt)">
          <b class="opt_value">{{ optionValue(opt) }}</b>
          <span v-if="optionLabel(opt) !== null" class="opt_label" v-html="optionLabel(opt)"></span>
        </li>
      </ul>
    </div>
  </div>
</template>

<script setup>

  import { computed, ref, watch } from "vue";
  import debounce from 'lodash/debounce';

  const emit = defineEmits(["update:modelValue", "inputClick", "optionSelected"]);

  const props = defineProps({
    modelValue: String,
    id: String,
    placeholder: String,
    name: String,
    inputClass: {
      type: [String, Object, Array],
      required: false,
      default: null
    },
    minLength: {
      type: Number,
      default: 0
    },
    debounce: {
      type: Number,
      required: false,
      default: 250
    },

    valueAttribute: String,
    labelAttribute: String,
    keyAttribute: String,

    onGetOptions: Function,
    searchOptions: Array
  });

  const options = ref([]);
  const rawValue = ref("");
  const isListOpen = ref(false);
  const activeListIndex = ref(0);

  const finalInputClass = computed(() => {
    let cls = ['input'];
    if (props.inputClass === null) {
      return cls;
    } else if (Array.isArray(props.inputClass)) {
      return cls.concat(props.inputClass);
    } else {
      cls.push(props.inputClass);
      return cls;
    }
  });

  watch(
      () => props.modelValue,
      (newValue) => { rawValue.value = newValue; },
      { immediate: true }
  );

  function optionClass(idx) {
    return activeListIndex.value === idx ? 'option active' : 'option';
  }

  function optionClick(opt) {
    selectOption(opt);
  }

  function optionKey(opt) {
    if (props.keyAttribute) {
      return opt[props.keyAttribute]
    } else if (props.valueAttribute) {
      return opt[props.valueAttribute];
    } else {
      return opt.toString();
    }
  }

  function optionValue(opt) {
    if (props.valueAttribute) {
      return opt[props.valueAttribute];
    } else {
      return opt.toString();
    }
  }

  function optionLabel(opt) {
    if (props.labelAttribute) {
      return opt[props.labelAttribute];
    } else {
      return null;
    }
  }

  function optionMousemove(idx) {
    activeListIndex.value = idx;
  }

  function clickHandler(evt) {
    emit('inputClick', evt);
  }

  function blurHandler(evt) {
    // blur fires before click.  If the blur was fired because the user clicked a list item, immediately hiding the list here
    // would prevent the click event from firing
    setTimeout(() => {
      isListOpen.value = false;
    },250);
  }

  function inputHandler(evt) {
    const newValue = evt.target.value;

    if (rawValue.value !== newValue) {

      rawValue.value = newValue;

      emit("update:modelValue", newValue);

      if (newValue.length >= Math.max(1, props.minLength)) {
        this.updateOptions(newValue);
      } else {
        isListOpen.value = false;
      }
    }
  }

  function keydownHandler(evt) {
    if (isListOpen.value === false)
      return;

    switch (evt.key) {
      case "ArrowUp":
        evt.preventDefault();
        activeListIndex.value = Math.max(0, activeListIndex.value - 1);
        break;
      case "ArrowDown":
        evt.preventDefault();
        activeListIndex.value = Math.min(options.value.length - 1, activeListIndex.value + 1);
        break;
      case "Enter":
        evt.preventDefault();
        selectOption(options.value[activeListIndex.value]);
        break;
      case "Escape":
        evt.preventDefault();
        isListOpen.value = false;
        break;
    }
  }

  function selectOption(opt) {
    rawValue.value = optionValue(opt);
    emit("update:modelValue", rawValue.value);
    emit("optionSelected", opt);
    isListOpen.value = false;
  }

  const updateOptions = debounce(function(value) {
    let p = null;
    if (props.searchOptions) {
      const reg = new RegExp("^" + value, "i");
      const matcher = o => reg.test(optionValue(o));
      p = Promise.resolve(props.searchOptions.filter(matcher));
    } else {
      p = props.onGetOptions(value)
    }

    p.then(opts => {
      options.value = opts;
      isListOpen.value = opts.length > 0;
      activeListIndex.value = 0;
    })
  }, props.debounce);

</script>

<style lang="scss" scoped>

  @use "bulma/sass/utilities" as bulma;

  $labelLineHeight: 0.8rem;

  input.input {
    &::placeholder {
      color: bulma.$grey-darker;
    }
  }

  .list {
    position: relative;
    z-index: 150;

    ul {
      background-color: white;
      position: absolute;
      width: 100%;
      border: 1px solid black;
    }
  }

  li.option {

    padding: 4px;
    margin-bottom: 2px;

    //transition: background-color 0.25s;

    &.active {
      color: white;
      background-color: bulma.$turquoise;
    }

    .opt_value {
    }

    .opt_label {
      display: block;
      overflow: hidden;
      text-overflow: ellipsis;
      font-size: 0.8rem;
      line-height: $labelLineHeight;
      max-height: $labelLineHeight * 2;
    }
  }

</style>