/* NSC -- new Scala compiler
 * Copyright 2005-2010 LAMP/EPFL
 * @author  Paul Phillips
 */

package scala.tools.nsc
package typechecker

import scala.collection.mutable
import scala.collection.mutable.ListBuffer
import scala.util.control.ControlThrowable
import scala.util.control.Exception.ultimately
import symtab.Flags._
import PartialFunction._

/** An interface to enable higher configurability of diagnostic messages
 *  regarding type errors.  This is barely a beginning as error messages are
 *  distributed far and wide across the codebase.  The plan is to partition
 *  error messages into some broad groups and provide some mechanism for
 *  being more or less verbose on a selective basis.  Possible groups include
 *  such examples as
 * 
 *    arity errors
 *    kind errors
 *    variance errors
 *    ambiguity errors
 *    volatility/stability errors
 *    implementation restrictions
 *
 *  And more, and there is plenty of overlap, so it'll be a process.
 *
 *  @author Paul Phillips
 *  @version 1.0
 */
trait TypeDiagnostics {
  self: Analyzer =>

  import global._
  import definitions._
  import global.typer.infer
  
  /** It can be quite difficult to know which of the many functions called "error"
   *  is being called at any given point in the compiler.  To alleviate this I am
   *  renaming such functions inside this trait based on where it originated.
   */
  def inferError(pos: Position, msg: String) = infer.error(pos, msg)
  
  /** The common situation of making sure nothing is erroneous could be
   *  nicer if Symbols, Types, and Trees all implemented some common interface
   *  in which isErroneous and similar would be placed.
   */
  def noErroneousTypes(tps: Type*)    = tps forall (x => !x.isErroneous)
  def noErroneousSyms(syms: Symbol*)  = syms forall (x => !x.isErroneous)
  def noErroneousTrees(trees: Tree*)  = trees forall (x => !x.isErroneous)
  
  /** A map of Positions to addendums - if an error involves a position in
   *  the map, the addendum should also be printed.
   */
  private var addendums = mutable.Map[Position, () => String]()

  def setAddendum(pos: Position, msg: () => String) =
    if (pos != NoPosition)
      addendums(pos) = msg

  def withAddendum(pos: Position) = (_: String) + addendums.getOrElse(pos, () => "")()

  def decodeWithNamespace(name: Name): String = {
    val prefix = if (name.isTypeName) "type " else "value "
    prefix + name.decode
  }
  
  /** Does the positioned line assigned to t1 precede that of t2?
   */
  def linePrecedes(t1: Tree, t2: Tree) = t1.pos.isDefined && t1.pos.isDefined && t1.pos.line < t2.pos.line
  
  def notAMember(sel: Tree, qual: Tree, name: Name) = {
    def decoded = decodeWithNamespace(name)
    
    def msg: String = name match {
      case nme.CONSTRUCTOR    => qual.tpe.widen+" does not have a constructor"
      case _                  =>
        def memberOf = if (qual.tpe.typeSymbol.isTypeParameterOrSkolem) "type parameter " else ""
        def possibleCause =
          if (linePrecedes(qual, sel))
            "\npossible cause: maybe a semicolon is missing before `"+decoded+"'?"
          else
            ""
        
        decoded+" is not a member of "+ memberOf + qual.tpe.widen + possibleCause
    }
    inferError(sel.pos, withAddendum(qual.pos)(msg))
  }
  
  /** Only prints the parameter names if they're not synthetic,
   *  since "x$1: Int" does not offer any more information than "Int".
   */
  private def methodTypeErrorString(tp: Type) = tp match {
    case mt @ MethodType(params, resultType)  =>
      def forString = 
        if (params exists (_.isSynthetic)) params map (_.tpe)
        else params map (_.defString)
                  
       forString.mkString("(", ",", ")") + resultType
    case x                                    => x.toString
  }

  def alternatives(tree: Tree): List[Type] = tree.tpe match {
    case OverloadedType(pre, alternatives)  => alternatives map pre.memberType
    case _                                  => Nil
  }
  def alternativesString(tree: Tree) =
    alternatives(tree) map (x => "  " + methodTypeErrorString(x)) mkString ("", " <and>\n", "\n")

  def missingParameterTypeError(fun: Tree, vparam: ValDef) = {
    val suffix = if (vparam.mods.isSynthetic) " for expanded function "+fun else ""
    
    inferError(vparam.pos, "missing parameter type" + suffix)
    ErrorType
  }
  
  def treeSymTypeMsg(tree: Tree): String = {
    val sym               = tree.symbol
    def hasParams         = tree.tpe.paramSectionCount > 0    
    def preResultString   = if (hasParams) ": " else " of type "

    def nullMessage       = "expression of type " + tree.tpe
    def overloadedMessage = "overloaded method " + sym + " with alternatives:\n" + alternativesString(tree)
    def moduleMessage     = "" + sym
    def defaultMessage    = moduleMessage + preResultString + tree.tpe
    def applyMessage      = defaultMessage + tree.symbol.locationString

    if (sym == null) nullMessage
    else if (sym.isOverloaded) overloadedMessage
    else if (sym.isModule) moduleMessage
    else if (sym.name == nme.apply) applyMessage
    else defaultMessage
  }
  
