// Vector CODE128 barcodes for jsPDF label layouts.
//
// Why vector and not a PNG: the label printers are 203 dpi Zebra thermal
// printers (1 dot = 0.125 mm). A raster barcode (jsbarcode/bwip PNG) gets
// resampled by CUPS onto that dot grid and its anti-aliased bar edges are then
// halftoned to 1-bit — bars end up blurry/speckled, especially on the small
// 35×17 mm labels. Filled rectangles are rasterised by Ghostscript with hard
// edges instead, so every bar is as crisp as the printer can make it.
//
// bwip-js (already a dependency, pure JS on Node) does the CODE128 encoding
// (auto subset switching A/B/C, checksum). We only take the bar geometry from
// its SVG output — one <path stroke-width="W" d="M x … L x …"> per bar width,
// in a viewBox whose unit is HALF a module (each bar is drawn as a stroked
// centre line of width 2·modules).

import * as bwipjs from 'bwip-js'

export interface Code128Bars {
  /** Total symbol width in modules (no quiet zones). */
  modules: number
  /** Bars as [left, width] in modules, left-to-right. */
  bars: Array<[number, number]>
}

export const encodeCode128 = (text: string): Code128Bars => {
  const svg: string = (bwipjs as any).toSVG({ bcid: 'code128', text: String(text), includetext: false })
  const vb = /viewBox="0 0 (\d+(?:\.\d+)?) (\d+(?:\.\d+)?)"/.exec(svg)
  if (!vb) throw new Error('bwip-js: no viewBox in SVG output')
  const modules = Number(vb[1]) / 2
  const bars: Array<[number, number]> = []
  const pathRe = /<path[^>]*stroke-width="(\d+(?:\.\d+)?)"[^>]*d="([^"]+)"/g
  let m: RegExpExecArray | null
  while ((m = pathRe.exec(svg))) {
    const w = Number(m[1]) / 2
    const dRe = /M(\d+(?:\.\d+)?) /g
    let d: RegExpExecArray | null
    while ((d = dRe.exec(m[2]))) {
      bars.push([Number(d[1]) / 2 - w / 2, w])
    }
  }
  if (!bars.length || !modules) throw new Error('bwip-js: empty CODE128 symbol')
  bars.sort((a, b) => a[0] - b[0])
  return { modules, bars }
}

export interface DrawBarcodeOptions {
  /** Minimum quiet zone on each side in mm (CODE128 spec: ≥10 modules). Default 2.5. */
  quietMm?: number
  /** Cap on the module width in mm (keeps very short codes from looking chunky). Default 0.5. */
  maxModuleMm?: number
}

/**
 * Draws a CODE128 barcode as filled rectangles, centred inside the box
 * (x, y, w, h) in the document's unit (mm). Returns the drawn bar area.
 * Throws when the text can't be encoded — callers fall back to an image.
 */
export const drawCode128 = (doc: any, text: string, x: number, y: number, w: number, h: number, opts: DrawBarcodeOptions = {}) => {
  const { modules, bars } = encodeCode128(text)
  const quiet = opts.quietMm ?? 2.5
  const maxModule = opts.maxModuleMm ?? 0.5
  // Module width: fill the box minus quiet zones, but never wider than maxModule.
  let module = (w - 2 * quiet) / modules
  if (module > maxModule) module = maxModule
  // Quiet zone must also be ≥ 10 modules.
  const q = Math.max(quiet, 10 * module)
  module = Math.min(module, (w - 2 * q) / modules)
  const barsW = module * modules
  const left = x + (w - barsW) / 2
  doc.setFillColor(0, 0, 0)
  for (const [bx, bw] of bars) {
    doc.rect(left + bx * module, y, bw * module, h, 'F')
  }
  return { x: left, y, w: barsW, h, module }
}

/**
 * Fits a single line of text into maxWidth: shrinks the font from maxFs down
 * to minFs, then truncates with an ellipsis. Leaves the doc at the chosen size.
 */
export const fitTextLine = (doc: any, text: string, maxWidth: number, maxFs: number, minFs: number) => {
  let str = String(text ?? '').replace(/\s+/g, ' ').trim()
  let fs = maxFs
  doc.setFontSize(fs)
  while (fs > minFs && doc.getTextWidth(str) > maxWidth) {
    fs = Math.max(minFs, Math.round((fs - 0.5) * 10) / 10)
    doc.setFontSize(fs)
  }
  if (doc.getTextWidth(str) > maxWidth) {
    while (str.length > 1 && doc.getTextWidth(str + '…') > maxWidth) {
      str = str.slice(0, -1).trimEnd()
    }
    str += '…'
  }
  return { text: str, fontSize: fs }
}
