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

object Todo{
  def main(args:Array[String]) = {
    def isTodo(f:File):Boolean = {
      f.isFile && f.ext == "kson" && {
        val name = f.name.toLowerCase
        name.contains("todo") && !name.contains("archive")
      }
    }
    val todo = new Todo( for(arg <- args.toSeq; dir = File(arg).canon; f <- dir.walk if isTodo(f)) yield f )
  }

}
class Todo(files:Seq[File]){
  //---helper functions 
  val DatePat = """(\d+)/(\d+)""".r
  def parseDate(date:String):Option[Date] = date match {
    case DatePat(m,d) => Some( Date(Date.now.cal.y , m.toInt, d.toInt) )
    case "today" | "now" => Some(Date.now.floorDay)
    case _ => None
  }
  // def formatWorkDay(time:Time):String = f"~${time.hr/8/5}%2.1f [work-weeks]"
  def formatWorkDay(time:Time):String = f"~${time.hr}%2.0fh"
  private def split(str:String) = str split """[\s,;]+""" 


  object Task{
    def get(line:KsonLine, index:Int):Option[Task] = {
      val EstimateLoePat = """\[\s*\]\s*(\d+)\s*(h|hr|d|day|m|min)\s+(.+)""".r
      val UnknownLoePat  = """\[\s*\]\s*(.+)""".r
      val (loe:Option[Time], desc:String) = line.root match {
        case Some(EstimateLoePat(num, "h"|"hr",desc))  => (Some(num.toInt.hr), desc)
        case Some(EstimateLoePat(num, "m"|"min",desc)) => (Some(num.toInt.minute), desc)
        case Some(EstimateLoePat(num, "d"|"day",desc)) => (Some(8.hr*num.toInt), desc)
        case Some(UnknownLoePat(desc)) => (None, desc)
        case _ => (None, "Bad task string form: "+line.root)
      }
      val due:Option[Date] = line.getString("due").flatMap(parseDate)
      due.map{d => Task(d, index, line, loe, desc)}
    }
  }
  //tasks require a due date
  case class Task(due:Date, index:Int, line:KsonLine, loe:Option[Time], desc:String){

    lazy val scope:String = line.scope.flatMap{_.root}.reverse mkString "/"
    lazy val parScope:String = line.scope.flatMap{_.root}.reverse.take(2) mkString "/"

    lazy val devs:List[String] = line.getString("dev").toList.flatMap(split).sorted;

    def sortTuple = (due.ms, index, scope, devs.mkString(",")) //Date object doesn't sort right inside the tuple ???

    override def toString = summary
    def summary = {
      def format(time:Option[Time]):String = time.map{"%3dh" format _.hr.round.toInt} getOrElse "???h"
      val loeString = format(loe)
      val devString = if(devs.isEmpty) "No dev ???" else devs mkString ","
      val budgetString = format(budget.map{_.time})
      Seq(
          "due"-> due.usMonthDay.fit(10),
          "loe"-> s"$loeString/$budgetString".fit(10),
          "dev"-> devString.fit(20),
          "scope"-> scope,
          "desc"-> desc
      ).map(Format.kvColor).mkString(" ")
      // f"${due.isoDay}  $loeString/$budgetString  ${scope}%-25s $devString%-15s $desc"
    }

    def loeDev(dev:String):Option[Time] = if(devs contains dev) loe.map{_/devs.size} else None  //TODO split hours fractionally instead of evently

    def budget:Option[Budget] = line.scope.flatMap{Budget.get}.headOption //search through parents for a budget grab the first
  }

