//
// Copyright (c) 2025, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   29 May 2025  Brian Frank  Split from JavaPrinter
//

using compiler

**
** Java transpiler printer for expressions
**
internal class JavaExprPrinter : JavaPrinter, ExprPrinter
{
  new make(JavaPrinter parent) : super(parent) {}

  override JavaPrinterState m() { super.m }

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

  override This nullLiteral(LiteralExpr x) { w("null") }

  override This trueLiteral(LiteralExpr x) { w("true") }

  override This falseLiteral(LiteralExpr x) { w("false") }

  override This intLiteral(LiteralExpr x) { w(x.val).w("L") }

  override This floatLiteral(LiteralExpr x) { w(x.val).w("D") }

  override This decimalLiteral(LiteralExpr x) { w("new java.math.BigDecimal(").str(x.val).w(")") }

  override This strLiteral(LiteralExpr x) { str(x.val) }

  override This uriLiteral(LiteralExpr x) { w("fan.sys.Uri.fromStr(").str(x.val).w(")") }

  override This durationLiteral(LiteralExpr x)
  {
    dur := (Duration)x.val
    return w("fan.sys.Duration.make(").w(dur.ticks).w("L)")
  }

  override This typeLiteral(LiteralExpr x)
  {
    doTypeLiteral(x.val)
  }

  private This doTypeLiteral(CType t)
  {
    if (t.pod.name == "sys")
    {
      if (t.isParameterized)
        qnType.w(".find(").str(t.signature).w(")")
      else
        qnSys.w(".").w(t.name).w("Type")
    }
    else
    {
      typeSig(t).w(".typeof\$()")
    }
    if (t.isNullable) w(".toNullable()")
    return this
  }

  override This slotLiteral(SlotLiteralExpr x)
  {
    find := x.slot is CField ? "field" : "method"
    doTypeLiteral(x.parent).w(".").w(find).w("(").str(x.name).w(")")
    return this
  }

  override This rangeLiteral(RangeLiteralExpr x)
  {
    return w("fan.sys.Range.")
          .w(x.exclusive ? "makeExclusive" : "makeInclusive")
          .w("(").expr(x.start).w(", ").expr(x.end).w(")")
  }

  override This listLiteral(ListLiteralExpr x)
  {
    type := (ListType)x.ctype
    qnList.w(".make(").doTypeLiteral(type.v).w(", ").w(x.vals.size).w(")")
    x.vals.each |item|
    {
      w("._add(").expr(item).w(")")
    }
    return this
  }

  override This mapLiteral(MapLiteralExpr x)
  {
    type := (MapType)x.ctype
    qnMap.w(".make(").doTypeLiteral(type.k).w(", ").doTypeLiteral(type.v).w(")")
    x.keys.each |key, i|
    {
      val := x.vals[i]
      w(".set(").expr(key).w(", ").expr(val).w(")")
    }
    return this
  }

//////////////////////////////////////////////////////////////////////////
// Compare / Type Checks
//////////////////////////////////////////////////////////////////////////

  override This notExpr(UnaryExpr x) { unaryExpr("!", x.operand) }

  override This compareNullExpr(UnaryExpr x) { oparen.expr(x.operand).w(" == null").cparen }

  override This compareNotNullExpr(UnaryExpr x) { oparen.expr(x.operand).w(" != null").cparen }

  override This sameExpr(BinaryExpr x) { binaryExpr(x.lhs, "==", x.rhs) }

  override This notSameExpr(BinaryExpr x) { binaryExpr(x.lhs, "!=", x.rhs) }

  override This orExpr(CondExpr x) { condExpr("||", x.operands) }

  override This andExpr(CondExpr x) { condExpr("&&", x.operands) }

  override This isExpr(TypeCheckExpr x)
  {
    check := x.check
    if (check.isParameterized)
      qnOpUtil.w(".is(").expr(x.target).w(", ").doTypeLiteral(check).w(")")
    else
      oparen.expr(x.target).w(" instanceof ").typeSigNullable(x.check, JavaParameterize.no).cparen
    return this
  }

  override This isnotExpr(TypeCheckExpr x)
  {
    w("!(").isExpr(x).w(")")
  }

  override This asExpr(TypeCheckExpr x)
  {
    qnOpUtil.w(".as(").typeSigNullable(x.check, JavaParameterize.no).w(".class, ").expr(x.target).w(")")
  }

