/*
   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

import scala.collection.concurrent.{TrieMap => CCTrieMap}

/** Prop: This top level class offers a quick lookup mechanism based off of Kson.watched files and scoped string map lookups as follows:
 * ex: Prop.of(File.home/".kson")/"email" | "default email"  
 * WARNING: mutable singleton properties lie within here.
 * */
object Prop{
   //hide a bit a mutability here with caching
   import Implicit.ec
   private val watchedFileCache:CCTrieMap[File,StringMap] = CCTrieMap.empty
   def of(f:File):StringMap = watchedFileCache.getOrElseUpdate(f, Kson.Watched(f))
   /**make sure the cache can be cleared*/
   def clear() = {watchedFileCache.clear(); println("cleared Prop.watchedFileCache.")}
}
//TODO keep comments and group them together to be added to the next node if they are by themselves or attached to the current node if on the ending line
//TODO add file include/import at a shifted depth and mechanism for watched kson chains

/**Keyed simple object notation is a much simpler and more readable key value notation without needing quotes or commas, and intended for a single line
  * 
  *Kson (Key Separated Object Notation) is a simple format used for many of the configuration formats.
  *The 'key' idea is to remain simple like CSV (Comma Separated Values). However, instead of using commas, use "<key>:" as
  *the separation delimiter. Then instead of receiving a list of strings for a line of CSV you receive back
  *key value pairs for a line of Kson.  This provides for a flexible configuration and clean data encoding framework.  
  *
  *Where CSV returns an table of values, Kson can return a list of key value pairs per line or can squash all the key values
  *together for a single key value lookup.
  *
  * For example "person first:Aaron last:Radke language:scala desc:Use simple keys //with some comments"
  *  gets parsed to Map("/"" -> "person", "first" -> "Aaron", "last" -> "Radke", "language" -> "scala", "quote" -> "Use simple keys")
  *
  */
object Kson{
   //--constants
   val interpolationPat = """\$[\w\.]+""".r
   // val pathSeperator = "." //TODO make the path and key separator configurable or think if this is worth it... optionally using '=' may be worth it, or making it configurable at the root of the kson file.
   // val keySeperator = ":"

   //--constructors
   def empty = new Kson(Vector.empty[KsonLine])
   def apply(file:File):Kson = apply(file.in)
   def apply(string:String):Kson = apply(Input(string))
   def apply(in:Input):Kson = {
      val lines = for(line <- in.lines; kson = KsonLine(line); if kson.nonEmpty) yield kson
      new Kson(lines.toVector)
   }
   def apply(files:Seq[File]):Kson = files.foldLeft( Kson.empty){case (kson, f) => kson ++ Kson(f) }

   /**
     * ### simple init
     *   val kson = Kson.watch(f)
     *
     * ### init with call back (not needed often since the config is backed by an hash trie cache to automatically hold the newest version)
     *   val kson = Kson.watch(f).onUpdate{k => println(kson("config.parameter") + "was upated")}
     *
     * ### delayed init (useful for a global config reference that has not been filled in with a separate file setup arguments)
     *   val kson = Kson.Watched.empty
     *   kson.update(File("config.kson"))  //updates the file and starts a watcher
     */
   def watch(file:File)(implicit ec: ExecutionContext):Watched = Watched(file)

   //--wrapper classes (keeps package hierarchy clean
   /**mutable that executes call backs on change modifications*/
   class Mutable[A <: Mutable[A]] extends StringMap{ //F-bounded polymophism to get type back as self type
      self:A => //allow the `this` pointer to represent the inherited subtype for onUpdate fluid api
      //--messy mutable fields
      private var _kson = Kson.empty  //private so only this guy updates it, and initially empty so will work in an monadic environment  (foreach over the list will not crash)
      private var onUpdateList:List[Kson => Unit] = Nil

      //-----
      def kson = _kson
      def update(k:Kson):Unit = {
         _kson = k  //use a new kson tree
         for(f <- onUpdateList) f(k)  //run all the update callbacks
      }

      //---string map interfaces that provide all the nice features for a config
      def getString(key:String):Option[String] = _kson getString key
      override def get[T](key:String)(implicit parser:Parsable[T]):Option[T] = _kson get key
      override def split[T](key:String,sep:String)(implicit parser:Parsable[T]):Vector[T] = _kson.split(key,sep)

