#
#
#           The Nim Compiler
#        (c) Copyright 2013 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

import
  options, strutils, os, tables, ropes, terminal, macros,
  lineinfos, pathutils

proc toCChar*(c: char; result: var string) =
  case c
  of '\0'..'\x1F', '\x7F'..'\xFF':
    result.add '\\'
    result.add toOctal(c)
  of '\'', '\"', '\\', '?':
    result.add '\\'
    result.add c
  else:
    result.add c

proc makeCString*(s: string): Rope =
  const MaxLineLength = 64
  result = nil
  var res = newStringOfCap(int(s.len.toFloat * 1.1) + 1)
  add(res, "\"")
  for i in 0 ..< len(s):
    if (i + 1) mod MaxLineLength == 0:
      add(res, "\"\L\"")
    toCChar(s[i], res)
  add(res, '\"')
  add(result, rope(res))


proc newFileInfo(fullPath: AbsoluteFile, projPath: RelativeFile): TFileInfo =
  result.fullPath = fullPath
  #shallow(result.fullPath)
  result.projPath = projPath
  #shallow(result.projPath)
  result.shortName = fullPath.extractFilename
  result.quotedName = result.shortName.makeCString
  result.quotedFullName = fullPath.string.makeCString
  result.lines = @[]
  when defined(nimpretty):
    if not result.fullPath.isEmpty:
      try:
        result.fullContent = readFile(result.fullPath.string)
      except IOError:
        #rawMessage(errCannotOpenFile, result.fullPath)
        # XXX fixme
        result.fullContent = ""

when defined(nimpretty):
  proc fileSection*(conf: ConfigRef; fid: FileIndex; a, b: int): string =
    substr(conf.m.fileInfos[fid.int].fullContent, a, b)

proc fileInfoKnown*(conf: ConfigRef; filename: AbsoluteFile): bool =
  var
    canon: AbsoluteFile
  try:
    canon = canonicalizePath(conf, filename)
  except OSError:
    canon = filename
  result = conf.m.filenameToIndexTbl.hasKey(canon.string)

proc fileInfoIdx*(conf: ConfigRef; filename: AbsoluteFile; isKnownFile: var bool): FileIndex =
  var
    canon: AbsoluteFile
    pseudoPath = false

  try:
    canon = canonicalizePath(conf, filename)
    shallow(canon.string)
  except OSError:
    canon = filename
    # The compiler uses "filenames" such as `command line` or `stdin`
    # This flag indicates that we are working with such a path here
    pseudoPath = true

  if conf.m.filenameToIndexTbl.hasKey(canon.string):
    result = conf.m.filenameToIndexTbl[canon.string]
  else:
    isKnownFile = false
    result = conf.m.fileInfos.len.FileIndex
    conf.m.fileInfos.add(newFileInfo(canon, if pseudoPath: RelativeFile filename
                                            else: relativeTo(canon, conf.projectPath)))
    conf.m.filenameToIndexTbl[canon.string] = result

proc fileInfoIdx*(conf: ConfigRef; filename: AbsoluteFile): FileIndex =
  var dummy: bool
  result = fileInfoIdx(conf, filename, dummy)

proc newLineInfo*(fileInfoIdx: FileIndex, line, col: int): TLineInfo =
  result.fileIndex = fileInfoIdx
  if line < int high(uint16):
    result.line = uint16(line)
  else:
    result.line = high(uint16)
  if col < int high(int16):
    result.col = int16(col)
  else:
    result.col = -1

proc newLineInfo*(conf: ConfigRef; filename: AbsoluteFile, line, col: int): TLineInfo {.inline.} =
  result = newLineInfo(fileInfoIdx(conf, filename), line, col)


proc concat(strings: openArray[string]): string =
  var totalLen = 0
  for s in strings: totalLen += s.len
  result = newStringOfCap totalLen
  for s in strings: result.add s

proc suggestWriteln*(conf: ConfigRef; s: string) =
  if eStdOut in conf.m.errorOutputs:
    if isNil(conf.writelnHook):
      writeLine(stdout, s)
      flushFile(stdout)
    else:
      conf.writelnHook(s)

