<template>
  <div>
    <div class="no-print">
      <div class="controls">
        <div class="controls-head">
          <h3>Barcode / QR Generator</h3>
          <router-link to="/tools" class="btn btn-secondary">Back to Tech Tools</router-link>
        </div>

        <!-- What to encode -->
        <div class="control-row">
          <label>
            Source
            <select v-model="source">
              <option value="single">One code</option>
              <option value="csv">CSV list</option>
            </select>
          </label>
          <label>
            Code type
            <select v-model="codetype">
              <option value="qr">QR</option>
              <option value="barcode">CODE128</option>
            </select>
          </label>
          <label v-if="codetype === 'qr'">
            Error correction
            <select v-model="errorcorrection">
              <option value="L">L - smallest code</option>
              <option value="M">M - standard</option>
              <option value="Q">Q</option>
              <option value="H">H - most robust</option>
            </select>
          </label>
        </div>

        <div v-if="source === 'single'" class="control-row">
          <label class="grow">
            Content (text or URL)
            <textarea v-model="singleContent" rows="2"
                      placeholder="https://example.com or any text"></textarea>
          </label>
          <label class="grow">
            Label (optional)
            <input v-model="singleLabel" type="text" placeholder="Printed under or beside the code" />
          </label>
          <label>
            Copies
            <input v-model.number="singleCopies" type="number" min="1" max="1000" />
          </label>
        </div>

        <div v-else class="csv-block">
          <div class="control-row">
            <label>
              CSV file
              <input type="file" accept=".csv,text/csv" @change="onCsvFile" />
            </label>
            <button class="btn btn-secondary" @click="downloadTemplate">Download template</button>
          </div>
          <label class="grow">
            ...or paste rows here
            <textarea v-model="csvText" rows="5"
                      placeholder="content,label,copies&#10;https://example.com,Front desk,1&#10;WJ-LF-0001,Line 1,2"></textarea>
          </label>
          <p class="control-note" v-if="csvError">{{ csvError }}</p>
          <p class="control-hint">
            Columns: <code>content</code> (required), <code>label</code>, <code>copies</code>.
            A header row is optional; without one the order is content, label, copies.
          </p>
        </div>

        <!-- Media -->
        <h4 class="section-heading">Label stock</h4>
        <div class="control-row">
          <label>
            Preset
            <select v-model="preset" @change="applyPreset">
              <option value="">Custom</option>
              <option v-for="option in PRESETS" :key="option.id" :value="option.id">
                {{ option.name }}
              </option>
            </select>
          </label>
          <label>Label W (in)<input v-model.number="labelwidth" type="number" step="0.01" min="0.25" /></label>
          <label>Label H (in)<input v-model.number="labelheight" type="number" step="0.01" min="0.25" /></label>
          <label>Padding (in)<input v-model.number="padding" type="number" step="0.005" min="0" /></label>
          <label>
            Layout
            <select v-model="layout">
              <option value="side">Code left, label right</option>
              <option value="stack">Code above label</option>
              <option value="codeonly">Code only</option>
            </select>
          </label>
        </div>

        <div class="control-row">
          <label v-if="codetype === 'qr'">Code size (in)<input v-model.number="codesize" type="number" step="0.0001" min="0.1" /></label>
          <label v-else>Bar height (in)<input v-model.number="barheight" type="number" step="0.01" min="0.1" /></label>
          <label>Quiet zone (in)<input v-model.number="quiet" type="number" step="0.005" min="0" /></label>
          <label>Label font (pt)<input v-model.number="labelfont" type="number" step="0.5" min="3" /></label>
          <label>Nudge X (in)<input v-model.number="nudgex" type="number" step="0.01" /></label>
          <label>Nudge Y (in)<input v-model.number="nudgey" type="number" step="0.01" /></label>
        </div>

        <!-- The dot-size trap: a thermal head cannot render a fraction of a dot,
             so a code sized off the dot grid gets uneven modules and scans
             badly. This says what the current size actually lands on. -->
        <div class="control-row">
          <label>
            Printer DPI
            <select v-model.number="dpi">
              <option :value="203">203</option>
              <option :value="300">300</option>
              <option :value="600">600</option>
            </select>
          </label>
          <p v-if="codetype === 'qr' && fitAdvice" class="control-hint fit-advice">
            {{ fitAdvice }}
          </p>
        </div>

        <div class="control-row">
          <button class="btn btn-primary" :disabled="!labels.length" @click="printAll">
            Print {{ labels.length }} label{{ labels.length === 1 ? '' : 's' }}
          </button>
          <button class="btn btn-secondary" :disabled="!labels.length" @click="printTest">
            Print 1 test label
          </button>
          <button class="btn btn-secondary" @click="resetSettings">Reset sizing</button>
        </div>

        <p v-if="truncated" class="control-note">
          Showing the first {{ MAX_LABELS }} rows. {{ truncated }} more were left out
          - split the CSV and print it in batches.
        </p>
        <p v-if="overlong.length" class="control-note">
          {{ overlong.length }} row(s) could not be encoded as CODE128 or were empty:
          {{ overlong.slice(0, 3).join(', ') }}{{ overlong.length > 3 ? ' ...' : '' }}
        </p>
      </div>
    </div>

    <!-- Preview doubles as the print surface: what is on screen is what prints. -->
    <div class="sheet">
      <div
        v-for="(label, index) in visibleLabels"
        :key="index"
        class="label"
        :class="['layout-' + layout, 'type-' + codetype]"
      >
        <div class="codebox">
          <img v-if="images[index]" :src="images[index]" class="code-img" alt="" />
        </div>
        <div v-if="layout !== 'codeonly' && label.label" class="label-text">
          {{ label.label }}
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
import QRCode from 'qrcode'
import JsBarcode from 'jsbarcode'

