<template>
  <div ref="top_pad"></div>
  <div class="top_bar" ref="top_bar">
    <h1>
      Salis data server »
      <span class="opts_name">
        {{ opts.name }}
        {{ query_in_progress ? '⧖' : '✓' }}
      </span>
    </h1>
    <form @change="trigger_reload">
      <span class="nobr">Entries: <input class="input_small" v-model="entries" /></span><wbr />
      <span class="nobr">nth: <input class="input_small" v-model="nth" /></span><wbr />
      <span class="nobr">X-axis: <select class="input_small" v-model="x_axis"><option v-for="axis in x_axes">{{ axis }}</option></select></span><wbr />
      <span class="nobr">X-low: <input v-model="x_low" /></span><wbr />
      <span class="nobr">X-high: <input v-model="x_high" /></span><wbr />
      <span class="nobr">Left: <input class="input_small" v-model="hm_left" /></span><wbr />
      <span class="nobr">Px-count: <input class="input_small" v-model="hm_px_count" /></span><wbr />
      <span class="nobr">Px-pow: <input class="input_small" v-model="hm_px_pow" /></span><wbr />
      <span class="reset_button" @click="reset_inputs">↻</span>
    </form>
  </div>
  <Section name="Options" visible>
    <table>
      <tr v-for="opt_fmt in opt_fmts">
        <td>{{ opt_fmt[0] }}:</td>
        <td>{{ opt_fmt[2](opts[opt_fmt[1]]) }}</td>
      </tr>
    </table>
  </Section>
  <!-- Render plots after simulation options have been loaded -->
  <div v-if="loaded">
    <Section :name="section" grid ref="plot_sections" :visible="section === 'General'" triggers_reload v-for="(section_plots, section) in plots">
      <Plot :name="name" :section="section" v-for="(_, name) in section_plots" />
    </Section>
    <Section :name="section" grid ref="heatmap_sections" triggers_reload v-for="(section_heatmaps, section) in heatmaps">
      <Plot is_heatmap :name="name" :section="section" v-for="(_, name) in section_heatmaps" />
    </Section>
  </div>
</template>

<script setup>
import { onMounted, provide, useTemplateRef, ref, watch } from 'vue'

import Plot from './Plot.vue'
import Section from './Section.vue'

const root = window.location.href
const id = v => v
const hex = v => v !== undefined ? `0x${v.toString(16)}` : ''
const hex_pow = v => v !== undefined ? `0x${(2 ** v).toString(16)}` : ''
const disabled = v => v ? 'disabled' : 'enabled'

const opt_fmts = [
  ['Ancestor', 'anc', id],
  ['Architecture', 'arch', id],
  ['Auto-save interval', 'auto_save_pow', hex_pow],
  ['Clones', 'clones', id],
  ['Cores', 'cores', id],
  ['Data push interval', 'data_push_pow', hex_pow],
  ['Mutator flip bit', 'muta_flip', id],
  ['Mutator range', 'muta_pow', hex_pow],
  ['Memory vector size', 'mvec_pow', hex_pow],
  ['Save file compression', 'no_compress', disabled],
  ['Seed', 'seed', hex],
]

let visible_plot_tables = []
let visible_heatmap_tables = []
let query_timeout = null
let plot_x_low = 0
let plot_redraw = false
let mvec_size = 0

const int_max = Number.MAX_SAFE_INTEGER
const uint32_max = (2 ** 32) - 1
const hm_max_pixels = 2 ** 11

const entries_def = 2000
const nth_def = 1
const x_axis_def = 'rowid'
const x_low_def = hex(0)
const x_high_def = hex(int_max)
const hm_left_def = hex(0)
const hm_px_count_def = hex(2 ** 10)

const entries = ref(entries_def)
const nth = ref(nth_def)
const x_axes = ref(['rowid', 'step'])
const x_axis = ref(x_axis_def)
const x_low = ref(x_low_def)
const x_high = ref(x_high_def)
const hm_left = ref(hm_left_def)
const hm_px_count = ref(hm_px_count_def)
const hm_px_pow = ref(hex(0))