proc msgQuit*(x: int8) = quit x
proc msgQuit*(x: string) = quit x

proc suggestQuit*() =
  raise newException(ESuggestDone, "suggest done")

# this format is understood by many text editors: it is the same that
# Borland and Freepascal use
const
  PosFormat    = "$1($2, $3) "
  KindFormat   = " [$1]"
  KindColor    = fgCyan
  ErrorTitle   = "Error: "
  ErrorColor   = fgRed
  WarningTitle = "Warning: "
  WarningColor = fgYellow
  HintTitle    = "Hint: "
  HintColor    = fgGreen
  # NOTE: currently line info line numbers start with 1,
  # but column numbers start with 0, however most editors expect
  # first column to be 1, so we need to +1 here
  ColOffset*   = 1

proc getInfoContextLen*(conf: ConfigRef): int = return conf.m.msgContext.len
proc setInfoContextLen*(conf: ConfigRef; L: int) = setLen(conf.m.msgContext, L)

proc pushInfoContext*(conf: ConfigRef; info: TLineInfo; detail: string = "") =
  conf.m.msgContext.add((info, detail))

proc popInfoContext*(conf: ConfigRef) =
  setLen(conf.m.msgContext, len(conf.m.msgContext) - 1)

proc getInfoContext*(conf: ConfigRef; index: int): TLineInfo =
  let L = conf.m.msgContext.len
  let i = if index < 0: L + index else: index
  if i >=% L: result = unknownLineInfo()
  else: result = conf.m.msgContext[i].info

const
  commandLineDesc = "command line"

template toFilename*(conf: ConfigRef; fileIdx: FileIndex): string =
  if fileIdx.int32 < 0 or conf == nil:
    (if fileIdx == commandLineIdx: commandLineDesc else: "???")
  else:
    conf.m.fileInfos[fileIdx.int32].shortName

proc toProjPath*(conf: ConfigRef; fileIdx: FileIndex): string =
  if fileIdx.int32 < 0 or conf == nil:
    (if fileIdx == commandLineIdx: commandLineDesc else: "???")
  else: conf.m.fileInfos[fileIdx.int32].projPath.string

proc toFullPath*(conf: ConfigRef; fileIdx: FileIndex): string =
  if fileIdx.int32 < 0 or conf == nil:
    result = (if fileIdx == commandLineIdx: commandLineDesc else: "???")
  else:
    result = conf.m.fileInfos[fileIdx.int32].fullPath.string

proc setDirtyFile*(conf: ConfigRef; fileIdx: FileIndex; filename: AbsoluteFile) =
  assert fileIdx.int32 >= 0
  conf.m.fileInfos[fileIdx.int32].dirtyFile = filename
  setLen conf.m.fileInfos[fileIdx.int32].lines, 0

proc setHash*(conf: ConfigRef; fileIdx: FileIndex; hash: string) =
  assert fileIdx.int32 >= 0
  shallowCopy(conf.m.fileInfos[fileIdx.int32].hash, hash)

proc getHash*(conf: ConfigRef; fileIdx: FileIndex): string =
  assert fileIdx.int32 >= 0
  shallowCopy(result, conf.m.fileInfos[fileIdx.int32].hash)

proc toFullPathConsiderDirty*(conf: ConfigRef; fileIdx: FileIndex): AbsoluteFile =
  if fileIdx.int32 < 0:
    result = AbsoluteFile(if fileIdx == commandLineIdx: commandLineDesc else: "???")
  elif not conf.m.fileInfos[fileIdx.int32].dirtyFile.isEmpty:
    result = conf.m.fileInfos[fileIdx.int32].dirtyFile
  else:
    result = conf.m.fileInfos[fileIdx.int32].fullPath

template toFilename*(conf: ConfigRef; info: TLineInfo): string =
  toFilename(conf, info.fileIndex)

template toProjPath*(conf: ConfigRef; info: TLineInfo): string =
  toProjPath(conf, info.fileIndex)