  case class TaskSet(tasks:Seq[Task]){
    lazy val loe:Time = tasks.flatMap(_.loe).foldLeft(0.s){_ + _}
    lazy val due:Date = if(tasks.isEmpty) Date.now + 40.day else tasks.maxBy{_.due.ms}.due //TODO warn if no due date is set
    lazy val span:Bound[Date] = Bound(Date.now.floorDay,  due) //TODO this date.now should not be hard coded...
    // def budgetLoe:Time = tasks.flatMap(_.budgetLoe).foldLeft(0.s){_ + _}
    //
    lazy val taskTime = tasks.flatMap(_.loe).foldLeft(0.s){_ + _}

    lazy val availableTime = workTime(span) //TODO select work day size from dev config
    lazy val utilization:Double = if(availableTime <= 0.s) 1.0 else taskTime/availableTime

    private def seekStart(end:Date, goal:Time, acc:Time):Date = {
      if(acc >= goal) end
      else seekStart( end - 1.day, goal, if(isWorkDay(end)) acc+8.hr else acc)
    }
    def start:Date = seekStart(span.end, taskTime, 0.s)

    // def timeSummary = f"count:${tasks.size} tasks:${formatWorkDay(taskTime)} calendar:${formatWorkDay(availableTime)} utilization:${utilization}%2.2f"
    def timeSummary = Seq( "count" -> tasks.size,
                           "tasks" -> formatWorkDay(taskTime),
                           "calendar" -> formatWorkDay(availableTime),
                           "utilization" -> utilization,
                           "due" -> due.usMonthDay,
                           "start" -> start.usMonthDay,
                           "scope" -> tasks.map{_.scope}.toSet.mkString(",")
                        ).map(Format.kvColor(_)).mkString(" ")
    // f"count:${tasks.size} tasks:${formatWorkDay(taskTime)} calendar:${formatWorkDay(availableTime)} utilization:${utilization}%2.2f"

  }

  // a `budget` is allow time from a given date (this may change based on new budget reports
  object Budget{
    private val BudgetPat = """(\d+)hr?s?\s*@\s*(\d+/\d+)""".r
    def get(line:KsonLine):Option[Budget] = {
      line.getString("budget") match {
        case Some(BudgetPat(num, d)) => parseDate(d) map {Budget(num.toInt.hr, _, line)}
        case _ => None
      }
    }
  }
  case class Budget(time:Time, start:Date, line:KsonLine){
    val id = line.path
    def nice = s"${time.hr}h @ ${start.isoDay}"
    override def toString = s"$id Budget($nice)"
  }

  //-- find tasks
  val kson = Kson(files)
  val taskLines = for(line <- kson.lines; root <- line.root if root startsWith "[ ]") yield line

  //-- find tasks
  val dueTasks = for(
      (line,index) <- taskLines.zipWithIndex;
      task <- Task.get(line, index)
  ) yield task

  //--find work dates while skipping ptos //TODO separate pto for each dev
  val ptos = (
    for(line <- kson.lines; pto <- line.getString("pto").toList.flatMap(split);
        date <- parseDate(pto)
    ) yield date
  ).toSet
  def isWorkDay(date:Date):Boolean = date.dayOfWeek.isWeekday && !ptos(date)
  def workDates(bound:Bound[Date]) = bound by 1.day filter isWorkDay
  def workTime(bound:Bound[Date], workDayLength:Time=8.hr):Time = workDayLength * workDates(bound).size

  println("\n#=== Budgets\n")
  //--all grouped by budget
  val budgetTasks:Seq[Task] = {
    for((budget,tasks) <- dueTasks.groupBy(_.budget)) yield {
      val budgetString = budget map {_.toString} getOrElse "No budget defined"
      println(s"\n#-- $budgetString\n")
      val (estimatedTasks, unEstimatedTasks) = tasks.partition(_.loe.isDefined)

      val loeSum = estimatedTasks.flatMap{_.loe}.foldLeft(0.s){_ + _}
      val budgetTime = budget.map{_.time}.getOrElse(0.s)
      val availableBudgetTime = budgetTime - loeSum
      val avgTaskTime = availableBudgetTime/unEstimatedTasks.size //assume similar loe for remaining tasks if a loe is not defined
      // for(task <- tasks.sortBy(_.sortTuple)) println(task)

      //TODO squeeze/dialate time if over budget to a budgetedLoE
      estimatedTasks ++ unEstimatedTasks.map{t => t.copy(loe=Some(avgTaskTime))}
    }
  }.toSeq.flatten.sortBy(_.sortTuple)