const opts = ref({})
const plots = ref({})
const heatmaps = ref({})
const loaded = ref(false)

const query_in_progress = ref(false)
const data = ref([])

const top_pad = useTemplateRef('top_pad')
const top_bar = useTemplateRef('top_bar')
const plot_sections = useTemplateRef('plot_sections')
const heatmap_sections = useTemplateRef('heatmap_sections')

const adjust_top_bar = () => top_pad.value.style.height = `${Math.round(top_bar.value.getBoundingClientRect().height)}px`

const update_visible_tables = () => {
  const plot_section_visibility = plot_sections.value.map(section => section.visible)
  const heatmap_section_visibility = heatmap_sections.value.map(section => section.visible)
  visible_plot_tables = Object.entries(plots.value).filter((_, i) => plot_section_visibility[i]).map((section, _) => [...new Set(Object.entries(section[1]).map(plot => plot[1].table))]).flat()
  visible_heatmap_tables = Object.entries(heatmaps.value).filter((_, i) => heatmap_section_visibility[i]).map((section, _) => [...new Set(Object.entries(section[1]).map(plot => plot[1].table))]).flat()
}

const sanitize = (input, min, max, def, fmt) => {
  if (isNaN(Number(input.value)) || input.value === '' || input.value < min || input.value >= max) {
    input.value = fmt(def)
  }
}

const max_hm_px_pow = () => Math.floor(Math.log2((mvec_size - Number(hm_left.value)) / Number(hm_px_count.value)))

const trigger_reload = () => {
  update_visible_tables()

  sanitize(entries, 1, uint32_max, entries_def, id)
  sanitize(nth, 1, uint32_max, nth_def, id)
  sanitize(x_low, 0, int_max, 0, hex)
  sanitize(x_high, 1, int_max, int_max, hex)

  if (opts.value.mvec_loop) {
    sanitize(hm_left, 0, uint32_max, 0, hex)
    sanitize(hm_px_count, 1, hm_max_pixels, 2 ** 10, hex)
    sanitize(hm_px_pow, 0, uint32_max, uint32_max, hex)
  } else {
    sanitize(hm_left, 0, mvec_size, 0, hex)
    sanitize(hm_px_count, 1, hm_max_pixels, 2 ** 10, hex)
    sanitize(hm_px_pow, 0, max_hm_px_pow(), max_hm_px_pow(), hex)
  }

  plot_x_low = Number(x_low.value)
  plot_redraw = true

  query()
}

const reset_inputs = () => {
  entries.value = entries_def
  nth.value = nth_def
  x_axis.value = x_axis_def
  x_low.value = x_low_def
  x_high.value = x_high_def
  hm_left.value = hm_left_def
  hm_px_count.value = hm_px_count_def
  hm_px_pow.value = max_hm_px_pow()

  trigger_reload()
}

const query_table = async (table, is_heatmap, x_high_now) => {
  const params = {
    table: table,
    entries: entries.value,
    nth: nth.value,
    x_axis: x_axis.value,
    x_low: plot_x_low,
    x_high: x_high_now,
    is_eva: is_heatmap,
    ...is_heatmap ? {
      hm_left: Number(hm_left.value),
      hm_px_count: Number(hm_px_count.value),
      hm_px_pow: Number(hm_px_pow.value),
    } : {},
  }

  const search_params = new URLSearchParams(params)
  const resp_table = await fetch(root + `data?${search_params}`, { method: 'GET' })
  const text_table = await resp_table.text()

  return JSON.parse(text_table)
}

