import scala.reflect.runtime.universe._
import scala.reflect.runtime.universe.definitions._
import scala.reflect.runtime.{currentMirror => cm}
import scala.reflect.ClassTag

package scala {
  object ExceptionUtils {
    def unwrapThrowable(ex: Throwable): Throwable = scala.reflect.runtime.ReflectionUtils.unwrapThrowable(ex)
  }
}

object Test extends App {
  def key(sym: Symbol) = {
    sym match {
      // initialize parameter symbols
      case meth: MethodSymbol => meth.paramLists.flatten.map(_.info)
    }
    sym + ": " + sym.info
  }

  def convert(value: Any, tpe: Type) = {
    import scala.runtime.BoxesRunTime._
    if (tpe =:= typeOf[Byte]) toByte(value)
    else if (tpe =:= typeOf[Short]) toShort(value)
    else if (tpe =:= typeOf[Char]) toCharacter(value)
    else if (tpe =:= typeOf[Int]) toInteger(value)
    else if (tpe =:= typeOf[Long]) toLong(value)
    else if (tpe =:= typeOf[Float]) toFloat(value)
    else if (tpe =:= typeOf[Double]) toDouble(value)
    else if (tpe =:= typeOf[String]) value.toString
    else if (tpe =:= typeOf[Boolean]) value.asInstanceOf[Boolean]
    else throw new Exception(s"not supported: value = $value, tpe = $tpe")
  }

  def test[T: ClassTag](tpe: Type, receiver: T, method: String, args: Any*): Unit = {
    def wrap[T](op: => T) =
      try {
        var result = op.asInstanceOf[AnyRef]
        if (scala.runtime.ScalaRunTime.isArray(result))
          result = scala.runtime.ScalaRunTime.toObjectArray(result).toList
        println(s"[${result.getClass}] =======> $result")
      } catch {
        case ex: Throwable =>
          val realex = scala.ExceptionUtils.unwrapThrowable(ex)
          println(realex.getClass + ": " + realex.getMessage)
      }
    val meth = tpe.decl(TermName(method).encodedName.toTermName)
    val testees = if (meth.isMethod) List(meth.asMethod) else meth.asTerm.alternatives.map(_.asMethod)
    testees foreach (testee => {
      val convertedArgs = args.zipWithIndex.map { case (arg, i) => convert(arg, testee.paramLists.flatten.apply(i).info) }
      print(s"testing ${tpe.typeSymbol.name}.$method(${testee.paramLists.flatten.map(_.info).mkString(','.toString)}) with receiver = $receiver and args = ${convertedArgs.map(arg => arg + ' '.toString + arg.getClass).toList}: ")
      wrap(cm.reflect(receiver).reflectMethod(testee)(convertedArgs: _*))
    })
  }
  def header(tpe: Type): Unit = {
    println(s"============\n$tpe")
    println("it's important to print the list of Byte's members")
    println("if some of them change (possibly, adding and/or removing magic symbols), we must update this test")
    tpe.members.toList.sortBy(key).foreach(sym => println(key(sym)))
  }

  def testNumeric[T: ClassTag](tpe: Type, value: T): Unit = {
    header(tpe)
    List("toByte", "toShort", "toChar", "toInt", "toLong", "toFloat", "toDouble") foreach (meth => test(tpe, value, meth))
    test(tpe, value, "==", 2)
    test(tpe, value, "!=", 2)
    test(tpe, value, "<", 2)
    test(tpe, value, "<=", 2)
    test(tpe, value, ">", 2)
    test(tpe, value, ">=", 2)
    test(tpe, value, "+", 2)
    test(tpe, value, "-", 2)
    test(tpe, value, "*", 2)
    test(tpe, value, "/", 2)
    test(tpe, value, "%", 2)
  }

  def testIntegral[T: ClassTag](tpe: Type, value: T): Unit = {
    testNumeric(tpe, value)
    test(tpe, value, "unary_~")
    test(tpe, value, "unary_+")
    test(tpe, value, "unary_-")
    test(tpe, value, "<<", 2)
    test(tpe, value, ">>", 2)
    test(tpe, value, ">>>", 2)
    test(tpe, value, "|", 2)
    test(tpe, value, "&", 2)
    test(tpe, value, "^", 2)
  }

  def testBoolean(): Unit = {
    header(typeOf[Boolean])
    test(typeOf[Boolean], true, "unary_!")
    test(typeOf[Boolean], true, "==", true)
    test(typeOf[Boolean], true, "!=", true)
    test(typeOf[Boolean], true, "||", true)
    test(typeOf[Boolean], true, "&&", true)
    test(typeOf[Boolean], true, "|", true)
    test(typeOf[Boolean], true, "&", true)
    test(typeOf[Boolean], true, "^", true)
  }

  def testUnit(): Unit = {
    header(typeOf[Unit])
  }

  testNumeric(typeOf[Byte], 2.toByte)
  testNumeric(typeOf[Short], 2.toShort)
  testNumeric(typeOf[Char], 2.toChar)
  testNumeric(typeOf[Int], 2.toInt)
  testNumeric(typeOf[Long], 2.toLong)
  testNumeric(typeOf[Float], 2.toFloat)
  testNumeric(typeOf[Double], 2.toDouble)
  testBoolean()
  testUnit()
}