///|
/// DHCP ports.
pub let dhcp_server_port : Int = 67

///|
pub let dhcp_client_port : Int = 68

///|
/// DHCP message types.
pub let msg_discover : Int = 1

///|
pub let msg_offer : Int = 2

///|
pub let msg_request : Int = 3

///|
pub let msg_decline : Int = 4

///|
pub let msg_ack : Int = 5

///|
pub let msg_nak : Int = 6

///|
pub let msg_release : Int = 7

///|
pub let msg_inform : Int = 8

///|
/// DHCP option codes.
pub let opt_subnet_mask : Int = 1

///|
pub let opt_router : Int = 3

///|
pub let opt_dns : Int = 6

///|
pub let opt_hostname : Int = 12

///|
pub let opt_requested_ip : Int = 50

///|
pub let opt_lease_time : Int = 51

///|
pub let opt_message_type : Int = 53

///|
pub let opt_server_id : Int = 54

///|
pub let opt_param_request_list : Int = 55

///|
pub let opt_vendor_class_id : Int = 60

///|
pub let opt_client_id : Int = 61

///|
pub let opt_end : Int = 255

///|
/// DHCP magic cookie.
pub let magic_cookie : Int = 0x63825363

///|
/// BOOTREQUEST / BOOTREPLY opcodes.
pub let boot_request : Int = 1

///|
pub let boot_reply : Int = 2

///|
fn push_u16_be(output : Array[Byte], value : Int) -> Unit {
  output.push(((value >> 8) & 0xFF).to_byte())
  output.push((value & 0xFF).to_byte())
}

///|
fn push_u32_be(output : Array[Byte], value : Int) -> Unit {
  output.push(((value >> 24) & 0xFF).to_byte())
  output.push(((value >> 16) & 0xFF).to_byte())
  output.push(((value >> 8) & 0xFF).to_byte())
  output.push((value & 0xFF).to_byte())
}

///|
fn read_u16_be(data : Bytes, offset : Int) -> Int {
  (data[offset].to_int() << 8) + data[offset + 1].to_int()
}

///|
fn read_u32_be(data : Bytes, offset : Int) -> Int {
  (data[offset].to_int() << 24) +
  (data[offset + 1].to_int() << 16) +
  (data[offset + 2].to_int() << 8) +
  data[offset + 3].to_int()
}

///|
fn append_bytes(output : Array[Byte], data : Bytes) -> Unit {
  for byte in data {
    output.push(byte)
  }
}

///|
/// A single DHCP option.
pub(all) struct DhcpOption {
  code : Int
  data : Bytes
} derive(Eq, Debug)

///|
/// DHCP message.
pub(all) struct DhcpMessage {
  op : Int // 1=request, 2=reply
  htype : Int // 1=ethernet
  hlen : Int // 6
  hops : Int
  xid : Int
  secs : Int
  flags : Int
  ciaddr : Int // client IP
  yiaddr : Int // your IP
  siaddr : Int // server IP
  giaddr : Int // gateway IP
  chaddr : Bytes // 16 bytes (MAC + padding)
  sname : Bytes // 64 bytes
  file : Bytes // 128 bytes
  options : Array[DhcpOption]
} derive(Eq, Debug)

///|
/// Create a DHCP Discover message.
pub fn build_discover(
  mac : Bytes,
  xid : Int,
  hostname? : String = "",
) -> DhcpMessage {
  let chaddr : Array[Byte] = []
  append_bytes(chaddr, mac)
  while chaddr.length() < 16 {
    chaddr.push(b'\x00')
  }
  let options : Array[DhcpOption] = []
  // Message type = Discover
  options.push(DhcpOption::{
    code: opt_message_type,
    data: Bytes::from_array([b'\x01']),
  })
  // Client ID (type=1 + MAC)
  let cid : Array[Byte] = [b'\x01']
  append_bytes(cid, mac)
  options.push(DhcpOption::{ code: opt_client_id, data: Bytes::from_array(cid) })
  // Hostname
  if hostname != "" {
    let hbytes = hostname.to_array().map(fn(c) { c.to_int().to_byte() })
    options.push(DhcpOption::{
      code: opt_hostname,
      data: Bytes::from_array(hbytes),
    })
  }
  // Parameter request list
  options.push(DhcpOption::{
    code: opt_param_request_list,
    data: Bytes::from_array([
      opt_subnet_mask.to_byte(),
      opt_router.to_byte(),
      opt_dns.to_byte(),
    ]),
  })
  DhcpMessage::{
    op: boot_request,
    htype: 1,
    hlen: 6,
    hops: 0,
    xid,
    secs: 0,
    flags: 0x8000, // broadcast flag
    ciaddr: 0,
    yiaddr: 0,
    siaddr: 0,
    giaddr: 0,
    chaddr: Bytes::from_array(chaddr),
    sname: Bytes::default(),
    file: Bytes::default(),
    options,
  }
}

