//
// Copyright (c) 2006, Brian Frank and Andy Frank
// Licensed under the Academic Free License version 3.0
//
// History:
//   15 Sep 05  Brian Frank  Creation
//    5 Sep 06  Brian Frank  Ported from Java to Fan
//

**
** CallResolver handles the process of resolving a CallExpr or
** UnknownVarExpr to a method call or a field access.
**
class CallResolver : CompilerSupport
{

//////////////////////////////////////////////////////////////////////////
// Construction
//////////////////////////////////////////////////////////////////////////

  **
  ** Construct with NameExpr (base class of CallExpr and UnknownVarExpr)
  **
  new make(Compiler compiler, TypeDef? curType, MethodDef? curMethod, NameExpr expr)
    : super(compiler)
  {
    this.curType   = curType
    this.curMethod = curMethod
    this.expr      = expr
    this.loc       = expr.loc
    this.target    = expr.target
    this.name      = expr.name

    call := expr as CallExpr
    if (call != null)
    {
      this.isVar   = false
      this.isItAdd = call.isItAdd
      this.args    = call.args
      this.found   = call.method
    }
    else
    {
      this.isVar = true
      this.args  = Expr[,]
    }
  }

//////////////////////////////////////////////////////////////////////////
// Resolve
//////////////////////////////////////////////////////////////////////////

  **
  ** Resolve into a method call or field access
  **
  Expr resolve()
  {
    try
    {
      if (isStaticLiteral) return result
      resolveBase
      find
      if (result != null) return result
      insertImplicitThisOrIt
      resolveToExpr
      inferClosureType
      resolveForeign
      constantFolding
      castForThisType
      safeToNullable
      ffiCoercion
      return result
    }
    catch (CompilerErr err)
    {
      expr.ctype = ns.error
      return expr
    }
  }

//////////////////////////////////////////////////////////////////////////
// Static Literal
//////////////////////////////////////////////////////////////////////////

  **
  ** If this is a standalone name without a base target
  ** such as "Foo" and the name maps to a type name, then
  ** this is a type literal.
  **
  Bool isStaticLiteral()
  {
    if (target == null && isVar)
    {
      stypes := curType.unit.importedTypes[name]

      // if more then, one first try to exclude those internal to other pods
      if (stypes != null && stypes.size > 1)
        stypes.exclude |t| { t.isInternal && t.pod.name != compiler.pod.name }

      if (stypes != null && !stypes.isEmpty)
      {
        if (stypes.size > 1)
          throw err("Ambiguous type: " + stypes.join(", "), loc)
        else
          result = StaticTargetExpr(loc, stypes.first)
        return true
      }
    }
    return false
  }

//////////////////////////////////////////////////////////////////////////
// Resolve Base
//////////////////////////////////////////////////////////////////////////

  **
  ** Resolve the base type which defines the slot we are calling.
  **
  Void resolveBase()
  {
    // if target unspecified, then assume a slot on the current
    // class otherwise the slot must be on the target type
    if (target == null)
    {
      // if we are in a closure - then base is the enclosing class;
      // if closure is it-block when we need to keep track of it too
      if (curType.isClosure)
      {
        base = curType.closure.enclosingType
        if (curType.closure.isItBlock) baseIt = curType.closure.itType
      }
      else
      {
        base = curType
      }
    }
    else
    {
      base = target.ctype
    }

    // if base is the error type, then we already logged an error
    // trying to resolve the target and it's pointless to continue
    if (base === ns.error) throw CompilerErr("ignore", loc, null)

    // sanity check
    if (base == null) throw err("Internal error", loc)
  }

//////////////////////////////////////////////////////////////////////////
// Find
//////////////////////////////////////////////////////////////////////////

  **
  ** Find the method or field with the specified name.
  **
  Void find()
  {
    // if already "found", then skip this step
    if (found != null) return

    // look it up in base type
    found = findOn(base)

    // if we have an it in scope, then also attempt to resolve against it
    if (baseIt != null)
    {
      foundIt := findOn(baseIt)

      // if we found a match on both base and it, that is an error
      if (isAmbiguous(found, foundIt))
      {
        // if we detected ambiguity, but the current type doesn't have
        // visibility to access the baseIt slot, then ignore it
        if (!CheckErrors.isSlotVisible(curType, foundIt))
          { foundIt = null }
        else
          throw err("Ambiguous slot '$name' on both 'this' ($base) and 'it' ($baseIt)", loc)
      }

      // resolved against implicit it
      if (foundIt != null)
      {
        found = foundIt
        foundOnIt = true
      }
    }

    // if still not found, then error
    if (found == null)
    {
      if (isVar)
      {
        if (target == null)
          throw err("Unknown variable '$name'", loc)
        else
          throw err("Unknown slot '$errSig'", loc)
      }
      else
      {
        ct := target as CallExpr
        if (ct != null && ct.isItAdd)
          throw err("'$ct.method.qname' must return This", loc)
        else if (this.isItAdd)
          throw err("No comma operator method found: '$errSig'", loc)
        else
          throw err("Unknown method '$errSig'", loc)
      }
    }
  }