      def keys:List[String] = kson.keys //kson.keys:a sorted list of the merged keys based on file order merged.keys:the set of keys (toList is some hash order)

     //IDEA add listener that executes call backs for changes in specific kson fields
      /**add a function to execute on updates*/
      def onUpdate(f: Kson => Unit):A = {onUpdateList ::= f; this} //return this for some fluent api
      /**clear the list of callbacks*/
      def clear = onUpdateList = Nil
   }
   /**mutable that executes call backs on change modifications (call backs are added with `onUpdate`)*/
   object Watched{
      def apply(file:File)(implicit ec: ExecutionContext) = {val watched = new Watched; watched.update(file); watched}
      def apply() = new Watched
      def empty = new Watched
   }
   class Watched extends Mutable[Watched]{  //delayed execution the watch
      def update(file:File)(implicit ec:ExecutionContext):Unit = {
        update(Kson(file)) //TODO should the initial load of of a file call all the call-backs ???
        file.watch{f => update(Kson(f))}; //watch the file for modifications and update the Kson with a new parse in the cached mutable version with callbacks
        {}
      }
   }

}

class Kson(val unscopedLines:Vector[KsonLine]) extends StringMap.Cached{
   def ++(that:Kson):Kson = new Kson(this.unscopedLines ++ that.unscopedLines)
   override def toString = lines.mkString("\n")

   def toByteArray:Array[Byte] = Output.toByteArray{out => unscopedLines.foreach(out println _.originString) }

   def >>(shift:Int) = new Kson(unscopedLines map (_ >> shift))
   def <<(shift:Int) = new Kson(unscopedLines map (_ << shift))

   private val emptyScopeAndLines:(List[KsonLine], List[KsonLine]) = ( List(), List() )
   lazy val lines:List[KsonLine] = unscopedLines.foldLeft( emptyScopeAndLines ){case ((scope,newLines), line) =>
      val thisScope:List[KsonLine] =  scope.dropWhile(_.depth >= line.depth)
      val newLine = new KsonLine(thisScope, line.roots, line.kvs, line.depth)
      val newScope = newLine :: thisScope
      (newScope, newLine :: newLines)
   }._2.reverse

   lazy val interpolated:Kson = new Kson(unscopedLines map {_ interpolate this})

   def interpolate(s:String):String = interpolate(0)(s)
   /**Generic interpolator function that turns things like $KEY into values if the $KEY does not exist it is left as is
    * this allows all $ to remain unless they are followed by a special key..
    * TODO make this not depend on Regex
    * TODO add bracketed function interpolators
    * TODO add implicit op bracketed function interpolators
    */
   private def interpolate(recursionDepth:Int)(str:String):String = /*Log(depth,s"interpolate str:$str")*/{
     //--depth stop (number of times to check string replace resolution
     if(recursionDepth > 100) str else
     //--regex
     str.replaceAllWith(Kson.interpolationPat){ pat => getString(pat drop 1) getOrElse pat }
   }

   private def stripRefTag(ref:String) = if(ref startsWith "$") ref.drop(1) else ref

   //get top level merge keyMap interpolated
   private def getMerged(key:String, recursionDepth:Int):Option[String] =
     //merged.get(key) map interpolate(recursionDepth)
     mergedGetWithInheritance(key) map interpolate(recursionDepth)
   private def getFromPath(key:String,recursionDepth:Int):Option[String] =
     getMerged(key,recursionDepth) orElse
     getFromPath(key.split('.').toList,recursionDepth)

   private def mergedGetWithInheritance(key:String):Option[String] = {
     //-search for inheritance matching key
     merged.get(key)
     //-search walking back through parents walking up tree
     .orElse{
       val path =  key.split('.').reverse.toList
       path match {
         case Nil => None
         case k :: Nil => merged get k
         //-skip parent and go to grand-parent
         case k :: x :: xs =>
           val inheritedKey =  (k :: xs).reverse.mkString(".") //parent-less
           mergedGetWithInheritance(inheritedKey)
       }
     }
   }