  override This coerceExpr(TypeCheckExpr x)
  {
    // Java will not cast between parameterized List/Map
    oparen.w("(").typeSig(x.check, JavaParameterize.no).w(")(").expr(x.target).w(")").cparen
  }

//////////////////////////////////////////////////////////////////////////
// Local Vars
//////////////////////////////////////////////////////////////////////////

  override This localExpr(LocalVarExpr x) { varName(x.name) }

  override This thisExpr(LocalVarExpr x) { w(selfVar ?: "this") }

  override This superExpr(LocalVarExpr x)
  {
    if (x.ctype.isMixin) typeSig(x.ctype).w(".")
    return w("super")
  }

  override This itExpr(LocalVarExpr x) { w(x.name) }

//////////////////////////////////////////////////////////////////////////
// Misc Expr
//////////////////////////////////////////////////////////////////////////

  override This ternaryExpr(TernaryExpr x)
  {
    w("(").expr(x.condition).w(" ? ").expr(x.trueExpr).w(" : ").expr(x.falseExpr).w(")")
  }

  override This throwExpr(ThrowExpr x)
  {
    w("throw ").expr(x.exception)
  }

  override This assignExpr(BinaryExpr x)
  {
    if (x.lhs.id === ExprId.field) return fieldAssign(x.lhs, x.rhs)
    return oparen.expr(x.lhs).w(" = ").expr(x.rhs).cparen
  }

//////////////////////////////////////////////////////////////////////////
// Call / Null Safe
//////////////////////////////////////////////////////////////////////////

  override This compareExpr(Expr lhs, Token op, Expr rhs)
  {
    qnOpUtil.w(".")
    switch (op)
    {
      case Token.eq:    w("compareEQ")
      case Token.notEq: w("compareNE")
      case Token.lt:    w("compareLT")
      case Token.ltEq:  w("compareLE")
      case Token.gt:    w("compareGT")
      case Token.gtEq:  w("compareGE")
      case Token.cmp:   w("compare")
      default:          throw Err(op.toStr)
    }
    w("(").expr(lhs).w(", ").expr(rhs).w(")")
    return this
  }

  override Str? unaryOperator(Str qname)
  {
    JavaUtil.unaryOperators.get(qname)
  }

  override Str? binaryOperator(Str qname)
  {
    JavaUtil.binaryOperators.get(qname)
  }

  override This callMethodExpr(CallExpr x)
  {
    m := x.method
    if (m.parent.isForeign)
    {
      // JAVA FFI constructor is Foo.<new>.<init>(...)
      if (x.method.name == "<new>") return this
      if (x.method.name == "<init>")
        return w("new ").typeSig(x.method.parent).w("(").args(x.args).w(")")
    }

    // if using Func.call always need a cast
    needCast := x.leave && m.parent.isFunc && m.name == "call" &&
                !m.returns.isVoid && !m.returns.isGenericParameter

    if (needCast) w("((").typeSig(m.returns).w(")")
    call(x.targetx, x.method, x.args)
    if (needCast) w(")")
    return this
  }

  private This call(Expr target, CMethod method, Expr[] args)
  {
    methodName := JavaUtil.methodName(method)

    // special handling for Obj.compare => fan.sys.FanObj.compare, etc
    if (useFanValCall(target, method))
    {
      if (method.parent.isObj) qnFanObj
      else qnFanVal(target.ctype)
      w(".").w(methodName).w("(")
      if (!method.isStatic) expr(target).args(args, true)
      else this.args(args)
      w(")")
      return this
    }

    // in Java static interface methods must be called on interface itself
    if (method.parent.isMixin && method.isStatic)
      target = StaticTargetExpr(target.loc, method.parent)

    return expr(target).w(".").w(methodName).w("(").args(args).w(")")
  }

  private Bool useFanValCall(Expr target, CMethod method)
  {
    targetType := target.ctype
    if (targetType == null) return false
    if (targetType.isMixin && method.parent.isObj) return true
    if (!JavaUtil.isJavaNative(targetType)) return false
    if (target.id === ExprId.superExpr) return false   // don't use FanObj.xxx for super.xxx
    return true
  }

  This args(Expr[] args, Bool forceComma := false)
  {
    args.each |arg, i|
    {
      if (i > 0 || forceComma) w(", ")
      expr(arg)
    }
    return this
  }

  override This trapExpr(CallExpr x)
  {
    qnFanObj.w(".trap(").expr(x.target).w(", ").str(x.name)
    if (!x.args.isEmpty)
    {
      w(", ").qnList.w(".makeObj(new Object[] {").args(x.args).w("})")
    }
    return w(")")
  }

