/*
   Copyright 2010 Aaron J. Radke

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

//TODO add mechanism to add java system properties so apps that need them set can find them
//TODO include the systemclass path of the luancher jar, that was not included (either in the classpath or the ng project)
//TODO make a fork that uses javaw
//TODO bt.kson option to launch a forked or bg process
//TODO run bt from a luanched ng cc.drx.Boot
//TODO make a bt.kson option lookup for a fast registry like lookup mechanism
//TODO make a custom ng boot that launches a cc.drx.ng.Server and friends server if not already launched
//TODO maybe revisit the trouble using xsbti.ServerMain (in the meantime decided to use ng server instead)
package cc.drx

import Repo.{Release,Org}

object Jansi{
  def apply[A](body: => A):A = {
    if(OS.kind == OS.Windows) org.fusesource.jansi.AnsiConsole.systemInstall()
    body
  }
}

object BootSbt{
  def main(args:Array[String]):Unit = xsbt.boot.Boot.main(args)  //TODO if this test ever works that means boots can be forwarded so make it work nice, if it doesn't try the xsbt.boot.Launch methods
}
object Boot{
  def pp(width:Int, key:String, value:String):Unit = println(
    s"${Color.Blue ansi key}${Color.Yellow ansi ":"}$value"
  )


  class Exit(val code: Int) extends xsbti.Exit

  def classpathFrom(conf:xsbti.AppConfiguration):Classpath = classpathFrom(conf.provider)
  def classpathFrom(provider:xsbti.AppProvider):Classpath = {
    val cpScala = Classpath(provider.scalaProvider.jars map File.apply)
    val cpBoot = Classpath(provider.mainClasspath map File.apply)
    val cpLauncher = Classpath.system
    cpScala ++ cpBoot ++ cpLauncher
  }

  case class AppId(org:String, prj:String, ver:String, main:String,
    cross:String="Disabled",
    extra:List[String]=Nil,
    cp:Classpath=Classpath(),
    scala:String="auto",
    ctx:StringMap
  ) extends xsbti.ApplicationID {
    def classpathExtra:Array[java.io.File] = cp.cp.map{_.file}.toArray
    def mainComponents = extra.toArray
    def groupID = org
    def name = prj
    def version = ver
    def mainClass = main
    def crossVersioned:Boolean = crossVersionedValue != xsbti.CrossValue.Disabled

    private def isCross:Boolean = crossVersionedValue != xsbti.CrossValue.Disabled

    def crossVersionedValue:xsbti.CrossValue = xsbti.CrossValue.valueOf(cross.trim.toLowerCase.capitalize)

    override def toString = {
      s"$prj ($ver) from $org using main:$main"+
        List(
          if(isCross) Some(s"cross: $crossVersioned") else None,
          if(cp.cp.nonEmpty) Some(s"cp: ${cp.toString}") else None,
          if(extra.nonEmpty) Some(s"extra: ${extra mkString ";"}") else None,
          if(scala!="auto") Some(s"scala: $scala") else None
        ).flatten.map{"\n   " + _}.mkString("")
    }

  }

  case class AppConf(appId:AppId, args:Array[String], base:File){
    override def toString = s"AppConf(appId:$appId args:${args mkString " "} base:$base)"

    def run:xsbti.MainResult = appId.ctx.getString("lang") match {
      case Some("shell") => {
        import Implicit.ec
        val shell = Shell(appId.main +: args, base)
        println(shell)
        val res = shell.run.blockWithoutWarning(1.yr)
        new Boot.Exit(0)
      }
      case _ => {
        new xsbti.Reboot{
            def arguments = args//conf.arguments.drop(1)
            def baseDirectory = base.file //conf.baseDirectory
            def scalaVersion = appId.scala
            def app = appId
        }
      }
    }

    /*
    def start:xsbti.Server = {
      new xsbti.Server{
        def uri  = new java.net.URI("sbt://localhost:9999") // FIXME setup an actual ip and port for the test
        def awaitTermination = {
          // val serverMain = provider.entryPoint.asSubclass(ServerMainClass).newInstance
          run //TODO launch the main blocking class here
          while(true){Thread.sleep(100)}
          new Boot.Exit(0)
        }
      }
    }
    */
  }

  private val javaVer = "java" + OS.java8.getOrElse("8","7")  //auto detect java 
  private val scopes = List("scala","java","sbt")

  /**find a boot conf from a key and launch it with a specified special args*/
  def find(conf: xsbti.AppConfiguration, key:String, args:Array[String]):Option[AppConf] = Timer("find boot config"){
    val base = File(conf.baseDirectory)
    val launchedScala = conf.provider.scalaProvider.version
    val cpBoot = classpathFrom(conf)

    // Print a message and the arguments to the application
    println(
      "Welcome to drx-boot. " +
         Color.Grey.ansi("A context dependent launcher/build-tool/boot-loader.")
    )
    print(s"Looking for ${Color.Green ansi key wrap "*"} params (using scala:$launchedScala)")

    val bootKeys = Timer("load kson config"){ BootKeys.load }
    val ctxOption = bootKeys.getContext(key)
    //--setup vars for launch mode
    lazy val boot:Option[AppConf] = for(
        ctx <- ctxOption;
        org <- ctx.get[String]("org");
        prj <- ctx.get[String]("prj");
        ver <- ctx.get[String]("ver") orElse ctx.get[String]("ver."+javaVer);
        main <- ctx.get[String]("main") //it must define a main method to be launched
      ) yield {
        val cross = ctx.get[String]("cross") | "Disabled"
        val scala:String = ctx.get[String]("scala") getOrElse "2.12.4" //orElse kson.get[String]("scala.scala.ver."+javaVer) getOrElse "2.12.2" //no auto scala, always enforce a specific version that is specified somewhere
        // val extra:List[String] = ctx.get[String]("extra").map{_.trim.split("[\\s,:]+").toList} getOrElse Nil
        val extra:List[String] = ctx.split[String]("extra","[\\s,:]+").toList
        val cp:Classpath = Classpath(ctx.split[File]("cp").toList)
        val debug:Boolean = ctx.getOrElse[Boolean]("debug", false)
        val lang:Option[String] = ctx getString "lang"
        val preArgs:Array[String] = ctx.split[String]("args","\\s+").toArray
        val fork:Boolean = ctx.getOrElse[Boolean]("fork", false)
        //val debug = false //FIXME make this debug BootInfo launcher set via a command line or property to remove this debug so the launcher will work
        //--add custom jvm Props
        // for((k,v) <- (ctx/'prop).toMap) sys.props(k) = v
        sys.props ++= (ctx/'prop).toMap

        if(fork || debug){
          println(cpBoot.nice)
          // hack the main with "cc.drx.BootInfo", and add the actual mainClass to the conf.args so the BootFork can launch the original mainClass and pull it off the argument stack
          val altMain = if(debug) "cc.drx.BootInfo" else "cc.drx.BootFork"
          val appId = new AppId(org,prj, ver, main=altMain, cross=cross, extra=extra, cp=cpBoot, scala="2.11.11", ctx)
          AppConf(appId, Array(main) ++ preArgs ++ args, base)
         }
         else{
           val appId = new AppId(org,prj, ver, main=main, cross=cross, extra=extra, cp=cp, scala=scala, ctx)
           println(s"proxy to a app: $appId...")
           AppConf(appId, preArgs ++ args, base)
        }
    }

    //--select result to return (None on errors)
    if(ctxOption.isEmpty){
        //TODO furuther filter the bootKeys by luanchable types
        println(s"${Red ansi "Error"}: Could not find launch parameters for key:${Orange ansi key}")
        val alts = bootKeys.keys filter (_ soundsLike key)
        if(alts.nonEmpty)
          println(s"   did you mean: " + alts.map{Green ansi _}.mkString(" | "))
        else{
          println("Available keys are:")
          bootKeys.keys map (_ fit 20) grouped 5 map {_ mkString ""} foreach println
        }
        None
    }
    else if(boot.isEmpty){
      println(s"${Red ansi "Error"}: invalid (missing) launch configuration for key:${Orange ansi key}")
      None
    }
    else boot //return the first match
  }
  //nicer Rich wrappers
  class ForkConf(val conf: xsbti.AppConfiguration){
    val cp = Boot.classpathFrom(conf)
    val thisArgs = conf.arguments
    val thisMain = conf.provider.id.mainClass
    val main = thisArgs.headOption getOrElse thisMain  //the passed in 
    val base = File(conf.baseDirectory)
    val args = thisArgs.drop(1)

    val shell = Shell(List("java","-cp", cp.toString, main) ++ args) //TODO make a BootFork that does the forked app work

    override def toString =
      List(
        Boot.pp(20, "BaseDirectory:",base.path),
        Boot.pp(20, "Args:", args.mkString(" ")),
        Boot.pp(20, "Main:",main),
        Boot.pp(20, "Classpath:","...\n"+cp.nice.indent)
      ).mkString("\n")
  }
}

