///|
pub(all) enum SceneTaskKind {
  Transform
  Visibility
  Animation
  Skinning
  Geometry
  Upload
} derive(Debug, ToJson, Eq)

///|
pub(all) enum SceneTaskState {
  Pending
  Ready
  Running
  Complete
  Blocked
  Failed
} derive(Debug, ToJson, Eq)

///|
pub(all) struct SceneTask {
  id : Int
  node : Int
  kind : SceneTaskKind
  state : SceneTaskState
  dependencies : Array[Int]
  dependents : Array[Int]
  cost : Int
  priority : Int
  frame : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct SceneSchedule {
  tasks : Array[SceneTask]
  ready : Array[Int]
  blocked : Array[Int]
  completed : Array[Int]
  frame : Int
  estimated_cost : Int
} derive(Debug, ToJson, Eq)

///|
pub(all) struct SceneSchedulerPolicy {
  max_tasks : Int
  max_cost_per_frame : Int
  max_dependencies : Int
  allow_failed_dependencies : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct SceneSchedulerResult {
  passed : Bool
  violations : Array[String]
  schedule : SceneSchedule
} derive(Debug, ToJson, Eq)

///|
pub fn scene_task_kind_rank(kind : SceneTaskKind) -> Int {
  match kind {
    Transform => 0
    Visibility => 1
    Animation => 2
    Skinning => 3
    Geometry => 4
    Upload => 5
  }
}

///|
pub fn scene_task_kind_name(kind : SceneTaskKind) -> String {
  match kind {
    Transform => "transform"
    Visibility => "visibility"
    Animation => "animation"
    Skinning => "skinning"
    Geometry => "geometry"
    Upload => "upload"
  }
}

///|
pub fn scene_task_state_name(state : SceneTaskState) -> String {
  match state {
    Pending => "pending"
    Ready => "ready"
    Running => "running"
    Complete => "complete"
    Blocked => "blocked"
    Failed => "failed"
  }
}

///|
pub fn scene_task(
  id : Int,
  node : Int,
  kind : SceneTaskKind,
  cost : Int,
  priority : Int,
) -> SceneTask {
  {
    id,
    node,
    kind,
    state: Pending,
    dependencies: [],
    dependents: [],
    cost: if cost < 0 {
      0
    } else {
      cost
    },
    priority,
    frame: 0,
  }
}

///|
pub fn scene_task_add_dependency(
  task : SceneTask,
  dependency : Int,
) -> SceneTask {
  if task.dependencies.contains(dependency) {
    task
  } else {
    { ..task, dependencies: task.dependencies + [dependency] }
  }
}

///|
pub fn scene_task_add_dependent(task : SceneTask, dependent : Int) -> SceneTask {
  if task.dependents.contains(dependent) {
    task
  } else {
    { ..task, dependents: task.dependents + [dependent] }
  }
}

///|
pub fn scene_task_is_terminal(task : SceneTask) -> Bool {
  task.state == Complete || task.state == Failed
}

///|
pub fn scene_task_is_ready(task : SceneTask, tasks : Array[SceneTask]) -> Bool {
  if task.state != Pending && task.state != Blocked {
    return false
  }
  for dependency in task.dependencies {
    match tasks.get(dependency) {
      Some(value) => if value.state != Complete { return false }
      None => return false
    }
  }
  true
}

///|
pub fn scene_task_dependency_cost(
  task : SceneTask,
  tasks : Array[SceneTask],
) -> Int {
  let mut result = 0
  for dependency in task.dependencies {
    match tasks.get(dependency) {
      Some(value) => result = result + value.cost
      None => ()
    }
  }
  result
}

///|
pub fn scene_task_score(task : SceneTask, tasks : Array[SceneTask]) -> Double {
  let priority = Double::from_int(task.priority) * 1000.0
  let kind = Double::from_int(6 - scene_task_kind_rank(task.kind)) * 100.0
  let dependency = Double::from_int(scene_task_dependency_cost(task, tasks))
  priority + kind - dependency - Double::from_int(task.cost)
}

///|
pub fn scene_sort_ready(ids : Array[Int], tasks : Array[SceneTask]) -> Unit {
  let count = ids.length()
  for pass in 0..
          match (tasks.get(left_id), tasks.get(right_id)) {
            (Some(left), Some(right)) =>
              if scene_task_score(left, tasks) < scene_task_score(right, tasks) {
                ids[index] = right_id
                ids[index + 1] = left_id
              }
            _ => ()
          }
        _ => ()
      }
    }
  }
}

///|
pub fn scene_schedule(tasks : Array[SceneTask]) -> SceneSchedule {
  let ready : Array[Int] = []
  let blocked : Array[Int] = []
  for index, task in tasks {
    if task.dependencies.length() == 0 {
      ready.push(index)
    } else {
      blocked.push(index)
    }
  }
  scene_sort_ready(ready, tasks)
  { tasks, ready, blocked, completed: [], frame: 0, estimated_cost: 0 }
}

///|
pub fn scene_schedule_add_task(
  schedule : SceneSchedule,
  task : SceneTask,
) -> SceneSchedule {
  let tasks = schedule.tasks.copy()
  tasks.push(task)
  let index = tasks.length() - 1
  let ready = schedule.ready.copy()
  let blocked = schedule.blocked.copy()
  if task.dependencies.length() == 0 {
    ready.push(index)
  } else {
    blocked.push(index)
  }
  { ..schedule, tasks, ready, blocked }
}

