//
// Copyright (c) 2023, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   11 May 2023  Matthew Giannini Creation
//

using compiler

**
** JsExpr
**
class JsExpr : JsNode
{
  new make(CompileEsPlugin plugin, Expr expr) : super(plugin, expr)
  {
  }

  override Expr? node() { super.node }
  virtual Expr expr() { this.node }

  internal Bool isLocalDefStmt := false

  override Void write()
  {
    switch (expr.id)
    {
      case ExprId.nullLiteral:     writeNullLiteral(expr)
      case ExprId.trueLiteral:     writeBoolLiteral(expr)
      case ExprId.falseLiteral:    writeBoolLiteral(expr)
      case ExprId.intLiteral:      writeIntLiteral(expr)
      case ExprId.floatLiteral:    writeFloatLiteral(expr)
      case ExprId.decimalLiteral:  writeDecimalLiteral(expr)
      case ExprId.strLiteral:      writeStrLiteral(expr)
      case ExprId.durationLiteral: writeDurationLiteral(expr)
      case ExprId.uriLiteral:      writeUriLiteral(expr)
      case ExprId.typeLiteral:     writeTypeLiteral(expr)
      case ExprId.slotLiteral:     writeSlotLiteral(expr)
      case ExprId.rangeLiteral:    writeRangeLiteral(expr)
      case ExprId.listLiteral:     writeListLiteral(expr)
      case ExprId.mapLiteral:      writeMapLiteral(expr)

      case ExprId.boolNot:         writeUnaryExpr(expr)
      case ExprId.cmpNull:         writeUnaryExpr(expr)
      case ExprId.cmpNotNull:      writeUnaryExpr(expr)

      case ExprId.elvis:           writeElvisExpr(expr)
      case ExprId.assign:          writeBinaryExpr(expr)
      case ExprId.same:            writeBinaryExpr(expr)
      case ExprId.notSame:         writeBinaryExpr(expr)
      case ExprId.ternary:         writeTernaryExpr(expr)

      case ExprId.boolOr:          writeCondExpr(expr)
      case ExprId.boolAnd:         writeCondExpr(expr)

      case ExprId.isExpr:          writeTypeCheckExpr(expr)
      case ExprId.isnotExpr:       writeTypeCheckExpr(expr)
      case ExprId.asExpr:          writeTypeCheckExpr(expr)
      case ExprId.coerce:          writeTypeCheckExpr(expr)

      case ExprId.call:            JsCallExpr(plugin, expr).write
      case ExprId.construction:    JsCallExpr(plugin, expr).write
      case ExprId.shortcut:        JsShortcutExpr(plugin, expr).write
      case ExprId.field:           writeFieldExpr(expr)
      case ExprId.closure:         writeClosure(expr)

      case ExprId.localVar:        writeLocalVarExpr(expr)
      case ExprId.thisExpr:        writeThisExpr
      case ExprId.superExpr:       writeSuperExpr(expr)
      case ExprId.itExpr:          writeItExpr(expr)
      case ExprId.staticTarget:    writeStaticTargetExpr(expr)
      case ExprId.throwExpr:       writeThrowExpr(expr)

      case ExprId.unknownVar:      writeUnknownVarExpr(expr)

      default:
        Err().trace
        expr.print(AstWriter()); Env.cur.out.printLine()
        throw err("Unknown ExprId: ${expr.id} ${expr.typeof}", expr.loc)
    }
  }

//////////////////////////////////////////////////////////////////////////
// Literals
//////////////////////////////////////////////////////////////////////////

  private Void writeNullLiteral(LiteralExpr x)
  {
    js.w("null", loc)
  }

  private Void writeBoolLiteral(LiteralExpr x)
  {
    js.w(x.val ? "true" : "false", loc)
  }

  private Void writeIntLiteral(LiteralExpr x)
  {
    js.w(x.val, loc)
  }

  private Void writeFloatLiteral(LiteralExpr x)
  {
    js.w("sys.Float.make(${x.val})", loc)
  }

  private Void writeDecimalLiteral(LiteralExpr x)
  {
    js.w("sys.Decimal.make(${x.val})", loc)
  }