class BootFork extends xsbti.AppMain{
  def run(appConf: xsbti.AppConfiguration):xsbti.MainResult = Jansi{
    println("#BootFork hijacked the boot to collect a claspath that can be forked.") //just print out the information
    val conf = new Boot.ForkConf(appConf)
    println(conf)
    val res = conf.shell.fork //do the fork work
    println(res)
    new Boot.Exit(0)
  }
}

class BootInfo extends xsbti.AppMain{
  def run(appConf: xsbti.AppConfiguration):xsbti.MainResult = Jansi{
    Timer("BootInfo"){
      println("#BootInfo hijacked the boot for debug purposes.") //just print out the information

      def pp(title:String, kvs:Map[String,String]):Unit = {
        println("# " + Color.Green.ansi(title))
        val maxWidth = kvs.keys.map{_.size}.max
        for((k,v) <- kvs.toList.sortBy{_._1.toLowerCase}) Boot.pp(maxWidth, k, v)
      }

      BootKeys.main(Array.empty[String]) //list the available bootkeys

      pp("Environment variables", sys.env )
      pp("JVM properties", sys.props.toMap ) //to an immutable map

      val conf = new Boot.ForkConf(appConf)
      println(conf) //just print out the information
      println(conf.shell) //just print out the information
      new Boot.Exit(0)
    }
  }
}