  override This safeCallExpr(CallExpr x)
  {
    target := x.target
    itExpr := SafeLocalVar(x.loc, x.target.ctype)

    // we add cast in (Cast)target to (it$)->(Cast)call(...)
    CType? cast := null
    if (x.target.id === ExprId.coerce)
    {
      cast = ((TypeCheckExpr)target).check
    }
    else if (x.method.returns.isThis)
    {
      cast = target.ctype
    }

    // NOTE: this only works if closure only uses effectively final locals
    return safe(target, x.ctype) |me|
    {
      if (cast != null) w("(").typeSig(cast).w(")")
      me.call(itExpr, x.method, x.args)
    }
  }

  ** Common code for "?.method()" and "?.field"
  ** NOTE: this requires a Java closure, so only works for effectively final locals
  private This safe(Expr target, CType returns, |This| restViaItArg)
  {
    if (returns.isThis) returns = target.ctype

    qnOpUtil.w(".<").typeSig(target.ctype)
    if ((returns.isVal && !returns.isNullable) || returns.isVoid)
    {
      // value types use safeVoid, safeBool(), safeInt(), or safeFloat()
      w(">safe${returns.name}(")
    }
    else
    {
      // Ojects use safe()
      w(",").typeSig(returns).w(">safe(")
    }
    expr(target).w(", (it\$)->")
    restViaItArg(this)
    w(")")
    return this
  }

  override This elvisExpr(BinaryExpr x)
  {
    // NOTE: this only works if closure only uses effectively final locals
    qnOpUtil.w(".<").typeSig(x.ctype).w(">elvis(")
      .expr(x.lhs)
      .w(", ()->")
    if (x.rhs.id === ExprId.throwExpr)
      w("{ ").expr(x.rhs).w("; }")
    else
      expr(x.rhs)
    return w(")")
  }

  override This ctorExpr(CallExpr x)
  {
    callMethodExpr(x)
  }

  override This staticTargetExpr(StaticTargetExpr x)
  {
    typeSig(x.ctype)
  }

  override This shortcutAssignExpr(ShortcutExpr x)
  {
    // get the variable
    var := x.target

    // if var is a coercion set that aside and get real variable
    TypeCheckExpr? coerce := null
    if (var.id == ExprId.coerce)
    {
      coerce = (TypeCheckExpr)var
      var = coerce.target
    }

    // now we have three variables: local, field, or indexed
    switch (var.id)
    {
      case ExprId.localVar: return shortcutAssignLocal(x, var, coerce)
      case ExprId.field:    return shortcutAssignField(x, var)
      case ExprId.shortcut: return shortcutAssignIndexed(x, var)
      default:              throw Err("$var.id | $x [$x.loc.toLocStr]")
    }
  }

  private This shortcutAssignLocal(ShortcutExpr x, LocalVarExpr local, TypeCheckExpr? coerce)
  {
    if (useJavaNumOp(x) || x.method.qname == "sys::Str.plus")
    {
      // Java operator support Int/Float or Str +=
      lhs := x.target
      rhs := x.args.first
      if (rhs == null)
      {
        op := JavaUtil.unaryOperators.getChecked(x.method.qname)
        return oparen.w(op).expr(lhs).cparen
      }
      else
      {
        op := JavaUtil.binaryOperators.getChecked(x.method.qname)
        return expr(lhs).sp.w(op).w("=").sp.expr(rhs)
      }
    }
    else
    {
      // treat as normal call
      w(local.name).w(" = ")
      if (coerce != null) w("(").typeSig(coerce.check).w(")")
      callMethodExpr(x)
      return this
    }
  }

  private This shortcutAssignField(ShortcutExpr x, FieldExpr fe)
  {
    // NOTE: this assumes idempotent field access
    loc := fe.loc
    getAndCall := CallExpr(loc, fe, x.method, x.args)
    getAndCall.synthetic = true // don't route thru unary/binary operators
    fieldAssign(fe, getAndCall)
    return this
  }

  private This shortcutAssignIndexed(IndexedAssignExpr x, ShortcutExpr indexing)
  {
    // NOTE: this assumes idempotent indexing access and key expression
    // given expression like coll[key] += arg
    loc     := x.loc
    coll    := indexing.target   // collection
    key     := indexing.args[0]  // index key
    getAndCall := CallExpr(loc, indexing, x.method, x.args)
    getAndCall.synthetic = true  // don't route thru unary/binary operators
    expr(coll).w(".set(").expr(key).w(", ").expr(getAndCall).w(")")
    return this
  }

