fs = require 'fs'
path = require 'path'
vm = require 'vm'
nodeREPL = require 'repl'
CoffeeScript = require './'
{merge, updateSyntaxError} = require './helpers'

sawSIGINT = no
transpile = no

replDefaults =
  prompt: 'coffee> ',
  historyFile: do ->
    historyPath = process.env.XDG_CACHE_HOME or process.env.HOME
    path.join historyPath, '.coffee_history' if historyPath
  historyMaxInputSize: 10240
  eval: (input, context, filename, cb) ->
    # XXX: multiline hack.
    input = input.replace /\uFF00/g, '\n'
    # Node's REPL sends the input ending with a newline and then wrapped in
    # parens. Unwrap all that.
    input = input.replace /^\(([\s\S]*)\n\)$/m, '$1'
    # Node's REPL v6.9.1+ sends the input wrapped in a try/catch statement.
    # Unwrap that too.
    input = input.replace /^\s*try\s*{([\s\S]*)}\s*catch.*$/m, '$1'

    # Require AST nodes to do some AST manipulation.
    {Block, Assign, Value, Literal, Call, Code, Root} = require './nodes'

    try
      # Tokenize the clean input.
      tokens = CoffeeScript.tokens input
      # Filter out tokens generated just to hold comments.
      if tokens.length >= 2 and tokens[0].generated and
         tokens[0].comments?.length isnt 0 and "#{tokens[0][1]}" is '' and
         tokens[1][0] is 'TERMINATOR'
        tokens = tokens[2...]
      if tokens.length >= 1 and tokens[tokens.length - 1].generated and
         tokens[tokens.length - 1].comments?.length isnt 0 and "#{tokens[tokens.length - 1][1]}" is ''
        tokens.pop()
      # Collect referenced variable names just like in `CoffeeScript.compile`.
      referencedVars = (token[1] for token in tokens when token[0] is 'IDENTIFIER')
      # Generate the AST of the tokens.
      ast = CoffeeScript.nodes(tokens).body
      # Add assignment to `__` variable to force the input to be an expression.
      ast = new Block [new Assign (new Value new Literal '__'), ast, '=']
      # Wrap the expression in a closure to support top-level `await`.
      ast     = new Code [], ast
      isAsync = ast.isAsync
      # Invoke the wrapping closure.
      ast    = new Root new Block [new Call ast]
      js     = ast.compile {bare: yes, locals: Object.keys(context), referencedVars, sharedScope: yes}
      if transpile
        js = transpile.transpile(js, transpile.options).code
        # Strip `"use strict"`, to avoid an exception on assigning to
        # undeclared variable `__`.
        js = js.replace /^"use strict"|^'use strict'/, ''
      result = runInContext js, context, filename
      # Await an async result, if necessary.
      if isAsync
        result.then (resolvedResult) ->
          cb null, resolvedResult unless sawSIGINT
        sawSIGINT = no
      else
        cb null, result
    catch err
      # AST's `compile` does not add source code information to syntax errors.
      updateSyntaxError err, input
      cb err

runInContext = (js, context, filename) ->
  if context is global
    vm.runInThisContext js, filename
  else
    vm.runInContext js, context, filename

addMultilineHandler = (repl) ->
  {inputStream, outputStream} = repl
  # Node 0.11.12 changed API, prompt is now _prompt.
  origPrompt = repl._prompt ? repl.prompt

  multiline =
    enabled: off
    initialPrompt: origPrompt.replace /^[^> ]*/, (x) -> x.replace /./g, '-'
    prompt: origPrompt.replace /^[^> ]*>?/, (x) -> x.replace /./g, '.'
    buffer: ''

  # Proxy node's line listener
  nodeLineListener = repl.listeners('line')[0]
  repl.removeListener 'line', nodeLineListener
  repl.on 'line', (cmd) ->
    if multiline.enabled
      multiline.buffer += "#{cmd}\n"
      repl.setPrompt multiline.prompt
      repl.prompt true
    else
      repl.setPrompt origPrompt
      nodeLineListener cmd
    return

  # Handle Ctrl-v
  inputStream.on 'keypress', (char, key) ->
    return unless key and key.ctrl and not key.meta and not key.shift and key.name is 'v'
    if multiline.enabled
      # allow arbitrarily switching between modes any time before multiple lines are entered
      unless multiline.buffer.match /\n/
        multiline.enabled = not multiline.enabled
        repl.setPrompt origPrompt
        repl.prompt true
        return
      # no-op unless the current line is empty
      return if repl.line? and not repl.line.match /^\s*$/
      # eval, print, loop
      multiline.enabled = not multiline.enabled
      repl.line = ''
      repl.cursor = 0
      repl.output.cursorTo 0
      repl.output.clearLine 1
      # XXX: multiline hack
      multiline.buffer = multiline.buffer.replace /\n/g, '\uFF00'
      repl.emit 'line', multiline.buffer
      multiline.buffer = ''
    else
      multiline.enabled = not multiline.enabled
      repl.setPrompt multiline.initialPrompt
      repl.prompt true
    return

