#       Nif library
# (c) Copyright 2024 Andreas Rumpf
#
# See the file "license.txt", included in this
# distribution, for details about the copyright.

## Create an index file for a NIF file.

import std / [tables, assertions, hashes, syncio]
import bitabs, lineinfos, nifreader, nifstreams, nifcursors, nifchecksums, symparser, vfs

when defined(nimony):
  import std / sha1
else:
  import "$nim"/dist/checksums/src/checksums/sha1
import ".." / models / [tags, nifindex_tags]

include compat2

proc entryKind(tag: TagId): NifIndexKind =
  if rawTagIsNifIndexKind(cast[TagEnum](tag)):
    result = cast[NifIndexKind](tag)
  else:
    result = NoIndexTag

proc isImportant(s: string): bool =
  var c = 0
  for ch in s:
    if ch == '.': inc c
  result = c >= 2

proc isExported(n: Cursor): bool =
  var n = n
  if n.kind == SymbolDef:
    inc n
    if n.kind != DotToken:
      return true
  return false

proc processForChecksum(dest: var Sha1State; content: var TokenBuf) =
  var n = beginRead(content)
  var nested = 0
  let inlineT = TagId(InlineIdx)
  while true:
    case n.kind
    of ParLe:
      var foundInline = false
      let k = entryKind(n.tagId)
      case k
      of LetIdx, VarIdx, CursorIdx, ConstIdx, TypeIdx, GletIdx, TletIdx, GvarIdx, TvarIdx:
        inc n # tag
        if isExported(n):
          updateLoop(dest, n, inlineT, foundInline) # SymbolDef
          updateLoop(dest, n, inlineT, foundInline) # Export marker
          updateLoop(dest, n, inlineT, foundInline) # pragmas
          updateLoop(dest, n, inlineT, foundInline) # type
          updateLoop(dest, n, inlineT, foundInline) # value
        skipToEnd n
      of TemplateIdx, MacroIdx, IteratorIdx:
        # these always have inline semantics
        inc n # tag
        if isExported(n):
          updateLoop(dest, n, inlineT, foundInline) # SymbolDef
          updateLoop(dest, n, inlineT, foundInline) # Export marker
          updateLoop(dest, n, inlineT, foundInline) # pattern
          updateLoop(dest, n, inlineT, foundInline) # typevars
          updateLoop(dest, n, inlineT, foundInline) # params
          updateLoop(dest, n, inlineT, foundInline) # retType
          updateLoop(dest, n, inlineT, foundInline) # pragmas
          updateLoop(dest, n, inlineT, foundInline) # effects
          updateLoop(dest, n, inlineT, foundInline) # body
        skipToEnd n
      of ProcIdx, FuncIdx, MethodIdx, ConverterIdx:
        inc n # tag
        if isExported(n):
          var dummy = false
          updateLoop(dest, n, inlineT, dummy) # SymbolDef
          updateLoop(dest, n, inlineT, dummy) # Export marker
          updateLoop(dest, n, inlineT, dummy) # pattern
          updateLoop(dest, n, inlineT, dummy) # typevars
          updateLoop(dest, n, inlineT, dummy) # params
          updateLoop(dest, n, inlineT, dummy) # retType
          updateLoop(dest, n, inlineT, foundInline) # pragmas
          updateLoop(dest, n, inlineT, dummy) # effects
          if foundInline:
            updateLoop(dest, n, inlineT, dummy) # body
          else:
            skip n
        skipToEnd n
      of NoIndexTag, InlineIdx, KvIdx, VvIdx, BuildIdx, IndexIdx,
         ExportIdx, FromexportIdx, ExportexceptIdx:
        inc n
        inc nested
    of ParRi:
      dec nested
      if nested == 0:
        break
      inc n
    else:
      inc n

type
  HookIndexEntry* = object
    typ*, hook*: SymId
    isGeneric*: bool

  IndexSections* = object
    converters*: seq[(SymId, SymId)] # string is for compat with `methods`
    exportBuf*: TokenBuf

proc getSymbolSection(tag: TagId; values: seq[(SymId, SymId)]): TokenBuf =
  result = createTokenBuf(30)
  result.addParLe tag

  for value in values:
    let (key, sym) = value
    result.buildTree TagId(KvIdx), NoLineInfo:
      if key == SymId(0):
        result.add dotToken(NoLineInfo)
      else:
        result.add symToken(key, NoLineInfo)
      result.add symToken(sym, NoLineInfo)

  result.addParRi()

proc createIndex*(infile: string; root: PackedLineInfo; buildChecksum: bool; sections: IndexSections) {.canRaise.} =
  let indexName = changeModuleExt(infile, ".s.idx.nif")
  var content = "(.nif27)\n(index\n"

  if sections.converters.len != 0:
    let converterSectionBuf = getSymbolSection(TagId(ConverterIdx), sections.converters)

    content.add toString(converterSectionBuf)
    content.add "\n"

  if sections.exportBuf.len != 0:
    content.add toString(sections.exportBuf)
    content.add "\n"

  if buildChecksum:
    var s = nifstreams.open(infile)
    discard processDirectives(s.r)
    var buf = fromStream(s)
    close s
    var checksum = newSha1State()
    processForChecksum(checksum, buf)
    let final = SecureHash checksum.finalize()
    content.add "(checksum \"" & $final & "\")"
  content.add "\n)\n"
  # OnlyIfChanged: clients (downstream nimsem/hexer) depend on this file.
  # When a module's interface and inline-proc bodies are unchanged, its
  # checksum is identical, so we keep the old mtime to avoid cascading
  # rebuilds through importers. nifmake's `needsRebuild` uses the freshest
  # of a node's outputs (max), so the always-written `.s.nif` still proves
  # "we ran since the inputs changed" for nimsem's own staleness check.
  let existingContent = try: vfsRead(indexName) except: ""
  if existingContent != content:
    vfsWrite(indexName, content)

