/*
   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.
*/
package cc.drx

//java package names renamed
import java.io.{File=>JFile}
import java.nio.file.{Path => JPath, Paths => JPaths, Files => JFiles}
import java.io.{InputStream => JIS}
import java.io.{OutputStream => JOS}

import java.nio.file.{WatchEvent,WatchService,WatchKey,StandardWatchEventKinds=>WEK}
import scala.collection.JavaConverters._

import scala.concurrent.blocking

//java packages 
import java.nio.file.attribute.PosixFilePermission

//--Object
object File{
   //--alias to common stream generator formats
   val GZ = StreamGenerator.GZ
   //TODO need to make a smart convert that looks at file extensions and does the right thing the typeclass for Covnert is already there as a nice possible solution...

   //--file helper objects
   implicit object ParsableFile extends Parsable[File]{ def apply(v:String):File = File(v) }

   case class Loc(file:File, line:Int)

   final class FileStringContext(val sc:StringContext) extends AnyVal{
     def file(args:Any*):File = File(sc.s(args:_*)).expandTilde
   }

   //--constructors
   def apply(path:JPath):File = new File(path.toFile)
   def apply(file:JFile):File = new File(file)
   def apply(path:String="."):File = new File(new JFile(path))

   //--specific constructions
   def roots = java.io.File.listRoots.toList map apply
   def root = File("/") //multiple meanings for windows but the list function will list the available mounts in windows
   lazy val home = File(System.getProperty("user.home"))  //cross platform method to generate
   def cwd = File(".") //curent working directory, pwd is the absolute version
   def pwd = File(".").canon //current working directory, pwd is the absolute canonical version

   /**Open a dialog box to choose a file **Blocking** */
   def chooseBlocking(dialogTitle:String="Choose a file or directory",
      startingDirectory:File=File(".")
   ):Option[File] = {
      var block = true
      var choice:Option[File] = None
      chooseOption(dialogTitle, startingDirectory){ f => choice = f ; block = false}
      @tailrec def waitForSignal:Option[File] = if(block){blocking{Thread.sleep(100)}; waitForSignal}
                                                else choice
      waitForSignal
   }
   /**Open a dialog box to choose a file **Non-Blocking** */
   def choose(dialogTitle:String="Choose a file or directory",
              startingDirectory:File=File(".")
   )(callback: File => Unit):Unit = chooseOption(dialogTitle, startingDirectory){
      case Some(f) => callback(f)
      case _ =>
   }
   private def chooseOption(dialogTitle:String="Choose a file or directory",
              startingDirectory:File=File(".")
   )(callback: Option[File] => Unit):Unit = {
      //Setup the chooser
      import javax.swing.JFileChooser
      import javax.swing.UIManager

      val previousLF = UIManager.getLookAndFeel
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName)
      val chooser = new JFileChooser
      UIManager.setLookAndFeel(previousLF)
      chooser setCurrentDirectory startingDirectory.file
      chooser setDialogTitle dialogTitle
      chooser setFileSelectionMode JFileChooser.FILES_AND_DIRECTORIES
      chooser setAcceptAllFileFilterUsed false

      //Setup the frame panel with an embedded chooser
      import javax.swing.{JFrame,JPanel}
      val frame = new JFrame
      val panel = new JPanel

      //Setup the chooser action commands
      chooser.addActionListener(
         new java.awt.event.ActionListener{
            override def actionPerformed(e: java.awt.event.ActionEvent) = {
               val cmd = e.getActionCommand match {
                  case JFileChooser.APPROVE_SELECTION =>
                     frame.setVisible(false)
                     frame.dispose
                     callback(Option(chooser.getSelectedFile) map File.apply)
                  case _ =>
                     frame.setVisible(false)
                     frame.dispose
                     callback(None)
               }

            }
         }
      )