// Rendering thousands of codes locks the tab, so stop at a number a person
// would actually feed a label printer in one run and SAY what was dropped.
const MAX_LABELS = 500

const PRESETS = [
  { id: 'zebra1x05', name: '1.00 x 0.50 in (Zebra gap)', labelwidth: 1.0, labelheight: 0.5, codesize: 0.4286, padding: 0, quiet: 0.035, labelfont: 7 },
  { id: 'zebra2x1', name: '2.00 x 1.00 in', labelwidth: 2.0, labelheight: 1.0, codesize: 0.85, padding: 0.03, quiet: 0.05, labelfont: 10 },
  { id: 'zebra225x125', name: '2.25 x 1.25 in', labelwidth: 2.25, labelheight: 1.25, codesize: 1.05, padding: 0.04, quiet: 0.06, labelfont: 11 },
  { id: 'zebra4x6', name: '4.00 x 6.00 in (shipping)', labelwidth: 4.0, labelheight: 6.0, codesize: 3.0, padding: 0.15, quiet: 0.12, labelfont: 20 },
  { id: 'badge', name: '2.13 x 3.38 in (badge)', labelwidth: 2.13, labelheight: 3.38, codesize: 1.5, padding: 0.15, quiet: 0.1, labelfont: 12 },
]

const source = ref('single')
const codetype = ref('qr')
const errorcorrection = ref('M')

const singleContent = ref('')
const singleLabel = ref('')
const singleCopies = ref(1)

const csvText = ref('')
const csvError = ref('')

const preset = ref('zebra1x05')
const labelwidth = ref(1.0)
const labelheight = ref(0.5)
const padding = ref(0)
const quiet = ref(0.035)
const codesize = ref(0.4286)
const barheight = ref(0.3)
const labelfont = ref(7)
const layout = ref('side')
const nudgex = ref(0)
const nudgey = ref(0)
const dpi = ref(203)

const images = ref([])
const testMode = ref(false)
const overlong = ref([])

const SETTING_KEYS = [
  'codetype', 'errorcorrection', 'preset', 'labelwidth', 'labelheight', 'padding',
  'quiet', 'codesize', 'barheight', 'labelfont', 'layout', 'nudgex', 'nudgey', 'dpi',
]
const settingRefs = {
  codetype, errorcorrection, preset, labelwidth, labelheight, padding,
  quiet, codesize, barheight, labelfont, layout, nudgex, nudgey, dpi,
}

// --- rows -------------------------------------------------------------------

// Split one CSV line, honoring quoted fields and "" escapes.
function splitCsvLine(line) {
  const fields = []
  let current = ''
  let inQuotes = false
  for (let i = 0; i < line.length; i++) {
    const char = line[i]
    if (inQuotes) {
      if (char === '"' && line[i + 1] === '"') { current += '"'; i++ }
      else if (char === '"') { inQuotes = false }
      else { current += char }
    } else if (char === '"') {
      inQuotes = true
    } else if (char === ',') {
      fields.push(current); current = ''
    } else {
      current += char
    }
  }
  fields.push(current)
  return fields.map(field => field.trim())
}

