///| Instruction scanning validates the binary framing of core operations and

///|
/// returns compact call-graph and control-flow facts without executing code.
pub struct InstructionStats {
  instruction_count : Int
  control_depth : Int
  control_block_count : Int
  branch_count : Int
  call_count : Int
  called_functions : Array[Int]
  memory_access_count : Int
  local_access_count : Int
  constant_count : Int
  bulk_memory_instruction_count : Int
  simd_instruction_count : Int
}

///|
pub fn InstructionStats::summary(self : InstructionStats) -> String {
  "instructions=" +
  self.instruction_count.to_string() +
  ", blocks=" +
  self.control_block_count.to_string() +
  ", branches=" +
  self.branch_count.to_string() +
  ", calls=" +
  self.call_count.to_string() +
  ", memory=" +
  self.memory_access_count.to_string() +
  ", locals=" +
  self.local_access_count.to_string() +
  ", constants=" +
  self.constant_count.to_string() +
  ", bulk-memory=" +
  self.bulk_memory_instruction_count.to_string() +
  ", simd=" +
  self.simd_instruction_count.to_string()
}

///|
fn skip_i64(cursor : Cursor) -> Result[Unit, DecodeError] {
  let start = cursor.position
  let mut count = 0
  let mut done = false
  while !done {
    if count == 10 {
      return Err(InvalidLeb128(start, "i64 immediate is too long"))
    }
    match cursor.read_u8() {
      Err(error) => return Err(error)
      Ok(byte) => {
        count = count + 1
        done = (byte & 0x80) == 0
      }
    }
  }
  Ok(())
}

///|
fn skip_block_type(cursor : Cursor) -> Result[Unit, DecodeError] {
  let position = cursor.position
  match cursor.peek_u8() {
    Err(error) => Err(error)
    Ok(0x40)
    | Ok(0x7f)
    | Ok(0x7e)
    | Ok(0x7d)
    | Ok(0x7c)
    | Ok(0x70)
    | Ok(0x6f) => cursor.skip(1)
    Ok(_) =>
      match cursor.read_var_i32() {
        Ok(_) => Ok(())
        Err(_) => Err(InvalidLeb128(position, "invalid block type"))
      }
  }
}

///|
fn skip_memory_immediate(cursor : Cursor) -> Result[Unit, DecodeError] {
  match cursor.read_var_u32() {
    Err(error) => Err(error)
    Ok(_) =>
      match cursor.read_var_u32() {
        Ok(_) => Ok(())
        Err(error) => Err(error)
      }
  }
}

///|
fn skip_br_table(cursor : Cursor) -> Result[Unit, DecodeError] {
  let count = match cursor.read_var_u32() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  for _ in 0..<(count + 1) {
    match cursor.read_var_u32() {
      Ok(_) => ()
      Err(error) => return Err(error)
    }
  }
  Ok(())
}

///|
fn read_two_u32(cursor : Cursor) -> Result[Unit, DecodeError] {
  match cursor.read_var_u32() {
    Err(error) => Err(error)
    Ok(_) =>
      match cursor.read_var_u32() {
        Ok(_) => Ok(())
        Err(error) => Err(error)
      }
  }
}

///|
fn skip_bulk_memory_immediate(cursor : Cursor) -> Result[Bool, DecodeError] {
  let position = cursor.position
  let subopcode = match cursor.read_var_u32() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match subopcode {
    0..=7 => Ok(false)
    8 | 10 | 12 | 14 =>
      match read_two_u32(cursor) {
        Ok(_) => Ok(true)
        Err(error) => Err(error)
      }
    9 | 11 | 13 | 15..=17 =>
      match cursor.read_var_u32() {
        Ok(_) => Ok(true)
        Err(error) => Err(error)
      }
    _ =>
      Err(
        UnsupportedFeature("0xfc subopcode " + subopcode.to_string(), position),
      )
  }
}

