# included in sem.nim

proc semInclude(c: var SemContext; dest: var TokenBuf; it: var Item) =
  var files: seq[ImportedFilename] = @[]
  var hasError = false
  let info = it.n.info
  var x = it.n
  skip it.n
  inc x, SkipTag # skip the `include`
  while x.kind != ParRi:
    filenameVal(x, files, hasError, allowAs = false)

  if hasError:
    c.buildErr dest, info, "wrong `include` statement"
  else:
    for f1 in items(files):
      let f2 = resolveFile(c.g.config.paths, getFile(info), f1.path)
      c.meta.includedFiles.add f2
      # check for recursive include files:
      var isRecursive = false
      for a in c.includeStack:
        if a == f2:
          isRecursive = true
          break

      if not isRecursive:
        var buf = parseFile(f2, c.g.config.paths, c.g.config.nifcachePath)
        c.includeStack.add f2
        #c.m.includes.add f2
        var n = cursorAt(buf, 0)
        semStmt(c, dest, n, false)
        c.includeStack.setLen c.includeStack.len - 1
      else:
        var m = ""
        for i in 0..<c.includeStack.len:
          m.add shortenDir c.includeStack[i]
          m.add " -> "
        m.add shortenDir f2
        c.buildErr dest, info, "recursive include: " & m

  producesVoid c, dest, info, it.typ

proc importSingleFile(c: var SemContext; dest: var TokenBuf; f1: ImportedFilename; origin: string;
                      mode: ImportFilter; exports: var seq[(string, ImportFilter)];
                      info: PackedLineInfo): SymId =
  let f2 = resolveFile(c.g.config.paths, origin, f1.path)
  if not fileExists(f2):
    c.buildErr dest, info, "file not found: " & f2
    return
  let suffix = moduleSuffix(f2, c.g.config.paths)
  result = SymId(0)
  if not c.processedModules.contains(suffix):
    c.meta.importedFiles.add f2
    if f1.plugin.len == 0:
      if (c.canSelfExec or c.inWhen > 0) and needsRecompile(f2, suffixToNif suffix):
        selfExec c, f2, (if f1.isSystem: " --isSystem" else: "")

    let moduleName = pool.strings.getOrIncl(f1.name)
    result = identToSym(c, moduleName, ModuleY)
    c.processedModules[suffix] = result
    let s = Sym(kind: ModuleY, name: result, pos: ImportedPos)
    if f1.name != "":
      c.currentScope.addOverloadable(moduleName, s)
    var moduleDecl = createTokenBuf(2)
    moduleDecl.addParLe(ModuleY, info)
    moduleDecl.addParRi()
    publish result, moduleDecl, SemcheckBodies
  else:
    result = c.processedModules.getOrQuit(suffix)
  let module = addr c.importedModules.mgetOrPut(result, ImportedModule(path: f2, fromPlugin: f1.plugin))
  loadInterface suffix, module.iface, result, c.importTab, c.converters, exports, mode
  # Track NIF snippet for this import using absolute path, for use by exprexec.
  # Skip system.nim since it's auto-imported and would cause cycles:
  if not f1.isSystem and suffix != SystemModuleSuffix:
    c.importSnippets.buildTree ImportS, info:
      c.importSnippets.addStrLit f2.toAbsolutePath

proc importSingleFile(c: var SemContext; dest: var TokenBuf; f1: ImportedFilename; origin: string;
                      filter: ImportFilter;
                      info: PackedLineInfo) =
  var exports: seq[(string, ImportFilter)] = @[] # ignored
  discard importSingleFile(c, dest, f1, origin, filter, exports, info)

proc importSingleFileConsiderExports(c: var SemContext; dest: var TokenBuf; f1: ImportedFilename; origin: string; filter: ImportFilter; info: PackedLineInfo) =
  var exports: seq[(string, ImportFilter)] = @[]
  let source = importSingleFile(c, dest, f1, origin, filter, exports, info)
  while exports.len != 0:
    var newExports: seq[(string, ImportFilter)] = @[]
    for ex in exports:
      let file = ImportedFilename(path: ex[0], name: "")
      let forward = importSingleFile(c, dest, file, origin, ex[1], newExports, info)
      let modEntry = addr c.importedModules.getOrQuit(source)
      if modEntry[].exports.hasKey(forward):
        mergeFilter(modEntry[].exports.getOrQuit(forward), ex[1])
      else:
        modEntry[].exports[forward] = ex[1]
    exports = newExports