  private Void writeStrLiteral(LiteralExpr x)
  {
    Str val := x.val
    esc := val.toCode('\"', true)[1..-2] // remove outer quotes
    js.w("\"${esc}\"", loc)
  }

  private Void writeDurationLiteral(LiteralExpr x)
  {
    js.w("sys.Duration.fromStr(\"${x.val.toStr}\")", loc)
  }

  private Void writeUriLiteral(LiteralExpr x)
  {
    val := x.val.toStr.toCode('\"', true)
    js.w("sys.Uri.fromStr(${val})", loc)
  }

  private Void writeTypeLiteral(LiteralExpr x)
  {
    writeType(x.val)
  }

  protected Void writeType(CType t, Loc? loc := this.loc)
  {
    if (t.isList || t.isMap || t.isFunc)
    {
      js.w("sys.Type.find(\"${t.signature}\")", loc)
    }
    else
    {
      js.w("${qnameToJs(t)}.type\$", loc)
      if (t.isNullable) js.w(".toNullable()", loc)
    }
  }

  private Void writeSlotLiteral(SlotLiteralExpr x)
  {
    writeType(x.parent)
    js.w(".slot(\"${x.name}\")", loc)
  }

  private Void writeRangeLiteral(RangeLiteralExpr x)
  {
    js.w("sys.Range.make(", loc)
    writeExpr(x.start)
    js.w(", ")
    writeExpr(x.end)
    if (x.exclusive) js.w(", true", loc)
    js.w(")")
  }

  private Void writeListLiteral(ListLiteralExpr x)
  {
    // inferredType := x.ctype
    // explicitType := null
    // if (x.explicitType != null)
    //   explicitType = x.explicitType
    of := ((CType)(x.explicitType ?: x.ctype)).deref->v

    js.w("sys.List.make(", loc)
    writeType(of)
    if (x.vals.size > 0)
    {
      js.w(", [")
      x.vals.each |v, i|
      {
        if (i > 0) js.w(", ")
        writeExpr(v)
      }
      js.w("]")
    }
    js.w(")")
  }

  private Void writeMapLiteral(MapLiteralExpr x)
  {
    js.w("sys.Map.__fromLiteral([", loc)
    x.keys.each |k, i| { if (i > 0) js.w(","); writeExpr(k) }
    js.w("], [")
    x.vals.each |v, i| { if (i > 0) js.w(","); writeExpr(v) }
    js.w("]")

    t := (MapType)(x.explicitType ?: x.ctype)
    js.w(", sys.Type.find(\"${t.k.signature}\")")
    js.w(", sys.Type.find(\"${t.v.signature}\")")
    js.w(")")
  }

//////////////////////////////////////////////////////////////////////////
// Elvis
//////////////////////////////////////////////////////////////////////////

  private Void writeElvisExpr(BinaryExpr be)
  {
    var := uniqName
    old := plugin.thisName
    plugin.thisName = "this\$"

    js.w("((this\$) => { let ${var} = ", loc)
    writeExpr(be.lhs)
    js.w("; if (${var} != null) return ${var}; ", loc)
    if (be.rhs isnot ThrowExpr) js.w("return ", loc)
    writeExpr(be.rhs)
    js.w("; })(${old})", loc)
    plugin.thisName = old
  }

//////////////////////////////////////////////////////////////////////////
// Ternary
//////////////////////////////////////////////////////////////////////////

  private Void writeTernaryExpr(TernaryExpr te)
  {
    var := uniqName
    old := plugin.thisName
    plugin.thisName = "this\$"
    js.w("((this\$) => { ", loc)
    js.w("if ("); writeExpr(te.condition); js.w(") ")
    if (te.trueExpr isnot ThrowExpr) js.w("return ", loc)
    writeExpr(te.trueExpr); js.w("; ")
    if (te.falseExpr isnot ThrowExpr) js.w("return ", loc)
    writeExpr(te.falseExpr); js.w("; ")
    js.w("})(${old})", loc)
    plugin.thisName = old
  }

//////////////////////////////////////////////////////////////////////////
// Unary
//////////////////////////////////////////////////////////////////////////

