package typeclassic

import scala.annotation.{ compileTimeOnly, StaticAnnotation }
import scala.language.experimental.macros
import scala.reflect.macros.whitebox.Context

import macrocompat._

@compileTimeOnly("typeclass annotation should have been automatically removed but was not")
class typeclass(excludeParents: List[String] = Nil, generateAllOps: Boolean = true) extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro TypeClassMacros.generateTypeClass
}


@bundle
class TypeClassMacros(val c: Context) {
  import c.universe._

  def generateTypeClass(annottees: c.Expr[Any]*): c.Expr[Any] = {
    annottees.map(_.tree) match {
      case (typeClass: ClassDef) :: Nil => modify(typeClass, None)
      case (typeClass: ClassDef) :: (companion: ModuleDef) :: Nil => modify(typeClass, Some(companion))
      case other :: Nil =>
        c.abort(c.enclosingPosition, "@typeclass can only be applied to traits or abstract classes that take 1 type parameter which is either a proper type or a type constructor")
    }
  }

  private def modify(typeClass: ClassDef, companion: Option[ModuleDef]) = {
    val (tparam, proper) = typeClass.tparams match {
      case hd :: Nil =>
        hd.tparams.size match {
          case 0 => (hd, true)
          case 1 => (hd, false)
          case n => c.abort(c.enclosingPosition, "@typeclass may only be applied to types that take a single proper type or type constructor")
        }
      case other => c.abort(c.enclosingPosition, "@typeclass may only be applied to types that take a single type parameter")
    }

    val modifiedTypeClass = typeClass // TODO

    val modifiedCompanion = generateCompanion(typeClass, tparam, proper, companion match {
      case Some(c) => c
      case None => q"object ${typeClass.name.toTermName} {}"
    })

    val result = c.Expr(q"""
      $modifiedTypeClass
      $modifiedCompanion
    """)

    trace(s"Generated type class ${typeClass.name}:\n" + showCode(result.tree))

    result
  }

  private def generateCompanion(typeClass: ClassDef, tparam0: TypeDef, proper: Boolean, comp: Tree): Tree = {
    val tparam = eliminateVariance(tparam0)

    val q"$mods object $name extends ..$bases { ..$body }" = comp

    q"""
      $mods object $name extends ..$bases {
        import scala.language.experimental.macros
        ..$body
        ${generateInstanceSummoner(typeClass, tparam)}
      }
    """
  }

  private def generateInstanceSummoner(typeClass: ClassDef, tparam: TypeDef): Tree = {
    q"""
      @_root_.typeclassic.op("$$y {$$x}")
      def apply[$tparam](implicit x1: ${typeClass.name}[${tparam.name}]): ${typeClass.name}[${tparam.name}] =
        macro _root_.typeclassic.OpsMacros.op10
    """
  }

  // This method is from simulacrum, contributed by paulp, and is licensed under 3-Clause BSD
  private def eliminateVariance(tparam: TypeDef): TypeDef = {
    // If there's another way to do this I'm afraid I don't know it.
    val u        = c.universe.asInstanceOf[c.universe.type with scala.reflect.internal.SymbolTable]
    val tparam0  = tparam.asInstanceOf[u.TypeDef]
    val badFlags = (Flag.COVARIANT | Flag.CONTRAVARIANT).asInstanceOf[Long]
    val fixedMods = tparam0.mods & ~badFlags
    TypeDef(fixedMods.asInstanceOf[c.universe.Modifiers], tparam.name, tparam.tparams, tparam.rhs)
  }

  private def trace(s: => String) = {
    // Macro paradise seems to always output info statements, even without -verbose
    if (sys.props.get("typeclassic.trace").isDefined) c.info(c.enclosingPosition, s, false)
  }
}