const query = async () => {
  if (query_in_progress.value) return

  clearTimeout(query_timeout)
  query_in_progress.value = true

  const high_params = new URLSearchParams({ x_axis: x_axis.value })
  const resp_x_high = await fetch(root + `x_high?${high_params}`, { method: 'GET' })
  const text_x_high = await resp_x_high.text()
  const json_x_high = JSON.parse(text_x_high)
  const x_high_max = json_x_high.x_high + 1
  const x_high_val = Number(x_high.value)
  const x_high_now = x_high_max < x_high_val ? x_high_max : x_high_val;

  const plot_query_results = await Promise.all(visible_plot_tables.map(table => query_table(table, false, x_high_now)))
  const heatmap_query_results = await Promise.all(visible_heatmap_tables.map(table => query_table(table, true, x_high_now)))
  const plot_query_values = Object.fromEntries(visible_plot_tables.map((table, i) => [table, plot_query_results[i]]))
  const heatmap_query_values = Object.fromEntries(visible_heatmap_tables.map((table, i) => [table, heatmap_query_results[i]]))

  // Keep track of the highest x-axis value fetched so far.
  // Future queries will set this as the minimum, which prevents re-fetching already stored data.
  plot_x_low = x_high_now

  data.value = { redraw: plot_redraw, plot_values: plot_query_values, heatmap_values: heatmap_query_values }
  plot_redraw = false

  query_in_progress.value = false
  query_timeout = setTimeout(query, 10000)
}

const with_big_ints = (_, val, { source }) => {
  if (Number.isInteger(val) && !Number.isSafeInteger(val)) {
    try { return BigInt(source) } catch {}
  }

  return val
}

onMounted(async () => {
  const resp_opts = await fetch(root + 'opts', { method: 'GET' })
  const resp_plots = await fetch(root + 'plots', { method: 'GET' })
  const resp_heatmaps = await fetch(root + 'heatmaps', { method: 'GET' })

  opts.value = JSON.parse(await resp_opts.text(), with_big_ints)
  plots.value = JSON.parse(await resp_plots.text())
  heatmaps.value = JSON.parse(await resp_heatmaps.text())
  loaded.value = true

  mvec_size = 2 ** opts.value.mvec_pow
  hm_px_pow.value = hex(max_hm_px_pow())

  // All tables should include one cycle column for each core.
  // This allows normalizing the plots against each core's cycle count
  // (i.e. making `cycl_#` the plots' x-axis).
  x_axes.value.push(...Array(opts.value.cores).keys().map(i => `cycl_${i}`))
})

watch(loaded, _ => {
  window.addEventListener('resize', adjust_top_bar)

  adjust_top_bar()
  update_visible_tables()
  query()
}, { flush: 'post' })

provide('plots', plots)
provide('heatmaps', heatmaps)
provide('entries', entries)
provide('x_axis', x_axis)
provide('hm_left', hm_left)
provide('hm_px_count', hm_px_count)
provide('hm_px_pow', hm_px_pow)
provide('data', data)
provide('trigger_reload', trigger_reload)
</script>

<style>
html {
  background-color: black;
  color: gray;
  font-family: sans-serif;
}

h1 {
  font-size: 18px;
  font-weight: 600;
  margin: 8px 0;
}

input, select {
  background-color: gray;
  border: none;
  color: black;
  font-family: monospace;
  font-size: 12px;
  margin: 0 4px;
  padding: 2px;
  width: 120px;
}

table {
  border-collapse: collapse;
  border-spacing: 0;
  height: 100%;
  width: 100%;
}

tr:nth-child(odd) {
  background-color: #111;
}

td {
  font-family: monospace;
  font-size: 14px;
  margin: 0;
  padding: 0;
}

.top_bar {
  background-color: #000;
  border-bottom: 1px solid gray;
  left: 0;
  padding: 8px;
  position: fixed;
  top: 0;
  width: 100%;
  z-index: 1;
}

.opts_name {
  color: #b58900;
  font-weight: normal;
}

.nobr {
  font-size: 12px;
  line-height: 28px;
  margin-right: 6px;
  white-space: nowrap;
}

.input_small {
  width: 80px;
}

.reset_button {
  background-color: black;
  border: 1.5px solid gray;
  color: gray;
  cursor: pointer;
  display: inline-block;
  font-family: monospace;
  font-size: 14px;
  height: 16px;
  line-height: 16px;
  margin: 0 4px;
  padding: 2px;
  text-align: center;
  width: 16px;
}
</style>