  private Void writeUnaryExpr(UnaryExpr x)
  {
    switch (x.id)
    {
      case ExprId.cmpNull:    writeExpr(x.operand); js.w(" == null", loc)
      case ExprId.cmpNotNull: writeExpr(x.operand); js.w(" != null", loc)
      default:
        js.w(x.opToken.symbol, loc)
        if (x.operand is BinaryExpr) js.w("(")
        writeExpr(x.operand)
        if (x.operand is BinaryExpr) js.w(")")
    }
  }

//////////////////////////////////////////////////////////////////////////
// Binary
//////////////////////////////////////////////////////////////////////////

  private Void writeBinaryExpr(BinaryExpr x)
  {
    symbol   := x.opToken.symbol
    lhs      := JsExpr(plugin, x.lhs)
    rhs      := JsExpr(plugin, x.rhs)
    leave    := x.leave
    isAssign := x.assignTarget != null

    // convert binary expressions testing for equivalence to
    // synthetic str "_once_" to undefined
    if (lhs.expr is FieldExpr)
    {
      fe := (FieldExpr)lhs.expr
      if (rhs.expr.id === ExprId.strLiteral &&
          symbol == "===" && rhs.expr->val == "_once_")
      {
        lhs.write
        js.w(" ${symbol} undefined", loc)
        return
      }
    }

    if (isAssign && lhs.expr is FieldExpr)
    {
      fe := (FieldExpr)lhs.expr
      if (leave)
      {
        // code like this seems to trigger this path:
        // foo = bar = 1
        var := uniqName
        old := plugin.thisName
        plugin.thisName = "this\$"
        js.w("((this\$) => { ", loc)
        js.w("let ${var} = ", loc); rhs.write; js.w("; ")

        // hack: use UnknownVarExpr so we can write this synthetic var
        lhs.writeSetter(JsExpr(plugin, UnknownVarExpr(loc, null, var))); js.w(";")
        js.w(" return ${var}; })(${old})", loc)

        plugin.thisName = old
      }
      else { lhs.writeSetter(rhs) }
    }
    else
    {
      if (isAssign && !isLocalDefStmt) js.w("(")
      lhs.write
      js.w(" ${symbol} ", loc)
      rhs.write
      if (isAssign && !isLocalDefStmt) js.w(")")
    }
  }

  private Void writeUnknownVarExpr(UnknownVarExpr expr)
  {
    js.w(expr.name)
  }

//////////////////////////////////////////////////////////////////////////
// Conditions
//////////////////////////////////////////////////////////////////////////

  private Void writeCondExpr(CondExpr ce)
  {
    symbol   := ce.opToken.symbol
    operands := ce.operands

    js.w("(")
    operands.each |op, i|
    {
      if (i > 0 && i < operands.size) js.w(" ${symbol} ", loc)
      writeExpr(op)
    }
    js.w(")")
  }

//////////////////////////////////////////////////////////////////////////
// Type Check
//////////////////////////////////////////////////////////////////////////

  private Void writeTypeCheckExpr(TypeCheckExpr x)
  {
    op := x.id == ExprId.coerce ? "coerce" : x.opStr
    if (op == "isnot")
    {
      js.w("!", loc)
      op = "is"
    }
    js.w("sys.ObjUtil.${op}(", loc)
    writeExpr(x.target)
    js.w(", ")
    writeType(x.check)
    js.w(")")
  }

//////////////////////////////////////////////////////////////////////////
// Field
//////////////////////////////////////////////////////////////////////////

  private JsExpr? setArg := null

  Void writeSetter(JsExpr rhs)
  {
    try
    {
      this.setArg = rhs
      this.write
    }
    finally this.setArg = null
  }

  private Void writeFieldExpr(FieldExpr fe)
  {
    if (fe.target is SuperExpr) writeSuperField(fe)
    else writeNormField(fe)
  }

  private Void writeSuperField(FieldExpr fe)
  {
    name := methodToJs(fe.field.name)

    // TODO: do we need anything special here for setArg != null ?
    // possibly if we change to getter/setter design
    // if (setArg != null) throw Err("TODO: setArg = ${setArg} name=${name}")

    writeExpr(fe.target)
    js.w(".${name}.call(${plugin.thisName}", loc)
    if (setArg != null)
    {
      js.w(", ")
      writeSetArg
    }
    js.w(")")
  }