class BootKeys(kson:Kson){
  //--private
  private lazy val rootPaths = for(line <- kson.lines if line.roots.nonEmpty) yield line.pathList //just the lines with a root key def
  private lazy val scopeMap = rootPaths.map{p => p.head -> p.tail.reverse.mkString("/")}.toMap
  def getContext(key:String):Option[StringMap.Scoped] = scopeMap.get(key).map(kson/_/key)

  //--public
  lazy val keys = rootPaths.map{_.head}.toSet //keys sorted in the order of the kson config file

  def print(requestKeys:Seq[String]):Unit = {

    val missingKeys = requestKeys.filterNot(scopeMap contains _)
    //--print missing key warning
    if(missingKeys.nonEmpty) {
      for(miss <- missingKeys){
        val alts = for(key <- keys if miss soundsLike key) yield key
        println(s"no key found for `$miss`" )
        if(alts.nonEmpty) println("   maybe try: " + alts.mkString(", ") )
      }
    }
    //-- no missing keys
    else{
      for(key   <- if(requestKeys.isEmpty) keys else requestKeys;
          scope <- scopeMap get key;  //lookup the scope just for the nice paren printing
          ctx   <- getContext(key);
          (org,prj,ver) <- ctx.get("org","prj","ver")
      ) {
        val scopeParen = "("+scope+")"
        println(f"$key%-20s $scopeParen%-25s $prj%-25s $ver%-10s  $org%-20s")
      }
    }
  }

