<template>
  <div
    :class="dropdownOnly ? 'eg-select-dropdown' : 'eg-select-wrapper'"
    :style="dropdownOnly ? overlayStyle : null"
    @mousedown="onRootMouseDown"
    @scroll.passive="dropdownOnly && _hideTooltip()"
  >
    <!-- Standalone mode: trigger button -->
    <div
      v-if="!dropdownOnly"
      class="eg-select-trigger"
      :class="{ 'eg-select-trigger--disabled': disabled, 'eg-select-trigger--open': open }"
      @click.stop="toggle"
    >
      <span
        class="eg-select-trigger-label"
        :class="{ 'eg-select-placeholder': selectedLabel == null }"
        @mouseenter="_showTooltip(selectedTooltip, $event.currentTarget)"
        @mouseleave="_hideTooltip"
      >
        {{ selectedLabel != null ? selectedLabel : placeholder }}
      </span>
      <span class="eg-select-caret">▾</span>
    </div>

    <!-- Options panel in grid dropdown-only mode -->
    <template v-if="dropdownOnly">
      <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
        <input
          ref="searchInput"
          class="eg-select-search-input"
          :value="searchQuery"
          placeholder="Qidirish..."
          autocomplete="off"
          autocorrect="off"
          autocapitalize="off"
          spellcheck="false"
          @input="onSearch"
          @keydown.stop="onSearchKey"
        />
      </div>
      <div
        v-for="(opt, idx) in filteredOptions"
        :key="opt.id != null ? opt.id : String(opt.value)"
        class="eg-select-option"
        :class="{
          'eg-select-option--active': String(opt.value) === String(value),
          'eg-select-option--highlighted': idx === highlight
        }"
        :style="!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null"
        @mousedown.prevent="pickOption(opt)"
        @mouseover="highlight = idx"
        @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
        @mouseleave="_hideTooltip"
      >
        <span
          v-if="!searchQuery"
          class="eg-tree-toggle"
          :class="{ 'eg-tree-toggle--leaf': !opt.hasChildren }"
          @mousedown.prevent.stop="toggleNode(idx)"
        >{{ opt.hasChildren ? (opt.expanded ? '▾' : '▸') : '' }}</span>
        <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
        {{ opt.label }}
      </div>
      <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
      <div v-if="clearable && selectedLabel != null" class="eg-select-footer">
        <button class="eg-select-btn eg-select-btn--clear" tabindex="-1" @mousedown.prevent="pickClear">{{ t.clear }}</button>
      </div>
    </template>

    <!-- Options panel in standalone mode -->
    <div
      v-else-if="open"
      class="eg-select-dropdown eg-select-dropdown--floating"
      @mousedown.stop.prevent
      @scroll.passive="_hideTooltip"
    >
      <div v-if="enableSearch" class="eg-select-search" @mousedown.stop>
        <input
          ref="searchInput"
          class="eg-select-search-input"
          :value="searchQuery"
          placeholder="Qidirish..."
          autocomplete="off"
          autocorrect="off"
          autocapitalize="off"
          spellcheck="false"
          @input="onSearch"
          @keydown.stop="onSearchKey"
        />
      </div>
      <div
        v-for="(opt, idx) in filteredOptions"
        :key="opt.id != null ? opt.id : String(opt.value)"
        class="eg-select-option"
        :class="{
          'eg-select-option--active': String(opt.value) === String(value),
          'eg-select-option--highlighted': idx === highlight
        }"
        :style="!searchQuery ? { paddingLeft: (12 + opt.depth * 16) + 'px' } : null"
        @mousedown.prevent="pickOption(opt)"
        @mouseover="highlight = idx"
        @mouseenter="_showTooltip(opt.tooltip, $event.currentTarget)"
        @mouseleave="_hideTooltip"
      >
        <span
          v-if="!searchQuery"
          class="eg-tree-toggle"
          :class="{ 'eg-tree-toggle--leaf': !opt.hasChildren }"
          @mousedown.prevent.stop="toggleNode(idx)"
        >{{ opt.hasChildren ? (opt.expanded ? '▾' : '▸') : '' }}</span>
        <span v-if="opt.color" class="eg-select-dot" :style="{ background: opt.color }"></span>
        {{ opt.label }}
      </div>
      <div v-if="!filteredOptions.length" class="eg-select-empty">{{ t.noOptions }}</div>
      <div v-if="clearable && selectedLabel != null" class="eg-select-footer">
        <button class="eg-select-btn eg-select-btn--clear" tabindex="-1" @mousedown.prevent="pickClear">{{ t.clear }}</button>
      </div>
    </div>

    <div v-if="hoverTooltip" class="eg-option-tooltip" :style="hoverTooltipStyle">{{ hoverTooltip.text }}</div>
  </div>