proc createIndex*(infile: string; buildChecksum: bool; root: PackedLineInfo) {.canRaise.} =
  createIndex(infile, root, buildChecksum, IndexSections())

proc writeFileAndIndex*(outfile: string; content: TokenBuf) {.canRaise.} =
  writeFile(content, outfile)
  createIndex(outfile, true, content[0].info)

type
  IndexVisibility* = enum
    Hidden, Exported
  NifIndexEntry* = object
    offset*: int
    info*: PackedLineInfo
    vis*: IndexVisibility

  NifIndex* = object
    converters*: seq[(string, string)] # map of dest types to converter symbols
    exports*: seq[(string, NifIndexKind, seq[StrId])] # module, export kind, filtered names

proc readSymbolSection(s: var Stream; tab: var seq[(string, string)]) =
  var t = next(s)
  var nested = 1
  while t.kind != EofToken:
    if t.kind == ParLe:
      inc nested
      if t.tagId == TagId(KvIdx):
        t = next(s)
        var key: string
        if t.kind == Symbol:
          key = pool.syms[t.symId]
        elif t.kind == Ident:
          key = pool.strings[t.litId]
        elif t.kind == DotToken:
          key = "."
        else:
          raiseAssert "invalid (kv) construct: symbol expected"
        t = next(s) # skip Symbol
        var value: string
        if t.kind == Symbol:
          value = pool.syms[t.symId]
        elif t.kind == Ident:
          value = pool.strings[t.litId]
        else:
          raiseAssert "invalid (kv) construct: symbol expected"
        t = next(s) # skip value symbol
        tab.add((key, value))
        if t.kind == ParRi:
          t = next(s)
          dec nested
        else:
          assert false, "invalid (kv) construct: ')' expected"
      else:
        assert false, "expected (kv) construct"
    elif t.kind == ParRi:
      dec nested
      if nested == 0:
        break
      t = next(s)
    else:
      assert false, "expected (kv) construct"
      #t = next(s)

proc readIndex*(indexName: string): NifIndex =
  var s = nifstreams.open(indexName)
  let res = processDirectives(s.r)
  assert res == Success

  result = default(NifIndex)
  var t = next(s)
  if t.tag != TagId(IndexIdx):
    assert false, "expected 'index' tag"
  t = next(s)
  while t.kind != ParRi and t.kind != EofToken:
    if t.kind == ParLe:
      case t.tagId
      of TagId(ConverterIdx):
        readSymbolSection(s, result.converters)
        t = next(s)
      of TagId(ExportIdx), TagId(FromexportIdx), TagId(ExportexceptIdx):
        let kind = cast[NifIndexKind](t.tag)
        t = next(s)
        assert t.kind == StringLit
        let path = pool.strings[t.litId]
        t = next(s)
        var names: seq[StrId] = @[]
        while t.kind != ParRi:
          assert t.kind == Ident
          names.add t.litId
          t = next(s)
        result.exports.add (path, kind, names)
        t = next(s)
      else:
        var nested = 1
        t = next(s)
        while t.kind != EofToken:
          case t.kind
          of ParLe: inc nested
          of ParRi:
            dec nested
            if nested == 0: break
          else: discard
          t = next(s)
        t = next(s)
    else:
      t = next(s)

proc readEmbeddedIndex*(s: var Stream): Table[string, NifIndexEntry] =
  ## Reads the simple embedded index (index (kv sym offset)...) from indexStartsAt position.
  result = initTable[string, NifIndexEntry]()
  let indexPos = indexStartsAt(s.r)
  if indexPos <= 0:
    return result
  let contentPos = offset(s.r)  # Save position
  s.r.jumpTo(indexPos)

  var previousOffset = 0
  var t = next(s)
  let exportedTagId = pool.tags.getOrIncl("x")
  if t.kind == ParLe and pool.tags[t.tagId] == ".index":
    t = next(s)
    while t.kind != EofToken and t.kind != ParRi:
      if t.kind == ParLe:
        let vis = if t.tagId == exportedTagId: Exported else: Hidden
        let info = t.info
        t = next(s)  # skip (kv
        var key = ""
        if t.kind == Symbol:
          key = pool.syms[t.symId]
        else:
          raiseAssert "invalid (kv) in .index: symbol expected"
        t = next(s)  # skip symbol
        if t.kind == IntLit:
          let offset = int(pool.integers[t.intId]) + previousOffset
          result[key] = NifIndexEntry(offset: offset, info: info, vis: vis)
          previousOffset = offset
        t = next(s)  # skip offset
        if t.kind == ParRi:
          t = next(s)  # skip )
      else:
        t = next(s)

  s.r.jumpTo(contentPos)  # Restore position

when isMainModule:
  import std / [os]
  try:
    createIndex paramStr(1), false, NoLineInfo
  except:
    quit "createIndex failed"
