///|
/// Aggregate shape of one traversed SQLite B-tree.
pub(all) struct BtreeStatistics {
  root_page : UInt64
  total_pages : Int
  interior_pages : Int
  leaf_pages : Int
  table_pages : Int
  index_pages : Int
  total_cells : Int
  maximum_depth : Int
  maximum_children : Int
} derive(Eq, Debug)

///|
pub fn summarize_btree(traversal : BtreeTraversal) -> BtreeStatistics {
  let mut interior_pages = 0
  let mut leaf_pages = 0
  let mut table_pages = 0
  let mut index_pages = 0
  let mut total_cells = 0
  let mut maximum_depth = 0
  let mut maximum_children = 0
  for visit in traversal.visits {
    if visit.page_type.is_interior() {
      interior_pages = interior_pages + 1
    } else {
      leaf_pages = leaf_pages + 1
    }
    if visit.page_type.is_table() {
      table_pages = table_pages + 1
    } else {
      index_pages = index_pages + 1
    }
    total_cells = total_cells + visit.cell_count
    if visit.depth > maximum_depth {
      maximum_depth = visit.depth
    }
    if visit.child_pages.length() > maximum_children {
      maximum_children = visit.child_pages.length()
    }
  }
  {
    root_page: traversal.root_page,
    total_pages: traversal.visits.length(),
    interior_pages,
    leaf_pages,
    table_pages,
    index_pages,
    total_cells,
    maximum_depth,
    maximum_children,
  }
}

///|
pub fn BtreeStatistics::is_single_page(self : BtreeStatistics) -> Bool {
  self.total_pages == 1 && self.maximum_depth == 0
}

///|
pub fn BtreeStatistics::average_cells_per_page(
  self : BtreeStatistics,
) -> Double {
  if self.total_pages == 0 {
    0.0
  } else {
    self.total_cells.to_double() / self.total_pages.to_double()
  }
}

///|
/// Every occurrence of one database page in a WAL, including frames that do
/// not become visible in the newest committed snapshot.
pub(all) struct WalPageHistory {
  page_number : UInt64
  frame_indexes : Array[Int]
  valid_frame_indexes : Array[Int]
  mut committed_frame : Int?
  uncommitted_frame_indexes : Array[Int]
} derive(Eq, Debug)

///|
pub fn WalPageHistory::occurrence_count(self : WalPageHistory) -> Int {
  self.frame_indexes.length()
}

///|
pub fn WalPageHistory::valid_occurrence_count(self : WalPageHistory) -> Int {
  self.valid_frame_indexes.length()
}

///|
pub fn WalPageHistory::has_uncommitted_change(self : WalPageHistory) -> Bool {
  self.uncommitted_frame_indexes.length() > 0
}

///|
pub fn WalPageHistory::latest_valid_frame(self : WalPageHistory) -> Int? {
  if self.valid_frame_indexes.length() == 0 {
    None
  } else {
    Some(self.valid_frame_indexes[self.valid_frame_indexes.length() - 1])
  }
}

///|
fn find_page_history(
  histories : Array[WalPageHistory],
  page_number : UInt64,
) -> Int? {
  for index = 0; index < histories.length(); index = index + 1 {
    if histories[index].page_number == page_number {
      return Some(index)
    }
  }
  None
}

///|
pub fn WalFile::page_histories(self : WalFile) -> Array[WalPageHistory] {
  let histories : Array[WalPageHistory] = []
  let commit_limit = match self.last_commit_frame {
    Some(value) => value
    None => 0
  }
  for frame in self.frames {
    let history_index = match find_page_history(histories, frame.page_number) {
      Some(value) => value
      None => {
        histories.push({
          page_number: frame.page_number,
          frame_indexes: [],
          valid_frame_indexes: [],
          committed_frame: None,
          uncommitted_frame_indexes: [],
        })
        histories.length() - 1
      }
    }
    let history = histories[history_index]
    history.frame_indexes.push(frame.index)
    if frame.is_valid() {
      history.valid_frame_indexes.push(frame.index)
      if frame.index <= commit_limit {
        history.committed_frame = Some(frame.index)
      } else {
        history.uncommitted_frame_indexes.push(frame.index)
      }
    }
  }
  histories
}

///|
pub fn WalFile::committed_transaction_count(self : WalFile) -> Int {
  let mut count = 0
  for transaction in self.transactions {
    if transaction.committed {
      count = count + 1
    }
  }
  count
}

///|
pub fn WalFile::uncommitted_frame_count(self : WalFile) -> Int {
  let mut count = 0
  for history in self.page_histories() {
    count = count + history.uncommitted_frame_indexes.length()
  }
  count
}

///|
pub(all) struct SnapshotStatistics {
  commit_frame : Int
  logical_page_count : UInt64
  changed_pages : Int
  changed_bytes : Int
  changed_ranges : Int
  pages_added_by_wal : Int
  largest_page_change : Int
} derive(Eq, Debug)

///|
pub fn summarize_snapshot(
  snapshot : SnapshotView,
) -> SnapshotStatistics raise ParseError {
  let differences = snapshot.differences()
  let mut changed_bytes = 0
  let mut changed_ranges = 0
  let mut pages_added_by_wal = 0
  let mut largest_page_change = 0
  for difference in differences {
    changed_bytes = changed_bytes + difference.changed_bytes
    changed_ranges = changed_ranges + difference.ranges.length()
    if difference.page_number > snapshot.database.page_count {
      pages_added_by_wal = pages_added_by_wal + 1
    }
    if difference.changed_bytes > largest_page_change {
      largest_page_change = difference.changed_bytes
    }
  }
  {
    commit_frame: snapshot.max_frame,
    logical_page_count: snapshot.logical_page_count,
    changed_pages: differences.length(),
    changed_bytes,
    changed_ranges,
    pages_added_by_wal,
    largest_page_change,
  }
}

///|
pub fn SnapshotStatistics::is_unchanged(self : SnapshotStatistics) -> Bool {
  self.changed_pages == 0 && self.changed_bytes == 0
}

///|
pub fn SnapshotStatistics::average_changed_bytes_per_page(
  self : SnapshotStatistics,
) -> Double {
  if self.changed_pages == 0 {
    0.0
  } else {
    self.changed_bytes.to_double() / self.changed_pages.to_double()
  }
}