template toFullPath*(conf: ConfigRef; info: TLineInfo): string =
  toFullPath(conf, info.fileIndex)

template toFullPathConsiderDirty*(conf: ConfigRef; info: TLineInfo): string =
  string toFullPathConsiderDirty(conf, info.fileIndex)

proc toMsgFilename*(conf: ConfigRef; info: FileIndex): string =
  let
    absPath = toFullPath(conf, info)
    relPath = toProjPath(conf, info)
  result = if (optListFullPaths in conf.globalOptions) or
              (relPath.len > absPath.len) or
              (relPath.count("..") > 2):
             absPath
           else:
             relPath

template toMsgFilename*(conf: ConfigRef; info: TLineInfo): string =
  toMsgFilename(conf, info.fileIndex)

proc toLinenumber*(info: TLineInfo): int {.inline.} =
  result = int info.line

proc toColumn*(info: TLineInfo): int {.inline.} =
  result = info.col

proc toFileLineCol*(conf: ConfigRef; info: TLineInfo): string {.inline.} =
  # consider calling `helpers.lineInfoToString` instead
  result = toMsgFilename(conf, info) & "(" & $info.line & ", " &
    $(info.col + ColOffset) & ")"

proc `$`*(conf: ConfigRef; info: TLineInfo): string = toFileLineCol(conf, info)

proc `$`*(info: TLineInfo): string {.error.} = discard

proc `??`* (conf: ConfigRef; info: TLineInfo, filename: string): bool =
  # only for debugging purposes
  result = filename in toFilename(conf, info)

type
  MsgFlag* = enum  ## flags altering msgWriteln behavior
    msgStdout,     ## force writing to stdout, even stderr is default
    msgSkipHook    ## skip message hook even if it is present
  MsgFlags* = set[MsgFlag]

proc msgWriteln*(conf: ConfigRef; s: string, flags: MsgFlags = {}) =
  ## Writes given message string to stderr by default.
  ## If ``--stdout`` option is given, writes to stdout instead. If message hook
  ## is present, then it is used to output message rather than stderr/stdout.
  ## This behavior can be altered by given optional flags.

  ## This is used for 'nim dump' etc. where we don't have nimsuggest
  ## support.
  #if conf.cmd == cmdIdeTools and optCDebug notin gGlobalOptions: return

  if not isNil(conf.writelnHook) and msgSkipHook notin flags:
    conf.writelnHook(s)
  elif optStdout in conf.globalOptions or msgStdout in flags:
    if eStdOut in conf.m.errorOutputs:
      writeLine(stdout, s)
      flushFile(stdout)
  else:
    if eStdErr in conf.m.errorOutputs:
      writeLine(stderr, s)
      # On Windows stderr is fully-buffered when piped, regardless of C std.
      when defined(windows):
        flushFile(stderr)

macro callIgnoringStyle(theProc: typed, first: typed,
                        args: varargs[typed]): untyped =
  let typForegroundColor = bindSym"ForegroundColor".getType
  let typBackgroundColor = bindSym"BackgroundColor".getType
  let typStyle = bindSym"Style".getType
  let typTerminalCmd = bindSym"TerminalCmd".getType
  result = newCall(theProc)
  if first.kind != nnkNilLit: result.add(first)
  for arg in children(args[0][1]):
    if arg.kind == nnkNilLit: continue
    let typ = arg.getType
    if typ.kind != nnkEnumTy or
       typ != typForegroundColor and
       typ != typBackgroundColor and
       typ != typStyle and
       typ != typTerminalCmd:
      result.add(arg)

macro callStyledWriteLineStderr(args: varargs[typed]): untyped =
  result = newCall(bindSym"styledWriteLine")
  result.add(bindSym"stderr")
  for arg in children(args[0][1]):
    result.add(arg)

template callWritelnHook(args: varargs[string, `$`]) =
  conf.writelnHook concat(args)

