///|
/// One verifier finding. `cluster` is 0 when the issue is volume-wide.
pub(all) struct VerifyIssue {
  code : String
  message : String
  cluster : Int
} derive(Eq, Debug)

///|
pub extend VerifyIssue with Eq::{equal, not_equal}

///|
pub extend VerifyIssue with @debug.Debug::{to_repr}

///|
fn issue(code : String, message : String, cluster : Int) -> VerifyIssue {
  { code, message, cluster, }
}

///|
pub fn Volume::verify(self : Volume) -> Array[VerifyIssue] {
  let issues : Array[VerifyIssue] = []
  match parse_layout(self.data) {
    Err(err) => {
      issues.push(issue("VFY001", err.message(), 0))
      return issues
    }
    Ok(_) => ()
  }
  if self.data.length() != self.total_bytes() {
    issues.push(
      issue(
        "VFY002",
        "image length \{self.data.length()} does not match \{self.total_bytes()} byte layout",
        0,
      ),
    )
    // Stop before FAT-copy comparison or directory indexing on a short image.
    if self.data.length() < self.total_bytes() {
      return issues
    }
  }
  match self.layout.kind {
    Fat32 =>
      if self.layout.root_cluster < 2 ||
        self.layout.root_cluster > self.layout.cluster_count + 1 {
        issues.push(
          issue(
            "VFY003",
            "FAT32 root cluster is invalid",
            self.layout.root_cluster,
          ),
        )
      }
    _ => ()
  }
  let used = Array::make(self.layout.cluster_count + 2, 0)
  self.verify_fat(issues, used)
  // FAT32 stores the root in an ordinary cluster chain, without a parent entry.
  // Claim every root cluster before traversing directory entries.
  if self.layout.kind == Fat32 {
    match follow_chain(self.data, self.layout, self.layout.root_cluster) {
      Err(err) => {
        issues.push(
          issue("VFY020", "/: \{err.message()}", self.layout.root_cluster),
        )
        self.verify_lost_clusters(issues, used)
        return issues
      }
      Ok(chain) => {
        let _ = self.mark_chain(chain, "/", issues, used)
      }
    }
  }
  self.verify_tree(root_cluster_of(self.layout), "/", issues, used)
  self.verify_lost_clusters(issues, used)
  issues
}

///|
fn Volume::verify_fat(
  self : Volume,
  issues : Array[VerifyIssue],
  used : Array[Int],
) -> Unit {
  let media_entry = match read_fat_raw(self.data, self.layout, 0) {
    Err(err) => {
      issues.push(issue("VFY010", err.message(), 0))
      return
    }
    Ok(value) => value
  }
  if (media_entry & 0xFF) != self.layout.media.to_int() {
    issues.push(
      issue(
        "VFY011",
        "FAT[0] media nibble \{media_entry & 0xFF} != BPB media \{self.layout.media.to_int()}",
        0,
      ),
    )
  }
  match read_fat_raw(self.data, self.layout, 1) {
    Err(err) => issues.push(issue("VFY012", err.message(), 1))
    Ok(_) => ()
  }
  for cluster = 2
      cluster <= self.layout.cluster_count + 1
      cluster = cluster + 1 {
    match read_fat_raw(self.data, self.layout, cluster) {
      Err(err) => issues.push(issue("VFY013", err.message(), cluster))
      Ok(value) =>
        if self.layout.kind.is_bad(value) {
          used[cluster] = 2
        } else if value == 0 {
          used[cluster] = 0
        } else if self.layout.kind.is_eoc(value) ||
          (value >= 2 && value <= self.layout.cluster_count + 1) {
          used[cluster] = 1
        } else {
          issues.push(
            issue(
              "VFY014",
              "FAT[\{cluster}] has out-of-range next \{value}",
              cluster,
            ),
          )
        }
    }
  }
  if self.layout.fat_count >= 2 {
    let len = self.layout.fat_bytes()
    let a = fat_base(self.layout, 0)
    let b = fat_base(self.layout, 1)
    let mut differ = 0
    for i = 0; i < len; i = i + 1 {
      if self.data[a + i] != self.data[b + i] {
        differ = differ + 1
      }
    }
    if differ > 0 {
      issues.push(
        issue("VFY015", "FAT copy 0 and 1 differ in \{differ} bytes", 0),
      )
    }
  }
}

