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

**
** Coercer handles all the logic for type casts
**
class Coercer : CompilerSupport
{
  **
  ** Constructor
  **
  new make(Compiler c) : super(c) {}

  **
  ** Ensure the specified expression is boxed to an object reference.
  **
  Expr box(Expr expr)
  {
    if (expr.ctype.isVal)
      return TypeCheckExpr.coerce(expr, expr.ctype.toNullable)
    else
      return expr
  }

  **
  ** Run the standard coerce method and ensure the result is boxed.
  **
  Expr coerceBoxed(Expr expr, CType expected, |->| onErr)
  {
    return box(coerce(expr, expected, onErr))
  }

  **
  ** Return if `coerce` would not report a compiler error.
  **
  Bool canCoerce(Expr expr, CType expected)
  {
    ok := true
    coerce(expr, expected) |->| { ok = false }
    return ok
  }

  **
  ** Coerce the target expression to the specified type.  If
  ** the expression is not type compatible run the onErr function.
  **
  Expr coerce(Expr expr, CType expected, |->| onErr)
  {
    // route to bridge for FFI coercion if either side if foreign
    if (expected.isForeign) return expected.bridge.coerce(expr, expected, onErr)
    if (expr.ctype.isForeign) return expr.ctype.bridge.coerce(expr, expected, onErr)

    // normal Fantom coercion behavior
    return doCoerce(expr, expected, onErr)
  }

  **
  ** Coerce the target expression to the specified type.  If
  ** the expression is not type compatible run the onErr function.
  ** Default Fantom behavior (no FFI checks).
  **
  Expr doCoerce(Expr expr, CType expected, |->| onErr)
  {
    // sanity check that expression has been typed
    CType actual := expr.ctype
    if ((Obj?)actual == null) throw NullErr("null ctype: ${expr}")

    // if the same type this is easy
    if (actual == expected)
    {
      // unless the expr is a method call with covariant override
      call := expr.asCall
      if (call != null && call.method.isCovariant)
        return coerceInheritedReturns(call, expected)

      return expr
    }

    // if actual type is nothing, then its of no matter
    if (actual.isNothing) return expr

    // we can never use a void expression
    if (actual.isVoid || expected.isVoid)
    {
      onErr()
      return expr
    }

    // if expr is always nullable (null literal, safe invoke, as),
    // then verify expected type is nullable
    if (expr.isAlwaysNullable)
    {
      if (!expected.isNullable) { onErr(); return expr }

      // null literals don't need cast to nullable types,
      // otherwise // fall-thru to apply coercion
      if (expr.id === ExprId.nullLiteral) return expr
    }

    // if the expression fits to type, that is ok
    if (actual.fits(expected))
    {
      // if we have any nullable/value difference we need a coercion
      if (forceCoerce(actual, expected))
        return TypeCheckExpr.coerce(expr, expected)
      else
        return expr
    }

    // if we can auto-cast to make the expr fit then do it - we
    // have to treat function auto-casting a little specially here
    if (actual.isFunc && expected.isFunc)
    {
      if (isFuncAutoCoerce(actual, expected))
        return TypeCheckExpr.coerce(expr, expected)
    }
    else
    {
      if (expected.fits(actual))
        return TypeCheckExpr.coerce(expr, expected)
    }

    // we have an error condition
    onErr()
    return expr
  }

  private Expr coerceInheritedReturns(CallExpr call, CType expected)
  {
    // check force coersion that Java transpiler might require
    // for a covariant overridden method with parameterized collection
    if (forceParameterizedCollectionCoerce(call.method.inheritedReturns, expected))
      return TypeCheckExpr.coerce(call, expected)
    else
      return call
  }

  private Bool isFuncAutoCoerce(CType actualType, CType expectedType)
  {
    // check if both are function types
    if (!actualType.isFunc || !expectedType.isFunc) return false
    actual   := actualType.toNonNullable as FuncType
    expected := expectedType.toNonNullable as FuncType

    // auto-cast to or from unparameterized 'sys::Func'
    if (actual == null || expected == null) return true

    // if actual function requires more parameters than
    // we are expecting, then this cannot be a match
    if (actual.arity > expected.arity) return false

    // check return type
    if (!isFuncAutoCoerceMatch(actual.returns, expected.returns))
      return false

    // check that each parameter is auto-castable
    return actual.params.all |CType actualParam, Int i->Bool|
    {
      expectedParam := expected.params[i]
      return isFuncAutoCoerceMatch(actualParam, expectedParam)
    }

    return true
  }

  private Bool isFuncAutoCoerceMatch(CType actual, CType expected)
  {
    if (actual.fits(expected)) return true
    if (expected.fits(actual)) return true
    if (isFuncAutoCoerce(actual, expected)) return true
    return false
  }

  **
  ** Force a coercion even *after* we have determined that 'from.fits(to)'
  **
  private Bool forceCoerce(CType from, CType to)
  {
    // if either side is a value type and we got past
    // the ctype equals check then we definitely need a coercion
    if (from.isVal || to.isVal) return true

    // configurable handling for parameterized collection types
    if (forceParameterizedCollectionCoerce(from, to))
      return true

    // if going from Obj? -> Obj we need a nullable coercion
    if (!to.isNullable) return from.isNullable

    return false
  }

  **
  ** Configurable handling for parameterized collection types
  **
  private Bool forceParameterizedCollectionCoerce(CType from, CType to)
  {
    needParameterizedCollectionCoerce(from) || needParameterizedCollectionCoerce(to)
  }

}