template styledMsgWriteln*(args: varargs[typed]) =
  if not isNil(conf.writelnHook):
    callIgnoringStyle(callWritelnHook, nil, args)
  elif optStdout in conf.globalOptions:
    if eStdOut in conf.m.errorOutputs:
      callIgnoringStyle(writeLine, stdout, args)
      flushFile(stdout)
  else:
    if eStdErr in conf.m.errorOutputs:
      if optUseColors in conf.globalOptions:
        callStyledWriteLineStderr(args)
      else:
        callIgnoringStyle(writeLine, stderr, args)
      # On Windows stderr is fully-buffered when piped, regardless of C std.
      when defined(windows):
        flushFile(stderr)

proc coordToStr(coord: int): string =
  if coord == -1: result = "???"
  else: result = $coord

proc msgKindToString*(kind: TMsgKind): string =
  # later versions may provide translated error messages
  result = MsgKindToStr[kind]

proc getMessageStr(msg: TMsgKind, arg: string): string =
  result = msgKindToString(msg) % [arg]

type
  TErrorHandling = enum doNothing, doAbort, doRaise

proc log*(s: string) {.procvar.} =
  var f: File
  if open(f, getHomeDir() / "nimsuggest.log", fmAppend):
    f.writeLine(s)
    close(f)

proc quit(conf: ConfigRef; msg: TMsgKind) {.gcsafe.} =
  if defined(debug) or msg == errInternal or hintStackTrace in conf.notes:
    {.gcsafe.}:
      if stackTraceAvailable() and isNil(conf.writelnHook):
        writeStackTrace()
      else:
        styledMsgWriteln(fgRed, "No stack traceback available\n" &
            "To create a stacktrace, rerun compilation with ./koch temp " &
            conf.command & " <file>")
  quit 1

proc handleError(conf: ConfigRef; msg: TMsgKind, eh: TErrorHandling, s: string) =
  if msg >= fatalMin and msg <= fatalMax:
    if conf.cmd == cmdIdeTools: log(s)
    quit(conf, msg)
  if msg >= errMin and msg <= errMax:
    inc(conf.errorCounter)
    conf.exitcode = 1'i8
    if conf.errorCounter >= conf.errorMax:
      quit(conf, msg)
    elif eh == doAbort and conf.cmd != cmdIdeTools:
      quit(conf, msg)
    elif eh == doRaise:
      raiseRecoverableError(s)

proc `==`*(a, b: TLineInfo): bool =
  result = a.line == b.line and a.fileIndex == b.fileIndex

proc exactEquals*(a, b: TLineInfo): bool =
  result = a.fileIndex == b.fileIndex and a.line == b.line and a.col == b.col

proc writeContext(conf: ConfigRef; lastinfo: TLineInfo) =
  const instantiationFrom = "template/generic instantiation from here"
  const instantiationOfFrom = "template/generic instantiation of `$1` from here"
  var info = lastinfo
  for i in 0 ..< len(conf.m.msgContext):
    let context = conf.m.msgContext[i]
    if context.info != lastinfo and context.info != info:
      if conf.structuredErrorHook != nil:
        conf.structuredErrorHook(conf, context.info, instantiationFrom,
                                 Severity.Error)
      else:
        let message = if context.detail == "":
          instantiationFrom
        else:
          instantiationOfFrom.format(context.detail)
        styledMsgWriteln(styleBright,
                         PosFormat % [toMsgFilename(conf, context.info),
                                      coordToStr(context.info.line.int),
                                      coordToStr(context.info.col+ColOffset)],
                         resetStyle,
                         message)
    info = context.info

proc ignoreMsgBecauseOfIdeTools(conf: ConfigRef; msg: TMsgKind): bool =
  msg >= errGenerated and conf.cmd == cmdIdeTools and optIdeDebug notin conf.globalOptions