</template>

<script>
// Approximate rendered height of one .eg-select-option row per grid `size`,
// matching the .eg-size-{small,large}/.eg-size-middle (default) CSS overrides
// (see ExcelGrid.vue's <style>) — used only to estimate whether the dropdown
// fits below the trigger before it's actually rendered (see overlayStyle).
const OPTION_ROW_H = { small: 22, middle: 28, large: 33 }

import { getMessages } from '../utils/i18n.js'

export default {
  name: 'EgTreeSelect',
  model: { prop: 'value', event: 'input' },
  props: {
    value: { default: null },
    options: { type: Array, default: () => [] },
    valueKey: { type: String, default: 'value' },
    labelKey: { type: String, default: 'label' },
    childrenKey: { type: String, default: 'children' },
    enableSearch: { type: Boolean, default: false },
    placeholder: { type: String, default: '' },
    disabled: { type: Boolean, default: false },
    dropdownOnly: { type: Boolean, default: false },
    triggerRect: { type: Object, default: null },
    size: { type: String, default: 'middle' },
    locale: { type: String, default: 'en' },
    clearable: { type: Boolean, default: true }
  },
  data() {
    return {
      open: false,
      searchQuery: '',
      highlight: -1,
      expandedMap: {},
      // True right after Arrow/Page keyboard navigation — lets Space commit the
      // highlighted option instead of typing a literal space into the search box.
      // Reset on every text edit so normal multi-word search typing is unaffected.
      _navigatedByKeyboard: false,
      // { text, rect } while a themed option tooltip should be visible — see
      // _showTooltip/_hideTooltip. rect is a snapshot from getBoundingClientRect()
      // at the moment the hover delay elapsed.
      hoverTooltip: null,
      _tooltipTimer: null
    }
  },
  computed: {
    t() { return getMessages(this.locale) },
    keys() {
      return { valueKey: this.valueKey, labelKey: this.labelKey, childrenKey: this.childrenKey }
    },
    flatOptions() {
      return this._treeFlatten(this.options || [], this.keys, this.expandedMap, 0, [])
    },
    allFlatOptions() {
      if (!this.enableSearch) return null
      return this._treeFlattenAll(this.options || [], this.keys)
    },
    filteredOptions() {
      if (!this.enableSearch || !this.searchQuery.trim()) return this.flatOptions
      const q = this.searchQuery.trim().toLowerCase()
      const pool = this.allFlatOptions || this.flatOptions
      return pool.filter(o => String(o.label || '').toLowerCase().includes(q))
    },
    selectedLabel() {
      const v = String(this.value == null ? '' : this.value)
      if (!v) return null
      const found = this._treeFindLabel(this.options || [], this.keys, v)
      return found != null ? found : null
    },
    selectedTooltip() {
      const v = String(this.value == null ? '' : this.value)
      if (!v) return null
      const node = this._treeFindNode(this.options || [], this.keys, v)
      if (!node) return null
      const label = node[this.keys.labelKey] != null ? node[this.keys.labelKey] : String(node[this.keys.valueKey])
      return node.tooltip != null ? node.tooltip : label
    },
    overlayStyle() {
      if (!this.dropdownOnly || !this.triggerRect) return { position: 'fixed', zIndex: 9999 }
      const rect = this.triggerRect
      const searchH = this.enableSearch ? 42 : 0
      const rowH = OPTION_ROW_H[this.size] || OPTION_ROW_H.middle
      const approxH = Math.min(this.filteredOptions.length * rowH + 8 + searchH, 320)
      const spaceBelow = window.innerHeight - rect.bottom
      const top = spaceBelow >= approxH ? rect.bottom : rect.top - approxH
      return {
        position: 'fixed',
        top: top + 'px',
        left: rect.left + 'px',
        minWidth: Math.max(rect.width, 120) + 'px',
        maxWidth: Math.min(360, window.innerWidth - rect.left - 16) + 'px',
        zIndex: 9999
      }
    },
    hoverTooltipStyle() {
      if (!this.hoverTooltip) return {}
      const { rect, text } = this.hoverTooltip
      const estH = 20 + Math.ceil(text.length / 40) * 16
      const spaceBelow = window.innerHeight - rect.bottom
      const top = spaceBelow >= estH + 8 ? rect.bottom + 6 : rect.top - estH - 6
      const left = Math.min(rect.left, window.innerWidth - 280 - 8)
      return { position: 'fixed', top: top + 'px', left: Math.max(8, left) + 'px', zIndex: 10000 }
    }
  },
  watch: {
    open(val) {
      if (val && this.enableSearch) {
        this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })
      }
    }
  },
  mounted() {
    if (this.dropdownOnly) {
      this._initExpanded()
      this._scrollIndexIntoView(this.highlight)
      if (this.enableSearch) {
        this.$nextTick(() => { this.$refs.searchInput && this.$refs.searchInput.focus() })
      }
    } else {
      document.addEventListener('click', this._onDocClick)
    }
  },
  beforeDestroy() {
    this._clearTooltipTimer()
    if (!this.dropdownOnly) {
      document.removeEventListener('click', this._onDocClick)
    }
  },
  methods: {
    _initExpanded() {
      const v = String(this.value == null ? '' : this.value)
      const path = this._treeFindPath(this.options || [], this.keys, v, []) || []
      const expandedMap = {}
      path.forEach(id => { expandedMap[id] = true })
      this.expandedMap = expandedMap
      const flat = this.flatOptions
      this.highlight = flat.findIndex(o => String(o.value) === v)
    },
    toggle() {
      if (this.disabled) return
      this.open = !this.open
      if (this.open) {
        this.searchQuery = ''
        this._initExpanded()
      }
    },
    pickOption(opt) {
      this.$emit('input', opt.value)
      this.$emit('select', { value: opt.value, label: opt.label })
      if (!this.dropdownOnly) {
        this.open = false
        this.searchQuery = ''
        this.highlight = -1
      }
    },
    // Reuses pickOption's write path (same input/select emit + dropdown-close
    // handling) — a clear is just "pick the empty option", not a separate flow.
    pickClear() {
      this.pickOption({ value: '', label: '' })
    },
    toggleNode(idx) {
      const opt = this.flatOptions[idx]
      if (!opt || !opt.hasChildren) return
      const next = { ...this.expandedMap }
      if (next[opt.id]) delete next[opt.id]
      else next[opt.id] = true
      this.expandedMap = next
      const newFlat = this.flatOptions
      const newIdx = newFlat.findIndex(o => o.id === opt.id)
      this.highlight = newIdx >= 0 ? newIdx : this.highlight
    },
    onSearch(e) {
      this._hideTooltip()
      this.searchQuery = e.target.value
      this.highlight = 0
      this._navigatedByKeyboard = false
    },
    onSearchKey(e) {
      const key = e.key
      const last = this.filteredOptions.length - 1
      if (key === 'Escape') {
        e.preventDefault()
        if (this.dropdownOnly) this.$emit('close')
        else { this.open = false; this.searchQuery = '' }
      } else if (key === 'ArrowDown') {
        e.preventDefault()
        this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)
        this._navigatedByKeyboard = true
        this._scrollHighlightedIntoView()
      } else if (key === 'ArrowUp') {
        e.preventDefault()
        this.highlight = Math.max((this.highlight || 0) - 1, 0)
        this._navigatedByKeyboard = true
        this._scrollHighlightedIntoView()
      } else if (key === 'PageDown') {
        e.preventDefault()
        this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)
        this._navigatedByKeyboard = true
        this._scrollHighlightedIntoView()
      } else if (key === 'PageUp') {
        e.preventDefault()
        this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)
        this._navigatedByKeyboard = true
        this._scrollHighlightedIntoView()
      } else if (key === 'Enter') {
        e.preventDefault()
        const opt = this.filteredOptions[this.highlight]
        if (opt) this.pickOption(opt)
      } else if (key === ' ' && this._navigatedByKeyboard) {
        // Space commits the option reached via Arrow/Page navigation instead of
        // typing a literal space — a plain (non-navigated) space still types normally.
        e.preventDefault()
        const opt = this.filteredOptions[this.highlight]
        if (opt) this.pickOption(opt)
      }
    },
    handleKey(e) {
      const key = e.key
      const last = this.filteredOptions.length - 1
      if (key === 'Escape') {
        e.preventDefault()
        this.$emit('close')
      } else if (key === 'Enter' || key === ' ') {
        e.preventDefault()
        const opt = this.filteredOptions[this.highlight]
        if (opt) this.pickOption(opt)
        else this.$emit('close')
      } else if (key === 'ArrowDown') {
        e.preventDefault()
        this.highlight = Math.min((this.highlight < 0 ? -1 : this.highlight) + 1, last)
        this._scrollHighlightedIntoView()
      } else if (key === 'ArrowUp') {
        e.preventDefault()
        this.highlight = Math.max((this.highlight || 0) - 1, 0)
        this._scrollHighlightedIntoView()
      } else if (key === 'PageDown') {
        e.preventDefault()
        this.highlight = this.highlight < 0 ? 0 : Math.min(this.highlight + 8, last)
        this._scrollHighlightedIntoView()
      } else if (key === 'PageUp') {
        e.preventDefault()
        this.highlight = this.highlight < 0 ? 0 : Math.max(this.highlight - 8, 0)
        this._scrollHighlightedIntoView()
      } else if (!this.searchQuery && (key === 'ArrowRight' || key === 'ArrowLeft')) {
        e.preventDefault()
        const opt = this.flatOptions[this.highlight]
        if (opt && opt.hasChildren) {
          if (key === 'ArrowRight' && !opt.expanded) this.toggleNode(this.highlight)
          if (key === 'ArrowLeft' && opt.expanded) this.toggleNode(this.highlight)
        }
      }
    },
    _scrollHighlightedIntoView() {
      this.$nextTick(() => {
        const items = this.$el.querySelectorAll('.eg-select-option')
        const el = items[this.highlight]
        if (!el) return
        const container = this.$el
        const searchEl = container.querySelector('.eg-select-search')
        const stickyH = searchEl ? searchEl.offsetHeight : 0
        const elTop = el.offsetTop
        const elBottom = elTop + el.offsetHeight
        const visTop = container.scrollTop + stickyH
        const visBottom = container.scrollTop + container.clientHeight
        if (elTop < visTop) {
          container.scrollTop = elTop - stickyH
        } else if (elBottom > visBottom) {
          container.scrollTop = elBottom - container.clientHeight
        }
      })
    },
    // Aligns option `idx` to the top of the visible list on initial open.
    // Setting scrollTop past the container's max is clamped by the browser,
    // so an option near the end of the list naturally ends up scrolled to
    // the bottom instead of leaving blank space below it.
    _scrollIndexIntoView(idx) {
      if (idx < 0) return
      this.$nextTick(() => {
        const items = this.$el.querySelectorAll('.eg-select-option')
        const el = items[idx]
        if (!el) return
        const container = this.$el
        const searchEl = container.querySelector('.eg-select-search')
        const stickyH = searchEl ? searchEl.offsetHeight : 0
        container.scrollTop = el.offsetTop - stickyH
      })
    },
    onRootMouseDown(e) {
      if (this.dropdownOnly) e.preventDefault()
    },
    _onDocClick(e) {
      if (!this.open) return
      if (this.$el && this.$el.contains(e.target)) return
      this.open = false
    },
    _treeFlatten(nodes, keys, expandedMap, depth, pathArr) {
      let out = []
      ;(nodes || []).forEach((node, idx) => {
        const path = pathArr.concat(idx)
        const id = path.join('-')
        const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []
        const hasChildren = children.length > 0
        const expanded = hasChildren && !!expandedMap[id]
        const label = node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])
        out.push({ value: node[keys.valueKey], label, color: node.color, tooltip: node.tooltip != null ? node.tooltip : label, id, depth, hasChildren, expanded })
        if (expanded) out = out.concat(this._treeFlatten(children, keys, expandedMap, depth + 1, path))
      })
      return out
    },
    _treeFlattenAll(nodes, keys) {
      const result = []
      const walk = (list) => {
        ;(list || []).forEach(node => {
          const children = Array.isArray(node[keys.childrenKey]) ? node[keys.childrenKey] : []
          const label = node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])
          result.push({ value: node[keys.valueKey], label, color: node.color, tooltip: node.tooltip != null ? node.tooltip : label, id: String(node[keys.valueKey]), depth: 0, hasChildren: false, expanded: false })
          if (children.length) walk(children)
        })
      }
      walk(nodes)
      return result
    },
    _treeFindLabel(nodes, keys, value) {
      for (const node of (nodes || [])) {
        if (String(node[keys.valueKey]) === value) {
          return node[keys.labelKey] != null ? node[keys.labelKey] : String(node[keys.valueKey])
        }
        const found = this._treeFindLabel(node[keys.childrenKey], keys, value)
        if (found != null) return found
      }
      return null
    },
    _treeFindNode(nodes, keys, value) {
      for (const node of (nodes || [])) {
        if (String(node[keys.valueKey]) === value) return node
        const found = this._treeFindNode(node[keys.childrenKey], keys, value)
        if (found != null) return found
      }
      return null
    },
    _treeFindPath(nodes, keys, value, pathArr) {
      for (let i = 0; i < (nodes || []).length; i++) {
        const node = nodes[i]
        const path = pathArr.concat(i)
        if (String(node[keys.valueKey]) === value) return [path.join('-')]
        const children = node[keys.childrenKey]
        if (Array.isArray(children)) {
          const sub = this._treeFindPath(children, keys, value, path)
          if (sub) return [path.join('-')].concat(sub)
        }
      }
      return null
    },
    // Shared by option rows and the closed trigger label — shows a themed
    // popover (see .eg-option-tooltip) after a short hover delay, instead of
    // the browser's native title tooltip.
    _showTooltip(text, el) {
      this._clearTooltipTimer()
      if (!text) return
      const rect = el.getBoundingClientRect()
      this._tooltipTimer = setTimeout(() => {
        this.hoverTooltip = { text, rect }
      }, 400)
    },
    _hideTooltip() {
      this._clearTooltipTimer()
      this.hoverTooltip = null
    },
    _clearTooltipTimer() {
      if (this._tooltipTimer) {
        clearTimeout(this._tooltipTimer)
        this._tooltipTimer = null
      }
    }
  }
}
</script>

<style>
/* Tree toggle */
.eg-tree-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  flex-shrink: 0;
  font-size: 10px;
  color: var(--eg-color-text-muted, var(--eg-color-text-muted-default));
  margin-top: 1px;
}
.eg-tree-toggle--leaf {
  visibility: hidden;
}
</style>