      //frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
      frame.getContentPane.add(java.awt.BorderLayout.CENTER, panel)
      panel.add(chooser)
      val loc = {
         val d = Display.insets()
         d.loc + d.size/2 - Vec(200,200)
      }
      frame.setLocation(loc.x.toInt,loc.y.toInt)
      frame.pack
      frame.toFront
      frame.setAlwaysOnTop(true)
      frame.setVisible(true)
   }

   //--directory listings
   private class DirectoryListingIterator(startingDir:File) extends Iterator[File]{
      private val stream = JFiles.newDirectoryStream(startingDir.file.toPath)
      private val it = stream.iterator
      private var opened = true
      private var autoClose = true
      def keepOpen:DirectoryListingIterator = {autoClose = false; this} // turn off autoClose
      def hasNext:Boolean = {
         val res = it.hasNext
         if(opened && autoClose && !it.hasNext){opened=false; stream.close}
         res
      }
      def next():File = File( it.next() )
   }
}
//-- case class
class File(val file:JFile) extends AnyVal{
   def toJava:java.io.File = file
   //--construction
   //-child path
   def /(that:File):File = File(path + "/" + that.path)
   def /(filename:String):File = if(filename == "" || filename == "/") this else File(new JFile(file,filename))
   def /(filename:Symbol):File = this / filename.name
   def /(glob:Glob):Iterator[File] = this.walk(glob, 999)
   def walk(glob:Glob, maxDepth:Int=0):Iterator[File] = walk(maxDepth) filter {f =>
     glob matches f.relativeTo(this).unixPath
   }

   // def *(glob:String):Iterator[File] TODO add glob by list or by walk depth
   def +(postfix:String):File = File(path + postfix)

   //-use current file as relative absolute
   def apply(path:String) = {
      val f = File(path)
      if(f.isAbs) f else this / path
   }

   /**default to string should be a cross platform representation of path separators 
    * Note: the system path form is still returned by _.path */
   override def toString = unixPath// vs file.toString  which is the java default

   //--operations
   def rename(that:File):Option[File] = if(file renameTo that.file) Some(that) else None
   def delete = file.delete

   //--properties
   def size = Bytes(file.length)

   def driveSize = {
     val f:java.io.File = root.file //get the root file object
     OS.Memory(free=Bytes(f.getFreeSpace),total=Bytes(f.getTotalSpace),max=Bytes(f.getUsableSpace))
   }

   def name:String = file.getName
   def path:String = file.getPath
   def unixPath = path.replace("\\","/")

   def basename = {
      val s = name
      val i = s.lastIndexOf(".")
      if(i < 1 || i == s.size-1) s    //-1 => no .;  0 => .hidden file;  N-1 => dangling.
      else s.substring(0,i)
   }
   def ext = {
      val s = name
      val i = s.lastIndexOf(".")
      if(i < 1 || i == s.size-1) ""    //-1 => no .;  0 => .hidden file;  N-1 => dangling.
      else s.substring(i+1)
   }

   def mime = Mime(ext)

   def isDir:Boolean = file.isDirectory
   def isFile:Boolean = file.isFile
   def isAbs:Boolean = file.isAbsolute
   def isHidden:Boolean = file.isHidden
   def canRead:Boolean = file.canRead

   /**provide a monadic expression for the existence of a file*/
   def fileOption:Option[File] = if(isFile) Some(this) else None

   //TODO add | syntax for orElse
   /**switch to a file or directory that exists else keep the last filename*/
   def orElse(that: => File):File = if(isFile || isDir) this else that
   /**if a file exists use it as an input else use the input.  This is nice to roll over to resources: File("lost.txt") or IO.resource("found.txt")*/
   def orElse(that: => Input):Input = if(isFile) this.in else that
   /**if the file doesn't exist download it*/
   def orElse(url:URL)(implicit ec:ExecutionContext):Future[File] = if(isFile) Future.successful(this) else url.async{_ copyTo this}

   def modified:Date = Date(file.lastModified)
   /**a setter way to set the last modified time, i.e a copy should sometimes do this*/
   def modified_=(date:Date) = file.setLastModified(date.ms)