proc rawMessage*(conf: ConfigRef; msg: TMsgKind, args: openArray[string]) =
  var
    title: string
    color: ForegroundColor
    kind: string
    sev: Severity
  case msg
  of errMin..errMax:
    sev = Severity.Error
    writeContext(conf, unknownLineInfo())
    title = ErrorTitle
    color = ErrorColor
  of warnMin..warnMax:
    sev = Severity.Warning
    if optWarns notin conf.options: return
    if msg notin conf.notes: return
    writeContext(conf, unknownLineInfo())
    title = WarningTitle
    color = WarningColor
    kind = WarningsToStr[ord(msg) - ord(warnMin)]
    inc(conf.warnCounter)
  of hintMin..hintMax:
    sev = Severity.Hint
    if optHints notin conf.options: return
    if msg notin conf.notes: return
    title = HintTitle
    color = HintColor
    if msg != hintUserRaw: kind = HintsToStr[ord(msg) - ord(hintMin)]
    inc(conf.hintCounter)
  let s = msgKindToString(msg) % args

  if conf.structuredErrorHook != nil:
    conf.structuredErrorHook(conf, unknownLineInfo(),
      s & (if kind.len > 0: KindFormat % kind else: ""), sev)

  if not ignoreMsgBecauseOfIdeTools(conf, msg):
    if kind.len > 0:
      styledMsgWriteln(color, title, resetStyle, s,
                       KindColor, `%`(KindFormat, kind))
    else:
      styledMsgWriteln(color, title, resetStyle, s)
  handleError(conf, msg, doAbort, s)

proc rawMessage*(conf: ConfigRef; msg: TMsgKind, arg: string) =
  rawMessage(conf, msg, [arg])

proc resetAttributes*(conf: ConfigRef) =
  if {optUseColors, optStdout} * conf.globalOptions == {optUseColors}:
    terminal.resetAttributes(stderr)

proc addSourceLine(conf: ConfigRef; fileIdx: FileIndex, line: string) =
  conf.m.fileInfos[fileIdx.int32].lines.add line

proc sourceLine*(conf: ConfigRef; i: TLineInfo): string =
  if i.fileIndex.int32 < 0: return ""

  if conf.m.fileInfos[i.fileIndex.int32].lines.len == 0:
    try:
      for line in lines(toFullPathConsiderDirty(conf, i)):
        addSourceLine conf, i.fileIndex, line.string
    except IOError:
      discard
  assert i.fileIndex.int32 < conf.m.fileInfos.len
  # can happen if the error points to EOF:
  if i.line.int > conf.m.fileInfos[i.fileIndex.int32].lines.len: return ""

  result = conf.m.fileInfos[i.fileIndex.int32].lines[i.line.int-1]

proc writeSurroundingSrc(conf: ConfigRef; info: TLineInfo) =
  const indent = "  "
  msgWriteln(conf, indent & $sourceLine(conf, info))
  if info.col >= 0:
    msgWriteln(conf, indent & spaces(info.col) & '^')

proc formatMsg*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg: string): string =
  let title = case msg
              of warnMin..warnMax: WarningTitle
              of hintMin..hintMax: HintTitle
              else: ErrorTitle
  result = PosFormat % [toMsgFilename(conf, info), coordToStr(info.line.int),
                        coordToStr(info.col+ColOffset)] &
           title &
           getMessageStr(msg, arg)

proc liMessage(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg: string,
               eh: TErrorHandling) =
  var
    title: string
    color: ForegroundColor
    kind:  string
    ignoreMsg = false
    sev: Severity
  case msg
  of errMin..errMax:
    sev = Severity.Error
    writeContext(conf, info)
    title = ErrorTitle
    color = ErrorColor
    # we try to filter error messages so that not two error message
    # in the same file and line are produced:
    #ignoreMsg = lastError == info and eh != doAbort
    conf.m.lastError = info
  of warnMin..warnMax:
    sev = Severity.Warning
    ignoreMsg = optWarns notin conf.options or msg notin conf.notes
    if not ignoreMsg: writeContext(conf, info)
    title = WarningTitle
    color = WarningColor
    kind = WarningsToStr[ord(msg) - ord(warnMin)]
    inc(conf.warnCounter)
  of hintMin..hintMax:
    sev = Severity.Hint
    ignoreMsg = optHints notin conf.options or msg notin conf.notes
    title = HintTitle
    color = HintColor
    if msg != hintUserRaw: kind = HintsToStr[ord(msg) - ord(hintMin)]
    inc(conf.hintCounter)
  let x = PosFormat % [toMsgFilename(conf, info), coordToStr(info.line.int),
                       coordToStr(info.col+ColOffset)]
  let s = getMessageStr(msg, arg)

  if not ignoreMsg:
    if conf.structuredErrorHook != nil:
      conf.structuredErrorHook(conf, info, s & (if kind.len > 0: KindFormat % kind else: ""), sev)
    if not ignoreMsgBecauseOfIdeTools(conf, msg):
      if kind.len > 0:
        styledMsgWriteln(styleBright, x, resetStyle, color, title, resetStyle, s,
                         KindColor, `%`(KindFormat, kind))
      else:
        styledMsgWriteln(styleBright, x, resetStyle, color, title, resetStyle, s)
      if hintSource in conf.notes:
        conf.writeSurroundingSrc(info)
  handleError(conf, msg, eh, s)

