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

## Low-level streams for high performance.

import
  pathutils
import std/strutils
when defined(nimPreviewSlimSystem):
  import std/syncio

# support `useGnuReadline`, `useLinenoise` for backwards compatibility
const hasRstdin = (defined(nimUseLinenoise) or defined(useLinenoise) or defined(useGnuReadline)) and
  not defined(windows)

when hasRstdin: import std/rdstdin

type
  TLLRepl* = proc (s: PLLStream, buf: pointer, bufLen: int): int
  OnPrompt* = proc() {.closure.}
  TLLStreamKind* = enum       # enum of different stream implementations
    llsNone,                  # null stream: reading and writing has no effect
    llsString,                # stream encapsulates a string
    llsFile,                  # stream encapsulates a file
    llsStdIn                  # stream encapsulates stdin
  TLLStream* = object of RootObj
    kind*: TLLStreamKind # accessible for low-level access (lexbase uses this)
    f*: File
    s*: string
    rd*, wr*: int             # for string streams
    lineOffset*: int          # for fake stdin line numbers
    repl*: TLLRepl            # gives stdin control to clients
    onPrompt*: OnPrompt

  PLLStream* = ref TLLStream

proc llStreamOpen*(data: sink string): PLLStream =
  PLLStream(kind: llsString, s: data)

proc llStreamOpen*(f: File): PLLStream =
  PLLStream(kind: llsFile, f: f)

proc llStreamOpen*(filename: AbsoluteFile, mode: FileMode): PLLStream =
  result = PLLStream(kind: llsFile)
  if not open(result.f, filename.string, mode): result = nil

proc llStreamOpen*(): PLLStream =
  PLLStream(kind: llsNone)

proc llReadFromStdin(s: PLLStream, buf: pointer, bufLen: int): int
proc llStreamOpenStdIn*(r: TLLRepl = llReadFromStdin, onPrompt: OnPrompt = nil): PLLStream =
  PLLStream(kind: llsStdIn, s: "", lineOffset: -1, repl: r, onPrompt: onPrompt)

proc llStreamClose*(s: PLLStream) =
  case s.kind
  of llsNone, llsString, llsStdIn:
    discard
  of llsFile:
    close(s.f)

when not declared(readLineFromStdin):
  # fallback implementation:
  proc readLineFromStdin(prompt: string, line: var string): bool =
    stdout.write(prompt)
    stdout.flushFile()
    result = readLine(stdin, line)
    if not result:
      stdout.write("\n")
      quit(0)

proc endsWith*(x: string, s: set[char]): bool =
  var i = x.len-1
  while i >= 0 and x[i] == ' ': dec(i)
  if i >= 0 and x[i] in s:
    result = true
  else:
    result = false

const
  LineContinuationOprs = {'+', '-', '*', '/', '\\', '<', '>', '!', '?', '^',
                          '|', '%', '&', '$', '@', '~', ','}
  AdditionalLineContinuationOprs = {'#', ':', '='}
  LineContinuationTokens = [
    "let", "var", "const", "type",  # section
    "object", "tuple",
    # from ./layouter.oprSet
    "div", "mod", "shl", "shr", "in", "notin", "is",
    "isnot", "not", "of", "as", "from", "..", "and", "or", "xor", 
  ]  # must be all `nimIdentNormalized`-ed

proc eqIdent(a, bNormalized: string): bool =
  a.nimIdentNormalize == bNormalized

proc endsWithIdent(s, subs: string): bool =
  let le = subs.len
  if le > s.len: return false
  s[^le .. ^1].eqIdent subs

proc continuesWithIdent(s, subs: string, start: int): bool =
  s.substr(start, start+subs.high).eqIdent subs

proc endsWithIdent(s, subs: string, endIdx: var int): bool =
  endIdx.dec subs.len
  result = s.continuesWithIdent(subs, endIdx+1)

proc containsObjectOf(x: string): bool =
  const sep = ' '
  var idx = x.rfind(sep)
  if idx == -1: return
  template eatWord(word) =  
    while x[idx] == sep: idx.dec
    result = x.endsWithIdent(word, idx)
    if not result: return
  eatWord "of"
  eatWord "object"
  result = true

proc endsWithLineContinuationToken(x: string): bool =
  result = false
  for tok in LineContinuationTokens:
    if x.endsWithIdent(tok):
      return true
  result = x.containsObjectOf