proc cyclicImport(c: var SemContext; dest: var TokenBuf; x: var Cursor) =
  ## Handle `import foo {.cyclic.}`. Registers the module sym and records
  ## the import for deferred resolution (after phase1 of all cycle group members).
  let info = x.info
  while x.kind != ParRi:
    var isCyclic = false
    if x.kind == ParLe and x.exprKind == PragmaxX:
      var y = x
      inc y
      skip y
      if y.substructureKind == PragmasU:
        inc y
        if y.kind == Ident and pool.strings[y.litId] == "cyclic":
          isCyclic = true

    if isCyclic:
      # Manually parse the pragmax wrapper to extract the filename
      inc x, SkipTag # enter PragmaxX
      var files: seq[ImportedFilename] = @[]
      var hasError = false
      filenameVal(x, files, hasError, allowAs = false)
      skip x, SkipPragmas # skip (pragmas cyclic)
      inc x, SkipParRi  # skip closing ParRi of PragmaxX

      if not hasError:
        let origin = getFile(info)
        for f1 in files:
          let f2 = resolveFile(c.g.config.paths, origin, f1.path)
          if not semos.fileExists(f2):
            c.buildErr dest, info, "file not found: " & f2
            continue
          let suffix = moduleSuffix(f2, c.g.config.paths)
          var moduleSym = SymId(0)
          if not c.processedModules.contains(suffix):
            c.meta.importedFiles.add f2
            let moduleName = pool.strings.getOrIncl(f1.name)
            moduleSym = identToSym(c, moduleName, ModuleY)
            c.processedModules[suffix] = moduleSym
            let s = Sym(kind: ModuleY, name: moduleSym, pos: ImportedPos)
            if f1.name != "":
              c.currentScope.addOverloadable(moduleName, s)
            var moduleDecl = createTokenBuf(2)
            moduleDecl.addParLe(ModuleY, info)
            moduleDecl.addParRi()
            publish moduleSym, moduleDecl, SemcheckBodies
          else:
            moduleSym = c.processedModules.getOrQuit(suffix)
          discard c.importedModules.mgetOrPut(moduleSym, ImportedModule(path: f2))
          # Defer the actual interface loading until after phase2
          c.deferredCyclicImports.add (suffix, moduleSym)
    else:
      # Non-cyclic import in same statement; skip it (should not happen in practice)
      skip x

proc doImports(c: var SemContext; dest: var TokenBuf; files: seq[ImportedFilename]; mode: ImportFilter; info: PackedLineInfo) =
  let origin = getFile(info)
  for f in files:
    importSingleFileConsiderExports c, dest, f, origin, mode, info

template maybeCyclic(c: var SemContext; dest: var TokenBuf; x: var Cursor) =
  if x.kind == ParLe and x.exprKind == PragmaxX:
    var y = x
    inc y
    skip y
    if y.substructureKind == PragmasU:
      inc y
      if y.kind == Ident and pool.strings[y.litId] == "cyclic":
        cyclicImport(c, dest, x)
        return

proc semImport(c: var SemContext; dest: var TokenBuf; it: var Item) =
  let info = it.n.info
  var x = it.n
  skip it.n
  inc x, SkipTag # skip the `import`
  maybeCyclic(c, dest, x)

  var files: seq[ImportedFilename] = @[]
  var hasError = false
  while x.kind != ParRi:
    filenameVal(x, files, hasError, allowAs = true)
  if hasError:
    c.buildErr dest, info, "wrong `import` statement"
  else:
    doImports c, dest, files, ImportFilter(kind: ImportAll), info

  producesVoid c, dest, info, it.typ

