--[[
   ******************************************************
   * Provide Lua functions for typesetting font tables. *
   * Author: Scott Pakin <scott-clsl@pakin.org>         *
   ******************************************************
--]]

-- Send formatted output to TeX.
function tprintf (fmt, ...)
   local st_fmt = string.format("\\scantokens{%s}", fmt)
   tex.print(string.format(st_fmt, ...))
end

-- Return the human-readable name of \testfont.
function get_font_name ()
   local fid = font.id("testfont")
   local fnt = font.getfont(fid)
   local fontname = fnt.fontname or fnt.psname or fnt.fullname or fnt.name
   return fontname
end

-- Return a string corresponding to a symbol table's header row.
function header_row (num_hexits)
   local hstr = {}
   for i = 0, 7 do
      hstr[#hstr + 1] = string.format([[& \multicolumn{1}{c|}{\ttfamily\rule{%.1fem}{1pt}%X}]],
         num_hexits*0.5,
         i)
   end
   hstr[#hstr + 1] = [[\\ \thickhline]]
   return table.concat(hstr, " ")
end

-- Return a string corresponding to a symbol table's footer row.
function footer_row (num_hexits)
   local fstr = {}
   for i = 8, 15 do
      fstr[#fstr + 1] = string.format([[& \multicolumn{1}{c|}{\ttfamily\rule{%.1fem}{1pt}%X}]],
         num_hexits*0.5,
         i)
   end
   fstr[#fstr + 1] = [[\\]]
   return table.concat(fstr, " ")
end

-- Compute the area of a glyph's bounding box.
function glyph_area (glyph)
   local bbox = glyph.boundingbox
   if bbox == nil then
      -- Metafont or PostScript font
      return glyph.width*(glyph.height + glyph.depth)
   else
      -- OpenType or TrueType font
      return (bbox[3] - bbox[1])*(bbox[4] - bbox[2])
   end
end

-- Return a set of valid code points in \testfont and, for
-- convenience, the minimum and maximum values in that table.
function valid_code_points ()
   -- First, consider the characters table.  This is valid for
   -- Metafont fonts but may include duplicates (two code points, same
   -- Unicode value) for non-Metafont fonts.
   local valid_glyphs = {}
   local glyphmin, glyphmax = 2^30, -1
   local fid = font.id("testfont")
   local fnt = font.getfont(fid)
   for i, v in pairs(fnt.characters) do
      if not (i == 'left_boundary' or i == 'right_boundary' or glyph_area(v) <= 0) then
         valid_glyphs[i] = true
         glyphmin = math.min(glyphmin, i)
         glyphmax = math.max(glyphmax, i)
      end
   end
   if glyphmax <= 255 then
      return valid_glyphs, glyphmin, glyphmax
   end

   -- Start over with a Unicode-centric approach.
   valid_glyphs = {}
   glyphmin, glyphmax = 2^30, -1
   local fname = string.gsub(fnt.filename, "harfloaded:", "")
   fnt = fontloader.open(kpse.lookup(fname))
   local ftbl = fontloader.to_table(fnt)
   for i, v in ipairs(ftbl.glyphs) do
      if v.unicode ~= -1 and glyph_area(v) > 0 then
         valid_glyphs[v.unicode] = true
         glyphmin = math.min(glyphmin, v.unicode)
         glyphmax = math.max(glyphmax, v.unicode)
      end
   end
   return valid_glyphs, glyphmin, glyphmax
end

-- Return a string corresponding to all rows of symbols.
function all_table_rows (valid_glyphs, glyphmin, glyphmax)
   -- Determine the number of hexits in the largest code point and use
   -- this to define a format string for row headers.
   local num_hexits = string.len(string.format("%X", glyphmax))
   local fmt_str = "%0" .. tostring(num_hexits) .. "X"

   -- Construct a list of row strings.
   local rows = {}
   for base = math.floor(glyphmin/16)*16, math.ceil(glyphmax/16)*16, 16 do
      -- Ensure we have at least one glyph in the current range.
      local have_glyph = false   -- At least one glyph in [base, base+15]
      for ofs = 0, 15 do
         if valid_glyphs[base + ofs] then
            have_glyph = true
            break
         end
      end
      if not have_glyph then
         goto continue
      end

      -- Output exactly two rows.
      local base_str = string.format(fmt_str, base):sub(1, -2)
      rows[#rows + 1] = string.format([[\multirow{2}*{\ttfamily\char`\"%s\rule{0.5em}{1pt}}]], base_str)
      for ofs = 0, 7 do
         if valid_glyphs[base + ofs] then
            rows[#rows + 1] = string.format([[& \char"%X]], base + ofs)
         else
            rows[#rows + 1] = "&"
         end
      end
      rows[#rows + 1] = [[\\* \cline{2-9}]]
      for ofs = 8, 15 do
         if valid_glyphs[base + ofs] then
            rows[#rows + 1] = string.format([[& \char"%X]], base + ofs)
         else
            rows[#rows + 1] = "&"
         end
      end
      rows[#rows + 1] = [[\\ \thickhline]]

      ::continue::
   end
   return table.concat(rows, " ")
end

-- Define a mapping from feature code to human-friendly name.  Source:
-- https://learn.microsoft.com/en-us/typography/opentype/spec/featurelist
-- (accessed 20-Dec-2025).  This is extended by get_feature_name to
-- cover cv01 through cv99 and ss01 through ss20.
feature_names = {
   aalt = "Access All Alternates",
   abvf = "Above-base Forms",
   abvm = "Above-base Mark Positioning",
   abvs = "Above-base Substitutions",
   afrc = "Alternative Fractions",
   akhn = "Akhand",
   apkn = "Kerning for Alternate Proportional Widths",
   blwf = "Below-base Forms",
   blwm = "Below-base Mark Positioning",
   blws = "Below-base Substitutions",
   calt = "Contextual Alternates",
   case = "Case-sensitive Forms",
   ccmp = "Glyph Composition/Decomposition",
   cfar = "Conjunct Form After Ro",
   chws = "Contextual Half-width Spacing",
   cjct = "Conjunct Forms",
   clig = "Contextual Ligatures",
   cpct = "Centered CJK Punctuation",
   cpsp = "Capital Spacing",
   cswh = "Contextual Swash",
   curs = "Cursive Positioning",
   c2pc = "Petite Capitals From Capitals",
   c2sc = "Small Capitals From Capitals",
   dist = "Distances",
   dlig = "Discretionary Ligatures",
   dnom = "Denominators",
   dtls = "Dotless Forms",
   expt = "Expert Forms",
   falt = "Final Glyph on Line Alternates",
   fin2 = "Terminal Forms #2",
   fin3 = "Terminal Forms #3",
   fina = "Terminal Forms",
   flac = "Flattened Accent Forms",
   frac = "Fractions",
   fwid = "Full Widths",
   half = "Half Forms",
   haln = "Halant Forms",
   halt = "Alternate Half Widths",
   hist = "Historical Forms",
   hkna = "Horizontal Kana Alternates",
   hlig = "Historical Ligatures",
   hngl = "Hangul",
   hojo = "Hojo Kanji Forms (JIS X 0212-1990 Kanji Forms)",
   hwid = "Half Widths",
   init = "Initial Forms",
   isol = "Isolated Forms",
   ital = "Italics",
   jalt = "Justification Alternates",
   jp78 = "JIS78 Forms",
   jp83 = "JIS83 Forms",
   jp90 = "JIS90 Forms",
   jp04 = "JIS2004 Forms",
   kern = "Kerning",
   lfbd = "Left Bounds",
   liga = "Standard Ligatures",
   ljmo = "Leading Jamo Forms",
   lnum = "Lining Figures",
   locl = "Localized Forms",
   ltra = "Left-to-right Alternates",
   ltrm = "Left-to-right Mirrored Forms",
   mark = "Mark Positioning",
   med2 = "Medial Forms #2",
   medi = "Medial Forms",
   mgrk = "Mathematical Greek",
   mkmk = "Mark to Mark Positioning",
   mset = "Mark Positioning via Substitution",
   nalt = "Alternate Annotation Forms",
   nlck = "NLC Kanji Forms",
   nukt = "Nukta Forms",
   numr = "Numerators",
   onum = "Oldstyle Figures",
   opbd = "Optical Bounds",
   ordn = "Ordinals",
   ornm = "Ornaments",
   palt = "Proportional Alternate Widths",
   pcap = "Petite Capitals",
   pkna = "Proportional Kana",
   pnum = "Proportional Figures",
   pref = "Pre-base Forms",
   pres = "Pre-base Substitutions",
   pstf = "Post-base Forms",
   psts = "Post-base Substitutions",
   pwid = "Proportional Widths",
   qwid = "Quarter Widths",
   rand = "Randomize",
   rclt = "Required Contextual Alternates",
   rkrf = "Rakar Forms",
   rlig = "Required Ligatures",
   rphf = "Reph Form",
   rtbd = "Right Bounds",
   rtla = "Right-to-left Alternates",
   rtlm = "Right-to-left Mirrored Forms",
   ruby = "Ruby Notation Forms",
   rvrn = "Required Variation Alternates",
   salt = "Stylistic Alternates",
   sinf = "Scientific Inferiors",
   size = "Optical size",
   smcp = "Small Capitals",
   smpl = "Simplified Forms",
   ssty = "Math Script-style Alternates",
   stch = "Stretching Glyph Decomposition",
   subs = "Subscript",
   sups = "Superscript",
   swsh = "Swash",
   titl = "Titling",
   tjmo = "Trailing Jamo Forms",
   tnam = "Traditional Name Forms",
   tnum = "Tabular Figures",
   trad = "Traditional Forms",
   twid = "Third Widths",
   unic = "Unicase",
   valt = "Alternate Vertical Metrics",
   vapk = "Kerning for Alternate Proportional Vertical Metrics",
   vatu = "Vattu Variants",
   vchw = "Vertical Contextual Half-width Spacing",
   vert = "Vertical Alternates",
   vhal = "Alternate Vertical Half Metrics",
   vjmo = "Vowel Jamo Forms",
   vkna = "Vertical Kana Alternates",
   vkrn = "Vertical Kerning",
   vpal = "Proportional Alternate Vertical Metrics",
   vrt2 = "Vertical Alternates and Rotation",
   vrtr = "Vertical Alternates for Rotation",
   zero = "Slashed Zero"
}

-- Given a feature code, return a human-friendly name.
function get_feature_name (feature)
   local first2 = string.sub(feature, 1, 2)
   if first2 == "cv" then
      return string.format("Character Variant %d",
                           tonumber(string.sub(feature, 3)))
   end
   if first2 == "ss" then
      return string.format("Stylistic Set %d",
                           tonumber(string.sub(feature, 3)))
   end
   return feature_names[feature]
end

-- Render a section header for a font.  We assume that \testfont
-- already has been set to the desired font.
function render_table_header (friendly_name)
   local fontname = get_font_name()
   tprintf([[\Needspace{0.25\textheight}]])
   tprintf("\\markboth{%s}{%s}", fontname, fontname)
   if friendly_name ~= nil and friendly_name ~= fontname then
      tprintf("\\subsection[%s]{%s (%s)}", fontname, fontname, friendly_name)
   else
      tprintf("\\subsection{%s}", fontname)
   end
end

-- Render a subsection header for a font feature.
function render_table_subheader (feature)
   local fontname = get_font_name()
   local featname = get_feature_name(feature)
   tprintf("\\markboth{%s, %s}{%s, %s}",
	   fontname, featname,
	   fontname, featname)
   tprintf("\\subsubsection{%s}", featname)
end

-- Render a font table.  We assume that \testfont already has been set
-- to the desired font.
function render_table_body ()
   local symtbl = {}

   -- Aquire a set of all defined code points in the font and use the
   -- largest code-point value to define a format string for column
   -- headers and footers.
   local valid_glyphs, glyphmin, glyphmax = valid_code_points()
   local num_hexits = string.len(string.format("%X", glyphmax))

   -- Construct an entire symbol table.
   symtbl[#symtbl + 1] = [=[
\begin{longtable}{r|*8{>{\testfont}c|}}
  \multicolumn{9}{l}{\small\textit{(continued from previous page)}} \\[2ex]
]=]
   symtbl[#symtbl + 1] = header_row(num_hexits)
   symtbl[#symtbl + 1] = "\\endhead"
   symtbl[#symtbl + 1] = header_row(num_hexits)
   symtbl[#symtbl + 1] = "\\endfirsthead"
   symtbl[#symtbl + 1] = footer_row(num_hexits)
   symtbl[#symtbl + 1] = [=[
  \multicolumn{9}{r}{} \\
  \multicolumn{9}{r}{\small\textit{(continued on next page)}}
  \endfoot
]=]
   symtbl[#symtbl + 1] = footer_row(num_hexits)
   symtbl[#symtbl + 1] = "\\endlastfoot"
   symtbl[#symtbl + 1] = all_table_rows(valid_glyphs, glyphmin, glyphmax)
   symtbl[#symtbl + 1] = "\\end{longtable}"

   -- Render the resulting LaTeX code, reparsing each line with \scantokens.
   for i, ln in ipairs(symtbl) do
      tprintf([[\scantextokens{%s}]], ln)
   end
end

-- Mark the end of a font subsection.  We assume that \testfont already
-- has been set to the desired font.
function render_table_subtrailer (feature)
   local fontname = get_font_name()
   local featname = get_feature_name(feature)
   tprintf("\\markboth{%s, %s}{%s, %s}",
	   fontname, featname,
	   fontname, featname)
end

-- Mark the end of a font section.  We assume that \testfont already
-- has been set to the desired font.
function render_table_trailer ()
   local fontname = get_font_name()
   tprintf("\\markboth{%s}{%s}", fontname, fontname)
end

-- For each specified feature, define \testfont then re-enter Lua to
-- render a table.
function render_feature_tables (filename, features, friendly_name)
   -- Set the font without features then render a header that
   -- represents that base font.
   local full_filename = kpse.lookup(filename)
   tprintf("\\font\\testfont=\"[%s]:mode=harf;\"\\relax", full_filename)
   tprintf("\\directlua{render_table_header(%q)}", friendly_name)

   -- Render one subheading and table per feature.
   for i, feat in ipairs(features) do
      -- "mode=harf" exposes the complete font to LuaLaTeX, not merely
      -- code points in [0x0, 0xFFFFF] or so.
      tprintf("\\font\\testfont=\"[%s]:mode=harf;%s;\"\\relax",
	      full_filename, feat)
      tprintf([[
\directlua{
  render_table_subheader(%q)
  render_table_body()
  render_table_subtrailer(%q)
}
]], feat, feat)
   end

   -- Render one trailer for all feature tables.
   tprintf("\\directlua{render_table_trailer()}")
end

-- Define \testfont then re-enter Lua to render a table.  If a list of
-- features is provided, typeset one table per feature.
function render_table (args)
   -- Extract mandatory and optional arguments.
   local filename = args[1]
   local features = args["features"]
   local friendly_name = args["name"]

   -- Handle the multi-feature case specially.
   if features ~= nil then
      render_feature_tables(filename, features, friendly_name)
      return
   end

   -- Handle the common case of no features.
   local base, ext = string.match(filename, "^(.*)(%.[^.]+)$")
   if ext == ".tfm" or ext == ".pfb" or ext == ".pfa" or ext == ".vf" then
      local stem = string.match(base, "^.-([^/]+)$")
      tprintf("\\font\\testfont={%s}\\relax", stem)
   else
      -- "mode=harf" exposes the complete font to LuaLaTeX, not merely
      -- code points in [0x0, 0xFFFFF] or so.
      tprintf("\\font\\testfont=\"[%s]:mode=harf;\"\\relax",
	      kpse.lookup(filename))
   end
   tprintf([=[
\directlua{
  render_table_header(%q)
  render_table_body()
  render_table_trailer()
}
]=], friendly_name)
end
