# A very simple Read-Eval-Print-Loop. Compiles one line at a time to JavaScript
# and evaluates it. Good for simple tests, or poking around the **Node.js** API.
# Using it looks like this:
#
#     coffee> console.log "#{num} bottles of beer" for num in [99..1]

# Start by opening up `stdin` and `stdout`.
stdin = process.openStdin()
stdout = process.stdout

# Require the **coffee-script** module to get access to the compiler.
CoffeeScript = require './coffee-script'
readline     = require 'readline'
{inspect}    = require 'util'
{Script}     = require 'vm'
Module       = require 'module'

# REPL Setup

# Config
REPL_PROMPT = 'coffee> '
REPL_PROMPT_MULTILINE = '------> '
REPL_PROMPT_CONTINUATION = '......> '
enableColours = no
unless process.platform is 'win32'
  enableColours = not process.env.NODE_DISABLE_COLORS

# Log an error.
error = (err) ->
  stdout.write (err.stack or err.toString()) + '\n'

## Autocompletion

# Regexes to match complete-able bits of text.
ACCESSOR  = /\s*([\w\.]+)(?:\.(\w*))$/
SIMPLEVAR = /(\w+)$/i

# Returns a list of completions, and the completed text.
autocomplete = (text) ->
  completeAttribute(text) or completeVariable(text) or [[], text]

# Attempt to autocomplete a chained dotted attribute: `one.two.three`.
completeAttribute = (text) ->
  if match = text.match ACCESSOR
    [all, obj, prefix] = match
    try obj = Script.runInThisContext obj
    catch e
      return
    return unless obj?
    obj = Object obj
    candidates = Object.getOwnPropertyNames obj
    while obj = Object.getPrototypeOf obj
      for key in Object.getOwnPropertyNames obj when key not in candidates
        candidates.push key
    completions = getCompletions prefix, candidates
    [completions, prefix]

# Attempt to autocomplete an in-scope free variable: `one`.
completeVariable = (text) ->
  free = text.match(SIMPLEVAR)?[1]
  free = "" if text is ""
  if free?
    vars = Script.runInThisContext 'Object.getOwnPropertyNames(Object(this))'
    keywords = (r for r in CoffeeScript.RESERVED when r[..1] isnt '__')
    candidates = vars
    for key in keywords when key not in candidates
      candidates.push key
    completions = getCompletions free, candidates
    [completions, free]

# Return elements of candidates for which `prefix` is a prefix.
getCompletions = (prefix, candidates) ->
  el for el in candidates when 0 is el.indexOf prefix

# Make sure that uncaught exceptions don't kill the REPL.
process.on 'uncaughtException', error

# The current backlog of multi-line code.
backlog = ''

# The main REPL function. **run** is called every time a line of code is entered.
# Attempt to evaluate the command. If there's an exception, print it out instead
# of exiting.
run = (buffer) ->
  # remove single-line comments
  buffer = buffer.replace /(^|[\r\n]+)(\s*)##?(?:[^#\r\n][^\r\n]*|)($|[\r\n])/, "$1$2$3"
  # remove trailing newlines
  buffer = buffer.replace /[\r\n]+$/, ""
  if multilineMode
    backlog += "#{buffer}\n"
    repl.setPrompt REPL_PROMPT_CONTINUATION
    repl.prompt()
    return
  if !buffer.toString().trim() and !backlog
    repl.prompt()
    return
  code = backlog += buffer
  if code[code.length - 1] is '\\'
    backlog = "#{backlog[...-1]}\n"
    repl.setPrompt REPL_PROMPT_CONTINUATION
    repl.prompt()
    return
  repl.setPrompt REPL_PROMPT
  backlog = ''
  try
    _ = global._
    returnValue = CoffeeScript.eval "_=(#{code}\n)", {
      filename: 'repl'
      modulename: 'repl'
    }
    if returnValue is undefined
      global._ = _
    repl.output.write "#{inspect returnValue, no, 2, enableColours}\n"
  catch err
    error err
  repl.prompt()

if stdin.readable and stdin.isRaw
  # handle piped input
  pipedInput = ''
  repl =
    prompt: -> stdout.write @_prompt
    setPrompt: (p) -> @_prompt = p
    input: stdin
    output: stdout
    on: ->
  stdin.on 'data', (chunk) ->
    pipedInput += chunk
    return unless /\n/.test pipedInput
    lines = pipedInput.split "\n"
    pipedInput = lines[lines.length - 1]
    for line in lines[...-1] when line
      stdout.write "#{line}\n"
      run line
    return
  stdin.on 'end', ->
    for line in pipedInput.trim().split "\n" when line
      stdout.write "#{line}\n"
      run line
    stdout.write '\n'
    process.exit 0
else
  # Create the REPL by listening to **stdin**.
  if readline.createInterface.length < 3
    repl = readline.createInterface stdin, autocomplete
    stdin.on 'data', (buffer) -> repl.write buffer
  else
    repl = readline.createInterface stdin, stdout, autocomplete

multilineMode = off

# Handle multi-line mode switch
repl.input.on 'keypress', (char, key) ->
  # test for Ctrl-v
  return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
  cursorPos = repl.cursor
  repl.output.cursorTo 0
  repl.output.clearLine 1
  multilineMode = not multilineMode
  repl._line() if not multilineMode and backlog
  backlog = ''
  repl.setPrompt (newPrompt = if multilineMode then REPL_PROMPT_MULTILINE else REPL_PROMPT)
  repl.prompt()
  repl.output.cursorTo newPrompt.length + (repl.cursor = cursorPos)

# Handle Ctrl-d press at end of last line in multiline mode
repl.input.on 'keypress', (char, key) ->
  return unless multilineMode and repl.line
  # test for Ctrl-d
  return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'd'
  multilineMode = off
  repl._line()

repl.on 'attemptClose', ->
  if multilineMode
    multilineMode = off
    repl.output.cursorTo 0
    repl.output.clearLine 1
    repl._onLine repl.line
    return
  if backlog or repl.line
    backlog = ''
    repl.historyIndex = -1
    repl.setPrompt REPL_PROMPT
    repl.output.write '\n(^C again to quit)'
    repl._line (repl.line = '')
  else
    repl.close()

repl.on 'close', ->
  repl.output.write '\n'
  repl.input.destroy()

repl.on 'line', run

repl.setPrompt REPL_PROMPT
repl.prompt()