   def url:URL = new URL(canon.toURI.toURL)
   def abs:File = File(file.getAbsoluteFile)
   def expandTilde:File = if(path(0) == '~') File.home/path.drop(1) else this
   def canon:File = File(expandTilde.getCanonicalFile)
   //FIXME test this because the getAbsoluteFile was automatic before, but now f.abs.parent is required and it uses the unix path expasion instead of File parent finding
   //def parent:Option[File] = Option(file.getAbsoluteFile.getParentFile) map File.apply
   def parent:Option[File] = {
     val p = unixPath
     val n = name
     if(p.endsWith("/"+n)) Some(File(p.dropRight(n.size+1))) else None
   }

   def companion(altExt:String):File = {
      val n = this.ext.size
      File( (if(n == 0) path + "." else path dropRight n) + altExt )
   }

   /**clean the file name to be a safe (without spaces and problematic characters)*/
   def safe:File = {
     //TODO what about parent directories in the path that may not be safe
     val safeName = name
      .replace(" ","_")  //messes with quotes
      .replace("/","_")  //bad for unix
      .replace("\\","_")  //bad for windows
      .replace(":","")    //bad for macos
     parent.map{_ / safeName} getOrElse File(safeName)
   }

   def relativeTo(that:File):File = {
      val path = JPaths.get(that.file.toURI) relativize JPaths.get(this.file.toURI)
      File(path.toFile)
   }

   final def ==(that:File):Boolean = this.file.getCanonicalFile == that.file.getCanonicalFile
   //final def ==(that:File) = this.canon.path == that.canon.path
   //final def ==(that:File) = this.file == that.file

   /**ensures current file parent directories exist by checking or making them, returns Success[File] if successful
    * This can be used as a sort of monadic wrapper around succesful parents
    * In most cases this removes the need for an explicit mkdir, assuming you should only need a directory if you have a file
    * Note: this assumes a file inside the directory is specified. It only makes parent files, not the file itself
    * Note: we need the canonical in system version of the file to make sure the parent directory is really possible on the system 
    */
   def mkParents:Try[File] = canon.parent match {
      case None =>               Fail((s"$this does not define a feasible parent on this system"))
      case Some(p) => {
        if(p.isFile)             Fail((s"parent of $this alreay exists and is not a directory"))
        else if(p.isDir)         Success(this) //the parent already exists and is a file
        else Try{p.file.mkdirs} match { //use java.io.File mkdirs to make the parents
          case Success(true)  => Success(this)
          case Success(false) => Fail(s"could not create parent dir $p")
          case Failure(ex)    => Failure(ex)
        }
      }
   }
   /**ensures a directory can be made successfully */
   def mkDir:Option[File] = {Try{file.mkdirs}; if(isDir) Some(this) else None }

   //TODO make this fail utility function a broader drx utility function, maybe at the package level
   private def Fail[A](msg:String):Failure[A] = Failure[A](new java.lang.Throwable(msg))
   //local copy helper functions
   private def cp(that:File):Unit = this.in copyTo that.out
   private def mv(that:File):Unit = this.in copyTo that.out
   private def cpTry(that:File):Try[File] = Try{cp(that); that.modified = this.modified; that}
   /**smart copy, make sure the destination parent directories exist and preserve the src file modification time*/
   def copyTo(that:File):Try[File] = that.mkParents flatMap cpTry

   def moveTo(that:File):Try[File] = that.mkParents flatMap { parent =>
     val mvOpt = java.nio.file.StandardCopyOption.REPLACE_EXISTING
     Try{ File(java.nio.file.Files.move(this.file.toPath, that.file.toPath, mvOpt).toFile) }
   }


