/* NSC -- new Scala compiler
 * Copyright 2005-2013 LAMP/EPFL
 * @author  Martin Odersky
 */

package scala.tools.nsc
package typechecker

import symtab.Flags._
import scala.reflect.internal.util.ListOfNil

/*
 *  @author  Martin Odersky
 *  @version 1.0
 */
trait Unapplies extends ast.TreeDSL {
  self: Analyzer =>

  import global._
  import definitions._
  import CODE.{ CASE => _, _ }
  import treeInfo.{ isRepeatedParamType, isByNameParamType }

  private def unapplyParamName = nme.x_0
  private def caseMods         = Modifiers(SYNTHETIC | CASE)

  // In the typeCompleter (templateSig) of a case class (resp its module),
  // synthetic `copy` (reps `apply`, `unapply`) methods are added. To compute
  // their signatures, the corresponding ClassDef is needed. During naming (in
  // `enterClassDef`), the case class ClassDef is added as an attachment to the
  // moduleClass symbol of the companion module.
  class ClassForCaseCompanionAttachment(val caseClass: ClassDef)

  /** Returns unapply or unapplySeq if available, without further checks.
   */
  def directUnapplyMember(tp: Type): Symbol = (tp member nme.unapply) orElse (tp member nme.unapplySeq)

  /** Filters out unapplies with multiple (non-implicit) parameter lists,
   *  as they cannot be used as extractors
   */
  def unapplyMember(tp: Type): Symbol = directUnapplyMember(tp) filter (sym => !hasMultipleNonImplicitParamLists(sym))

  object HasUnapply {
    def unapply(tp: Type): Option[Symbol] = unapplyMember(tp).toOption
  }

  private def toIdent(x: DefTree) = Ident(x.name) setPos x.pos.focus

  private def classType(cdef: ClassDef, tparams: List[TypeDef]): Tree = {
    // SI-7033 Unattributed to avoid forcing `cdef.symbol.info`.
    val tycon = Ident(cdef.symbol)
    if (tparams.isEmpty) tycon else AppliedTypeTree(tycon, tparams map toIdent)
  }

  private def constrParamss(cdef: ClassDef): List[List[ValDef]] = {
    val ClassDef(_, _, _, Template(_, _, body)) = resetAttrs(cdef.duplicate)
    val DefDef(_, _, _, vparamss, _, _) = treeInfo firstConstructor body
    vparamss
  }

  private def constrTparamsInvariant(cdef: ClassDef): List[TypeDef] = {
    val ClassDef(_, _, tparams, _) = resetAttrs(cdef.duplicate)
    val tparamsInvariant = tparams.map(tparam => copyTypeDef(tparam)(mods = tparam.mods &~ (COVARIANT | CONTRAVARIANT)))
    tparamsInvariant
  }

  /** The return value of an unapply method of a case class C[Ts]
   *  @param param  The name of the parameter of the unapply method, assumed to be of type C[Ts]
   *  @param caseclazz  The case class C[Ts]
   */
  private def caseClassUnapplyReturnValue(param: Name, caseclazz: ClassDef) = {
    def caseFieldAccessorValue(selector: ValDef): Tree = {
      // Selecting by name seems to be the most straight forward way here to
      // avoid forcing the symbol of the case class in order to list the accessors.
      def selectByName = Ident(param) DOT caseAccessorName(caseclazz.symbol, selector.name)
      // But, that gives a misleading error message in neg/t1422.scala, where a case
      // class has an illegal private[this] parameter. We can detect this by checking
      // the modifiers on the param accessors.
      // We just generate a call to that param accessor here, which gives us an inaccessible
      // symbol error, as before.
      def localAccessor = caseclazz.impl.body find {
        case t @ ValOrDefDef(mods, selector.name, _, _) => mods.isPrivateLocal
        case _                                          => false
      }
      localAccessor.fold(selectByName)(Ident(param) DOT _.symbol)
    }

    // Working with trees, rather than symbols, to avoid cycles like SI-5082
    constrParamss(caseclazz).take(1).flatten match {
      case Nil => TRUE
      case xs  => SOME(xs map caseFieldAccessorValue: _*)
    }
  }

  /** The module corresponding to a case class; overrides toString to show the module's name
   */
  def caseModuleDef(cdef: ClassDef): ModuleDef = {
    val params = constrParamss(cdef)
    def inheritFromFun = !cdef.mods.hasAbstractFlag && cdef.tparams.isEmpty && (params match {
      case List(ps) if ps.length <= MaxFunctionArity => true
      case _ => false
    })
    def createFun = {
      def primaries = params.head map (_.tpt)
      gen.scalaFunctionConstr(primaries, toIdent(cdef), abstractFun = true)
    }

    def parents        = if (inheritFromFun) List(createFun) else Nil
    def toString       = DefDef(
      Modifiers(OVERRIDE | FINAL | SYNTHETIC),
      nme.toString_,
      Nil,
      ListOfNil,
      TypeTree(),
      Literal(Constant(cdef.name.decode)))

    companionModuleDef(cdef, parents, List(toString))
  }

  def companionModuleDef(cdef: ClassDef, parents: List[Tree] = Nil, body: List[Tree] = Nil): ModuleDef = atPos(cdef.pos.focus) {
    ModuleDef(
      Modifiers(cdef.mods.flags & AccessFlags | SYNTHETIC, cdef.mods.privateWithin),
      cdef.name.toTermName,
      gen.mkTemplate(parents, noSelfType, NoMods, Nil, body, cdef.impl.pos.focus))
  }