proc semImportExcept(c: var SemContext; dest: var TokenBuf; it: var Item) =
  let info = it.n.info
  var x = it.n
  skip it.n
  inc x, SkipTag # skip the `importexcept`

  maybeCyclic(c, dest, x)

  var files: seq[ImportedFilename] = @[]
  var hasError = false
  filenameVal(x, files, hasError, allowAs = true)
  if hasError:
    c.buildErr dest, info, "wrong `import except` statement"
  else:
    var excluded = initHashSet[StrId]()
    while x.kind != ParRi:
      excluded.incl takeIdent(x)
    doImports c, dest, files, ImportFilter(kind: ImportExcept, list: excluded), info

  producesVoid c, dest, info, it.typ

proc semFromImport(c: var SemContext; dest: var TokenBuf; it: var Item) =
  let info = it.n.info
  var x = it.n
  skip it.n
  inc x, SkipTag # skip the `from`

  maybeCyclic(c, dest, x)

  var files: seq[ImportedFilename] = @[]
  var hasError = false
  filenameVal(x, files, hasError, allowAs = true)
  if hasError:
    c.buildErr dest, info, "wrong `from import` statement"
  else:
    var included = initHashSet[StrId]()
    while x.kind != ParRi:
      if x.kind == ParLe and x.exprKind == NilX:
        # from a import nil
        skip x
      else:
        included.incl takeIdent(x)
    doImports c, dest, files, ImportFilter(kind: FromImport, list: included), info

  producesVoid c, dest, info, it.typ

proc findModuleSymbol(n: Cursor): SymId =
  result = SymId(0)
  if n.kind == Symbol:
    let res = tryLoadSym(n.symId)
    if res.status == LacksNothing and symKind(res.decl) == ModuleY:
      result = n.symId
  elif n.kind == ParLe and exprKind(n) in {OchoiceX, CchoiceX}:
    # if any sym in choice is module sym, count it as a module reference
    # this emulates behavior that was caused by sym order shenanigans before, could be removed
    var n = n
    inc n
    while n.kind != ParRi:
      result = findModuleSymbol(n)
      if result != SymId(0): break
      inc n

proc semExportSymbol(c: var SemContext; dest: var TokenBuf; n: var Cursor) =
  let info = n.info
  if n.exprKind == DotX:
    var it = Item(n: n, typ: c.types.autoType)
    let oldPhase = c.phase
    c.phase = SemcheckBodies
    semExpr c, dest, it
    c.phase = oldPhase
    n = it.n
  else:
    let ident = takeIdent(n)
    if ident == StrId(0):
      c.buildErr dest, info, "not an identifier"
      return
    discard buildSymChoice(c, dest, ident, info, FindAll)

proc registerExportName(c: var SemContext; moduleSym: SymId; strId: StrId) =
  if c.exports.hasKey(moduleSym):
    let entry = addr c.exports.getOrQuit(moduleSym)
    case entry[].kind
    of ImportAll:
      # nothing to do, already exported
      discard
    of FromImport:
      entry[].list.incl strId
    of ImportExcept:
      entry[].list.excl strId
  else:
    c.exports[moduleSym] = ImportFilter(kind: FromImport, list: initHashSet[StrId]())
    c.exports.getOrQuit(moduleSym).list.incl strId

proc doExport(c: var SemContext; dest: var TokenBuf; sym: SymId; info: PackedLineInfo) =
  let res = tryLoadSym(sym)
  let isModule = res.status == LacksNothing and res.decl.symKind == ModuleY
  if isModule:
    # overrides previous export mode if exists
    c.exports[sym] = ImportFilter(kind: ImportAll)
  else:
    let name = pool.syms[sym]
    let suffix = extractModule(name)
    if suffix == "":
      c.buildErr dest, info, "cannot export non-global symbol"
      return
    elif suffix == c.thisModuleSuffix:
      # XXX
      c.buildErr dest, info, "exporting local symbol not implemented"
      return
    let moduleSym = c.processedModules.getOrQuit(suffix)
    var basename = name
    extractBasename(basename)
    registerExportName(c, moduleSym, pool.strings.getOrIncl(basename))
    # Enum types carry their fields as separately-named symbols. Exporting only
    # the type name leaves the field names filtered out at the import site
    # (e.g. `export NifKind` wouldn't bring `ParLe`/`DotToken` into scope).
    # Walk the enum body and enroll each field basename in the same filter.
    if res.status == LacksNothing and res.decl.symKind == TypeY:
      let decl = asTypeDecl(res.decl)
      if decl.body.typeKind in {EnumT, OnumT, HoleyEnumT, AnumT}:
        var field = asEnumDecl(decl.body).firstField
        while field.kind != ParRi:
          if field.substructureKind == EfldU:
            let local = asLocal(field)
            if local.name.kind == SymbolDef:
              var fname = pool.syms[local.name.symId]
              extractBasename(fname)
              registerExportName(c, moduleSym, pool.strings.getOrIncl(fname))
          skip field