   private def getFromPath(path:List[String],recursionDepth:Int):Option[String] = {
      //Log(depth,s"getFromPath path:$path depth:$depth")
      val recursionDepthNext = recursionDepth+1
      //---recursionDepth death loop
      if(recursionDepth > 100) None
      //---walk the tree
      else {
        path match {
           case Nil      => None
           case k :: Nil => getMerged(k,recursionDepthNext)  //root element
           case k :: x :: xs  =>
             def dereference(newParent:Option[String]):Option[String] = {
               val altScope  = newParent map stripRefTag getOrElse k
               val altKey    = altScope + "." + x
               val altPath   = altKey :: xs
               //-search for specific field
               getFromPath(altPath, recursionDepthNext)
             }

             //-search for exact matching key
             def exact = merged get k
             //-search for a referenced parent 
             def interpolated = getMerged(k,recursionDepth) //interpolated parent found

             dereference(exact) orElse dereference(interpolated)
        }
      }
   }

   lazy val forest:Forest[MTree[KsonLine]] = MTree.nest(lines)(_.depth < _.depth)

   private val keyLineNumber:CCTrieMap[String,Double] = CCTrieMap.empty
   lazy val merged:Map[String,String] = lines.zipWithIndex.foldLeft(Map.empty[String,String]){case (m, (line,lineNumber)) =>
      val prefixes = (line :: line.scope) flatMap {_.roots.headOption} map (_+".")
      val prefix:String = prefixes.reverse.mkString
      val prefixExt:String = prefixes.drop(1).reverse.mkString
      val prefixedLine = for( (k,v) <- line) yield (prefix+k , v) //prefix the head root value to the keys if one exists
      val extensions = line.roots zip line.roots.drop(1) map {case (k,v) => (prefixExt+k,"$"+v)}
      //println(s"$line | extensions:$extensions roots:${line.roots}") //DEBUG
      for( ((k,_),i) <- prefixedLine.zipWithIndex) keyLineNumber(k) = (lineNumber + i/999d) //use doubles to sort keys within lines
      val newMerge = m ++ prefixedLine ++ extensions
      newMerge
   }

   //--StringMap.Cached interfaces
   lazy val keys:List[String] = merged.keys.toList.sortBy{k => keyLineNumber.getOrElse(k, 0d)}
   def getStringNoCache(key:String) = getFromPath(key,0) //provides getString for StringMap
}
object KsonLine{
   private def escape(string:String):String =
     string
       .replace("::",   "$colonColon")
       .replace("://",  "$colonSlashSlash")
       .replace("\\:",  "$colon")
       .replace("\\=",  "$equals")
       .replace("\\/",  "$slash")

   private def unescape(string:String):String =
     string
       .replace("$colonColon",      "::")
       .replace("$colonSlashSlash", "://")
       .replace("$colon",           ":")
       .replace("$equals",          "=")
       .replace("$slash",           "/")

   private val whitespace = Set(' ', '\t')

   private def rootList(root:String):List[String] = root.split("""\s*<-\s*""").toList
   @tailrec private def parseRight(baseReversed:String,kvs:List[(String,String)]=Nil):(Option[String],List[(String,String)]) = {
      val (v,rest) = baseReversed.trim.span(_ != ':')
      if(rest.isEmpty){
         val r = unescape(v.trim.reverse)
         if(r == "")   (None,    kvs)
         else          (Some(r), kvs)
      }
      else {
         val (k,restBase) = rest.tail.trim.span(!_.isWhitespace)
         val moreKvs = (unescape(k.trim.reverse),unescape(v.trim.reverse)) :: kvs
         if(restBase.trim == "")   (None, moreKvs)
         else                      parseRight(restBase, moreKvs)
      }
   }