const CONTENT_HEADERS = ['content', 'text', 'data', 'value', 'url', 'qr']
const LABEL_HEADERS = ['label', 'name', 'caption', 'description']
const COPIES_HEADERS = ['copies', 'qty', 'quantity', 'count']

function parseCsv(text) {
  const lines = text.split(/\r?\n/).filter(line => line.trim() !== '')
  if (!lines.length) return []

  let columns = { content: 0, label: 1, copies: 2 }
  let start = 0
  const first = splitCsvLine(lines[0]).map(field => field.toLowerCase())
  if (first.some(field => CONTENT_HEADERS.includes(field))) {
    // Named header: map by name so column order does not matter.
    const indexOf = names => first.findIndex(field => names.includes(field))
    columns = {
      content: indexOf(CONTENT_HEADERS),
      label: indexOf(LABEL_HEADERS),
      copies: indexOf(COPIES_HEADERS),
    }
    start = 1
  }

  const rows = []
  for (let i = start; i < lines.length; i++) {
    const fields = splitCsvLine(lines[i])
    const content = columns.content >= 0 ? (fields[columns.content] || '') : ''
    if (!content) continue
    const copies = columns.copies >= 0 ? parseInt(fields[columns.copies], 10) : 1
    rows.push({
      content,
      label: columns.label >= 0 ? (fields[columns.label] || '') : '',
      copies: Number.isFinite(copies) && copies > 0 ? Math.min(copies, 1000) : 1,
    })
  }
  return rows
}

// Every row expanded by its copies count, capped.
const allLabels = computed(() => {
  let rows = []
  if (source.value === 'single') {
    const content = singleContent.value.trim()
    if (content) {
      const copies = Number.isFinite(singleCopies.value) && singleCopies.value > 0
        ? singleCopies.value : 1
      rows = [{ content, label: singleLabel.value.trim(), copies }]
    }
  } else {
    try {
      csvError.value = ''
      rows = parseCsv(csvText.value)
      if (csvText.value.trim() && !rows.length) {
        csvError.value = 'No usable rows found. Every row needs a content value.'
      }
    } catch (error) {
      csvError.value = 'Could not read that CSV: ' + error.message
      rows = []
    }
  }

  const expanded = []
  for (const row of rows) {
    for (let i = 0; i < row.copies; i++) {
      expanded.push({ content: row.content, label: row.label })
    }
  }
  return expanded
})

const truncated = computed(() =>
  Math.max(0, allLabels.value.length - MAX_LABELS))

const labels = computed(() => allLabels.value.slice(0, MAX_LABELS))

// Printing one test label prints the first only; the preview follows so what
// you see is what comes out.
const visibleLabels = computed(() =>
  testMode.value ? labels.value.slice(0, 1) : labels.value)

// --- fit advice -------------------------------------------------------------

// Modules across the QR at the current content and error correction, quiet
// zone excluded (the blank label supplies that).
const qrModules = computed(() => {
  const content = labels.value[0]?.content
  if (!content || codetype.value !== 'qr') return 0
  try {
    return QRCode.create(content, { errorCorrectionLevel: errorcorrection.value }).modules.size
  } catch {
    return 0
  }
})

const fitAdvice = computed(() => {
  const modules = qrModules.value
  if (!modules) return ''
  const dotsPerModule = (codesize.value * dpi.value) / modules
  const whole = Math.floor(dotsPerModule)
  if (whole < 2) {
    return `${modules} modules at ${dotsPerModule.toFixed(2)} dots each - too small to scan `
      + `reliably. Shorten the content, drop error correction, or use bigger stock.`
  }
  const snapped = (modules * whole) / dpi.value
  const moduleMm = (whole / dpi.value) * 25.4
  if (Math.abs(snapped - codesize.value) < 0.002) {
    return `${modules} modules at exactly ${whole} dots each (${moduleMm.toFixed(3)} mm). Good.`
  }
  return `${modules} modules at ${dotsPerModule.toFixed(2)} dots each. Uneven - `
    + `use ${snapped.toFixed(4)} in for a whole ${whole} dots per module.`
})