  private CSlot? findOn(CType base)
  {
    // if base is the error type, short circuit
    if (base === ns.error) return null

    // attempt to resolve the slot by name
    CSlot? found
    try
      found = base.slot(name)
    catch (Err e)
      err("Cannot resolve $name on $base; $e.msg", loc)
    if (found == null) return null

    // if the resolved slot is on a FFI type then we have to
    // delegate back to bridge because we might support both methods
    // and fields overloaded by the same name
    if (found.isForeign)
      found = found.parent.bridge.resolveSlotAccess(base, name, isVar)

    // if we resolve a method call against a field that is an error,
    // unless the field is a function in which case this is sugar
    // for field.call(...)
    if (found is CField && !isVar)
    {
      field := (CField)found
      if (field.fieldType.isFunc)
        isFuncFieldCall = true
      else
        throw err("Expected method, not field '$errSig'", loc)
    }

    return found
  }

  private Bool isAmbiguous(CSlot? onBase, CSlot? onIt)
  {
    // unless we found on both base and baseIt, it is not ambiguous
    if (onBase == null || onIt == null) return false

    // if they are both the same static method, it doesn't matter
    if (onBase.qname == onIt.qname && onBase.isStatic) return false

    // if we are calling an instance slot in a static context,
    // then we can assume that we are binding to it
    if (!onBase.isStatic && !onIt.isStatic && curType.closure.enclosingSlot.isStatic)
      return false

    return true
  }

  private Str errSig() { "${base.qname}.${name}" }

//////////////////////////////////////////////////////////////////////////
// Implicit This
//////////////////////////////////////////////////////////////////////////

  **
  ** If the call has no explicit target, and is a instance field
  ** or method, then we need to insert an implicit this or it.
  **
  private Void insertImplicitThisOrIt()
  {
    if (target != null) return
    if (found.isStatic || found.isCtor) return
    if (curMethod.isStatic) return

    if (foundOnIt)
    {
      target = ItExpr(loc, baseIt)
    }
    else if (curType.isClosure)
    {
      closure := curType.closure
      if (!closure.enclosingSlot.isStatic)
        target = FieldExpr(loc, ThisExpr(loc, closure.enclosingType), closure.outerThisField)
    }
    else
    {
      target = ThisExpr(loc, curType)
    }
  }

//////////////////////////////////////////////////////////////////////////
// Resolve Expr Type
//////////////////////////////////////////////////////////////////////////

  **
  ** Compute the expression type the call itself (what gets left on the stack).
  **
  private Void resolveToExpr()
  {
    if (found is CField)
    {
      result = resolveToFieldExpr
      if (isFuncFieldCall)
      {
        callMethod := ((CField)found).fieldType.method("call")
        result = CallExpr.makeWithMethod(loc, result, callMethod, args)
      }
    }
    else
    {
      result = resolveToCallExpr
    }
  }

  private CallExpr resolveToCallExpr()
  {
    method := (CMethod)found

    call := expr as CallExpr
    if (call == null)
    {
      call = CallExpr(loc)
      call.name   = name
      call.args   = args
    }
    call.target   = target
    call.isSafe   = expr.isSafe
    call.noParens = isVar

    call.method = method
    if (method.isInstanceCtor)
      call.ctype = method.parent
    else
      call.ctype = method.returnType

    return call
  }

  private FieldExpr resolveToFieldExpr()
  {
    f := (CField)found

    field := FieldExpr(loc)
    field.target = target
    field.name   = name
    field.field  = f
    field.ctype  = f.fieldType
    field.isSafe = expr.isSafe

    return field
  }

//////////////////////////////////////////////////////////////////////////
// Infer Closure Type
//////////////////////////////////////////////////////////////////////////

  **
  ** If the last argument to the resolved call is a closure,
  ** then use the method to infer the function type
  **
  private Void inferClosureType()
  {
    if (result is CallExpr)
    {
      base := foundOnIt ? this.baseIt : this.base
      result = inferClosureTypeFromCall(this, result, base)
    }
  }