  override This postfixLeaveExpr(ShortcutExpr x)
  {
    // only support Int/Float
    if (isJavaNumVal(x.method.parent))
    {
      switch (x.target.id)
      {
        case ExprId.localVar: if (postfixLeaveLocal(x)) return this
        case ExprId.field:    if (postfixLeaveField(x)) return this
      }
    }

    // fallback to shortcut - this generates invalid code!
    warn("Postfix leave unsupported: $x", x.loc)
    return shortcutAssignExpr(x)
  }

  private Bool postfixLeaveLocal(ShortcutExpr x)
  {
    oparen.expr(x.target).cparen.postfixOperator(x)
    return true
  }

  private Bool postfixLeaveField(ShortcutExpr x)
  {
    fe := (FieldExpr)x.target
    expr(fe.target).w(".").fieldName(fe.field).postfixOperator(x)
    return true
  }

  private This postfixOperator(ShortcutExpr x)
  {
    name := x.method.name
    if (name == "increment") return w("++")
    if (name == "decrement") return w("--")
    else throw Err("Postfix $x.method.qname")
  }

  private Bool useJavaNumOp(ShortcutExpr x)
  {
    isJavaNumVal(x.method.parent) && x.target.id === ExprId.localVar
  }

  private Bool isJavaNumVal(CType t) { t.isInt || t.isFloat }

//////////////////////////////////////////////////////////////////////////
// Fields
//////////////////////////////////////////////////////////////////////////

  override This fieldExpr(FieldExpr x)
  {
    f := x.field
    if (x.isSafe)
    {
      return safe(x.target, f.type) |me|
      {
        itExpr := SafeLocalVar(x.loc, x.target.ctype)
        me.w("(").typeSig(f.type).w(")").getField(itExpr, x)
      }
    }
    else
    {
      return getField(x.target, x)
    }
  }

  private This getField(Expr? target, FieldExpr x)
  {
    // special handling for ie fan.sys.FanBool.xxx
    field := x.field
    targetType := target?.ctype
    if (targetType != null && JavaUtil.isJavaNative(targetType))
    {
      qnFanVal(targetType).w(".").fieldName(field)
      return this
    }

    // in Java static interface methods must be called on interface itself
    if (field.parent.isMixin && field.isStatic)
      target = StaticTargetExpr(x.loc, field.parent)

    if (target != null) expr(target).w(".")
    fieldName(field)
    if (useFieldCall(x)) w("()")

    return this
  }

  private Bool useFieldCall(FieldExpr x)
  {
    // if inside a setter
    if (curMethod.isGetter || curMethod.isSetter)
    {
      // just in case
      if (x.useAccessor) return true

      // always use call syntax for super.myField
      if  (x.target.id === ExprId.superExpr) return true

      // if its my own field itself then we can skip
      if (x.target.ctype == curMethod.parent) return false
    }

    // don't use calls on synthetic fields
    if (x.field.isSynthetic) return false

    // allow direct field access for sys
    if (x.field.parent.pod.name == "sys") return x.useAccessor

    return true
  }

  private This fieldAssign(FieldExpr x, Expr rhs)
  {
    // if we are in the static init of a mixin, then our fields
    // are declared on an inner class named Fields
    field := x.field
    if (curMethod.isStaticInit && curMethod.parent.isMixin)
    {
      fieldName(field).w(" = ").expr(rhs)
      return this
    }

    if (x.target != null) expr(x.target).w(".")
    fieldName(field)
    if (closure != null && field.isConst && field.parent != closure)
      w("\$init(this, ").expr(rhs).w(")")
    else if (assignViaSetter(x))
      w("(").expr(rhs).w(")")
    else
      w(" = ").expr(rhs)
    return this
  }

  private Bool assignViaSetter(FieldExpr x)
  {
    if (x.useAccessor) return true
    if (x.field.parent.isSynthetic) return false
    if (curType == x.field.parent) return false
    if (x.field.isConst && curMethod.isCtor) return false
    return true
  }

  override This closureExpr(ClosureExpr x)
  {
    callMethodExpr(x.substitute)
  }

}

**************************************************************************
** SafeLocalVar
**************************************************************************

class SafeLocalVar : ItExpr
{
  new make(Loc loc, CType type) : super(loc, type) {}
  override Str name() { "it\$" }
}