  private Void writeNormField(FieldExpr fe)
  {
    old         := plugin.thisName
    name        := methodToJs(fe.field.name)
    parent      := fe.field.parent
    useAccessor := fe.useAccessor
    isSet       := this.setArg != null

    // use accessor if referring to an enum
    if (fe.field.isEnum) useAccessor = true
    // use the accessor for static fields unless we are in the static initializer;
    // in which case we need to access the private fields directly to initialize them.
    else if (fe.field.isStatic) useAccessor = !plugin.curMethod.isStaticInit

    // force use of the accessor methods if we are accessing the
    // field outside its type since we declare all fields as private
    // in the generated js code
    //
    // else if the field is declared on this type and we are doing a set,
    // but the field has a private setter, then we don't use the accessor
    if (parent.qname != plugin.curType.qname) useAccessor = true
    else if (isSet && (fe.field.setter?.isPrivate ?: false)) useAccessor = false

    writeTarget := |->| {
      if (fe.target == null) js.w(qnameToJs(parent), fe.loc)
      else JsExpr(plugin, fe.target).write
    }

    if (fe.isSafe)
    {
      v := uniqName
      plugin.thisName = "this\$"
      js.w("((this\$) => { let ${v}=", loc)
      writeTarget()
      js.w("; return (${v}==null) ? null : ${v}", loc)
      plugin.thisName = old
    }
    else
    {
      writeTarget()
      if (name == "\$this") return // skip $this ref for closures
    }
    js.w(".")
    if (useAccessor)
    {
      if (setArg==null) js.w("${name}()", fe.loc)
      else
      {
        if (fe.field.isConst) name = "__${name}"
        js.w("${name}(")
        writeSetArg()
        js.w(")")
      }
    }
    else
    {
      js.w("${fieldToJs(fe.field.name)}", fe.loc)
      if (setArg != null)
      {
        js.w(" = ")
        writeSetArg()
      }
    }

    if (fe.isSafe) js.w("; })(${old})", loc)
  }

  private Void writeSetArg()
  {
    arg := this.setArg
    this.setArg = null
    arg.write
    this.setArg = arg
  }

//////////////////////////////////////////////////////////////////////////
// Closure
//////////////////////////////////////////////////////////////////////////

  private Void writeClosure(ClosureExpr ce)
  {
    plugin.closureSupport.writeClosure(ce)
  }

//////////////////////////////////////////////////////////////////////////
// Throw
//////////////////////////////////////////////////////////////////////////

  private Void writeThrowExpr(ThrowExpr te)
  {
    js.w("throw ", loc)
    writeExpr(te.exception)
  }

//////////////////////////////////////////////////////////////////////////
// This
//////////////////////////////////////////////////////////////////////////

  private Void writeThisExpr() { js.w(plugin.thisName, loc) }

//////////////////////////////////////////////////////////////////////////
// Super
//////////////////////////////////////////////////////////////////////////

  private Void writeSuperExpr(SuperExpr se)
  {
    t := se.explicitType ?: se.ctype
    js.w("${qnameToJs(t)}.prototype", loc)
  }

//////////////////////////////////////////////////////////////////////////
// It
//////////////////////////////////////////////////////////////////////////

  private Void writeItExpr(ItExpr ie) { js.w("it", loc) }

//////////////////////////////////////////////////////////////////////////
// StaticTarget
//////////////////////////////////////////////////////////////////////////

  private Void writeStaticTargetExpr(StaticTargetExpr st)
  {
    js.w(qnameToJs(st.ctype), loc)
  }

//////////////////////////////////////////////////////////////////////////
// LocalVar
//////////////////////////////////////////////////////////////////////////

  private Void writeLocalVarExpr(LocalVarExpr x)
  {
    js.w(nameToJs(x.var.name), loc)
  }
}

**************************************************************************
** JsCallExpr
**************************************************************************

