/* NSC -- new Scala compiler
 * Copyright 2005-2016 LAMP/EPFL
 */
package scala
package tools.nsc
package interpreter

import scala.language.dynamics

import scala.beans.BeanProperty
import scala.collection.JavaConverters._
import scala.reflect.classTag
import scala.reflect.internal.util.Position
import scala.tools.nsc.util.stringFromReader
import javax.script._, ScriptContext.{ ENGINE_SCOPE, GLOBAL_SCOPE }
import java.io.{ Closeable, Reader }

/* A REPL adaptor for the javax.script API. */
class Scripted(@BeanProperty val factory: ScriptEngineFactory, settings: Settings, out: JPrintWriter)
  extends AbstractScriptEngine with Compilable {

  def createBindings: Bindings = new SimpleBindings

  // dynamic context bound under this name
  final val ctx = "$ctx"

  // the underlying interpreter, tweaked to handle dynamic bindings
  val intp = new IMain(settings, out) {
    import global.{ Name, TermName }

    /* Modify the template to snag definitions from dynamic context.
     * So object $iw { x + 42 } becomes object $iw { def x = $ctx.x ; x + 42 }
     */
    override protected def importsCode(wanted: Set[Name], wrapper: Request#Wrapper, definesClass: Boolean, generousImports: Boolean) = {

      // cull references that can be satisfied from the current dynamic context
      val contextual = wanted & contextNames

      if (contextual.nonEmpty) {
        val neededContext = (wanted &~ contextual) + TermName(ctx)
        val ComputedImports(header, preamble, trailer, path) = super.importsCode(neededContext, wrapper, definesClass, generousImports)
        val adjusted = contextual.map { n =>
            val valname = n.decodedName
            s"""def `$valname` = $ctx.`$valname`
                def `${valname}_=`(x: _root_.java.lang.Object) = $ctx.`$valname` = x"""
          }.mkString(preamble, "\n", "\n")
        ComputedImports(header, adjusted, trailer, path)
      }
      else super.importsCode(wanted, wrapper, definesClass, generousImports)
    }

    // names available in current dynamic context
    def contextNames: Set[Name] = {
      val ctx = compileContext
      val terms = for {
        scope   <- ctx.getScopes.asScala
        binding <- Option(ctx.getBindings(scope)) map (_.asScala) getOrElse Nil
        key = binding._1
      } yield (TermName(key): Name)
      terms.to[Set]
    }

    // save first error for exception; console display only if debugging
    override lazy val reporter: ReplReporter = new ReplReporter(this) {
      override def display(pos: Position, msg: String, severity: Severity): Unit =
        if (isReplDebug) super.display(pos, msg, severity)
      override def error(pos: Position, msg: String): Unit = {
        if (firstError.isEmpty) firstError = Some((pos, msg))
        super.error(pos, msg)
      }
      override def reset() = { super.reset() ; firstError = None }
    }
  }
  intp.initializeSynchronous()

  var compileContext: ScriptContext = getContext

  val scriptContextRep = new intp.ReadEvalPrint

  def dynamicContext_=(ctx: ScriptContext): Unit = scriptContextRep.callEither("set", ctx)

  def dynamicContext: ScriptContext = scriptContextRep.callEither("value") match {
    case Right(ctx: ScriptContext) => ctx
    case Left(e) => throw e
    case Right(other) => throw new ScriptException(s"Unexpected value for context: $other")
  }

  if (intp.isInitializeComplete) {
    // compile the dynamic ScriptContext object holder
    val ctxRes = scriptContextRep compile s"""
      |import _root_.javax.script._
      |object ${scriptContextRep.evalName} {
      |  var value: ScriptContext = _
      |  def set(x: _root_.scala.Any) = value = x.asInstanceOf[ScriptContext]
      |}
    """.stripMargin
    if (!ctxRes) throw new ScriptException("Failed to compile ctx")
    dynamicContext = getContext

    // Bridge dynamic references and script context
    val dynRes = intp compileString s"""
     |package scala.tools.nsc.interpreter
     |import _root_.scala.language.dynamics
     |import _root_.javax.script._, ScriptContext.ENGINE_SCOPE
     |object dynamicBindings extends _root_.scala.Dynamic {
     |  def context: ScriptContext = ${ scriptContextRep.evalPath }.value
     |  // $ctx.x retrieves the attribute x
     |  def selectDynamic(field: _root_.java.lang.String): _root_.java.lang.Object = context.getAttribute(field)
     |  // $ctx.x = v
     |  def updateDynamic(field: _root_.java.lang.String)(value: _root_.java.lang.Object) = context.setAttribute(field, value, ENGINE_SCOPE)
     |}
     |""".stripMargin
    if (!dynRes) throw new ScriptException("Failed to compile dynamicBindings")
    intp beQuietDuring {
      intp interpret s"val $ctx: _root_.scala.tools.nsc.interpreter.dynamicBindings.type = _root_.scala.tools.nsc.interpreter.dynamicBindings"
      intp bind ("$engine" -> (this: ScriptEngine with Compilable))
    }
  }

  // Set the context for dynamic resolution and run the body.
  // Defines attributes available for evaluation.
  // Avoid reflective access if using default context.
  def withScriptContext[A](context: ScriptContext)(body: => A): A =
    if (context eq getContext) body else {
      val saved = dynamicContext
      dynamicContext = context
      try body
      finally dynamicContext = saved
    }
  // Defines attributes available for compilation.
  def withCompileContext[A](context: ScriptContext)(body: => A): A = {
    val saved = compileContext
    compileContext = context
    try body
    finally compileContext = saved
  }

  // not obvious that ScriptEngine should accumulate code text
  private var code = ""

  private var firstError: Option[(Position, String)] = None

  /* All scripts are compiled. The supplied context defines what references
   * not in REPL history are allowed, though a different context may be
   * supplied for evaluation of a compiled script.
   */
  def compile(script: String, context: ScriptContext): CompiledScript =
    withCompileContext(context) {
      val cat = code + script
      intp.compile(cat, synthetic = false) match {
        case Right(req)          =>
          code = ""
          new WrappedRequest(req)
        case Left(IR.Incomplete) =>
          code = cat + "\n"
          new CompiledScript {
            def eval(context: ScriptContext): Object = null
            def getEngine: ScriptEngine = Scripted.this
          }
        case Left(_)             =>
          code = ""
          throw firstError map {
            case (pos, msg) => new ScriptException(msg, script, pos.line, pos.column)
          } getOrElse new ScriptException("compile-time error")
      }
    }

  // documentation
  //protected var context: ScriptContext
  //def getContext: ScriptContext = context

  /* Compile with the default context. All references must be resolvable. */
  @throws[ScriptException]
  def compile(script: String): CompiledScript = compile(script, context)

  @throws[ScriptException]
  def compile(reader: Reader): CompiledScript = compile(stringFromReader(reader), context)

  /* Compile and evaluate with the given context. */
  @throws[ScriptException]
  def eval(script: String, context: ScriptContext): Object = compile(script, context).eval(context)

  @throws[ScriptException]
  def eval(reader: Reader, context: ScriptContext): Object = compile(stringFromReader(reader), context).eval(context)

  private class WrappedRequest(val req: intp.Request) extends CompiledScript {
    var first = true

    private def evalEither(r: intp.Request, ctx: ScriptContext) = {
      if (ctx.getWriter == null && ctx.getErrorWriter == null && ctx.getReader == null) r.lineRep.evalEither
      else {
        val closeables = Array.ofDim[Closeable](2)
        val w = if (ctx.getWriter == null) Console.out else {
          val v = new WriterOutputStream(ctx.getWriter)
          closeables(0) = v
          v
        }
        val e = if (ctx.getErrorWriter == null) Console.err else {
          val v = new WriterOutputStream(ctx.getErrorWriter)
          closeables(1) = v
          v
        }
        val in = if (ctx.getReader == null) Console.in else ctx.getReader
        try {
          Console.withOut(w) {
            Console.withErr(e) {
              Console.withIn(in) {
                r.lineRep.evalEither
              }
            }
          }
        } finally {
          closeables foreach (c => if (c != null) c.close())
        }
      }
    }

    /* First time, cause lazy evaluation of a memoized result.
     * Subsequently, instantiate a new object for evaluation.
     * Per the API: Checked exception types thrown by underlying scripting implementations
     * must be wrapped in instances of ScriptException.
     */
    @throws[ScriptException]
    override def eval(context: ScriptContext) = withScriptContext(context) {
      if (first) {
        val result = evalEither(req, context) match {
          case Left(e: RuntimeException) => throw e
          case Left(e: Exception)        => throw new ScriptException(e)
          case Left(e)                   => throw e
          case Right(result)             => result.asInstanceOf[Object]
        }
        intp recordRequest req
        first = false
        result
      } else {
        val defines = req.defines
        if (defines.isEmpty) {
          Scripted.this.eval(s"new ${req.lineRep.readPath}")
          intp recordRequest duplicate(req)
          null
        } else {
          val instance = s"val $$INSTANCE = new ${req.lineRep.readPath};"
          val newline  = (defines map (s => s"val ${s.name} = $$INSTANCE${req.accessPath}.${s.name}")).mkString(instance, ";", ";")
          val newreq   = intp.requestFromLine(newline).right.get
          val ok = newreq.compile

          val result = evalEither(newreq, context) match {
            case Left(e: RuntimeException) => throw e
            case Left(e: Exception)        => throw new ScriptException(e)
            case Left(e)                   => throw e
            case Right(result)             => intp recordRequest newreq ; result.asInstanceOf[Object]
          }
          result
        }
      }
    }

    def duplicate(req: intp.Request) = new intp.Request(req.line, req.trees)

    def getEngine: ScriptEngine = Scripted.this
  }
}