proc endsWithOpr*(x: string): bool =
  result = x.endsWith(LineContinuationOprs)

proc continueLine(line: string, inTripleString: bool): bool {.inline.} =
  result = inTripleString or line.len > 0 and (
        line[0] == ' ' or
        line.endsWith(LineContinuationOprs+AdditionalLineContinuationOprs) or
        line.endsWithLineContinuationToken()
      )

proc countTriples(s: string): int =
  result = 0
  var i = 0
  while i+2 < s.len:
    if s[i] == '"' and s[i+1] == '"' and s[i+2] == '"':
      inc result
      inc i, 2
    inc i

proc llReadFromStdin(s: PLLStream, buf: pointer, bufLen: int): int =
  s.s = ""
  s.rd = 0
  var line = newStringOfCap(120)
  var triples = 0
  while true:
    if not readLineFromStdin(if s.s.len == 0: ">>> " else: "... ", line):
      # now readLineFromStdin meets EOF (ctrl-D/Z) or ctrl-C
      quit()
    s.s.add(line)
    s.s.add("\n")
    inc triples, countTriples(line)
    if not continueLine(line, (triples and 1) == 1): break
  inc(s.lineOffset)
  result = min(bufLen, s.s.len - s.rd)
  if result > 0:
    copyMem(buf, addr(s.s[s.rd]), result)
    inc(s.rd, result)

proc llStreamRead*(s: PLLStream, buf: pointer, bufLen: int): int =
  case s.kind
  of llsNone:
    result = 0
  of llsString:
    result = min(bufLen, s.s.len - s.rd)
    if result > 0:
      copyMem(buf, addr(s.s[0 + s.rd]), result)
      inc(s.rd, result)
  of llsFile:
    result = readBuffer(s.f, buf, bufLen)
  of llsStdIn:
    if s.onPrompt!=nil: s.onPrompt()
    result = s.repl(s, buf, bufLen)

proc llStreamReadLine*(s: PLLStream, line: var string): bool =
  setLen(line, 0)
  case s.kind
  of llsNone:
    result = true
  of llsString:
    while s.rd < s.s.len:
      case s.s[s.rd]
      of '\r':
        inc(s.rd)
        if s.s[s.rd] == '\n': inc(s.rd)
        break
      of '\n':
        inc(s.rd)
        break
      else:
        line.add(s.s[s.rd])
        inc(s.rd)
    result = line.len > 0 or s.rd < s.s.len
  of llsFile:
    result = readLine(s.f, line)
  of llsStdIn:
    result = readLine(stdin, line)

proc llStreamWrite*(s: PLLStream, data: string) =
  case s.kind
  of llsNone, llsStdIn:
    discard
  of llsString:
    s.s.add(data)
    inc(s.wr, data.len)
  of llsFile:
    write(s.f, data)

proc llStreamWriteln*(s: PLLStream, data: string) =
  llStreamWrite(s, data)
  llStreamWrite(s, "\n")

proc llStreamWrite*(s: PLLStream, data: char) =
  var c: char
  case s.kind
  of llsNone, llsStdIn:
    discard
  of llsString:
    s.s.add(data)
    inc(s.wr)
  of llsFile:
    c = data
    discard writeBuffer(s.f, addr(c), sizeof(c))

proc llStreamWrite*(s: PLLStream, buf: pointer, buflen: int) =
  case s.kind
  of llsNone, llsStdIn:
    discard
  of llsString:
    if buflen > 0:
      setLen(s.s, s.s.len + buflen)
      copyMem(addr(s.s[0 + s.wr]), buf, buflen)
      inc(s.wr, buflen)
  of llsFile:
    discard writeBuffer(s.f, buf, buflen)

proc llStreamReadAll*(s: PLLStream): string =
  const
    bufSize = 2048
  case s.kind
  of llsNone, llsStdIn:
    result = ""
  of llsString:
    if s.rd == 0: result = s.s
    else: result = substr(s.s, s.rd)
    s.rd = s.s.len
  of llsFile:
    result = newString(bufSize)
    var bytes = readBuffer(s.f, addr(result[0]), bufSize)
    var i = bytes
    while bytes == bufSize:
      setLen(result, i + bufSize)
      bytes = readBuffer(s.f, addr(result[i + 0]), bufSize)
      inc(i, bytes)
    setLen(result, i)