internal class JsCallExpr : JsExpr
{
  new make(CompileEsPlugin plugin, CallExpr ce) : super(plugin, ce)
  {
    this.ce = ce
    this.name = methodToJs(ce.method.name)

    if (ce.method != null)
    {
      this.parent   = ce.method.parent
      this.isCtor   = ce.method.isCtor
      this.isObj    = ce.method.parent.qname == "sys::Obj"
      this.isFunc   = ce.method.parent.qname == "sys::Func"
      this.isPrim   = isPrimitive(ce.method.parent)
      this.isStatic = ce.method.isStatic
    }

    if (ce.target != null)
    {
      // resolveType := |CType ctype->CType| {
      //   t := ctype is TypeRef ? ctype->t : ctype
      //   if (t is NullableType) t = t->root
      //   return t
      // }
      this.targetType = ce.target.ctype == null ? this.parent : ce.target.ctype
      resolved := resolveType(ce.target.ctype)
      funcType := c.ns.resolveType("sys::Func")
      isClos = resolved.fits(funcType)
    }

    // force these methods to route thru ObjUtil if not a super.xxx expr
    if ((name == "equals" || name == "compare") && (ce.target isnot SuperExpr)) isObj = true
  }

  CallExpr ce { private set }
  Str name                            // js method name
  Bool isObj        := false          // is target sys::Obj
  Bool isFunc       := false          // is target sys::Func
  Bool isPrim       := false          // is target a primitive type (Int,Bool,etc.)
  Bool isCtor       := false          // is this a ctor call
  Bool isStatic     := false          // is this a static method
  Bool isClos       := false          // is this a Func/Closure call
  CType? parent     := null           // method parent type
  CType? targetType := null           // call target type
  Str? safeVar      := null           // var that target expr is held in for safe-nav

  override Void write()
  {
    // skip mock methods used to insert implicit runtime checks
    if (ce.method is MockMethod) return

    // skip instance inits
    if (ce.method.name.startsWith("instance\$init\$")) return

    if (ce.isSafe)
    {
      // wrap if safe-nav
      safeVar := uniqName
      old := plugin.thisName
      plugin.thisName = "this\$"
      js.w("((this\$) => { let ${safeVar} = ", loc)
      if (ce.target == null) js.w(plugin.thisName, loc)
      else writeExpr(ce.target)
      js.w("; if (${safeVar} == null) return null; return ", loc)
      writeCall
      js.w("; })(${old})", loc)
      plugin.thisName = old
    }
    else
    {
      writeCall
    }
  }

  protected Void writeCall()
  {
    if (isObj)
    {
      js.w("sys.ObjUtil.${name}(", loc)
      if (isStatic) writeArgs
      else
      {
        writeTarget
        writeArgs(true)
      }
      js.w(")")
    }
    else if (isPrim || isFunc)
    {
      js.w("${qnameToJs(targetType)}.${name}(", loc)
      if (isStatic) writeArgs
      else
      {
        writeTarget
        writeArgs(true)
      }
      js.w(")")
    }
    else if (ce.isCtorChain)
    {
      js.w("${qnameToJs(targetType)}.${name}\$(${plugin.thisName}", loc)
      writeArgs(true)
      js.w(")")
    }
    else if (ce.target is SuperExpr)
    {
      writeSuper
    }
    else
    {
      writeTarget
      // if native closure, we invoke the func directly (don't do Func.call())
      if (isClos) js.w("(")
      else
      {
        if ((targetType?.isForeign ?: false) && isCtor && name == "<init>")
          js.w(".javaInit(", loc)
        else
          js.w(".${name}(", loc)
      }
      writeArgs
      js.w(")")
    }
  }

  protected Void writeSuper()
  {
    writeExpr(ce.target)
    js.w(".${name}.call(${plugin.thisName}", loc)
    writeArgs(true)
    js.w(")")
  }

  protected Void writeTarget()
  {
    if (isStatic || isCtor) js.w(qnameToJs(parent))
    else if (safeVar != null) js.w(safeVar)
    else if (ce.target == null) js.w(plugin.thisName)
    else writeExpr(ce.target)
  }