object Scripted {
  import IMain.{ defaultSettings, defaultOut }
  import java.util.Arrays.asList
  import scala.util.Properties.versionString

  class Factory extends ScriptEngineFactory {
    @BeanProperty val engineName      = "Scala REPL"

    @BeanProperty val engineVersion   = "2.0"

    @BeanProperty val extensions      = asList("scala")

    @BeanProperty val languageName    = "Scala"

    @BeanProperty val languageVersion = versionString

    @BeanProperty val mimeTypes       = asList("application/x-scala")

    @BeanProperty val names           = asList("scala")

    def getMethodCallSyntax(obj: String, m: String, args: String*): String = args.mkString(s"$obj.$m(", ", ", ")")

    def getOutputStatement(toDisplay: String): String = s"Console.println($toDisplay)"

    def getParameter(key: String): Object = key match {
      case ScriptEngine.ENGINE           => engineName
      case ScriptEngine.ENGINE_VERSION   => engineVersion
      case ScriptEngine.LANGUAGE         => languageName
      case ScriptEngine.LANGUAGE_VERSION => languageVersion
      case ScriptEngine.NAME             => names.get(0)
      case _ => null
    }

    def getProgram(statements: String*): String = statements.mkString("object Main extends _root_.scala.App {\n\t", "\n\t", "\n}")

