<template>
  <div class="jskos-vue-itemSelect">
    <!-- Typeahead dropdown (coming from ItemSuggest) -->
    <ItemSuggest
      v-if="searchForSuggest"
      ref="itemSuggest"
      :search="searchForSuggest"
      :placeholder="placeholder"
      @select="onSuggestSelect" />
    
    <slot />

    <!-- Optional picker: ConceptTree (browse & pick) -->
    <div
      v-if="hasTree"
      class="jskos-vue-itemSelect-tree">
      <button
        type="button"
        @click="toggleTree">
        <arrow :direction="treeCollapsed ? 'right' : 'down'" />
        Hierarchy
      </button>

      <ConceptTree
        v-if="!treeCollapsed"
        ref="conceptTree"
        :concepts="treeConcepts"
        :model-value="treeSelected"
        :registry="registry"
        :scheme="scheme"
        @select="onTreeSelect"
        @open="onTreeOpen" />
    </div>
  </div>
</template>

<script setup>
import { ref, nextTick, computed } from "vue"
import * as jskos from "jskos-tools"
import Arrow from "./Arrow.vue"
import ItemSuggest from "./ItemSuggest.vue"
import ConceptTree from "./ConceptTree.vue"
import { localOptionsToSuggest as optionsToSuggest } from "../utils.js"

defineOptions({ name: "ItemSelect" })

const props = defineProps({
  // Local options (for small sets like languages)
  options: { type: Array, default: null },

  // Remote search returning OpenSearch Suggest: [q, labels[], desc[], uris[]]
  // (Same contract as ItemSuggest expects.)
  search: { type: Function, default: null },

  // Minimum query length before searching
  minChars: { type: Number, default: 1 },

  // Optional ConceptTree picker
  treeConcepts: { type: Array, default: null },
  treeLoadNarrower: { type: Function, default: null },

  // Optional: resolve a URI into a full item object if it wasn't in the suggestion cache
  // resolve: async (uri) => concept/item
  resolve: { type: Function, default: null },

  placeholder: { type: String, default: "Search…" },

  // load concepts via registry object
  registry: { type: Object, default: null },
  // load concepts from this concept scheme
  scheme: { type: Object, default: null },
})

const emit = defineEmits(["select"])

const conceptTree = ref(null)
const itemSuggest = ref(null)
const treeSelected = ref(null) // highlighted/active concept in ConceptTree
const treeCollapsed = ref(true)

// Cache last suggestion items by URI so we can emit full objects, not only { uri }
const cacheByUri = ref(Object.create(null))

// --- helpers (same normalization as before) ---
function itemWithScheme(item) {
  return item.inScheme?.length || !props.scheme
    ? item
    : { ...item, inScheme: [props.scheme] }
}

function normalize(item) {
  if (!item?.uri) {
    return null
  }

  const rawLabel = item.__label || jskos.prefLabel(item)

  const notation =
    (Array.isArray(item.notation) && item.notation[0]) ||
    jskos.notation(itemWithScheme(item))

  const rawLabelStr = String(rawLabel)
  const cleanedLabel =
    notation && rawLabelStr.startsWith(String(notation))
      ? rawLabelStr.slice(String(notation).length).trim()
      : rawLabelStr

  return {
    ...item,
    __label: rawLabelStr,
    notation: (Array.isArray(item.notation) && item.notation.length)
      ? item.notation
      : (notation ? [notation] : undefined),
    prefLabel: item.prefLabel
      ? { ...item.prefLabel, und: cleanedLabel }
      : { und: cleanedLabel },
  }
}

// Use the shared local-options search, but keep ItemSelect's normalization.
// The matched objects are cached so selecting a URI can emit the full item.
function localOptionsToSuggest(q) {
  return optionsToSuggest(props.options, q, {
    minChars: props.minChars,
    normalize,
    onMatches(matches) {
      const nextCache = Object.create(null)
      for (const it of matches) {
        nextCache[it.uri] = it
      }
      cacheByUri.value = nextCache
    },
  })
}

