///|
/// Full table index space: imported tables first, then local declarations.
/// Other import kinds never advance this index. Export aliases collapse to the
/// first export position; explicit initializers remain unmodelled even if simple.
priv struct TableInfo {
  index : Int
  imported : Bool
  offset : Int
  mut export_offset : Int?
  initializer_offset : Int?
}

///|
fn table_context(
  data : Bytes,
  sections : Sections,
) -> Result[Array[TableInfo], WasmError] {
  let tables : Array[TableInfo] = []
  match sections.imports {
    Some(section) => {
      let reader = match reader_at_payload(data, section) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let count = match read_u32_as_int(reader) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      for i = 0; i < count; i = i + 1 {
        for j = 0; j < 2; j = j + 1 {
          match skip_name(reader) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
        let offset = reader.position()
        let kind = match reader.read_byte() {
          Ok(value) => value
          Err(error) => return Err(error)
        }
        if kind == 1 {
          match read_table_type(reader) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
          tables.push({
            index: tables.length(),
            imported: true,
            offset,
            export_offset: None,
            initializer_offset: None,
          })
        } else {
          match reader.seek(offset) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
          match skip_import_desc(reader) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
      }
      match finish_section(reader, Import) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
    }
    None => ()
  }
  match sections.tables {
    Some(section) => {
      let definitions = match decode_tables(data, section) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      for table in definitions {
        tables.push({
          index: tables.length(),
          imported: false,
          offset: table.offset,
          export_offset: None,
          initializer_offset: table.initializer_offset,
        })
      }
    }
    None => ()
  }
  match sections.exports {
    Some(section) => {
      let reader = match reader_at_payload(data, section) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let count = match read_u32_as_int(reader) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      for i = 0; i < count; i = i + 1 {
        match skip_name(reader) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        let offset = reader.position()
        let kind = match reader.read_byte() {
          Ok(value) => value
          Err(error) => return Err(error)
        }
        let index = match read_u32_as_int(reader) {
          Ok(value) => value
          Err(error) => return Err(error)
        }
        if kind == 1 &&
          index < tables.length() &&
          tables[index].export_offset is None {
          tables[index].export_offset = Some(offset)
        }
      }
      match finish_section(reader, Export) {
        Ok(_) => ()
        Err(error) => return Err(error)
      }
    }
    None => ()
  }
  Ok(tables)
}

///|
/// Operand order is significant: table.init = [element, table],
/// table.copy = [destination, source]. Reads (get/size) are not writes.
fn written_table(instruction : Instruction) -> Int? {
  match instruction.opcode {
    TableInit => Some(instruction.operands[1])
    TableSet | TableCopy | TableGrow | TableFill =>
      Some(instruction.operands[0])
    _ => None
  }
}

///|
fn table_gap(
  site : IndirectSite,
  opcode : Opcode,
  kind : GraphDiagnosticKind,
  source : GraphDiagnosticSource?,
) -> GraphDiagnostic {
  {
    kind,
    caller: site.caller,
    offset: site.offset,
    opcode,
    type_index: Some(site.type_index),
    table_index: Some(site.table_index),
    source,
  }
}

///|
/// One reason per import/export/initializer and one per distinct writer offset.
/// These sources are independent: known candidates never suppress these gaps.
fn table_diagnostics(
  site : IndirectSite,
  opcode : Opcode,
  tables : Array[TableInfo],
  writes : Map[Int, Array[GraphDiagnosticSource]],
  diagnostics : Array[GraphDiagnostic],
) -> Unit {
  if site.table_index >= tables.length() {
    diagnostics.push(table_gap(site, opcode, UnknownTableContents, None))
  } else {
    let table = tables[site.table_index]
    // Assert the context's complete index space by construction, not local ids.
    if table.index == site.table_index {
      if table.imported {
        diagnostics.push(
          table_gap(
            site,
            opcode,
            ImportedTableContents,
            Some({ caller: None, offset: table.offset, opcode: None, }),
          ),
        )
      }
      match table.export_offset {
        Some(offset) =>
          diagnostics.push(
            table_gap(
              site,
              opcode,
              ExportedTableContents,
              Some({ caller: None, offset, opcode: None, }),
            ),
          )
        None => ()
      }
      match table.initializer_offset {
        Some(offset) =>
          diagnostics.push(
            table_gap(
              site,
              opcode,
              UnmodeledTableInitializer,
              Some({ caller: None, offset, opcode: None, }),
            ),
          )
        None => ()
      }
    } else {
      diagnostics.push(table_gap(site, opcode, UnknownTableContents, None))
    }
  }
  match writes.get(site.table_index) {
    Some(sources) =>
      for source in sources {
        diagnostics.push(
          table_gap(site, opcode, RuntimeTableMutation, Some(source)),
        )
      }
    None => ()
  }
}