///|
/// Errors in story data or execution are reported rather than indexing unchecked memory.
pub(all) suberror Fault {
  Invalid(String)
}

///|
/// A resumable interpreter. File and terminal I/O belong to the embedding host.
struct Machine {
  mem : Array[Int32]
  original : Bytes
  version : Int32
  static_base : Int32
  globals : Int32
  objects : Int32
  abbreviations : Int32
  mut pc : Int32
  frames : Array[Frame]
  mut output : StringBuilder
  mut halted : Bool
  mut waiting : Int32
  mut input_buffer : Int32
  mut parse_buffer : Int32
  mut input_store : Int32
  mut rng : Int32
  seed_source : () -> Int32
  streams : Array[Int32]
  mut screen : Bool
  undo : Array[Saved]
  upper : Array[Int32]
  rows : Array[Int32]
  columns : Array[Int32]
  mut window : Int32
  mut split : Int32
  mut font : Int32
  mut output_count : Int32
}

///|
priv struct Saved {
  memory : Array[Int32]
  frames : Array[Frame]
  pc : Int32
}

///|
priv struct Frame {
  locals : Array[Int32]
  stack : Array[Int32]
  return_pc : Int32
  store : Int32
  argc : Int32
}

///|
/// Load a raw v3 or v5 story. Blorb containers must be extracted by the host.
/// Hosts can provide fresh seeds for normal play and @random 0. The default
/// seed source is deterministic for reproducible embedding and tests.
pub fn Machine::new(
  story : Bytes,
  seed_source~ : () -> Int32 = fn() { 1 },
) -> Machine raise Fault {
  if story.length() < 64 {
    raise Invalid("truncated header")
  }
  let mem = Array::makei(story.length(), i => story[i].to_int())
  let version = mem[0]
  if version != 3 && version != 5 {
    raise Invalid("only story versions 3 and 5 are supported")
  }
  let word = fn(i) { mem[i] * 256 + mem[i + 1] }
  let static_base = word(14)
  let globals = word(12)
  let objects = word(10)
  let pc = word(6)
  let max_size = if version == 3 { 131072 } else { 262144 }
  if story.length() > max_size ||
    static_base < 64 ||
    static_base > story.length() ||
    globals < 64 ||
    globals > static_base - 480 ||
    objects < 64 ||
    objects >= static_base ||
    pc < 64 ||
    pc >= story.length() {
    raise Invalid("invalid memory map")
  }
  let m = Machine::{
    mem,
    original: story,
    version,
    static_base,
    globals,
    objects,
    abbreviations: word(24),
    pc,
    frames: [{ locals: [], stack: [], return_pc: 0, store: -1, argc: 0, }],
    output: StringBuilder(),
    halted: false,
    waiting: 0,
    input_buffer: 0,
    parse_buffer: 0,
    input_store: -1,
    rng: seed_source() & 65535,
    seed_source,
    streams: [],
    screen: true,
    undo: [],
    upper: Array::make(2000, 32),
    rows: [1, 1],
    columns: [1, 1],
    window: 0,
    split: 0,
    font: 1,
    output_count: 0,
  }
  // Advertise only implemented capabilities. No timed input, sound, graphics or mouse.
  m.mem[1] = if version == 3 { (m.mem[1] & 0x8f) | 0x30 } else { 0 }
  m.mem[16] = m.mem[16] & 0x01
  m.mem[17] = m.mem[17] & 0x53
  m.mem[30] = 6
  m.mem[31] = 65
  m.mem[32] = 25
  m.mem[33] = 80
  m.mem[34] = 0
  m.mem[35] = 80
  m.mem[36] = 0
  m.mem[37] = 25
  m.mem[38] = 1
  m.mem[39] = 1
  m.mem[44] = 2
  m.mem[45] = 9
  // Standard revision stays zero until full conformance has been established.
  m.mem[50] = 0
  m.mem[51] = 0
  m
}

///|
fn Machine::byte(self : Machine, addr : Int32) -> Int32 raise Fault {
  if addr < 0 || addr >= self.mem.length() {
    raise Invalid("read outside story at \{addr}")
  }
  self.mem[addr]
}

///|
fn Machine::word(self : Machine, addr : Int32) -> Int32 raise Fault {
  let hi = self.byte(addr)
  hi * 256 + self.byte(addr + 1)
}

///|
fn Machine::put_byte(
  self : Machine,
  addr : Int32,
  value : Int32,
) -> Unit raise Fault {
  if addr < 0 || addr >= self.static_base {
    raise Invalid("write outside dynamic memory at \{addr}")
  }
  self.mem[addr] = value & 255
}