///|
fn Volume::verify_tree(
  self : Volume,
  dir_cluster : Int,
  path : String,
  issues : Array[VerifyIssue],
  used : Array[Int],
) -> Unit {
  // An explicit worklist bounds traversal even for deeply nested images.
  let pending : Array[(Int, String)] = [(dir_cluster, path)]
  let mut next = 0
  while next < pending.length() {
    let (dir_cluster, path) = pending[next]
    next = next + 1
    match read_directory(self.data, self.layout, dir_cluster) {
      Err(err) =>
        issues.push(issue("VFY020", "\{path}: \{err.message()}", dir_cluster))
      Ok(records) =>
        for i = 0; i < records.length(); i = i + 1 {
          let record = records[i]
          if record.is_dot() {
            continue
          }
          if record.lfn_offsets.length() > 0 {
            let checksum = lfn_checksum(record.short_name)
            for j = 0; j < record.lfn_offsets.length(); j = j + 1 {
              match u8_at(self.data, record.lfn_offsets[j] + 13) {
                Err(_) =>
                  issues.push(
                    issue("VFY021", "truncated LFN at \{path}", dir_cluster),
                  )
                Ok(found) =>
                  if found != checksum {
                    issues.push(
                      issue(
                        "VFY022",
                        "\{path}/\{record.name} LFN checksum mismatch",
                        dir_cluster,
                      ),
                    )
                  }
              }
            }
          }
          if record.cluster == 0 &&
            record.size > 0 &&
            !record.is_dir() &&
            !record.is_volume_id() {
            issues.push(
              issue(
                "VFY023",
                "\{path}/\{record.name} has size \{record.size} but cluster 0",
                0,
              ),
            )
          }
          if record.is_dir() && record.cluster == 0 && !record.is_volume_id() {
            issues.push(
              issue(
                "VFY026",
                "\{path}/\{record.name} directory has cluster 0",
                0,
              ),
            )
          }
          if record.cluster != 0 && !record.is_volume_id() {
            match follow_chain(self.data, self.layout, record.cluster) {
              Err(err) =>
                issues.push(
                  issue(
                    "VFY024",
                    "\{path}/\{record.name}: \{err.message()}",
                    record.cluster,
                  ),
                )
              Ok(chain) => {
                let exclusive = self.mark_chain(
                  chain,
                  join_path(path, record.name),
                  issues,
                  used,
                )
                if !record.is_dir() {
                  let capacity = chain_byte_length(self.layout, chain)
                  if record.size > capacity {
                    issues.push(
                      issue(
                        "VFY025",
                        "\{path}/\{record.name} size \{record.size} exceeds chain \{capacity}",
                        record.cluster,
                      ),
                    )
                  }
                }
                // A shared chain can point to an ancestor or to this directory.
                // Report the cross-link, but never expand it a second time.
                if record.is_dir() && exclusive {
                  pending.push((record.cluster, join_path(path, record.name)))
                }
              }
            }
          }
        }
    }
  }
}

///|
fn Volume::mark_chain(
  self : Volume,
  chain : Array[Int],
  path : String,
  issues : Array[VerifyIssue],
  used : Array[Int],
) -> Bool {
  let _ = self
  let mut exclusive = true
  for i = 0; i < chain.length(); i = i + 1 {
    let cluster = chain[i]
    if cluster < 2 || cluster >= used.length() {
      issues.push(
        issue("VFY030", "\{path} uses invalid cluster \{cluster}", cluster),
      )
      exclusive = false
      continue
    }
    if used[cluster] == 3 {
      exclusive = false
      issues.push(
        issue("VFY031", "\{path} cross-links cluster \{cluster}", cluster),
      )
    } else if used[cluster] == 2 {
      exclusive = false
      issues.push(
        issue("VFY032", "\{path} uses a bad cluster \{cluster}", cluster),
      )
    } else {
      used[cluster] = 3
    }
  }
  exclusive
}

///|
fn Volume::verify_lost_clusters(
  self : Volume,
  issues : Array[VerifyIssue],
  used : Array[Int],
) -> Unit {
  let mut lost = 0
  for cluster = 2
      cluster <= self.layout.cluster_count + 1
      cluster = cluster + 1 {
    if used[cluster] == 1 {
      lost = lost + 1
    }
  }
  if lost > 0 {
    issues.push(
      issue(
        "VFY040",
        "\{lost} allocated clusters are not reachable from directories",
        0,
      ),
    )
  }
}

///|
pub fn Volume::issue_codes(self : Volume) -> Array[String] {
  let issues = self.verify()
  let codes : Array[String] = []
  for i = 0; i < issues.length(); i = i + 1 {
    codes.push(issues[i].code)
  }
  codes
}