  protected Void writeArgs(Bool hasFirstArg := false)
  {
    if (ce.isDynamic)
    {
      if (hasFirstArg) js.w(",")
      js.w("\"${ce.name}\", sys.List.make(sys.Obj.type\$.toNullable(), [", loc)
      hasFirstArg = false
    }

    ce.args.each |arg, i|
    {
      if (hasFirstArg || i > 0) js.w(", ")
      writeExpr(arg)
    }

    if (ce.args.last is ClosureExpr && typedFuncs.contains(name))
    {
      ClosureExpr ce := ce.args.last
      js.w(", ")
      writeType(ce.doCall.returnType)
    }

    if (ce.isDynamic) js.w("])")
  }
  static const Str[] typedFuncs := ["map", "mapNotNull", "flatMap", "groupBy", "mapToList"]
}

**************************************************************************
** JsShortcutExpr
**************************************************************************

internal class JsShortcutExpr : JsCallExpr
{
  new make(CompileEsPlugin plugin, ShortcutExpr se) : super(plugin, se)
  {
    this.se = se
    this.isIndexedAssign = se is IndexedAssignExpr
    this.isPostfixLeave  = se.isPostfixLeave

    switch (se.opToken.symbol)
    {
      case "!=": this.name = "compareNE"
      case "<":  this.name = "compareLT"
      case "<=": this.name = "compareLE"
      case ">=": this.name = "compareGE"
      case ">":  this.name = "compareGT"
    }

    if (se.isAssign)     assignTarget = findAssignTarget(se.target)
    if (isIndexedAssign) assignIndex  = findIndexedAssign(se.target).args[0]
  }

  ShortcutExpr se { private set }
  // Bool isAssign        := false    // does this expr assign
  Bool isIndexedAssign := false    // is indexed assign
  Bool isPostfixLeave  := false    // is postfix expr
  // Bool leave           := false    // leave result of expr on "stack"
  Bool fieldSet        := false    // transiently used for field sets
  Expr? assignTarget   := null     // target of assignment
  Expr? assignIndex    := null     // indexed assign: index

  private Expr findAssignTarget(Expr expr)
  {
    if (expr is LocalVarExpr || expr is FieldExpr) return expr
    t := Type.of(expr).field("target", false)
    if (t != null) return findAssignTarget(t.get(expr))
    throw err("No base Expr found", loc)
  }

  private ShortcutExpr findIndexedAssign(Expr expr)
  {
    if (expr is ShortcutExpr) return expr
    t := Type.of(expr).field("target", false)
    if (t != null) return findIndexedAssign(t.get(expr))
    throw err("No base Expr found", loc)
  }

  override Void write()
  {
    if (fieldSet)
    {
      return super.write
    }
    if (isIndexedAssign)
    {
      return doWriteIndexedAssign
    }
    if (se.isPostfixLeave)
    {
      var := uniqName
      old := plugin.thisName
      plugin.thisName = "this\$"
      js.w("((this\$) => { let ${var} = ", loc)
      writeExpr(assignTarget); js.w(";")
      doWrite
      js.w("; return ${var}; })(${old})", loc)
      plugin.thisName = old
    }
    else doWrite
  }

  private Void doWrite()
  {
    if (se.isAssign)
    {
      if (assignTarget is FieldExpr)
      {
        fieldSet = true
        fe := (FieldExpr)assignTarget
        JsExpr(plugin, fe).writeSetter(this)
        fieldSet = false
        return
      }
      else
      {
        writeExpr(assignTarget)
        js.w(" = ", loc)
      }
    }
    super.write
  }

  private Void doWriteIndexedAssign()
  {
    newVal := uniqName
    oldVal := uniqName
    ref    := uniqName
    index  := uniqName
    old    := plugin.thisName
    retVal := isPostfixLeave ? oldVal : newVal
    plugin.thisName = "this\$"
    js.w("((this\$) => { ", loc)
    js.w("let ${ref} = ", loc); writeExpr(assignTarget); js.w("; ")
    js.w("let ${index} = ", loc); writeExpr(assignIndex); js.w("; ")
    js.w("let ${newVal} = ", loc); super.write; js.w("; ")
    if (isPostfixLeave) js.w("let ${oldVal} = ${ref}.get(${index}); ", loc)
    js.w("${ref}.set(${index},${newVal}); ", loc)
    js.w(" return ${retVal}; })(${old})", loc)
    plugin.thisName = old
  }
}