  def applyErrorMsg(tree: Tree, msg: String, argtpes: List[Type], pt: Type) = {
    def asParams(xs: List[Any]) = xs.mkString("(", ", ", ")")

    def resType   = if (pt isWildcard) "" else " with expected result type " + pt
    def allTypes  = (alternatives(tree) flatMap (_.paramTypes)) ++ argtpes :+ pt

    withDisambiguation(allTypes: _*) {
      treeSymTypeMsg(tree) + msg + asParams(argtpes) + resType
    }
  }
  
  def disambiguate(ss: List[String]) = ss match {
    case Nil      => Nil
    case s :: ss  => s :: (ss map { case `s` => "(some other)"+s ; case x => x })
  }
  
  // todo: use also for other error messages
  def existentialContext(tp: Type) = tp.existentialSkolems match {
    case Nil  => ""
    case xs   => " where " + (disambiguate(xs map (_.existentialToString)) mkString ", ")
  }
  
  def foundReqMsg(found: Type, req: Type): String =
    withDisambiguation(found, req) {
      ";\n found   : " + found.toLongString + existentialContext(found) +
       "\n required: " + req + existentialContext(req)
    }

  /** If two given types contain different type variables with the same name
   *  differentiate the names by including owner information.  Also, if the
   *  type error is because of a conflict between two identically named
   *  classes and one is in package scala, fully qualify the name so one
   *  need not deduce why "java.util.Iterator" and "Iterator" don't match.
   *  Another disambiguation performed is to address the confusion present
   *  in the following snippet:
   *    def f[Int](x: Int) = x + 5.
   */
  def withDisambiguation[T](types: Type*)(op: => T): T = {
    object SymExtractor {
      def unapply(x: Any) = x match {
        case t @ TypeRef(_, sym, _)   => Some(t -> sym)
        case t @ ConstantType(value)  => Some(t -> t.underlying.typeSymbol)
        case _                        => None
      }
    }
    val typerefs =
      for (tp <- types.toList ; SymExtractor(t, sym) <- tp) yield
        t -> sym
    
    val savedNames    = typerefs map { case (_, sym) => sym -> sym.name } toMap
    def restoreNames  = savedNames foreach { case (sym, name) => sym.name = name }

    def isAlreadyAltered(sym: Symbol) = sym.name != savedNames(sym)
    
    def modifyName(sym: Symbol)(f: String => String): Unit =
      sym.name = newTypeName(f(sym.name.toString))

    def scalaQualify(sym: Symbol) =
      if (sym.owner.isScalaPackageClass)
        modifyName(sym)("scala." + _)

    def explainName(sym: Symbol) = {
      scalaQualify(sym)
      
      if (!isAlreadyAltered(sym))
        modifyName(sym)(_ + "(in " + sym.owner + ")")
    }

    ultimately(restoreNames) {
      for ((t1, sym1) <- typerefs ; (t2, sym2) <- typerefs ; if sym1 != sym2 && (sym1 isLess sym2)) {
                
        if (t1.toString == t2.toString) {   // type variable collisions
          List(sym1, sym2) foreach explainName
          if (sym1.owner == sym2.owner)
            sym2.name = newTypeName("(some other)"+sym2.name)
        }
        else if (sym1.name == sym2.name) {  // symbol name collisions
          List(sym1, sym2) foreach { x =>
            if (x.owner.isScalaPackageClass)
              modifyName(x)("scala." + _)
            else if (x.isTypeParameterOrSkolem)
              explainName(x)
          }
        }
      }
      
      // performing the actual operation
      op
    }
  }
  
  trait TyperDiagnostics {
    self: Typer =>

    private def contextError(pos: Position, msg: String) = context.error(pos, msg)
    private def contextError(pos: Position, err: Throwable) = context.error(pos, err)
    
    def symWasOverloaded(sym: Symbol) = sym.owner.isClass && sym.owner.info.member(sym.name).isOverloaded
    def cyclicAdjective(sym: Symbol)  = if (symWasOverloaded(sym)) "overloaded" else "recursive"
    
    /** Returns Some(msg) if the given tree is untyped apparently due
     *  to a cyclic reference, and None otherwise.
     */
    def cyclicReferenceMessage(sym: Symbol, tree: Tree) = condOpt(tree) {      
      case ValDef(_, _, tpt, _) if tpt.tpe == null        => "recursive "+sym+" needs type"
      case DefDef(_, _, _, _, tpt, _) if tpt.tpe == null  => List(cyclicAdjective(sym), sym, "needs result type") mkString " "
    }
  
    /** Report a type error.
     *
     *  @param pos0   The position where to report the error
     *  @param ex     The exception that caused the error
     */
    def reportTypeError(pos: Position, ex: TypeError) {
      if (ex.pos == NoPosition) ex.pos = pos
      if (!context.reportGeneralErrors) throw ex
      if (settings.debug.value) ex.printStackTrace()

      ex match {
        case CyclicReference(sym, info: TypeCompleter) =>
          contextError(ex.pos, cyclicReferenceMessage(sym, info.tree) getOrElse ex.getMessage())
          
          if (sym == ObjectClass) 
            throw new FatalError("cannot redefine root "+sym)
        case _ =>
          contextError(ex.pos, ex)
      }
    }
  }
}