///|
/// Encode a DHCP message to bytes.
pub fn encode_dhcp_message(msg : DhcpMessage) -> Bytes {
  let output : Array[Byte] = []
  output.push(msg.op.to_byte())
  output.push(msg.htype.to_byte())
  output.push(msg.hlen.to_byte())
  output.push(msg.hops.to_byte())
  push_u32_be(output, msg.xid)
  push_u16_be(output, msg.secs)
  push_u16_be(output, msg.flags)
  push_u32_be(output, msg.ciaddr)
  push_u32_be(output, msg.yiaddr)
  push_u32_be(output, msg.siaddr)
  push_u32_be(output, msg.giaddr)
  // chaddr - 16 bytes
  for i = 0; i < 16; i = i + 1 {
    if i < msg.chaddr.length() {
      output.push(msg.chaddr[i])
    } else {
      output.push(b'\x00')
    }
  }
  // sname - 64 bytes
  for i = 0; i < 64; i = i + 1 {
    if i < msg.sname.length() {
      output.push(msg.sname[i])
    } else {
      output.push(b'\x00')
    }
  }
  // file - 128 bytes
  for i = 0; i < 128; i = i + 1 {
    if i < msg.file.length() {
      output.push(msg.file[i])
    } else {
      output.push(b'\x00')
    }
  }
  // magic cookie
  push_u32_be(output, magic_cookie)
  // options
  for opt in msg.options {
    output.push(opt.code.to_byte())
    output.push(opt.data.length().to_byte())
    append_bytes(output, opt.data)
  }
  // end option
  output.push(opt_end.to_byte())
  Bytes::from_array(output)
}

///|
/// Parse a DHCP message from bytes.
pub fn parse_dhcp_message(data : Bytes) -> DhcpMessage raise @frame.FrameError {
  guard data.length() >= 240 else {
    raise @frame.FrameError::InvalidMacLength(data.length())
  }
  let op = data[0].to_int()
  let htype = data[1].to_int()
  let hlen = data[2].to_int()
  let hops = data[3].to_int()
  let xid = read_u32_be(data, 4)
  let secs = read_u16_be(data, 8)
  let flags = read_u16_be(data, 10)
  let ciaddr = read_u32_be(data, 12)
  let yiaddr = read_u32_be(data, 16)
  let siaddr = read_u32_be(data, 20)
  let giaddr = read_u32_be(data, 24)
  let ch : Array[Byte] = []
  for i = 28; i < 44; i = i + 1 {
    ch.push(data[i])
  }
  let chaddr = Bytes::from_array(ch)
  let sn : Array[Byte] = []
  for i = 44; i < 108; i = i + 1 {
    sn.push(data[i])
  }
  let sname = Bytes::from_array(sn)
  let fl : Array[Byte] = []
  for i = 108; i < 236; i = i + 1 {
    fl.push(data[i])
  }
  let file = Bytes::from_array(fl)
  // Parse options after magic cookie (offset 240)
  let options : Array[DhcpOption] = []
  let mut pos = 240
  while pos < data.length() {
    let code = data[pos].to_int()
    pos = pos + 1
    if code == opt_end {
      break
    }
    if code == 0 {
      // Pad option
      continue
    }
    guard pos < data.length() else { break }
    let len = data[pos].to_int()
    pos = pos + 1
    guard pos + len <= data.length() else { break }
    let d : Array[Byte] = []
    for i = 0; i < len; i = i + 1 {
      d.push(data[pos + i])
    }
    options.push(DhcpOption::{ code, data: Bytes::from_array(d) })
    pos = pos + len
  }
  DhcpMessage::{
    op,
    htype,
    hlen,
    hops,
    xid,
    secs,
    flags,
    ciaddr,
    yiaddr,
    siaddr,
    giaddr,
    chaddr,
    sname,
    file,
    options,
  }
}

///|
/// Get the DHCP message type from options.
pub fn get_message_type(msg : DhcpMessage) -> Int {
  for opt in msg.options {
    if opt.code == opt_message_type && opt.data.length() >= 1 {
      return opt.data[0].to_int()
    }
  }
  0
}

///|
/// Get a human-readable label for a DHCP message type.
pub fn message_type_label(msg_type : Int) -> String {
  match msg_type {
    1 => "DISCOVER"
    2 => "OFFER"
    3 => "REQUEST"
    4 => "DECLINE"
    5 => "ACK"
    6 => "NAK"
    7 => "RELEASE"
    8 => "INFORM"
    _ => "Unknown(" + msg_type.to_string() + ")"
  }
}

///|
/// Format DHCP message summary.
pub fn format_dhcp_message(msg : DhcpMessage) -> String {
  let msg_type = get_message_type(msg)
  let op_str = if msg.op == boot_request { "REQUEST" } else { "REPLY" }
  let lines : Array[String] = []
  lines.push("DHCP " + message_type_label(msg_type) + " (" + op_str + ")")
  lines.push("xid=0x" + @frame.uint32_to_hex(msg.xid))
  lines.push("options=" + msg.options.length().to_string())
  lines.join("\n")
}