<script setup lang="ts">
import { ref, computed } from 'vue'

const props = withDefaults(defineProps<{
  data: Uint8Array
  height?: number
  highlight?: { start: number; end: number } | null
}>(), { height: 400, highlight: null })

const emit = defineEmits<{
  'byte-hover': [offset: number | null]
}>()

const BYTES = 16
const ROW_H = 20

const wrapEl = ref<HTMLElement | null>(null)
const scrollTop = ref(0)

const totalRows = computed(() => Math.ceil(props.data.length / BYTES))
const firstRow  = computed(() => Math.floor(scrollTop.value / ROW_H))
const lastRow   = computed(() => Math.min(firstRow.value + Math.ceil(props.height / ROW_H) + 4, totalRows.value))

const rows = computed(() => {
  const out = []
  for (let r = firstRow.value; r < lastRow.value; r++) {
    const off = r * BYTES
    const slice = props.data.subarray(off, off + BYTES)
    const hex = Array.from(slice).map(b => b.toString(16).padStart(2, '0'))
    const asc = Array.from(slice).map(b => (b >= 0x20 && b < 0x7f) ? String.fromCharCode(b) : '.')
    out.push({ off, hex, asc })
  }
  return out
})

function isHi(offset: number): boolean {
  return props.highlight != null
    && offset >= props.highlight.start
    && offset < props.highlight.end
}

function onScroll(e: Event) {
  scrollTop.value = (e.target as HTMLElement).scrollTop
}

defineExpose({
  scrollTo(offset: number) {
    const row = Math.floor(offset / BYTES)
    wrapEl.value?.scrollTo({ top: row * ROW_H, behavior: 'smooth' })
  },
})
</script>

<template>
  <div ref="wrapEl" class="hd-wrap" :style="{ height: height + 'px' }" @scroll="onScroll">
    <div :style="{ height: totalRows * ROW_H + 'px', position: 'relative' }">
      <div :style="{ transform: `translateY(${firstRow * ROW_H}px)` }">
        <div v-for="row in rows" :key="row.off" class="hd-row">
          <span class="hd-off">{{ row.off.toString(16).padStart(8, '0') }}</span>

          <span class="hd-hex">
            <span
              v-for="(h, bi) in row.hex"
              :key="bi"
              :class="['hd-byte', { 'hd-byte-hi': isHi(row.off + bi) }, bi === 8 ? 'hd-byte-gap' : '']"
              @mouseenter="emit('byte-hover', row.off + bi)"
              @mouseleave="emit('byte-hover', null)"
            >{{ h }}</span>
          </span>

          <span class="hd-asc">|<span
            v-for="(a, bi) in row.asc"
            :key="bi"
            :class="['hd-asc-char', { 'hd-byte-hi': isHi(row.off + bi) }]"
            @mouseenter="emit('byte-hover', row.off + bi)"
            @mouseleave="emit('byte-hover', null)"
          >{{ a }}</span>|</span>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.hd-wrap {
  overflow-y: auto;
  font-family: 'Courier New', Courier, monospace;
  font-size: 12px;
  border: 1px solid var(--el-border-color);
  border-radius: 4px;
  background: var(--el-fill-color-blank);
}
.hd-row {
  display: flex;
  height: 20px;
  align-items: center;
  gap: 12px;
  padding: 0 8px;
}
.hd-row:hover {
  background: var(--el-fill-color-light);
}
.hd-off {
  color: var(--el-text-color-placeholder);
  min-width: 6em;
  user-select: none;
}
.hd-hex {
  display: inline-flex;
  color: var(--el-text-color-primary);
}
.hd-byte {
  display: inline-block;
  width: 3ch;
  cursor: default;
  border-radius: 2px;
}
/* Extra left margin to create the double-space gap between the two groups of 8 */
.hd-byte-gap {
  margin-left: 1ch;
}
.hd-byte-hi {
  background: var(--el-color-primary-light-7);
  color: var(--el-color-primary);
}
.hd-asc {
  color: var(--el-text-color-secondary);
}
.hd-asc-char {
  display: inline-block;
  width: 1ch;
  cursor: default;
  border-radius: 2px;
}
</style>