// --- rendering --------------------------------------------------------------

// Each code renders to an SVG data URI, for two reasons. An <img> prints
// reliably where a live canvas or inline SVG does not (same finding the asset
// label pages are built on), and SVG rasterizes at the printer's resolution
// with hard module edges - a PNG would be downscaled to the label size and
// smear the very edges a scanner reads.
//
// margin 0 on the QR: the quiet zone is blank label supplied by --tool-quiet,
// so none of the code box is spent on white we cannot then adjust.
function svgDataUri(svg) {
  return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svg)
}

async function renderOne(row) {
  const text = row.content
  if (!text) return ''
  if (codetype.value === 'qr') {
    const svg = await QRCode.toString(text, {
      type: 'svg',
      errorCorrectionLevel: errorcorrection.value,
      margin: 0,
    })
    return svgDataUri(svg)
  }
  const element = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
  JsBarcode(element, text, {
    format: 'CODE128',
    displayValue: false,
    margin: 0,
    width: 2,
    height: 100,
  })
  return svgDataUri(new XMLSerializer().serializeToString(element))
}

let renderToken = 0
async function renderAll() {
  const token = ++renderToken
  const failed = []
  const next = []
  for (const row of labels.value) {
    try {
      next.push(await renderOne(row))
    } catch {
      // CODE128 rejects some characters; an unencodable row must be named,
      // not silently dropped to a blank sticker.
      next.push('')
      failed.push(row.content.slice(0, 20))
    }
    if (token !== renderToken) return
  }
  images.value = next
  overlong.value = failed
}

// --- media vars -------------------------------------------------------------

// @page cannot read a scoped style, so the sizing lives in custom properties on
// the root element. Chromium honors var() in @page size.
function applyVars() {
  const root = document.documentElement
  root.style.setProperty('--tool-label-w', labelwidth.value + 'in')
  root.style.setProperty('--tool-label-h', labelheight.value + 'in')
  root.style.setProperty('--tool-pad', padding.value + 'in')
  root.style.setProperty('--tool-code', codesize.value + 'in')
  root.style.setProperty('--tool-barh', barheight.value + 'in')
  // Both symbologies need clear space: a QR wants a quiet zone around it, and
  // CODE128 wants one at each end (10x the narrow bar). It doubles as the gap
  // between the code and the label text, so zeroing it for barcodes let the
  // bars run into the caption.
  root.style.setProperty('--tool-quiet', quiet.value + 'in')
  root.style.setProperty('--tool-font', labelfont.value + 'pt')
  root.style.setProperty('--tool-nudge-x', nudgex.value + 'in')
  root.style.setProperty('--tool-nudge-y', nudgey.value + 'in')
}

function clearVars() {
  const root = document.documentElement
  for (const name of ['--tool-label-w', '--tool-label-h', '--tool-pad', '--tool-code',
    '--tool-barh', '--tool-quiet', '--tool-font', '--tool-nudge-x', '--tool-nudge-y']) {
    root.style.removeProperty(name)
  }
}

function applyPreset() {
  const chosen = PRESETS.find(option => option.id === preset.value)
  if (!chosen) return
  labelwidth.value = chosen.labelwidth
  labelheight.value = chosen.labelheight
  codesize.value = chosen.codesize
  padding.value = chosen.padding
  quiet.value = chosen.quiet
  labelfont.value = chosen.labelfont
}

function resetSettings() {
  preset.value = 'zebra1x05'
  applyPreset()
  layout.value = 'side'
  barheight.value = 0.3
  nudgex.value = 0
  nudgey.value = 0
}

// --- csv helpers ------------------------------------------------------------

function onCsvFile(event) {
  const file = event.target.files && event.target.files[0]
  if (!file) return
  const reader = new FileReader()
  reader.onload = () => { csvText.value = String(reader.result || '') }
  reader.onerror = () => { csvError.value = 'Could not read that file.' }
  reader.readAsText(file)
}

function downloadTemplate() {
  const csv = [
    'content,label,copies',
    'https://example.com/asset/1,Bay 12 press,1',
    'WJ-LF-0001,Line 1 fixture,2',
  ].join('\n')
  const link = document.createElement('a')
  link.href = URL.createObjectURL(new Blob([csv], { type: 'text/csv' }))
  link.download = 'code_labels_template.csv'
  link.click()
  URL.revokeObjectURL(link.href)
}

