/*
   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.
*/
// vim: set ts=3 sw=3 et:

package cc.drx

import scala.sys.process.Process
import java.io.OutputStream

object OS{

   //helper function for OS kinss for version maps (keeps the compile tree small)
   private def versionMap(str:String):Map[Version,String] = str.split(',').map{d =>
    val List(k,v) = d.split('=').toList
    Version(k) -> v
   }.toMap

   //--os types (Kind)
   sealed trait Kind {
     //--required
     def name:String

     //--overidable
     def codenames:Map[Version,String] = Map.empty[Version,String]

     //--overidable
     def codename:String = codenames.get(OS.version take 2) getOrElse ""
     final override def toString:String = if(codename == "") name else name + codename
   }

   //--Windows (Kind)
   case object Windows extends Kind {
     val name = "Windows"
     //https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions
     override lazy val codenames = versionMap("10.0=10,6.3=8.1,6.2=8,6.1=7,6.0=Vista,5.2=XP Pro,5.1=XP,4.9=ME,5.0=2000,4.10=98")
   }

   //--Unix (Kind)
   sealed trait Unix extends Kind
   case object MacOS extends Unix{
     val name = "MacOS"
     // https://en.wikipedia.org/wiki/MacOS
     override lazy val codenames = versionMap("10.7=Lion,10.8=Mountain Lion,10.9=Mavericks,10.10=Yosemite,10.11=El Capitan,10.12=Sierra,10.13=High Sierra,10.14=Mojave,10.15=Catalina,11.0=Big Sur")
   }
   case object Linux extends Unix{
     val name = "MacOS"
   }

   //--Unknown (Kind)
   case object Unknown extends Kind{
     val name = "Unknown"
   }

   //--property getters
   def prop(key:String):String = System.getProperty(key)  //FIXME thee are dangerous null producers
   def env(key:String):String = System.getenv(key)

   lazy val Env  = StringMap(scala.sys.env)
   lazy val Prop = StringMap(scala.sys.props.toMap) //make the bidirectional mutable map immutable at the point of eval
   lazy val Conf = StringMap(scala.sys.env ++ scala.sys.props.toMap)

   //--runtime java and scala versions TODO shouldn't this be under the version constructor?
   lazy val javaVersion:Version = Version(prop("java.version"))
   lazy val java7:Boolean = javaVersion.similar(Version("1.7.x"),2)
   lazy val java8:Boolean = javaVersion.similar(Version("1.8.x"),2)
   lazy val java9:Boolean = javaVersion.similar(Version("1.9.x"),2)
   lazy val java10:Boolean = javaVersion.similar(Version("10.x"),1)
   lazy val java11:Boolean = javaVersion.similar(Version("11.x"),1)
   lazy val java12:Boolean = javaVersion.similar(Version("12.x"),1)
   lazy val java13:Boolean = javaVersion.similar(Version("13.x"),1)
   lazy val java14:Boolean = javaVersion.similar(Version("14.x"),1)
   lazy val java15:Boolean = javaVersion.similar(Version("15.x"),1)
   lazy val java16:Boolean = javaVersion.similar(Version("16.x"),1)
   lazy val java17:Boolean = javaVersion.similar(Version("17.x"),1)

   lazy val scalaVersion:Version = Version(scala.util.Properties.versionString.dropWhile(!_.isDigit))
   lazy val scala213:Boolean = scalaVersion.similar(Version("2.13.x"), 2)
   lazy val scala212:Boolean = scalaVersion.similar(Version("2.12.x"), 2)
   lazy val scala211:Boolean = scalaVersion.similar(Version("2.11.x"), 2)
   lazy val scala210:Boolean = scalaVersion.similar(Version("2.10.x"), 2)
   lazy val scala3:Boolean = scalaVersion.similar(Version("3.x.x"), 1)
   //TODO
   lazy val graalNative:Boolean = ???//vm == "Graal??" //java.vm.name
   lazy val graalJVM:Boolean    = vm startsWith "GraalVM" //java.vm.name

   lazy val scalaJVM:Boolean    = vm startsWith "Java HotSpot" //java.vm.name
   lazy val scalaJS:Boolean     = vm startsWith "Scala.js"  //java.vm.name.
   lazy val scalaNative:Boolean = vm startsWith "Scala Native" //java.vm.name
   // lazy val drx:Version =  ???