  /** The apply method corresponding to a case class
   */
  def factoryMeth(mods: Modifiers, name: TermName, cdef: ClassDef): DefDef = {
    val tparams   = constrTparamsInvariant(cdef)

    val cparamss  = constrParamss(cdef)
    def classtpe = classType(cdef, tparams)
    atPos(cdef.pos.focus)(
      DefDef(mods, name, tparams, cparamss, classtpe,
        New(classtpe, mmap(cparamss)(gen.paramToArg)))
    )
  }

  /** The apply method corresponding to a case class
   */
  def caseModuleApplyMeth(cdef: ClassDef): DefDef = factoryMeth(caseMods, nme.apply, cdef)

  /** The unapply method corresponding to a case class
   */
  def caseModuleUnapplyMeth(cdef: ClassDef): DefDef = {
    val tparams    = constrTparamsInvariant(cdef)
    val method     = constrParamss(cdef) match {
      case xs :: _ if xs.nonEmpty && isRepeatedParamType(xs.last.tpt) => nme.unapplySeq
      case _                                                          => nme.unapply
    }
    val cparams    = List(ValDef(Modifiers(PARAM | SYNTHETIC), unapplyParamName, classType(cdef, tparams), EmptyTree))
    val resultType = if (!settings.isScala212) TypeTree() else { // fix for SI-6541 under -Xsource:2.12
    def repeatedToSeq(tp: Tree) = tp match {
        case AppliedTypeTree(Select(_, tpnme.REPEATED_PARAM_CLASS_NAME), tps) => AppliedTypeTree(gen.rootScalaDot(tpnme.Seq), tps)
        case _                                                                => tp
      }
      constrParamss(cdef) match {
        case Nil | Nil :: _ =>
          gen.rootScalaDot(tpnme.Boolean)
        case params :: _ =>
          val constrParamTypes = params.map(param => repeatedToSeq(param.tpt))
          AppliedTypeTree(gen.rootScalaDot(tpnme.Option), List(treeBuilder.makeTupleType(constrParamTypes)))
      }
    }
    val ifNull     = if (constrParamss(cdef).head.isEmpty) FALSE else REF(NoneModule)
    val body       = nullSafe({ case Ident(x) => caseClassUnapplyReturnValue(x, cdef) }, ifNull)(Ident(unapplyParamName))

    atPos(cdef.pos.focus)(
      DefDef(caseMods, method, tparams, List(cparams), resultType, body)
    )
  }

  /**
   * Generates copy methods for case classes. Copy only has defaults on the first
   * parameter list, as of SI-5009.
   *
   * The parameter types of the copy method need to be exactly the same as the parameter
   * types of the primary constructor. Just copying the TypeTree is not enough: a type `C`
   * might refer to something else *inside* the class (i.e. as parameter type of `copy`)
   * than *outside* the class (i.e. in the class parameter list).
   *
   * One such example is t0054.scala:
   *   class A {
   *     case class B(x: C) extends A { def copy(x: C = x) = ... }
   *     class C {}      ^                          ^
   *   }                (1)                        (2)
   *
   * The reference (1) to C is `A.this.C`. The reference (2) is `B.this.C` - not the same.
   *
   * This is fixed with a hack currently. `Unapplies.caseClassCopyMeth`, which creates the
   * copy method, uses empty `TypeTree()` nodes for parameter types.
   *
   * In `Namers.enterDefDef`, the copy method gets a special type completer (`enterCopyMethod`).
   * Before computing the body type of `copy`, the class parameter types are assigned the copy
   * method parameters.
   *
   * This attachment class stores the copy method parameter ValDefs as an attachment in the
   * ClassDef of the case class.
   */
  def caseClassCopyMeth(cdef: ClassDef): Option[DefDef] = {
    def isDisallowed(vd: ValDef) = isRepeatedParamType(vd.tpt) || isByNameParamType(vd.tpt)
    val classParamss  = constrParamss(cdef)

    if (cdef.symbol.hasAbstractFlag || mexists(classParamss)(isDisallowed)) None
    else {
      def makeCopyParam(vd: ValDef, putDefault: Boolean) = {
        val rhs = if (putDefault) toIdent(vd) else EmptyTree
        val flags = PARAM | (vd.mods.flags & IMPLICIT) | (if (putDefault) DEFAULTPARAM else 0)
        // empty tpt: see comment above
        val tpt = atPos(vd.pos.focus)(TypeTree() setOriginal vd.tpt)
        treeCopy.ValDef(vd, Modifiers(flags), vd.name, tpt, rhs)
      }

      val tparams = constrTparamsInvariant(cdef)
      val paramss = classParamss match {
        case Nil => Nil
        case ps :: pss =>
          ps.map(makeCopyParam(_, putDefault = true)) :: mmap(pss)(makeCopyParam(_, putDefault = false))
      }

      val classTpe = classType(cdef, tparams)
      val argss = mmap(paramss)(toIdent)
      val body: Tree = New(classTpe, argss)
      val copyDefDef = atPos(cdef.pos.focus)(
        DefDef(Modifiers(SYNTHETIC), nme.copy, tparams, paramss, TypeTree(), body)
      )
      Some(copyDefDef)
    }
  }
}
