///|
pub(all) struct SnapshotHistory {
  format_version : Int
  root : String
  snapshots : Array[Snapshot]
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct HistoryPoint {
  created_at : Int64
  total_size : Int64
  files : Int
  directories : Int
  scan_errors : Int
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct HistorySummary {
  observations : Int
  first_at : Int64
  last_at : Int64
  first_size : Int64
  last_size : Int64
  minimum_size : Int64
  maximum_size : Int64
  net_change : Int64
  average_change : Int64
} derive(Eq, Debug, ToJson, FromJson)

///|
pub(all) struct PathTrend {
  path : String
  first_size : Int64
  last_size : Int64
  minimum_size : Int64
  maximum_size : Int64
  net_change : Int64
  observations : Int
} derive(Eq, Debug, ToJson, FromJson)

///|
pub fn SnapshotHistory::new(root : String) -> SnapshotHistory {
  SnapshotHistory::{ format_version: 1, root, snapshots: [], }
}

///|
pub fn history_from_snapshot(snapshot : Snapshot) -> SnapshotHistory {
  SnapshotHistory::{
    format_version: 1,
    root: snapshot.root,
    snapshots: [snapshot],
  }
}

///|
pub fn append_history_snapshot(
  history : SnapshotHistory,
  snapshot : Snapshot,
) -> SnapshotHistory raise {
  if history.root != snapshot.root {
    raise Failure("Snapshot root does not match history root: " + snapshot.root)
  }
  if !history.snapshots.is_empty() &&
    snapshot.created_at <=
    history.snapshots[history.snapshots.length() - 1].created_at {
    raise Failure("History snapshots must have increasing creation times")
  }
  let snapshots = history.snapshots.copy()
  snapshots.push(snapshot)
  { ..history, snapshots, }
}

///|
pub fn history_points(history : SnapshotHistory) -> Array[HistoryPoint] {
  history.snapshots.map(fn(snapshot) {
    HistoryPoint::{
      created_at: snapshot.created_at,
      total_size: snapshot.total_size(),
      files: snapshot.file_count(),
      directories: snapshot.directory_count(),
      scan_errors: snapshot.errors.length(),
    }
  })
}

///|
pub fn summarize_history(history : SnapshotHistory) -> HistorySummary? {
  if history.snapshots.is_empty() {
    return None
  }
  let points = history_points(history)
  let first = points[0]
  let last = points[points.length() - 1]
  let mut minimum = first.total_size
  let mut maximum = first.total_size
  for point in points {
    if point.total_size < minimum {
      minimum = point.total_size
    }
    if point.total_size > maximum {
      maximum = point.total_size
    }
  }
  let net = last.total_size - first.total_size
  HistorySummary::{
    observations: points.length(),
    first_at: first.created_at,
    last_at: last.created_at,
    first_size: first.total_size,
    last_size: last.total_size,
    minimum_size: minimum,
    maximum_size: maximum,
    net_change: net,
    average_change: if points.length() < 2 {
      0L
    } else {
      net / (points.length() - 1).to_int64()
    },
  }
  |> Some
}

///|
fn file_size_map(snapshot : Snapshot) -> Map[String, Int64] {
  let sizes : Map[String, Int64] = Map([])
  for entry in snapshot.entries {
    if entry.is_file() {
      sizes[entry.path] = entry.size
    }
  }
  sizes
}

///|
pub fn path_trends(history : SnapshotHistory) -> Array[PathTrend] {
  if history.snapshots.is_empty() {
    return []
  }
  let paths : Map[String, Bool] = Map([])
  let maps : Array[Map[String, Int64]] = []
  for snapshot in history.snapshots {
    let sizes = file_size_map(snapshot)
    for path, _ in sizes {
      paths[path] = true
    }
    maps.push(sizes)
  }
  let result : Array[PathTrend] = []
  for path, _ in paths {
    let first_size = maps[0].get(path).unwrap_or(0L)
    let last_size = maps[maps.length() - 1].get(path).unwrap_or(0L)
    let mut minimum = first_size
    let mut maximum = first_size
    let mut observations = 0
    for sizes in maps {
      let value = sizes.get(path).unwrap_or(0L)
      if sizes.get(path) is Some(_) {
        observations += 1
      }
      if value < minimum {
        minimum = value
      }
      if value > maximum {
        maximum = value
      }
    }
    result.push(PathTrend::{
      path,
      first_size,
      last_size,
      minimum_size: minimum,
      maximum_size: maximum,
      net_change: last_size - first_size,
      observations,
    })
  }
  result.sort_by(fn(a, b) {
    if a.net_change > b.net_change {
      -1
    } else if a.net_change < b.net_change {
      1
    } else if a.path < b.path {
      -1
    } else {
      1
    }
  })
  result
}

///|
pub fn fastest_growing_paths(
  history : SnapshotHistory,
  limit : Int,
) -> Array[PathTrend] {
  let growing = path_trends(history).filter(fn(trend) { trend.net_change > 0L })
  if growing.length() > limit {
    growing[:limit].to_owned()
  } else {
    growing
  }
}

///|
pub fn render_history(history : SnapshotHistory, limit? : Int = 10) -> String {
  let lines : Array[String] = [
    "MoonDisk snapshot history",
    "Root: " + history.root,
    "Observations: " + history.snapshots.length().to_string(),
  ]
  match summarize_history(history) {
    None => lines.push("No snapshots have been recorded.")
    Some(summary) => {
      lines.push("First size: " + format_bytes(summary.first_size))
      lines.push("Latest size: " + format_bytes(summary.last_size))
      lines.push("Net change: " + format_bytes(summary.net_change))
      lines.push("Peak size: " + format_bytes(summary.maximum_size))
      lines.push("")
      lines.push("Fastest-growing files")
      for trend in fastest_growing_paths(history, limit) {
        lines.push(
          pad_left(format_bytes(trend.net_change), 12) + "  " + trend.path,
        )
      }
    }
  }
  lines.join("\n")
}

///|
pub fn history_to_json(history : SnapshotHistory) -> String {
  ToJson::to_json(history).stringify(indent=2)
}

///|
pub fn history_from_json(source : String) -> SnapshotHistory raise {
  let history : SnapshotHistory = @json.from_json(@json.parse(source))
  if history.format_version != 1 {
    raise Failure("Unsupported snapshot history version")
  }
  let mut previous = -1L
  for snapshot in history.snapshots {
    if snapshot.root != history.root {
      raise Failure("Snapshot root does not match history root")
    }
    if snapshot.created_at <= previous {
      raise Failure("Snapshot history is not ordered by creation time")
    }
    previous = snapshot.created_at
  }
  history
}

///|
pub async fn write_history(path : String, history : SnapshotHistory) -> Unit {
  @fs.write_file(path, history_to_json(history))
}

///|
pub async fn read_history(path : String) -> SnapshotHistory {
  history_from_json(@fs.read_file(path).text())
}