///|
// Route B object emission: turn the binary emitter's output into a Mach-O
// object with real relocations (BRANCH26 for calls, PAGE21/PAGEOFF12 for
// global addresses, UNSIGNED for data pointers). This is what lets --emit obj
// stop depending on clang.

///|
fn arm64_obj_bytes(a : Array[Int]) -> Bytes {
  let out : Array[Byte] = []
  for b in a {
    out.push(b.to_byte())
  }
  Bytes::from_array(out[:])
}

///|
// Every relocation target must have a symbol table entry; defined targets point
// at their section, undefined ones (libc calls) are N_UNDF/N_EXT.
fn arm64_obj_sym(
  syms : Array[@object.MachoSym],
  index : Map[String, Int],
  name : String,
  sect : Int,
  value : Int,
  is_ext : Bool,
) -> Int {
  match index.get(name) {
    Some(i) => i
    None => {
      let i = syms.length()
      index[name] = i
      syms.push(@object.MachoSym::{ name: "_" + name, sect, value, is_ext, })
      i
    }
  }
}

///|
pub fn emit_arm64_object(
  funcs : Array[@types.Fn],
  datas : Array[@types.Dat],
  interner : @util.Interner,
  apple : Bool,
) -> Bytes raise {
  arm64_emit_reset()
  let text_words : Array[Int] = []
  let fns : Array[(String, Int)] = []
  let calls : Array[(Int, String)] = []
  let addrs : Array[(Int, Int, Int, String, Int)] = []
  let defined : Array[String] = []
  for fn_ in funcs {
    defined.push(fn_.name)
  }
  for d in datas {
    defined.push(d.str)
  }
  for fn_ in funcs {
    let (w, c, a) = emit_arm64_bin_fn_ex(fn_, interner, apple, defined)
    let off = text_words.length()
    fns.push((fn_.name, off))
    for x in w {
      text_words.push(x)
    }
    for x in c {
      calls.push((off + x.0, x.1))
    }
    for x in a {
      addrs.push((off + x.0, off + x.1, x.2, x.3, x.4))
    }
  }
  let (datas_map, data_bytes0, data_refs) = arm64_bin_layout_data(datas)
  // Pointer entries are relocatable in an object file: zero the addend and let
  // the linker fill in the address via an UNSIGNED relocation.
  let data_bytes = data_bytes0.copy()

  let syms : Array[@object.MachoSym] = []
  let index : Map[String, Int] = Map([])
  for f in fns {
    let mut is_ext = false
    for fn_ in funcs {
      if fn_.name == f.0 && fn_.is_export {
        is_ext = true
      }
    }
    ignore(arm64_obj_sym(syms, index, f.0, 1, f.1 * 4, is_ext))
  }
  for d in datas_map {
    let mut is_ext = false
    for dt in datas {
      if dt.is_export && dt.str == d.0 {
        is_ext = true
      }
    }
    ignore(arm64_obj_sym(syms, index, d.0, 2, d.1, is_ext))
  }
  // Floating-point constants live in __TEXT,__const as local LfpN symbols.
  let (fp_map, fp_bytes) = arm64_fp_layout()
  for p in fp_map {
    index[p.0] = syms.length()
    syms.push(@object.MachoSym::{
      name: "L" + p.0,
      sect: 3,
      value: p.1,
      is_ext: false,
    })
  }

  let data_relocs : Array[@object.MachoReloc] = []
  for r in data_refs {
    let w = r.2
    // A non-pcrel ARM64_RELOC_UNSIGNED takes its addend from the bytes at the
    // relocation site, so store the symbol-relative offset there.
    let mut v = r.3
    if v < 0 {
      v = v + (1 << (w * 8))
    }
    for i in 0..> (i * 8)) & 0xFF
    }
    let i = arm64_obj_sym(syms, index, r.1, 0, 0, true)
    data_relocs.push(@object.MachoReloc::{
      offset: r.0,
      sym: i,
      pcrel: false,
      length: if w == 8 {
        3
      } else {
        2
      },
      rtype: @object.ARM64_RELOC_UNSIGNED,
      is_extern: true,
    })
  }

  let text_relocs : Array[@object.MachoReloc] = []
  for c in calls {
    // Calls always go through a relocation so the linker can resolve them,
    // whether the callee is in this object or external.
    let i = arm64_obj_sym(syms, index, c.1, 0, 0, true)
    text_relocs.push(@object.MachoReloc::{
      offset: c.0 * 4,
      sym: i,
      pcrel: true,
      length: 2,
      rtype: @object.ARM64_RELOC_BRANCH26,
      is_extern: true,
    })
  }
  for a in addrs {
    // A directly referenced symbol that is defined in this object can use a
    // plain adrp/add; an external one must go through the GOT.
    let is_def = defined.contains(a.3)
    let i = arm64_obj_sym(syms, index, a.3, 0, 0, true)
    text_relocs.push(@object.MachoReloc::{
      offset: a.0 * 4,
      sym: i,
      pcrel: true,
      length: 2,
      rtype: if is_def {
        @object.ARM64_RELOC_PAGE21
      } else {
        @object.ARM64_RELOC_GOT_LOAD_PAGE21
      },
      is_extern: true,
    })
    if a.4 != 0 {
      text_relocs.push(@object.MachoReloc::{
        offset: a.1 * 4,
        sym: a.4,
        pcrel: false,
        length: 2,
        rtype: @object.ARM64_RELOC_ADDEND,
        is_extern: false,
      })
    }
    text_relocs.push(@object.MachoReloc::{
      offset: a.1 * 4,
      sym: i,
      pcrel: false,
      length: 2,
      rtype: if is_def {
        @object.ARM64_RELOC_PAGEOFF12
      } else {
        @object.ARM64_RELOC_GOT_LOAD_PAGEOFF12
      },
      is_extern: true,
    })
  }

  let sections : Array[@object.MachoSection] = [
    @object.MachoSection::{
      name: "__text",
      seg: "__TEXT",
      data: @object.words_to_bytes(text_words),
      align: 2,
      flags: @object.section_flags_text(),
      relocs: text_relocs,
    },
    @object.MachoSection::{
      name: "__data",
      seg: "__DATA",
      data: arm64_obj_bytes(data_bytes),
      align: 3,
      flags: @object.section_flags_data(),
      relocs: data_relocs,
    },
    @object.MachoSection::{
      name: "__const",
      seg: "__TEXT",
      data: arm64_obj_bytes(fp_bytes),
      align: 4,
      flags: @object.section_flags_data(),
      relocs: [],
    },
  ]
  @object.write_object(sections, syms)
}