///|
fn Machine::put_word(
  self : Machine,
  addr : Int32,
  value : Int32,
) -> Unit raise Fault {
  if addr < 0 || addr >= self.static_base - 1 {
    raise Invalid("word write outside dynamic memory")
  }
  self.mem[addr] = (value >> 8) & 255
  self.mem[addr + 1] = value & 255
}

///|
fn Machine::fetch(self : Machine) -> Int32 raise Fault {
  let value = self.byte(self.pc)
  self.pc += 1
  value
}

///|
fn signed(value : Int32) -> Int32 {
  if (value & 32768) != 0 {
    (value & 65535) - 65536
  } else {
    value & 65535
  }
}

///|
fn Machine::frame(self : Machine) -> Frame {
  self.frames[self.frames.length() - 1]
}

///|
fn Machine::variable(
  self : Machine,
  number : Int32,
  peek~ : Bool = false,
) -> Int32 raise Fault {
  let f = self.frame()
  if number == 0 {
    if f.stack.is_empty() {
      raise Invalid("evaluation stack underflow")
    }
    if peek {
      f.stack[f.stack.length() - 1]
    } else {
      f.stack.pop().unwrap()
    }
  } else if number < 16 {
    if number < 0 || number > f.locals.length() {
      raise Invalid("invalid local variable")
    }
    f.locals[number - 1]
  } else {
    if number > 255 {
      raise Invalid("invalid global variable")
    }
    self.word(self.globals + 2 * (number - 16))
  }
}

///|
fn Machine::assign(
  self : Machine,
  number : Int32,
  value : Int32,
  replace~ : Bool = false,
) -> Unit raise Fault {
  let f = self.frame()
  let value = value & 65535
  if number == 0 {
    if replace {
      if f.stack.is_empty() {
        raise Invalid("evaluation stack underflow")
      }
      f.stack[f.stack.length() - 1] = value
    } else {
      if f.stack.length() >= 65536 {
        raise Invalid("evaluation stack limit")
      }
      f.stack.push(value)
    }
  } else if number < 16 {
    if number < 0 || number > f.locals.length() {
      raise Invalid("invalid local variable")
    }
    f.locals[number - 1] = value
  } else {
    if number > 255 {
      raise Invalid("invalid global variable")
    }
    self.put_word(self.globals + 2 * (number - 16), value)
  }
}

///|
fn Machine::store(self : Machine, value : Int32) -> Unit raise Fault {
  let dest = self.fetch()
  self.assign(dest, value)
}

///|
fn Machine::ret(self : Machine, value : Int32) -> Unit raise Fault {
  if self.frames.length() == 1 {
    raise Invalid("return from initial execution context")
  }
  let f = self.frames.pop().unwrap()
  self.pc = f.return_pc
  if f.store >= 0 {
    self.assign(f.store, value)
  }
}

///|
fn Machine::branch(self : Machine, condition : Bool) -> Unit raise Fault {
  let b = self.fetch()
  let offset = if (b & 64) != 0 {
    b & 63
  } else {
    let n = ((b & 63) << 8) | self.fetch()
    if (n & 8192) != 0 {
      n - 16384
    } else {
      n
    }
  }
  if condition == ((b & 128) != 0) {
    if offset <= 1 && offset >= 0 {
      self.ret(offset)
    } else {
      self.pc += offset - 2
    }
  }
}

///|
fn Machine::call(
  self : Machine,
  args : Array[Int32],
  stores : Bool,
) -> Unit raise Fault {
  if args.is_empty() {
    raise Invalid("call without routine")
  }
  let dest = if stores { self.fetch() } else { -1 }
  if args[0] == 0 {
    if dest >= 0 {
      self.assign(dest, 0)
    }
    return
  }
  if self.frames.length() >= 4096 {
    raise Invalid("call stack limit")
  }
  let return_pc = self.pc
  self.pc = args[0] * (if self.version == 3 { 2 } else { 4 })
  let count = self.fetch()
  if count > 15 {
    raise Invalid("invalid routine local count")
  }
  let locals = Array::make(count, 0)
  for i in 0.. String {
  let result = self.output.to_string()
  self.output = StringBuilder()
  self.output_count = 0
  result
}

///|
/// Inspect execution position for diagnostics.
pub fn Machine::position(self : Machine) -> Int32 {
  self.pc
}

///|
/// True after @quit.
pub fn Machine::is_halted(self : Machine) -> Bool {
  self.halted
}