## This tool generates the different enums from the doc/tags.md file.

import std / [strutils, sets]

proc toNimName(s: string; suffix: string): string =
  s[0].toUpperAscii & s.substr(1) & suffix

type
  EnumList = enum
    NiflerKind,
    CallConv,
    NifcExpr, NifcStmt, NifcType, NifcOther, NifcPragma, NifcTypeQualifier, NifcSym,
    NimonyExpr, NimonyStmt, NimonyType, NimonyOther, NimonyPragma, NimonySym, HookKind, ControlFlowKind,
    NifIndexKind,
    NjvlKind

proc toSuffix(e: EnumList): (string, string) =
  case e
  of NiflerKind: ("L", "None")
  of CallConv: ("", "NoCallConv")
  of NifcExpr: ("C", "NoExpr")
  of NifcStmt: ("S", "NoStmt")
  of NifcType: ("T", "NoType")
  of NifcOther: ("U", "NoSub")
  of NifcPragma: ("P", "NoPragma")
  of NifcTypeQualifier: ("Q", "NoQualifier")
  of NifcSym: ("Y", "NoSym")
  of NimonyExpr: ("X", "NoExpr")
  of NimonyStmt: ("S", "NoStmt")
  of NimonyType: ("T", "NoType")
  of NimonyOther: ("U", "NoSub")
  of NimonyPragma: ("P", "NoPragma")
  of NimonySym: ("Y", "NoSym")
  of ControlFlowKind: ("F", "NoControlFlow")
  of HookKind: ("H", "NoHook")
  of NifIndexKind: ("Idx", "NoIndexTag")
  of NjvlKind: ("V", "NoVTag")

proc shortcutToEnumList(shortcut: string): EnumList =
  try:
    result = parseEnum[EnumList](shortcut.strip())
  except ValueError:
    quit "unknown enum type: " & shortcut

const
  Header = "# Generated by tools/gen_tags.nim from doc/tags.md. DO NOT EDIT!\n"

type
  EnumField = object
    name: string
    tag: string
    value: int
    desc: string
  EnumImpls = array[EnumList, seq[EnumField]]

proc writeClassifier(f: File; e: EnumList; fields: seq[EnumField]) =
  var first = ""
  var last = ""
  var prev = -1
  var holes: seq[int] = @[]
  for f in fields:
    if first.len == 0: first = f.tag
    last = f.tag
    if prev >= 0 and f.value != prev + 1:
      for h in prev + 1..f.value - 1:
        holes.add h
    prev = f.value
  f.write "\n\nproc rawTagIs" & $e & "*(raw: TagEnum): bool {.inline.} ="
  if holes.len <= 3:
    f.write "\n  raw >= " & toNimName(first, "TagId") & " and raw <= " & toNimName(last, "TagId")
    for h in holes:
      f.write " and raw != TagEnum(" & $h & ")"
    f.write "\n"
  else:
    f.write "\n  raw in {"
    var i = 0
    for field in fields:
      if i > 0: f.write ", "
      f.write toNimName(field.tag, "TagId")
      inc i
    f.write "}\n"

proc writeModel(basename: string; data: EnumImpls; first, last: EnumList) =
  let f = open(basename & "_tags.nim", fmWrite)
  f.writeLine Header
  f.writeLine "import tags"
  for e in first..last:
    f.write "\ntype"
    f.write "\n  " & $e & "* = enum"
    f.write "\n    " & toSuffix(e)[1]
    for field in data[e]:
      f.write "\n    " & field.name
      f.write " = (ord(" & toNimName(field.tag, "TagId") & "), " & escape(field.tag) & ")"
      if field.desc.len > 0:
        f.write "  ## " & field.desc
    if e != NiflerKind:
      writeClassifier(f, e, data[e])
  f.write "\n"

proc writeTagsFile(output: string; data: seq[(string, int)]) =
  let f = open(output, fmWrite)
  f.writeLine Header
  f.writeLine "type\n  TagEnum* = enum"
  f.writeLine "    InvalidTagId"
  for d in data:
    f.writeLine "    " & toNimName(d[0], "TagId")
  f.writeLine "const"
  f.writeLine "  TagData*: array[TagEnum, (string, int)] = ["
  f.write "    (" & escape("InvalidTagId") & ", 0)"
  for d in data:
    f.write ",\n"
    f.write "    (" & escape(d[0]) & ", " & $d[1] & ")"
  f.writeLine "\n  ]"

proc extractTagName(s: string): string =
  var i = 0
  while i < s.len and s[i] == ' ': inc i
  if i < s.len and s[i] == '`': inc i
  if i < s.len and s[i] == '(': inc i
  if i < s.len and s[i] in IdentStartChars:
    let start = i
    while i < s.len and s[i] in IdentChars:
      inc i
    result = s.substr(start, i - 1)
  else:
    quit "Cannot extract tag name from: " & s

proc genTags(inp: File) =
  var i = -2
  var enumDecls = default EnumImpls
  var tags: seq[(string, int)] = @[]
  var knownTags = initHashSet[string]()
  var inTable = false
  for line in lines(inp):
    inc i
    if i <= 0: continue # skip header
    if not line.startsWith('|'):
      if inTable: break # table ended, stop processing
      continue
    inTable = true
    var parts = line.split("|")
    if parts.len != 5: continue
    if parts[1].strip().startsWith('-'):
      continue # table separator row e.g. |---|---|---|
    let tagName = extractTagName parts[1]
    if knownTags.containsOrIncl(tagName):
      quit "DUPLICATE TAG: " & tagName
    tags.add (tagName, i)

    let desc = parts[3].strip()
    let affectedEnums = parts[2].split(",")
    assert affectedEnums.len > 0
    for a in affectedEnums:
      let e = shortcutToEnumList(a)
      enumDecls[e].add EnumField(
        name: toNimName(tagName, toSuffix(e)[0]),
        tag: tagName,
        value: i,
        desc: desc
      )

  writeTagsFile "src/models/tags.nim", tags

  writeModel "src/models/nifler", enumDecls, NiflerKind, NiflerKind
  writeModel "src/models/callconv", enumDecls, CallConv, CallConv
  writeModel "src/models/nifc", enumDecls, NifcExpr, NifcSym
  writeModel "src/models/nimony", enumDecls, NimonyExpr, ControlFlowKind
  writeModel "src/models/nifindex", enumDecls, NifIndexKind, NifIndexKind
  writeModel "src/models/njvl", enumDecls, NjvlKind, NjvlKind

proc main =
  var inp = open("doc/tags.md", fmRead)
  genTags(inp)
  inp.close()

main()