   //--special properties
   lazy val name = prop("os.name")
   lazy val vm   = prop("java.vm.name")
   lazy val arch = prop("os.arch")
   // lazy val bit:Int  = prop/"os.arch.data.model" | 64 //default 64 FIXME lookup arch name to get actual bits
   lazy val version = Version(prop("os.version"))
   lazy val user = prop("user.name")
   lazy val pwd:File  = Prop / "user.dir" | File(".").canon
   lazy val home = File(prop("user.home"))
   lazy val kind:Kind = {
      val n = name.toLowerCase
      if(n contains "mac") MacOS
      else if(n contains "win") Windows
      else if(n contains "linux") Linux
      else Unknown
   }
   lazy val isMac   = kind == MacOS
   lazy val isWin   = kind == Windows
   lazy val isLinux = kind == Linux

   //--fantastic robust detection trick from https://stackoverflow.com/a/22039095/622016
   private lazy val traceClasses = new Exception().getStackTrace.map{_.getClassName}
   lazy val isSbt = traceClasses contains "sbt.Run"
   lazy val isBt = traceClasses contains "xsbt.boot.Boot"
   lazy val isLauncher= isSbt || isBt

   private def pathList(path:String):List[File] = (
     path.trim split prop("path.separator") map {_.trim} filter (_ != "") map File.apply map {_.canon}
   ).toList
   lazy val path:List[File] = pathList( prop("java.library.path") )
   lazy val classpath:List[File] = pathList( prop("java.class.path") )
   lazy val bootpath:List[File] = pathList( prop("sun.boot.library.path") )
   lazy val bootClasspath:List[File] = pathList( prop("sun.boot.class.path") )
   def where(basename:String) = for(p <- path; f <- p.list; if f.isFile && f.basename == basename) yield f

   private lazy val runtime = java.lang.Runtime.getRuntime

   lazy val cpus = runtime.availableProcessors
   lazy val bit  = System.getProperty("sun.arch.data.model").toInt
   case class Memory(free:Bytes,total:Bytes,max:Bytes){
     lazy val used:Bytes = total - free
   }
   def heap = Memory(Bytes(runtime.freeMemory), Bytes(runtime.totalMemory), Bytes(runtime.maxMemory))

   def envOption(k:String):Option[String] = Option(env(k))
   def computer:String = envOption("COMPUTERNAME") orElse envOption("HOSTNAME") getOrElse "localhost"

   private lazy val localHost = java.net.InetAddress.getLocalHost
   lazy val hostname:String = localHost.getHostName
   lazy val ip:String       = localHost.getHostAddress

   // lazy val info:String = Macro.ksonColor(kind.name,arch,version,javaVersion,scalaVersion,heap.max,cpus,bit,user)
   lazy val info:String =
     List("name"->kind.name, "arch"->arch, "version"->version, "java"->javaVersion, "scala"->scalaVersion, "heap"->heap.max, "cpus"->cpus, "bit"->bit, "user"->user, "codename" -> kind.codename, "computer" -> computer)
      .map{case (k,v) => k.toString -> v.toString}.map(Format.kvColor).mkString(" ")
   def drive = File.roots mapWith {_.driveSize}

   private def wmic(domain:String, keys:Seq[String]):Map[String,String] = {
     import Implicit.ec
     lazy val KeyValue = """(\w+)=(.+)""".r
     Shell("wmic",domain,"get",keys mkString ",", "/format:list").lines.blockWithoutWarning(1.s).getOrElse(Nil).collect{
       case KeyValue(k,v) => k -> v
     }.toMap
   }
   def ram:Option[Memory] = kind match {
     case Windows =>
       val keys = Vector("FreePhysicalMemory","TotalVisibleMemorySize","TotalVirtualMemorySize")
       val kvs = wmic("os", keys).map{case (k,v) => k -> Bytes(v.toInt)}.toMap
       for(free <- kvs get keys(0); total <- kvs get keys(1); max <- kvs get keys(2)) yield Memory(free, total, max)
     case _:Unix => None //TODO add an implementation
     case _ => None
   }

   //--launchers
   private lazy val desktop = java.awt.Desktop.getDesktop
   def open(f:File) = desktop.open(f.canon.file)
   def edit(f:File) = desktop.edit(f.canon.file)
   def browse(f:File) = desktop.browse(f.canon.file.toURI)
   def browse(uri:String) = desktop.browse(new java.net.URI(uri))
   def open(uri:String) = browse(uri)

   //--process kill
   //this Pid class acts as the common interface between linux and windows for 'ps' & 'kill'
   case class Pid(pid:Int, cmd:String){
      def kill = OS.kill(pid)
   }