   def chmod(perm:Int):Unit = {
      def rwx(perm:Int):(Boolean,Boolean,Boolean) = ((perm & 4) > 0, (perm & 2) > 0, (perm & 1) > 0)

      val (ro,wo,xo) = rwx(perm / 100)
      val (rg,wg,xg) = rwx(perm % 100 / 10)
      val (ra,wa,xa) = rwx(perm % 100 % 10)

      //> java 7
      Try{
         val perms = Seq(
            ro -> PosixFilePermission.OWNER_READ,
            wo -> PosixFilePermission.OWNER_WRITE,
            xo -> PosixFilePermission.OWNER_EXECUTE,

            rg -> PosixFilePermission.GROUP_READ,
            wg -> PosixFilePermission.GROUP_WRITE,
            xg -> PosixFilePermission.GROUP_EXECUTE,

            ra -> PosixFilePermission.OTHERS_READ,
            wa -> PosixFilePermission.OTHERS_WRITE,
            xa -> PosixFilePermission.OTHERS_EXECUTE
         ).filter(_._1).map(_._2).toSet

         JFiles.setPosixFilePermissions(file.toPath, Java(perms) )
      } getOrElse {
         //< java 7
         val r = ro | rg | ra
         val w = wo | wg | wa
         val x = xo | xg | xa

         val rGlobal = rg | ra
         val wGlobal = wg | wa
         val xGlobal = xg | xa

         file.setReadable(  r, !rGlobal)  // set if any are writable, set owner only 
         file.setWritable(  w, !wGlobal)
         file.setExecutable(x, !xGlobal)
      }
      {}
   }
   /**Smart listing files or directories or cross platform root capabilities*/
   def list:Iterator[File] = {
      if(path == "/" || path == "\\"){ //root path case works even for Windows by listing the available drives
         val roots = File.roots
         if(roots.size == 1) roots(0).list  //if only one mount list its directories  (this is the unix case and if only a C: drive)
         else roots.iterator
      }
      else if(this.isDir)
         Try{new File.DirectoryListingIterator(this)} getOrElse {println(s"warning could not list file $this"); Iterator()} //list the files in the directory if no exception
      else if(this.isFile)
         Iterator(this)   //if this is a file return an iterator over a single file
      else if(path startsWith "~") (OS.home / path.drop(1)).list
      else Iterator[File]()
   }
   def root:File = {
     val  p = this.canon.path
     File.roots.find(p startsWith _.path) getOrElse File.root.canon
   }

   /**walk is equivalent to walk(maxDepth=0,=>false,0) is equivalent to list */
   def walk:Iterator[File] = list.flatMap{f =>
      if(f.isDir) Iterator(f) ++ f.walk
      else Iterator(f)
   }
   /**walk(maxDepth=0) is equivalent to list */
   def walk(maxDepth:Int):Iterator[File] = walkAtDepth(maxDepth=Some(maxDepth), skipDir = (_) => false) //don't skip any file (directory)
   def walkSkipIf(skipDir:File=>Boolean):Iterator[File] = walkAtDepth(maxDepth=None, skipDir=skipDir)
   private def walkAtDepth(
      maxDepth:Option[Int],
      skipDir:File=>Boolean = (_) => false, //don't skip directories by default
      currentDepth:Int      = 0 //accumulator should be set
   ):Iterator[File] = list.flatMap{f =>
      val notAtMaxDepth = maxDepth map {currentDepth < _} getOrElse true
      if(notAtMaxDepth && f.isDir && f.canRead && !skipDir(f))
         Iterator(f) ++ f.walkAtDepth(maxDepth,skipDir,currentDepth+1)
      else Iterator(f)
   }

   //--stream generators
   /*
   private var specifiedStreamGenerator:Option[IO.StreamGenerator] = None
   private def findStreamGenerator:IO.StreamGenerator = {
      if(specifiedStreamGenerator.isDefined) specifiedStreamGenerator.get
      else streamGeneratorLibrary.find(_.exts contains ext) getOrElse IO.Normal
   }
   def as(sg:IO.StreamGenerator):File = {specifiedStreamGenerator = Some(sg); this}
   */