proc fatal*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") =
  # this fixes bug #7080 so that it is at least obvious 'fatal'
  # was executed.
  conf.m.errorOutputs = {eStdOut, eStdErr}
  liMessage(conf, info, msg, arg, doAbort)

proc globalError*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") =
  liMessage(conf, info, msg, arg, doRaise)

proc globalError*(conf: ConfigRef; info: TLineInfo, arg: string) =
  liMessage(conf, info, errGenerated, arg, doRaise)

proc localError*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") =
  liMessage(conf, info, msg, arg, doNothing)

proc localError*(conf: ConfigRef; info: TLineInfo, arg: string) =
  liMessage(conf, info, errGenerated, arg, doNothing)

proc localError*(conf: ConfigRef; info: TLineInfo, format: string, params: openArray[string]) =
  localError(conf, info, format % params)

proc message*(conf: ConfigRef; info: TLineInfo, msg: TMsgKind, arg = "") =
  liMessage(conf, info, msg, arg, doNothing)

proc internalError*(conf: ConfigRef; info: TLineInfo, errMsg: string) =
  if conf.cmd == cmdIdeTools and conf.structuredErrorHook.isNil: return
  writeContext(conf, info)
  liMessage(conf, info, errInternal, errMsg, doAbort)

proc internalError*(conf: ConfigRef; errMsg: string) =
  if conf.cmd == cmdIdeTools and conf.structuredErrorHook.isNil: return
  writeContext(conf, unknownLineInfo())
  rawMessage(conf, errInternal, errMsg)

template assertNotNil*(conf: ConfigRef; e): untyped =
  if e == nil: internalError(conf, $instantiationInfo())
  e

template internalAssert*(conf: ConfigRef, e: bool) =
  if not e: internalError(conf, $instantiationInfo())

proc quotedFilename*(conf: ConfigRef; i: TLineInfo): Rope =
  if i.fileIndex.int32 < 0:
    result = makeCString "???"
  elif optExcessiveStackTrace in conf.globalOptions:
    result = conf.m.fileInfos[i.fileIndex.int32].quotedFullName
  else:
    result = conf.m.fileInfos[i.fileIndex.int32].quotedName

proc listWarnings*(conf: ConfigRef) =
  msgWriteln(conf, "Warnings:")
  for warn in warnMin..warnMax:
    msgWriteln(conf, "  [$1] $2" % [
      if warn in conf.notes: "x" else: " ",
      lineinfos.WarningsToStr[ord(warn) - ord(warnMin)]
    ])

proc listHints*(conf: ConfigRef) =
  msgWriteln(conf, "Hints:")
  for hint in hintMin..hintMax:
    msgWriteln(conf, "  [$1] $2" % [
      if hint in conf.notes: "x" else: " ",
      lineinfos.HintsToStr[ord(hint) - ord(hintMin)]
    ])

proc lintReport*(conf: ConfigRef; info: TLineInfo, beau, got: string) =
  let m = "'$2' should be: '$1'" % [beau, got]
  if optStyleError in conf.globalOptions:
    localError(conf, info, m)
  else:
    message(conf, info, hintName, m)