    def getScriptEngine: ScriptEngine = {
      val settings = new Settings()
      settings.usemanifestcp.value = true
      Scripted(this, settings)
    }
  }

  def apply(factory: ScriptEngineFactory = new Factory, settings: Settings = defaultSettings, out: JPrintWriter = defaultOut) = {
    settings.Yreplclassbased.value = true
    settings.usejavacp.value       = true
    val s = new Scripted(factory, settings, out)
    s.setBindings(s.createBindings, ScriptContext.ENGINE_SCOPE)
    s
  }
}

import java.io.Writer
import java.nio.{ ByteBuffer, CharBuffer }
import java.nio.charset.{ Charset, CodingErrorAction }
import CodingErrorAction.{ REPLACE => Replace }

/* An OutputStream that decodes bytes and flushes to the writer. */
class WriterOutputStream(writer: Writer) extends OutputStream {
  val decoder = Charset.defaultCharset.newDecoder
  decoder onMalformedInput Replace
  decoder onUnmappableCharacter Replace

  val byteBuffer = ByteBuffer.allocate(64)
  val charBuffer = CharBuffer.allocate(64)

  override def write(b: Int): Unit = {
    byteBuffer.put(b.toByte)
    byteBuffer.flip()
    val result = decoder.decode(byteBuffer, charBuffer, /*eoi=*/ false)
    if (byteBuffer.remaining == 0) byteBuffer.clear()
    if (charBuffer.position() > 0) {
      charBuffer.flip()
      writer write charBuffer.toString
      charBuffer.clear()
    }
  }
  override def close(): Unit = {
    decoder.decode(byteBuffer, charBuffer, /*eoi=*/ true)
    decoder.flush(charBuffer)
  }
  override def toString = charBuffer.toString
}