   //TODO replace the process calls with the Shell object type at all??
   /**this (blocking) run is a simple way to use the cc.drx.Shell utility but but make several simple choices
    * 1.) blocks for 1.min ??? this is bad
    * 2.) unwraps the future and the try (returns an empty list in failure cases
    * 3.) automatically chooses an global executionContext
    * 4.) easy api for adding working directory locatiou
    */
   // def run(cmd:String, cwd:File):List[String] = Shell(cmd,Some(cwd),List.empty).run(Implicit.ec).block(1.minute).getOrElse(Nil)
   // def run(cmd:String):List[String] = Shell(cmd).run(Implicit.ec).block(1.minute).getOrElse(Nil)
   // def run(cmd:String):Future[List[String]] = Shell(cmd).lines

   private lazy val WMICPid = """[^,]*,([^,]*),(\d+)""".r
   private lazy val PSPid = """\s*\d+\s+(\d+)\s+(.*)""".r
   def ps:Iterator[Pid] = kind match {
      case Windows =>
         for(WMICPid(cmd,pid) <- Process("""wmic process get commandline,processid /format:csv""").lineIterator) yield Pid(pid.toInt,cmd)
      case _:Unix =>
         for(PSPid(pid,cmd) <- Process(s"ps -u $user").lineIterator) yield Pid(pid.toInt,cmd)
      case _ => ???
   }
   //convenience functions to filter processes searching for a key within a process
   def ps(key:String):Vector[Pid] = (for(p <- ps if p.cmd contains key ) yield p).toVector
   //convinence functions to provide a jps like solution for systems without jps installed
   def jps:Vector[Pid] = (for(p <- ps if p.cmd.toLowerCase contains "java") yield p).toVector
   def jps(key:String):Vector[Pid] = (for(p <- jps if p.cmd contains key) yield p).toVector

   //Note: this requires the jps command from the jdk on the PATH
   def kill(pid:Int):Unit = kind match {
      case Windows => Process(s"taskkill /F /PID $pid").!!; {}
      case _:Unix => Process(s"kill -9 $pid").!!; {}
      case _ => println("Warning: The os must be known")
   }
   def kill(pid:Pid):Unit = pid.kill
   def kill(key:String):Unit = ps(key) foreach kill
   def jkill(key:String):Unit = jps(key) foreach kill

   //private lazy val toolkit = java.awt.Toolkit.getDefaultToolkit
   //def isCapsLock = toolkit.getLockingKeyState(java.awt.event.KeyEvent.VK_CAPS_LOCK)
   //def isNumLock = toolkit.getLockingKeyState(java.awt.event.KeyEvent.VK_NUM_LOCK)
   //
   import Implicit.ec

   //-- terminal info
   /**use tput to get the current terminal size http://stackoverflow.com/a/263900/622016 ; only works for unix*/
   def terminalSize:Future[Vec] = for(
     cols <- Shell("tput cols").resultString;
     rows <- Shell("tput lines").resultString
   ) yield Vec(cols.toInt,rows.toInt)

}

object Display{

   //def mc = mouseInfo.getDevice.getDefaultConfiguration
   def currentGC = java.awt.MouseInfo.getPointerInfo.getDevice.getDefaultConfiguration
   def insets(gc:java.awt.GraphicsConfiguration=currentGC):Display = {
      val insets = java.awt.Toolkit.getDefaultToolkit.getScreenInsets(gc)
      val bounds = gc.getBounds
      val loc = Vec(bounds.x+insets.left, bounds.y+insets.top)
      val size = Vec(
         bounds.width  - insets.right - insets.left,
         bounds.height - insets.top   - insets.bottom
      )
      Display(loc, size)
   }
   def fullScreen(gc:java.awt.GraphicsConfiguration=currentGC):Display = {
      val bounds = gc.getBounds
      val loc = Vec(bounds.x, bounds.y)
      val size = Vec(bounds.width, bounds.height)
      Display(loc, size)
   }
   def allScreens:Display = {
      val ge = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment
      val bounds = ge.getScreenDevices.map{_.getDefaultConfiguration.getBounds}
      val minX = bounds.map{b => b.x}.min
      val minY = bounds.map{b => b.y}.min
      val maxX = bounds.map{b => b.x + b.width}.max
      val maxY = bounds.map{b => b.y + b.height}.max

      Display(Vec(minX,minY), Vec(maxX-minX, maxY-minY))
   }

   //TODO move this screen shot to the display class so sub rect can be selected
   import java.awt.image.BufferedImage
   import java.awt.{Toolkit, Rectangle}
   private lazy val robot = new java.awt.Robot()
   def screenshotImage:BufferedImage = robot.createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()));
   def screenshot(file:File):Try[File] = file.mkParents.flatMap{file =>
     Try{ javax.imageio.ImageIO.write(screenshotImage, file.ext, file); file }
   }
   def screenshot:Try[File] = screenshot(file"export/screen-${Date.now.krypton.ms}.png")
}
case class Display(loc:Vec, size:Vec)