   def in:Input = Input(new java.io.BufferedInputStream(new java.io.FileInputStream(file),8*1024))
   private def mkOutput(append:Boolean):Try[Output] = mkParents.map{f =>
     Output(new java.io.BufferedOutputStream(new java.io.FileOutputStream(f.file,append),8*1024))
   }
   def out:Output = mkOutput(append=false).get
   def outAppend:Output = mkOutput(append=true).get

   //--file watch functions
   def watch(func:File => Unit)(implicit ec: ExecutionContext):WatchService =
      if(isDir) watch("CMD",recursive=true)(func)
      else watch("CM")(func)

   /** debounce checks fmod and create differences,  delay:adds a slight delay before f-execution*/
   def watch(setup:String,recursive:Boolean=false,debounce:Time=1.s,delay:Time=20.ms)(func:File => Unit)(implicit ec:ExecutionContext):WatchService = {
      val wek = Map(
         'C' -> WEK.ENTRY_CREATE,
         'M' -> WEK.ENTRY_MODIFY,
         'D' -> WEK.ENTRY_DELETE
      )
      val weks:Array[java.nio.file.WatchEvent.Kind[_]] = setup.toUpperCase.toArray map wek
      val modifiers = JailBreak.field[WatchEvent.Modifier]("com.sun.nio.file.SensitivityWatchEventModifier.HIGH").toOption.toList //only add mod if successfully found 

      val dir:File  = if(isDir) this else parent getOrElse this
      val dirPath:JPath = dir.file.toPath
      val watcher = dirPath.getFileSystem.newWatchService

      val registeredPaths = TrieMap.empty[WatchKey,JPath] //crazy map is required to back out the absolute file path from multiple registered paths
      def register(dir:File):Unit = {
         //println(s"Register watching $dir")
         val path = dir.file.toPath
         val wk = path.register(watcher, weks, modifiers:_*)
         registeredPaths(wk) = path
      }
      //--register
      register(dir) //parent
      if(recursive && isDir) for(f <- walk; if f.isDir) register(f) //recursively

      //--map for file modifications
      val lastModified = TrieMap.empty[File,Date]

      @tailrec def watch:Unit = {
         val key = watcher.take
         //Thread.sleep(1000)//1sec
         Java.toScala(key.pollEvents).foreach{e =>
            /*
            e.kind match {
               case WEK.ENTRY_MODIFY => println(s"Modified $file")
               case WEK.ENTRY_CREATE => println(s"Created $file")
               case WEK.ENTRY_DELETE => println(s"Deleted $file")
            }
            */
            val eventPath = e.context.asInstanceOf[JPath]
            val dir = File(registeredPaths.getOrElse(key,dirPath).toFile)
            val eventFile = dir / eventPath.toFile.getName
            if(isDir || (isFile && this == eventFile)){
               val bounced = (lastModified contains eventFile) && (eventFile.modified - lastModified(eventFile) < debounce)
               if(!bounced){ delay.sleepWithoutWarning(); func(eventFile) } //Note the sleep is awkwardly required for Notepad++ causes some hickups for auto watch files that are near instantly read...
            }
            lastModified(eventFile) = eventFile.modified
         }
         if(key.reset) watch
         else { key.cancel; watcher.close}
      }
      Future{watch} onComplete {
         case Success(_) => println(s"Done watching $this")
         case Failure(e:java.io.FileNotFoundException) => watch //keep watching
         case Failure(e) => println(s"Stopped watching $this: $e")
      }
      watcher
   }
   //--conversion utilities
   def convert[A<:FileKind, B<:FileKind](tgt:File)(implicit fc:FileConverter[A,B], ec:EC):Future[File] = fc.convert(this, tgt)
   def convert[A<:FileKind, B<:FileKind](implicit fc:FileConverter[A,B], ec:EC):Future[File] = fc.convert(this)
   //auto detect file types
   def convertTo(tgt:File)(implicit fcs:FileConverters, ec:EC):Future[File] = fcs.convert(this,tgt)
}