proc semExport(c: var SemContext; dest: var TokenBuf; it: var Item) =
  let info = it.n.info
  var x = it.n
  skip it.n
  inc x, SkipTag # skip the `export`

  while x.kind != ParRi:
    let info = x.info
    var symBuf = createTokenBuf(8)
    semExportSymbol(c, symBuf, x)
    var syms = beginRead(symBuf)
    case syms.kind
    of Ident:
      c.buildErr dest, info, "undeclared identifier: " & pool.strings[syms.litId]
    of Symbol:
      doExport(c, dest, syms.symId, info)
    of ParLe:
      case syms.exprKind
      of ErrX:
        dest.add symBuf
      of OchoiceX, CchoiceX:
        inc syms
        while syms.kind != ParRi:
          assert syms.kind == Symbol
          doExport(c, dest, syms.symId, info)
          inc syms
      else:
        c.buildErr dest, info, "not an identifier for `export`"
    else:
      c.buildErr dest, info, "not an identifier for `export`"

  producesVoid c, dest, info, it.typ

proc doExportExcept(c: var SemContext; dest: var TokenBuf; moduleSym, sym: SymId; info: PackedLineInfo) =
  let name = pool.syms[sym]
  let suffix = extractModule(name)
  if not c.processedModules.hasKey(suffix) or
      c.processedModules.getOrQuit(suffix) != moduleSym:
    # doesn't belong to exported module, no need to consider
    return
  var basename = ensureMove name
  extractBasename(basename)
  let strId = pool.strings.getOrIncl(basename)
  if c.exports.hasKey(moduleSym):
    let entry = addr c.exports.getOrQuit(moduleSym)
    case entry[].kind
    of ImportAll:
      entry[] = ImportFilter(kind: ImportExcept, list: initHashSet[StrId]())
      entry[].list.incl strId
    of FromImport:
      entry[].list.excl strId
    of ImportExcept:
      entry[].list.incl strId
  else:
    c.exports[moduleSym] = ImportFilter(kind: ImportExcept, list: initHashSet[StrId]())
    c.exports.getOrQuit(moduleSym).list.incl strId

proc semExportExcept(c: var SemContext; dest: var TokenBuf; it: var Item) =
  let info = it.n.info
  var x = it.n
  skip it.n
  inc x, SkipTag # skip the `exportexcept`

  let moduleSymStart = dest.len
  var m = Item(n: x, typ: c.types.autoType)
  semExpr c, dest, m, {AllowModuleSym} # get module sym
  x = m.n
  let moduleSym = findModuleSymbol(cursorAt(dest, moduleSymStart))
  endRead(dest)
  dest.shrink moduleSymStart
  if moduleSym == SymId(0):
    c.buildErr dest, info, "expected module for `export except`"
    return

  while x.kind != ParRi:
    let info = x.info
    var symBuf = createTokenBuf(8)
    semExportSymbol(c, symBuf, x)
    var syms = beginRead(symBuf)
    case syms.kind
    of Ident:
      c.buildErr dest, info, "undeclared identifier: " & pool.strings[syms.litId]
    of Symbol:
      doExportExcept(c, dest, moduleSym, syms.symId, info)
    of ParLe:
      case syms.exprKind
      of ErrX:
        dest.add symBuf
      of OchoiceX, CchoiceX:
        inc syms
        while syms.kind != ParRi:
          assert syms.kind == Symbol
          doExportExcept(c, dest, moduleSym, syms.symId, info)
          inc syms
      else:
        c.buildErr dest, info, "not an identifier for `export`"
    else:
      c.buildErr dest, info, "not an identifier for `export`"

  producesVoid c, dest, info, it.typ