  //--all sorted tasks (with auto filled budgets)
  // budgetTasks foreach println
  //

  //--find specific *dev* tasks and re-budget tasks based specific *dev* and
  //TODO make a way to see if this was an auto specified time or not
  val devTasks = for(t <- budgetTasks; loe <- t.loeDev("radke")) yield t.copy(loe=Some(loe))
  devTasks foreach println

  //--grouped by parallel tasks
  // println("\n#=== Parallel dates \n")
  // for((due, tasks) <- devTasks groupBy (_.due) ) {
  //   println(s"#---$due")
  //   tasks foreach println
  // }
  println("\n#=== Parallel scope\n")
  for((parScope, seqTasks) <- devTasks groupBy (_.parScope) ) {
    println(s"#---$parScope")
    // seqTasks.sortBy(_.sortTuple) foreach println
    for((due, tasks) <- seqTasks.groupBy(_.due).toList sortBy {_._1.ms} ) {
      val ts = TaskSet(tasks)
      println(ts.timeSummary)
      // tasks.sortBy(_.sortTuple) foreach println
    }
    //   println(s"#---$due")
    //   tasks foreach println
  }
  val taskSets = for((parScope, seqTasks) <- devTasks groupBy (_.parScope);
                      (due, tasks) <- seqTasks.groupBy(_.due);
                      ts = TaskSet(tasks)
                    ) yield ts
  println("\n#=== Tasks by utilization pressure\n")
  for(ts <- taskSets.toList.sortBy(_.utilization).reverse)
      println(ts.timeSummary)

  //--available work days
  val taskSet = TaskSet(devTasks)

  {
    val cal = taskSet.span.max.cal
    println(s"\n#=== Calendar now to ${cal.month.short} ${cal.d} ${taskSet.timeSummary}\n")
    for(date <- workDates(taskSet.span)){  //TODO filter based on specific dev pto and available weekends
      println( "## " + date.niceWork)
    }
  }

  //--
  //TODO schedule a task based on final due date via dynamic programming of due date stack serial by *dev*
  //--schedule start dates based on available workDates
  //TODO sort tasks into linear order
  //TODO spread across due dates
  // for(devTasks.reverse foreach println
  // sort tasks??  
  //

  //-- span and nudge scheduler..???
  // optimize:  by minimizing the amount of work at any given time while meeting deadlines
  //
  // [4     4     4     4 ]           
  // [2     2     2     2     2     2]
  // =>
  // [4     4     4     4 ]           
  // [1     1     1     1     4     4]
  // =>
  // [4     4     4     4 ]            | a*4             = 4*4 = 16  =>   4  0  0  =  16
  // [1     1     0.5   0.5  4.5  4.5] |       b*4 + c*2 = 6*2 = 12       0  4  2     12
  //                      a + b = c => | a   + b   - c   = 0              1  1 -1     0
  //b*4 + c*2 = 12
  //b+4 = c
  // => b*4 + (b+4)*2 = 12 
  // => b*6 + 8 = 12
  // => b*6     = 4
  // => b       = 4/6
  //
  // => b       = 2/3
  // => c       = 2/3 + 4
  // => a       = 4
  //
  //
  // [4     4     4     4 ]              a*2 + b*2 +           = 16   = [ 2  2  0  0]  = 16
  //            [ 4     4     4     4]             + c*2 + d*2 = 16       0  0  2  2   = 16
  // =>                                                                   1  -1 -1 0   = 0
  // [5     5     3     3 ]              a = b + c                        1  0   0 -1  = 0
  //            [ 3     3     5     5]   a = d
  //
  //
  // [4     4     4     4 ]           
  //            [ 2     2     2     2]
  // =>
  // [4     4     4     4 ]           
  //            [ 0     0     4     4]
  //  

  //first pass: statistic summary task/ and availability
  //first pass: all serial alignment?

  //--number of current tasks
  println("numTasks:" + taskSet.tasks.size)
  200.ms.sleep
}