///|
fn skip_simd_immediate(cursor : Cursor) -> Result[Unit, DecodeError] {
  let position = cursor.position
  let subopcode = match cursor.read_var_u32() {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  match subopcode {
    0..=11 => skip_memory_immediate(cursor)
    12 | 13 => cursor.skip(16)
    21..=34 => cursor.skip(1)
    14..=20 | 35..=255 => Ok(())
    _ =>
      Err(
        UnsupportedFeature("0xfd subopcode " + subopcode.to_string(), position),
      )
  }
}

///|
fn is_plain_numeric(opcode : Int) -> Bool {
  (opcode >= 0x45 && opcode <= 0xc4) || opcode == 0xd1
}

///|
pub fn scan_instructions(
  bytes : Array[Int],
  span : Span,
) -> Result[InstructionStats, DecodeError] {
  let cursor = Cursor::with_bounds(bytes, span.start, span.end)
  let mut instruction_count = 0
  let mut depth = 0
  let mut blocks = 0
  let mut branches = 0
  let mut calls = 0
  let called_functions : Array[Int] = []
  let mut memory = 0
  let mut locals = 0
  let mut constants = 0
  let mut bulk_memory = 0
  let mut simd = 0
  let mut saw_terminal_end = false
  while !cursor.is_finished() {
    let position = cursor.position
    let opcode = match cursor.read_u8() {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    instruction_count = instruction_count + 1
    match opcode {
      0x00 | 0x01 | 0x0f | 0x1a | 0x1b | 0x1c => ()
      0x02 | 0x03 | 0x04 => {
        match skip_block_type(cursor) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        depth = depth + 1
        blocks = blocks + 1
      }
      0x05 =>
        if depth == 0 {
          return Err(ValidationError(position, "else outside a block"))
        }
      0x0b =>
        if depth == 0 {
          if !cursor.is_finished() {
            return Err(
              ValidationError(position, "bytes follow outer function end"),
            )
          }
          saw_terminal_end = true
        } else {
          depth = depth - 1
        }
      0x0c | 0x0d | 0x14 => {
        match cursor.read_var_u32() {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        if opcode == 0x0c || opcode == 0x0d {
          branches = branches + 1
        }
      }
      0x10 | 0x12 => {
        let callee = match cursor.read_var_u32() {
          Ok(value) => value
          Err(error) => return Err(error)
        }
        called_functions.push(callee)
        calls = calls + 1
      }
      0x0e =>
        match skip_br_table(cursor) {
          Ok(_) => branches = branches + 1
          Err(error) => return Err(error)
        }
      0x11 | 0x13 => {
        match read_two_u32(cursor) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        calls = calls + 1
      }
      0x20 | 0x21 | 0x22 => {
        match cursor.read_var_u32() {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        locals = locals + 1
      }
      0x23 | 0x24 | 0x25 | 0x26 =>
        match cursor.read_var_u32() {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
      0x28..=0x3e => {
        match skip_memory_immediate(cursor) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        memory = memory + 1
      }
      0x3f | 0x40 =>
        match cursor.read_var_u32() {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
      0x41 => {
        match cursor.read_var_i32() {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        constants = constants + 1
      }
      0x42 => {
        match skip_i64(cursor) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        constants = constants + 1
      }
      0x43 => {
        match cursor.skip(4) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        constants = constants + 1
      }
      0x44 => {
        match cursor.skip(8) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
        constants = constants + 1
      }
      0xd0 =>
        match cursor.read_u8() {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
      0xd2 =>
        match cursor.read_var_u32() {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
      0xfc =>
        match skip_bulk_memory_immediate(cursor) {
          Ok(true) => bulk_memory = bulk_memory + 1
          Ok(false) => ()
          Err(error) => return Err(error)
        }
      0xfd =>
        match skip_simd_immediate(cursor) {
          Ok(_) => simd = simd + 1
          Err(error) => return Err(error)
        }
      0xfe | 0xfb =>
        return Err(
          UnsupportedFeature("prefixed opcode " + opcode.to_string(), position),
        )
      _ if is_plain_numeric(opcode) => ()
      _ =>
        return Err(UnsupportedFeature("opcode " + opcode.to_string(), position))
    }
  }
  if !saw_terminal_end || depth != 0 {
    Err(
      ValidationError(
        span.end,
        "function instruction sequence is missing its final end",
      ),
    )
  } else {
    Ok({
      instruction_count,
      control_depth: depth,
      control_block_count: blocks,
      branch_count: branches,
      call_count: calls,
      called_functions,
      memory_access_count: memory,
      local_access_count: locals,
      constant_count: constants,
      bulk_memory_instruction_count: bulk_memory,
      simd_instruction_count: simd,
    })
  }
}

///|
pub fn scan_code_body(
  binary : Module,
  body : CodeBody,
) -> Result[InstructionStats, DecodeError] {
  scan_instructions(binary.bytes, body.instruction_span)
}

///|
pub struct FunctionCallSummary {
  function_index : Int
  callees : Array[Int]
}

///|
pub fn build_call_graph(
  decoded : DecodedModule,
) -> Result[Array[FunctionCallSummary], DecodeError] {
  let summaries : Array[FunctionCallSummary] = []
  let mut function_index = decoded.imported_function_count()
  for body in decoded.code_bodies {
    let stats = match scan_code_body(decoded.binary(), body) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    summaries.push({ function_index, callees: stats.called_functions })
    function_index = function_index + 1
  }
  Ok(summaries)
}