// --- printing ---------------------------------------------------------------

function printAll() {
  window.print()
}

async function printTest() {
  testMode.value = true
  await nextTick()
  window.print()
  testMode.value = false
}

// --- lifecycle --------------------------------------------------------------

function saveSettings() {
  const state = {}
  for (const key of SETTING_KEYS) state[key] = settingRefs[key].value
  try { localStorage.setItem('toolsCodeGenerator', JSON.stringify(state)) } catch { /* private mode */ }
}

onMounted(() => {
  try {
    const saved = JSON.parse(localStorage.getItem('toolsCodeGenerator') || '{}')
    for (const key of SETTING_KEYS) {
      if (saved[key] !== undefined) settingRefs[key].value = saved[key]
    }
  } catch { /* ignore a corrupt entry */ }
  applyVars()
  renderAll()
})

onBeforeUnmount(clearVars)

watch(SETTING_KEYS.map(key => settingRefs[key]), () => {
  applyVars()
  saveSettings()
})

watch([labels, codetype, errorcorrection], renderAll)
</script>

<style scoped>
/* One label per page. Chromium reads var() here; the print dialog must be set
   to Margins = None and Scale = 100%. */
@page { size: var(--tool-label-w) var(--tool-label-h); margin: 0; }

.no-print { padding: 20px; }

.controls {
  background: var(--bg-card);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 20px;
  max-width: 66rem;
}

.controls-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin-bottom: 1rem;
}

.controls-head h3 { margin: 0; }

.section-heading {
  margin: 1.25rem 0 0.5rem;
  font-size: 0.9rem;
  color: var(--text-light);
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

.control-row {
  display: flex;
  flex-wrap: wrap;
  gap: 14px;
  align-items: flex-end;
  margin-bottom: 12px;
}

.control-row label {
  display: flex;
  flex-direction: column;
  font-size: 0.8125rem;
  gap: 4px;
  color: var(--text-light);
}

.control-row label.grow { flex: 1 1 18rem; }

.control-row input,
.control-row select,
.control-row textarea,
.csv-block input,
.csv-block textarea {
  padding: 6px;
  font-size: 0.875rem;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-family: inherit;
}

.control-row input[type="number"] { width: 7rem; }

.csv-block label {
  display: flex;
  flex-direction: column;
  font-size: 0.8125rem;
  gap: 4px;
  color: var(--text-light);
}

.control-note { color: var(--warning); font-size: 0.8125rem; margin: 8px 0 0; }
.control-hint { color: var(--text-light); font-size: 0.8125rem; margin: 8px 0 0; }
.fit-advice { flex: 1 1 22rem; align-self: center; }

.sheet {
  padding: 20px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.label {
  box-sizing: border-box;
  width: var(--tool-label-w);
  height: var(--tool-label-h);
  padding: var(--tool-pad);
  background: #fff;
  color: #000;
  overflow: hidden;
  display: flex;
  align-items: center;
  position: relative;
  left: var(--tool-nudge-x);
  top: var(--tool-nudge-y);
  outline: 1px dashed var(--border);
}

.label.layout-stack { flex-direction: column; justify-content: center; }
.label.layout-codeonly { justify-content: center; }

.codebox {
  flex: 0 0 auto;
  margin: var(--tool-quiet);
  width: var(--tool-code);
  height: var(--tool-code);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* A QR is square, so its box is code-size on a side. A CODE128 is wide and
   short: it takes the width left over on the label and only its own bar
   height, or the whole width when nothing shares the label with it. */
.label.type-barcode .codebox {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  height: var(--tool-barh);
}

.code-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.label-text {
  flex: 1 1 auto;
  min-width: 0;
  text-align: center;
  font-family: "Arial Narrow", Arial, Helvetica, sans-serif;
  font-size: var(--tool-font);
  font-weight: 700;
  line-height: 1.05;
  overflow-wrap: anywhere;
}

@media print {
  .no-print { display: none !important; }
  .sheet { padding: 0; display: block; gap: 0; }
  .label {
    outline: none;
    break-after: page;
    page-break-after: always;
  }
  .label:last-of-type { break-after: auto; page-break-after: auto; }
}
</style>