  **
  ** If the last argument to the resolved call is a closure,
  ** then use the method to infer the function type.  If the
  ** last arg is a closure, but the call doesn't take a closure,
  ** then translate into an implicit call to Obj.with
  **
  static Expr inferClosureTypeFromCall(CompilerSupport support, CallExpr call, CType base)
  {
    // check if last argument is closure
    c := call.args.last as ClosureExpr
    if (c == null) return call

    // if the resolved slot is a method where the last param
    // is expected to be a function type, then use that to
    // infer the type signature of the closure
    m := call.method
    lastParam := m.params.last?.paramType?.deref?.toNonNullable as FuncType
    if (lastParam != null && call.args.size == m.params.size &&
        c.signature.params.size <= lastParam.params.size)
    {
      if (call.method.name == "with")
        lastParam = FuncType.makeItBlock(base)
      else
        lastParam = lastParam.parameterizeThis(base)
      c.setInferredSignature(lastParam)
      return call
    }

    // otherwise if the closure is an it-block, we infer
    // its type to be the result of the target expression
    if (c.isItBlock)
    {
      // if call is This, switch it to base (passes thru to toWith)
      if (call.ctype.isThis) call.ctype = base

      // can't chain it-block if call returns Void
      if (call.ctype.isVoid)
      {
        support.err("Cannot apply it-block to Void expr", call.loc)
        return call
      }

      // remove the function parameter and turn this into:
      //  call(args).toWith(c)
      call.args.removeAt(-1)
      return c.toWith(call)
    }
    return call
  }

//////////////////////////////////////////////////////////////////////////
// FFI
//////////////////////////////////////////////////////////////////////////

  **
  ** If we have a FFI call, then give the foreign bridge a chance
  ** to resolve the method and deal with method overloading.  Note
  ** at this point we've already resolved the call by name to *some*
  ** method (in the find step).  But this callback gives the bridge
  ** a chance to resolve to the *correct* overloaded method.  We need
  ** to this during ResolveExpr in order to infer local variables
  ** correctly.
  **
  private Void resolveForeign()
  {
    bridge := found.usesBridge
    if (bridge != null && result is CallExpr)
      result = bridge.resolveCall(result)
  }

//////////////////////////////////////////////////////////////////////////
// Constant Folding
//////////////////////////////////////////////////////////////////////////

  **
  ** If the epxression is a call, check for constant folding.
  **
  private Void constantFolding()
  {
    // only do const folding on method calls (which inculdes shortcut ops)
    call := result as CallExpr
    if (call == null) return

    // skip constant folding for testSys
    if (curType != null && compiler.pod.name == "testSys") return

    result = ConstantFolder(compiler).fold(call)
  }

//////////////////////////////////////////////////////////////////////////
// Cast for This Type
//////////////////////////////////////////////////////////////////////////

  **
  ** If the epxression is a call which returns sys::This,
  ** then we need to insert an implicit cast.
  **
  private Void castForThisType()
  {
    // only care about calls that return This
    if (!result.ctype.isThis) return

    // check that we are calling a method
    method := found as CMethod
    if (method == null) return

    // the result of a method which returns This
    // is always the base target type - if we aren't
    // calling against the original declaring type
    // then we also need an implicit cast operation
    base := foundOnIt ? this.baseIt : this.base
    result.ctype = base
    if (method.inheritedReturnType != base)
      result = TypeCheckExpr.coerce(result, base) { from = method.inheritedReturnType }
  }

//////////////////////////////////////////////////////////////////////////
// Safe to Nullable
//////////////////////////////////////////////////////////////////////////

  **
  ** If the epxression is a safe call using "?.", then
  ** the resulting expression type is nullable.
  **
  private Void safeToNullable()
  {
    if (expr.isSafe)
    {
      result.ctype = result.ctype.toNullable
    }
  }

//////////////////////////////////////////////////////////////////////////
// FFI Coercion
//////////////////////////////////////////////////////////////////////////

  **
  ** If this field access or method call returns a type which
  ** isn't directly represented in the Fantom type system, then
  ** implicitly coerce it
  **
  private Void ffiCoercion()
  {
    if (result.ctype.isForeign)
    {
      foreign := result.ctype
      inferred := foreign.inferredAs
      if (foreign !== inferred)
      {
        result = foreign.bridge.coerce(result, inferred) |->|
        {
          throw err("Cannot coerce call return to Fantom type", loc)
        }
      }
    }
  }

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

  TypeDef? curType     // current type of scope
  MethodDef? curMethod // current method of scope
  NameExpr expr        // original expression being resolved
  Loc loc              // location of original expression
  Expr? target         // target base or null
  Str name             // slot name to resolve
  Bool isItAdd         // are we resolving "," it-block add
  Bool isVar           // are we resolving simple variable
  Bool isFuncFieldCall // is this a field.call(...) on func field
  Expr[] args          // arguments or null if simple variable
  CType? base          // resolveBase()
  CType? baseIt        // resolveBase()
  CSlot? found         // find()
  Bool foundOnIt       // was find() resolved against it
  Expr? result         // resolveToExpr()

}