   def apply(string:String):KsonLine = {
      val escapedString = escape(string)
      val commentIndex = escapedString indexOf "//"
      val base = if(commentIndex < 0) escapedString else escapedString take commentIndex
      //distribued depth calculation
      // [ ]  depth -20  top level??
      // [ ]  depth -10
      val (baseString, depth) = {
          //-- calculate indentation based depth
          val (spaces, post) = base span whitespace.apply
          val spaceDepth = spaces.size

          //-- git/properties like header
          if((post startsWith "[") && (post endsWith "]"))
            post.drop(1).dropRight(1).trim -> (-30 + spaceDepth).sat(-30,0)
          //-- colon header:
          else if( post.endsWith(":") ){
            post.dropRight(1).trim -> (-10 + spaceDepth).sat(-10,0)
          }
          //-- markdown like headers
          else if(post startsWith "#") {
              val (bangs, postBang) = post span (_ == '#')
              postBang.trim -> (-20 + bangs.size + spaceDepth).sat(-20,0)
          }
          //-- normal header (positive depth)
          else
              post.trim -> spaceDepth
      }

      //---new custom
      val (r,kvs) = parseRight(baseString.reverse)
      r match {
         case Some(r) => new KsonLine(List(), rootList(r), kvs, depth)
         case None    => new KsonLine(List(), List(),      kvs, depth)
      }
   }
   def unapply(line:KsonLine):Option[(List[String],Option[String],List[(String,String)])] = Some((line.scope.flatMap{_.root}, line.root, line.kvs))

   def apply(kvs:List[(String,String)])             = new KsonLine(List(), List(),     kvs,0)
   def apply(root:String,kvs:List[(String,String)]) = new KsonLine(List(), List(root), kvs,0)
}
class KsonLine(val scope:List[KsonLine], val roots:List[String],val kvs:List[(String,String)], val depth:Int) extends Iterable[(String,String)] with StringMap{
   lazy val root:Option[String] = if(roots.isEmpty) None else Some(roots mkString " <- ")

   //--support StringMap interface
   def iterator = kvs.iterator

   override lazy val toList = (for(k <- keys; v <- getString(k)) yield (k,v)).toList  //required since StringMap and Iterable collide on this method

   private lazy val stringMapLineOnly = kvs.toMap
   private def keysLineOnly = kvs.map{_._1} //keep order of the keys
   private def getStringLineOnly(key:String):Option[String] = stringMapLineOnly get key //TODO for separation of concerns, should this really climb up the parent list for a match

   // def keys = kvs.map{_._1} //keep order of the keys
   // def getString(key:String):Option[String] = getStringLineOnly(key)

   lazy val keys = scope.reverse.flatMap{_.keysLineOnly} ++ keysLineOnly
   def getString(key:String) = getString(key, this :: scope)  //inherited lookup getStringInherited

   /**search the scope upward looking for the key (or the closest parent with the key defined*/
   private def getString(key:String, scope:List[KsonLine]):Option[String] = scope match {
      case Nil => None //getString(key) //kvs.toMap get key
      case p :: ps => p.getStringLineOnly(key) orElse getString(key,ps)
   }

   def pathList:List[String] = (this :: scope).flatMap{_.root}
   // def path:String           = if(scope.isEmpty) "" else (this :: scope).flatMap{_.root} 
   def path:String           = pathList.reverse.mkString("/")
   override def toString = {
   val s = if(scope.isEmpty) "<none>" else scope.flatMap{_.root}.reverse mkString("/")
      val k = kvs.foldLeft(root.getOrElse("")){case (r,(k,v)) => s"$r $k:$v"}.trim
      s"KsonLine(depth:$depth scope:$s line:{$k})"
   }
   def originString:String = root ++: kvs.map{case (k,v) => s"$k:$v"} mkString " "
   def kvString:String = kvs.map{case (k,v) => s"$k:$v"} mkString " "

   override def isEmpty = kvs.isEmpty && root.isEmpty
   // override def nonEmpty = !kvs.isEmpty || !root.isEmpty

   override def equals(that:Any) = that match {
      case that:KsonLine => this.roots == that.roots && this.kvs == that.kvs && this.depth == that.depth
      case _ => false
   }
   override def hashCode:Int = roots.## + 7*depth + 31*kvs.##

   def >>(shift:Int) = new KsonLine(scope, roots, kvs, depth + shift)
   def <<(shift:Int) = new KsonLine(scope, roots, kvs, depth - shift)

   def interpolate(s:String):String = s.replaceAllWith(Kson.interpolationPat){v => getString(v drop 1, scope) getOrElse v}
   def interpolate(s:String,kson:Kson):String = kson.interpolate(interpolate(s))
   def interpolate(kson:Kson):KsonLine = {
      def f(s:String) = interpolate(s,kson)
      new KsonLine(Nil, roots.take(1) map f, kvs map {case (k,v) => (f(k), f(v)) }, depth)
   }
}