package local

import java.lang.reflect.InvocationTargetException

import sbt._
import sbt.internal.inc.ScalaInstance
import sbt.internal.inc.classpath.{ ClasspathUtilities, FilteredLoader }

object LocalScriptedPlugin extends AutoPlugin {
  override def requires = plugins.JvmPlugin

  object autoImport extends ScriptedKeys
}

trait ScriptedKeys {
  val publishLocalBinAll = taskKey[Unit]("")
  val scriptedUnpublished = inputKey[Unit](
    "Execute scripted without publishing sbt first. " +
      "Saves you some time when only your test has changed"
  )
  val scriptedSource = settingKey[File]("")
  val scriptedPrescripted = taskKey[File => Unit]("")
}

object Scripted {
  // This is to workaround https://github.com/sbt/io/issues/110
  sys.props.put("jna.nosys", "true")

  val RepoOverrideTest = config("repoOverrideTest") extend Compile

  import sbt.complete._

  // Paging, 1-index based.
  final case class ScriptedTestPage(page: Int, total: Int)

  // FIXME: Duplicated with ScriptedPlugin.scriptedParser, this can be
  // avoided once we upgrade build.properties to 0.13.14
  def scriptedParser(scriptedBase: File): Parser[Seq[String]] = {
    import DefaultParsers._

    val scriptedFiles: NameFilter = ("test": NameFilter) | "pending"
    val pairs = (scriptedBase * AllPassFilter * AllPassFilter * scriptedFiles).get map {
      (f: File) =>
        val p = f.getParentFile
        (p.getParentFile.getName, p.getName)
    }
    val pairMap = pairs.groupBy(_._1).mapValues(_.map(_._2).toSet)

    val id = charClass(c => !c.isWhitespace && c != '/').+.string
    val groupP = token(id.examples(pairMap.keySet)) <~ token('/')

    // A parser for page definitions
    val pageNumber = (NatBasic & not('0', "zero page number")).flatMap { i =>
      if (i <= pairs.size) Parser.success(i)
      else Parser.failure(s"$i exceeds the number of tests (${pairs.size})")
    }
    val pageP: Parser[ScriptedTestPage] = ("*" ~> pageNumber ~ ("of" ~> pageNumber)) flatMap {
      case (page, total) if page <= total => success(ScriptedTestPage(page, total))
      case (page, total)                  => failure(s"Page $page was greater than $total")
    }

    // Grabs the filenames from a given test group in the current page definition.
    def pagedFilenames(group: String, page: ScriptedTestPage): Seq[String] = {
      val files = pairMap.get(group).toSeq.flatten.sortBy(_.toLowerCase)
      val pageSize = if (page.total == 0) files.size else files.size / page.total
      // The last page may loose some values, so we explicitly keep them
      val dropped = files.drop(pageSize * (page.page - 1))
      if (page.page == page.total) dropped
      else dropped.take(pageSize)
    }

    def nameP(group: String) = {
      token("*".id | id.examples(pairMap.getOrElse(group, Set.empty[String])))
    }

    val PagedIds: Parser[Seq[String]] =
      for {
        group <- groupP
        page <- pageP
        files = pagedFilenames(group, page)
        // TODO -  Fail the parser if we don't have enough files for the given page size
        //if !files.isEmpty
      } yield files map (f => s"$group/$f")

    val testID = (for (group <- groupP; name <- nameP(group)) yield (group, name))
    val testIdAsGroup = matched(testID) map (test => Seq(test))

    //(token(Space) ~> matched(testID)).*
    (token(Space) ~> (PagedIds | testIdAsGroup)).* map (_.flatten)
  }

  def doScripted(
      scriptedSbtInstance: ScalaInstance,
      sourcePath: File,
      bufferLog: Boolean,
      args: Seq[String],
      prescripted: File => Unit,
      launchOpts: Seq[String],
      scalaVersion: String,
      sbtVersion: String,
      classpath: Seq[File],
      logger: Logger
  ): Unit = {
    logger.info(s"About to run tests: ${args.mkString("\n * ", "\n * ", "\n")}")
    logger.info("")

    // Force Log4J to not use a thread context classloader otherwise it throws a CCE
    sys.props(org.apache.logging.log4j.util.LoaderUtil.IGNORE_TCCL_PROPERTY) = "true"

    val noJLine = new FilteredLoader(scriptedSbtInstance.loader, "jline." :: Nil)
    val loader = ClasspathUtilities.toLoader(classpath, noJLine)
    val bridgeClass = Class.forName("sbt.scriptedtest.ScriptedRunner", true, loader)

    // Interface to cross class loader
    type SbtScriptedRunner = {
      def runInParallel(
          resourceBaseDirectory: File,
          bufferLog: Boolean,
          tests: Array[String],
          launchOpts: Array[String],
          prescripted: java.util.List[File],
          scalaVersion: String,
          sbtVersion: String,
          classpath: Array[File],
          instances: Int
      ): Unit
    }

    val initLoader = Thread.currentThread.getContextClassLoader
    try {
      Thread.currentThread.setContextClassLoader(loader)
      val bridge =
        bridgeClass.getDeclaredConstructor().newInstance().asInstanceOf[SbtScriptedRunner]
      try {
        // Using java.util.List to encode File => Unit.
        val callback = new java.util.AbstractList[File] {
          override def add(x: File): Boolean = { prescripted(x); false }
          def get(x: Int): sbt.File = ???
          def size(): Int = 0
        }
        val instances: Int = (System.getProperty("sbt.scripted.parallel.instances") match {
          case null => 1
          case i    => scala.util.Try(i.toInt).getOrElse(1)
        }) match {
          case i if i > 0 => i
          case _          => 1
        }
        import scala.language.reflectiveCalls
        bridge.runInParallel(
          sourcePath,
          bufferLog,
          args.toArray,
          launchOpts.toArray,
          callback,
          scalaVersion,
          sbtVersion,
          classpath.toArray,
          instances
        )
      } catch { case ite: InvocationTargetException => throw ite.getCause }
    } finally {
      Thread.currentThread.setContextClassLoader(initLoader)
    }
  }
}