///|
pub fn scene_schedule_link(
  schedule : SceneSchedule,
  task_id : Int,
  dependency_id : Int,
) -> SceneSchedule {
  let tasks = schedule.tasks.copy()
  match (tasks.get(task_id), tasks.get(dependency_id)) {
    (Some(task), Some(dependency)) => {
      tasks[task_id] = scene_task_add_dependency(task, dependency_id)
      tasks[dependency_id] = scene_task_add_dependent(dependency, task_id)
      { ..schedule, tasks, }
    }
    _ => schedule
  }
}

///|
pub fn scene_schedule_refresh(schedule : SceneSchedule) -> SceneSchedule {
  let ready : Array[Int] = []
  let blocked : Array[Int] = []
  for index, task in schedule.tasks {
    if scene_task_is_ready(task, schedule.tasks) {
      ready.push(index)
    } else if !scene_task_is_terminal(task) {
      blocked.push(index)
    }
  }
  scene_sort_ready(ready, schedule.tasks)
  { ..schedule, ready, blocked }
}

///|
pub fn scene_schedule_take(
  schedule : SceneSchedule,
  budget : Int,
) -> (SceneSchedule, Array[Int]) {
  let refreshed = scene_schedule_refresh(schedule)
  let tasks = refreshed.tasks.copy()
  let ready = refreshed.ready.copy()
  let selected : Array[Int] = []
  let mut used = 0
  while ready.length() > 0 {
    let id = ready.remove(0)
    match tasks.get(id) {
      Some(task) =>
        if used + task.cost <= budget {
          tasks[id] = { ..task, state: Running, frame: refreshed.frame }
          selected.push(id)
          used = used + task.cost
        } else {
          ready.insert(0, id)
          break
        }
      None => ()
    }
  }
  ({ ..refreshed, tasks, ready, estimated_cost: used }, selected)
}

///|
pub fn scene_schedule_complete(
  schedule : SceneSchedule,
  task_id : Int,
) -> SceneSchedule {
  let tasks = schedule.tasks.copy()
  let completed = schedule.completed.copy()
  match tasks.get(task_id) {
    Some(task) =>
      if !scene_task_is_terminal(task) {
        tasks[task_id] = { ..task, state: Complete }
        completed.push(task_id)
      }
    None => ()
  }
  scene_schedule_refresh({ ..schedule, tasks, completed })
}

///|
pub fn scene_schedule_fail(
  schedule : SceneSchedule,
  task_id : Int,
) -> SceneSchedule {
  let tasks = schedule.tasks.copy()
  match tasks.get(task_id) {
    Some(task) => tasks[task_id] = { ..task, state: Failed }
    None => ()
  }
  scene_schedule_refresh({ ..schedule, tasks, })
}

///|
pub fn scene_schedule_advance(
  schedule : SceneSchedule,
  frame : Int,
) -> SceneSchedule {
  {
    ..schedule,
    frame: if frame < schedule.frame {
      schedule.frame
    } else {
      frame
    },
  }
}

///|
pub fn scene_schedule_completed_cost(schedule : SceneSchedule) -> Int {
  let mut cost = 0
  for task in schedule.tasks {
    if task.state == Complete {
      cost = cost + task.cost
    }
  }
  cost
}

///|
pub fn scene_schedule_pending_cost(schedule : SceneSchedule) -> Int {
  let mut cost = 0
  for task in schedule.tasks {
    if !scene_task_is_terminal(task) {
      cost = cost + task.cost
    }
  }
  cost
}

///|
pub fn scene_schedule_cycle_nodes(schedule : SceneSchedule) -> Array[Int] {
  let result : Array[Int] = []
  for task in schedule.tasks {
    if task.state == Blocked && task.dependencies.length() > 0 {
      let mut all_blocked = true
      for dependency in task.dependencies {
        match schedule.tasks.get(dependency) {
          Some(value) => if value.state == Complete { all_blocked = false }
          None => ()
        }
      }
      if all_blocked {
        result.push(task.id)
      }
    }
  }
  result
}

///|
pub fn default_scene_scheduler_policy() -> SceneSchedulerPolicy {
  {
    max_tasks: 100000,
    max_cost_per_frame: 1000000,
    max_dependencies: 64,
    allow_failed_dependencies: false,
  }
}

///|
pub fn scene_scheduler_result(
  schedule : SceneSchedule,
  policy : SceneSchedulerPolicy,
) -> SceneSchedulerResult {
  let violations : Array[String] = []
  if schedule.tasks.length() > policy.max_tasks {
    violations.push("task count exceeds policy")
  }
  if schedule.estimated_cost > policy.max_cost_per_frame {
    violations.push("frame cost exceeds policy")
  }
  for task in schedule.tasks {
    if task.dependencies.length() > policy.max_dependencies {
      violations.push("task dependency count exceeds policy")
      break
    }
    if !policy.allow_failed_dependencies {
      for dependency in task.dependencies {
        match schedule.tasks.get(dependency) {
          Some(value) =>
            if value.state == Failed {
              violations.push("failed dependency blocks schedule")
              break
            }
          None => violations.push("task references missing dependency")
        }
      }
    }
  }
  { passed: violations.length() == 0, violations, schedule }
}

///|
pub fn scene_schedule_lines(schedule : SceneSchedule) -> Array[String] {
  [
    "tasks=\{schedule.tasks.length()}",
    "ready=\{schedule.ready.length()}",
    "blocked=\{schedule.blocked.length()}",
    "completed=\{schedule.completed.length()}",
    "frame=\{schedule.frame}",
    "estimated_cost=\{schedule.estimated_cost}",
    "completed_cost=\{scene_schedule_completed_cost(schedule)}",
    "pending_cost=\{scene_schedule_pending_cost(schedule)}",
  ]
}