  def dep(key:String):Option[(String,String,String)] = {
    for(
      scope <- scopeMap get key;  //lookup the scope just for the nice paren printing
      ctx   <- getContext(key);
      orgPrjVer <- ctx.get("org","prj","ver")
    ) yield orgPrjVer
  }
}
//--object BootKeys
object BootKeys{

  def findBootFile:Option[File] = {
      val defaultFile = File.cwd/".bt" orElse
        File.cwd/".bt.kson" orElse
        File.cwd/"bt.kson" orElse
        File.cwd/".bt.kson" orElse
        File.home/".bt" orElse
        File.home/".bt.kson" orElse
        File.home/"bt.kson"
    // println(s"in ${if(defaultFile.isFile) defaultFile else "default"} ")
      if(defaultFile.isFile) Some(defaultFile) else None
  }
  def loadConfig:Kson = {
    val ksonDefault = Input.resource(file"bt.kson").map{Kson.apply} getOrElse Kson.empty //should always be there
    val ksonBootFile = findBootFile.map{Kson(_)} getOrElse Kson.empty
    ksonDefault ++ ksonBootFile
  }

  def load:BootKeys = new BootKeys(loadConfig)
  def apply(kson:Kson):BootKeys = new BootKeys(kson)

  def main(args:Array[String]):Unit = Jansi{
    println("Boot config bt.kson keys")

    val bootKeys = Timer("load boot keys"){BootKeys.load}

    Timer("find/list keys"){ bootKeys print args } //TODO use fuzzy finding match for missing keys

    Console.flush
    1.s.sleep//sleep to prevent early stream is cut before finsihing
  }
}

class Boot extends xsbti.AppMain{
  def run(conf: xsbti.AppConfiguration):xsbti.MainResult = Jansi{
    val key = conf.arguments.headOption getOrElse "sbt" //TODO add an auto boot detector app that checks if it is an sbt or gradle or mvn project or undefined project
    val args = conf.arguments.drop(1)
    Boot.find(conf,key,args).map{_.run}.getOrElse{new Boot.Exit(0)}
    //TODO add console/shell runner if empty args???
  }
}

/**Boot console to loop and load the boot keys onces, and TODO provide key completion and path executible complesion and lookup (system independent)*/
class BootConsole extends xsbti.AppMain{
  private val exitKeys = Set("exit","quit",":q", ":exit", ":quite", "exit()")
  def run(conf: xsbti.AppConfiguration):xsbti.MainResult = Jansi{
    // import Implicit.ec
    // Console.flush
    def exitBoot():Unit = println(Red ansi "exiting Boot console")
    //--define the prompt
    @tailrec def prompt():Unit = {
      print(s"\n${Green ansi "bt"}${Grey ansi "> "}")
      // val line:String = ??? //scala.io.StdIn.readLine().trim  //TODO use a char buffer to support tab complete and offer a generic 2.10 solution
      val line:String = Input.std.promptLine.trim
      if(exitKeys contains line) exitBoot() else {
        val arguments = line.split("""\s+""")
        val key = arguments.headOption getOrElse "sbt" //TODO add an auto boot detector app that checks if it is an sbt or gradle or mvn project or undefined project
        val args = arguments.drop(1)
        val boot = Boot.find(conf, key, args)
        // println(s"console boot: $boot")
        // boot.map{_.run}.getOrElse{new Boot.Exit(0)}
        boot.foreach{bt =>
          // println(conf.provider.newMain())
          // sbt.boot.Launch
          bt.run //FIXME how to really run the boot? from the sbtLauncher??? need to makea new provider??
        }
        // conf.provider.loader
        prompt() //recursivly show the prompt
      }
    }
    //--recursively read/eval/execute the prompt
    prompt()
    new Boot.Exit(0)
  }
}

/*
//run a shell command (passing through system independent; i.e. fixes for windows)
object BootShell{
  def main(args:Array[String]):Unit = Jansi{
    import Implicit.ec
    val shell = Shell(args)
    val res = shell.run.blockWithoutWarning(1.yr)
    Console.flush
    // res getOrElse 1 //1 is an error
  }
}
*/