function providerSearch(q) {
  if (props.search) {
    return props.search(q)
  }
  if (props.registry && props.scheme?.uri) {
    return props.registry.suggest({ search: q, params: { voc: props.scheme.uri } })
  }
  return Promise.resolve([q, [], [], []])
}


// Wrap remote search so we can also populate cacheByUri (labels+uris -> normalized objects)
function remoteSearchWrapped(q) {
  const query = (q || "").trim()
  if (!query || query.length < props.minChars) {
    return Promise.resolve([query, [], [], []])
  }

  return Promise.resolve(providerSearch(query)).then((os) => {
    const labels = os?.[1] || []
    const uris = os?.[3] || []

    const nextCache = Object.create(null)
    for (let i = 0; i < uris.length; i++) {
      const uri = uris[i]
      const label = labels[i] || uri
      const it = normalize({ uri, __label: label })
      if (it) {
        nextCache[uri] = it
      }
    }
    cacheByUri.value = nextCache

    return os
  })
 
}

// This is the function we pass to ItemSuggest.
const searchForSuggest = computed(() => {
  if (props.options?.length) {
    return localOptionsToSuggest
  } else if (props.search) {
    return remoteSearchWrapped
  } else if (props.registry && props.scheme?.uri) {
    return remoteSearchWrapped
  } else {
    console.error("Could not create suggest function for ItemSelect")
  }
  return null
})

const hasTree = computed(() => props.treeConcepts?.length || props.registry?.getTop)

// Keep the optional tree in step with the selected concept. The tree starts
// collapsed, so opening it first gives Vue a tick to mount ConceptTree before
// navigateToUri tries to expand and highlight the selected concept.
async function syncTreeTo(concept) {
  if (!hasTree.value || !concept?.uri) {
    return
  }
  treeSelected.value = concept
  treeCollapsed.value = false
  await nextTick()
  if (!conceptTree.value) {
    return
  }
  await conceptTree.value.navigateToUri(concept, {
    select: false,
    onlyIfNotInView: true,
  })
}

async function toggleTree() {
  treeCollapsed.value = !treeCollapsed.value
  if (!treeCollapsed.value && treeSelected.value?.uri) {
    await syncTreeTo(treeSelected.value)
  }
}

const resolver = computed(() => {
  if (props.resolve) {
    return props.resolve
  } else if (props.registry?.getConcepts) {
    return async uri => props.registry.getConcepts({ concepts: [{ uri }]}).then(c => c?.[0])
  }
  return null
})

// Resolve a selected URI to an item object (cache -> resolve() -> fallback {uri})
async function itemFromUri(uri) {
  const cached = cacheByUri.value?.[uri]
  if (cached) {
    return cached
  }

  if (resolver.value) {
    try {
      const full = await resolver.value(uri)
      return normalize(full) || { uri }
    } catch(e) {
      console.error(e)
      return { uri }
    }
  }

  return { uri }
}

// --- event handlers ---
async function onSuggestSelect(ev) {
  const uri = ev?.uri
  if (!uri) {
    return
  }

  const concept = await itemFromUri(uri)
  emit("select", concept)

  await syncTreeTo(concept)
}

// ConceptTree emits { item, row, ... }
async function onTreeSelect(ev) {
  if (!ev?.item) {
    return
  }

  const concept = normalize(ev.item) || ev.item
  treeSelected.value = concept
  emit("select", concept)
}

function onTreeOpen(concept) {
  if (props.treeLoadNarrower) {
    return props.treeLoadNarrower(concept)
  }
}

defineExpose({
  focus() {
    itemSuggest.value?.focus?.()
  },
})
</script>

<style>
.jskos-vue-itemSelect {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.jskos-vue-itemSelect-tree > button {
  border: 0;
  background: transparent;
  font-size: 100%;
  margin: 0;
  padding: 0;
}
</style>