# Store and load command history from a file
addHistory = (repl, filename, maxSize) ->
  lastLine = null
  try
    # Get file info and at most maxSize of command history
    stat = fs.statSync filename
    size = Math.min maxSize, stat.size
    # Read last `size` bytes from the file
    readFd = fs.openSync filename, 'r'
    buffer = Buffer.alloc size
    fs.readSync readFd, buffer, 0, size, stat.size - size
    fs.closeSync readFd
    # Set the history on the interpreter
    repl.history = buffer.toString().split('\n').reverse()
    # If the history file was truncated we should pop off a potential partial line
    repl.history.pop() if stat.size > maxSize
    # Shift off the final blank newline
    repl.history.shift() if repl.history[0] is ''
    repl.historyIndex = -1
    lastLine = repl.history[0]

  fd = fs.openSync filename, 'a'

  repl.addListener 'line', (code) ->
    if code and code.length and code isnt '.history' and code isnt '.exit' and lastLine isnt code
      # Save the latest command in the file
      fs.writeSync fd, "#{code}\n"
      lastLine = code

  # XXX: The SIGINT event from REPLServer is undocumented, so this is a bit fragile
  repl.on 'SIGINT', -> sawSIGINT = yes
  repl.on 'exit', -> fs.closeSync fd

  # Add a command to show the history stack
  repl.commands[getCommandId(repl, 'history')] =
    help: 'Show command history'
    action: ->
      repl.outputStream.write "#{repl.history[..].reverse().join '\n'}\n"
      repl.displayPrompt()

getCommandId = (repl, commandName) ->
  # Node 0.11 changed API, a command such as '.help' is now stored as 'help'
  commandsHaveLeadingDot = repl.commands['.help']?
  if commandsHaveLeadingDot then ".#{commandName}" else commandName

module.exports =
  start: (opts = {}) ->
    [major, minor, build] = process.versions.node.split('.').map (n) -> parseInt(n, 10)

    if major < 6
      console.warn "Node 6+ required for CoffeeScript REPL"
      process.exit 1

    CoffeeScript.register()
    process.argv = ['coffee'].concat process.argv[2..]
    if opts.transpile
      transpile = {}
      try
        transpile.transpile = require('@babel/core').transform
      catch
        try
          transpile.transpile = require('babel-core').transform
        catch
          console.error '''
            To use --transpile with an interactive REPL, @babel/core must be installed either in the current folder or globally:
              npm install --save-dev @babel/core
            or
              npm install --global @babel/core
            And you must save options to configure Babel in one of the places it looks to find its options.
            See https://coffeescript.org/#transpilation
          '''
          process.exit 1
      transpile.options =
        filename: path.resolve process.cwd(), '<repl>'
      # Since the REPL compilation path is unique (in `eval` above), we need
      # another way to get the `options` object attached to a module so that
      # it knows later on whether it needs to be transpiled. In the case of
      # the REPL, the only applicable option is `transpile`.
      Module = require 'module'
      originalModuleLoad = Module::load
      Module::load = (filename) ->
        @options = transpile: transpile.options
        originalModuleLoad.call @, filename
    opts = merge replDefaults, opts
    repl = nodeREPL.start opts
    runInContext opts.prelude, repl.context, 'prelude' if opts.prelude
    repl.on 'exit', -> repl.outputStream.write '\n' if not repl.closed
    addMultilineHandler repl
    addHistory repl, opts.historyFile, opts.historyMaxInputSize if opts.historyFile
    # Adapt help inherited from the node REPL
    repl.commands[getCommandId(repl, 'load')].help = 'Load code from a file into this REPL session'
    repl
