// Copyright (c) 2024 Epic Games Tools
// Licensed under the MIT license (https://opensource.org/license/mit/)

////////////////////////////////
// x64 Unwind Function

internal DW_UnwindResult
dw_unwind_x64(String8           raw_text,
              String8           raw_eh_frame,
              String8           raw_eh_frame_hdr,
              Rng1U64           text_vrange,
              Rng1U64           eh_frame_vrange,
              Rng1U64           eh_frame_hdr_vrange,
              U64               default_image_base,
              U64               image_base,
              U64               stack_pointer,
              DW_RegsX64       *regs,
              DW_ReadMemorySig *read_memory,
              void             *read_memory_ud)
{
  // TODO: What if ELF has two sections with instructions and pointer is ecnoded relative to .text2?
  Temp scratch = scratch_begin(0, 0);

  DW_UnwindResult result = {0};

  dw_unwind_init_x64();
  
  // rebase
  U64 rebase_voff_to_vaddr = (image_base - default_image_base);
  
  // get ip register values
  U64 ip_value = regs->rip;
  U64 ip_voff  = ip_value - rebase_voff_to_vaddr;
  
  // check sections
  B32 has_needed_sections = (raw_text.size > 0 && raw_eh_frame.size > 0);
  if (!has_needed_sections) {
    result.is_invalid = 1;
  }
  
  //- get frame info range
  void    *frame_base  = raw_eh_frame.str;
  Rng1U64  frame_range = rng_1u64(0, raw_eh_frame.size);

  //- section vaddrs
  U64 text_base_vaddr = text_vrange.min + rebase_voff_to_vaddr;
  U64 frame_base_voff = text_vrange.min;
  U64 data_base_vaddr = eh_frame_hdr_vrange.min + rebase_voff_to_vaddr;
  
  //- find cfi records
  DW_CFIRecords cfi_recs = {0};
  if (has_needed_sections) {
    DW_EhPtrCtx ptr_ctx    = {0};
    ptr_ctx.raw_base_vaddr = frame_base_voff;
    ptr_ctx.text_vaddr     = text_base_vaddr;
    ptr_ctx.data_vaddr     = data_base_vaddr;
    ptr_ctx.func_vaddr     = 0;
    if (raw_eh_frame_hdr.size) {
      cfi_recs = dw_unwind_eh_frame_hdr_from_ip_fast_x64(raw_eh_frame, raw_eh_frame_hdr, &ptr_ctx, ip_voff);
    } else {
      cfi_recs = dw_unwind_eh_frame_cfi_from_ip_slow_x64(raw_eh_frame, &ptr_ctx, ip_voff);
    }
  }
  
  //- check cfi records
  if (!cfi_recs.valid) {
    result.is_invalid = 1;
  }
  
  //- cfi machine setup
  DW_CFIMachine machine = {0};
  if (cfi_recs.valid) {
    DW_EhPtrCtx ptr_ctx    = {0};
    ptr_ctx.raw_base_vaddr = frame_base_voff;
    ptr_ctx.text_vaddr     = text_base_vaddr;
    ptr_ctx.data_vaddr     = data_base_vaddr;
    ptr_ctx.func_vaddr     = cfi_recs.fde.ip_voff_range.min + rebase_voff_to_vaddr; // TODO: it's not super clear how to set up this member, need more test cases
    machine = dw_unwind_make_machine_x64(DW_UNWIND_X64__REG_SLOT_COUNT, &cfi_recs.cie, &ptr_ctx);
  }
  
  // initial row
  DW_CFIRow *init_row = 0;
  if (cfi_recs.valid) {
    Rng1U64    init_cfi_range = cfi_recs.cie.cfi_range;
    DW_CFIRow *row            = dw_unwind_row_alloc_x64(scratch.arena, machine.cells_per_row);
    if (dw_unwind_machine_run_to_ip_x64(frame_base, init_cfi_range, &machine, max_U64, row)) {
      init_row = row;
    }
    if (init_row == 0) {
      result.is_invalid = 1;
    }
  }
  
  // main row
  DW_CFIRow *main_row = 0;
  if (init_row != 0) {
    // upgrade machine with new equipment
    dw_unwind_machine_equip_initial_row_x64(&machine, init_row);
    dw_unwind_machine_equip_fde_ip_x64(&machine, cfi_recs.fde.ip_voff_range.min);
    
    // decode main row
    Rng1U64    main_cfi_range = cfi_recs.fde.cfi_range;
    DW_CFIRow *row            = dw_unwind_row_alloc_x64(scratch.arena, machine.cells_per_row);
    if (dw_unwind_machine_run_to_ip_x64(frame_base, main_cfi_range, &machine, ip_value, row)) {
      main_row = row;
    }
    if (main_row == 0) {
      result.is_invalid = 1;
    }
  }

  // apply main row to modify the registers
  if (main_row != 0) {
    result = dw_unwind_x64__apply_frame_rules(raw_eh_frame, main_row, text_base_vaddr, read_memory, read_memory_ud, stack_pointer, regs);
  }
  
  scratch_end(scratch);
  return result;
}

internal DW_UnwindResult
dw_unwind_x64__apply_frame_rules(String8           raw_eh_frame,
                                 DW_CFIRow        *row,
                                 U64               text_base_vaddr,
                                 DW_ReadMemorySig *read_memory,
                                 void             *read_memory_ud,
                                 U64               stack_pointer,
                                 DW_RegsX64       *regs)
{
  DW_UnwindResult result = {0};

  U64 missed_read_addr = 0;
  
  //- setup a dwarf expression machine
  DW_ExprMachineConfig dwexpr_config = {0};
  dwexpr_config.max_step_count       = 0xFFFF;
  dwexpr_config.read_memory          = read_memory;
  dwexpr_config.read_memory_ud       = read_memory_ud;
  dwexpr_config.regs                 = regs;
  dwexpr_config.text_section_base    = &text_base_vaddr;
  
  //- compute cfa
  U64 cfa = 0;
  switch (row->cfa_cell.rule) {
    case DW_CFI_CFA_Rule_RegOff: {
      // TODO: have we done anything to gaurantee reg_idx here?
      U64 reg_idx = row->cfa_cell.reg_idx;
      
      // is this a roll-over CFA?
      B32 is_roll_over_cfa = 0;
      if (reg_idx == DW_Reg_x64_Rsp) {
        DW_CFIRegisterRule rule = row->cells[reg_idx].rule;
        if (rule == DW_CFIRegisterRule_Undefined || rule == DW_CFIRegisterRule_SameValue) {
          is_roll_over_cfa = 1;
        }
      }
      
      // compute cfa
      if (is_roll_over_cfa) {
        cfa = stack_pointer + row->cfa_cell.offset;
      } else {
        cfa = regs->r[reg_idx] + row->cfa_cell.offset;
      }
    } break;
    
    case DW_CFI_CFA_Rule_Expr: {
      Rng1U64     expr_range = row->cfa_cell.expr;
      DW_Location location   = dw_expr__eval(0, raw_eh_frame.str, expr_range, &dwexpr_config);
      if (location.non_piece_loc.kind == DW_SimpleLocKind_Fail && location.non_piece_loc.fail_kind == DW_LocFailKind_MissingMemory) {
        missed_read_addr = location.non_piece_loc.fail_data;
        goto error_out;
      }
      if (location.non_piece_loc.kind == DW_SimpleLocKind_Address) {
        cfa = location.non_piece_loc.addr;
      }
    } break;
  }
  
  // compute registers
  {
    DW_CFICell *cell     = row->cells;
    DW_RegsX64  new_regs = {0};
    for (U64 i = 0; i < DW_UNWIND_X64__REG_SLOT_COUNT; ++i, ++cell) {
      // compute value
      U64 v = 0;
      switch (cell->rule) {
        default:
        {
          Assert(!"UNEXPECTED-RULE");
        } break;
        
        case DW_CFIRegisterRule_Undefined:
        {
          Assert(!"UNDEFINED");
        } break;
        
        case DW_CFIRegisterRule_SameValue:
        {
          v = regs->r[i];
        } break;
        
        case DW_CFIRegisterRule_Offset:
        {
          U64 addr = cfa + cell->n;
          U64 read_size = read_memory(addr, sizeof(v), &v, read_memory_ud);
          if (read_size != sizeof(v)) {
            missed_read_addr = addr;
            goto error_out;
          }
        } break;
        
        case DW_CFIRegisterRule_ValOffset:
        {
          v = cfa + cell->n;
        } break;
        
        case DW_CFIRegisterRule_Register:
        {
          v = regs->r[i];
        } break;
        
        case DW_CFIRegisterRule_Expression:
        {
          Rng1U64     expr_range = cell->expr;
          U64         addr       = 0;
          DW_Location location   = dw_expr__eval(0, raw_eh_frame.str, expr_range, &dwexpr_config);
          if (location.non_piece_loc.kind == DW_SimpleLocKind_Fail && location.non_piece_loc.fail_kind == DW_LocFailKind_MissingMemory) {
            missed_read_addr = location.non_piece_loc.fail_data;
            goto error_out;
          }
          if (location.non_piece_loc.kind == DW_SimpleLocKind_Address) {
            addr = location.non_piece_loc.addr;
          }
          U64 read_size = read_memory(addr, sizeof(v), &v, read_memory_ud);
          if (read_size != sizeof(v)) {
            missed_read_addr = addr;
            goto error_out;
          }
        } break;
        
        case DW_CFIRegisterRule_ValExpression:
        {
          Rng1U64     expr_range = cell->expr;
          DW_Location location   = dw_expr__eval(0, raw_eh_frame.str, expr_range, &dwexpr_config);
          if (location.non_piece_loc.kind == DW_SimpleLocKind_Fail && location.non_piece_loc.fail_kind == DW_LocFailKind_MissingMemory) {
            missed_read_addr = location.non_piece_loc.fail_data;
            goto error_out;
          }
          if (location.non_piece_loc.kind == DW_SimpleLocKind_Address) {
            v = location.non_piece_loc.addr;
          }
        } break;
      }
      
      // commit value to output slot
      new_regs.r[i] = v;
    }
    
    // commit all new regs
    MemoryCopy(regs, &new_regs, sizeof(new_regs));
  }
  
  //- save new stack pointer
  result.stack_pointer = cfa;
  
  error_out:;
  if (missed_read_addr) {
    result.is_invalid       = 1;
    result.missed_read      = 1;
    result.missed_read_addr = missed_read_addr;
  }
  
  return result;
}

////////////////////////////////
// Helper Functions

internal void
dw_unwind_init_x64(void)
{
  local_persist B32 did_init = 0;
  
  if (!did_init) {
    did_init = 1;
    
    // control bits tables
    dw_unwind__cfa_control_bits_kind1[DW_CFA_Nop            ] = 0x000;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_SetLoc         ] = 0x809;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_AdvanceLoc1    ] = 0x801;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_AdvanceLoc2    ] = 0x802;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_AdvanceLoc4    ] = 0x804;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_OffsetExt      ] = 0x2AA;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_RestoreExt     ] = 0x20A;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_Undefined      ] = 0x20A;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_SameValue      ] = 0x20A;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_Register       ] = 0x6AA;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_RememberState  ] = 0x000;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_RestoreState   ] = 0x000;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_DefCfa         ] = 0x2AA;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_DefCfaRegister ] = 0x20A;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_DefCfaOffset   ] = 0x00A;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_DefCfaExpr     ] = 0x00A;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_Expr           ] = 0x2AA;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_OffsetExtSf    ] = 0x2BA;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_DefCfaSf       ] = 0x2BA;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_DefCfaOffsetSf ] = 0x00B;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_ValOffset      ] = 0x2AA;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_ValOffsetSf    ] = 0x2BA;
    dw_unwind__cfa_control_bits_kind1[DW_CFA_ValExpr        ] = 0x2AA;
    
    dw_unwind__cfa_control_bits_kind2[DW_CFA_AdvanceLoc  >> 6] = 0x800;
    dw_unwind__cfa_control_bits_kind2[DW_CFA_Offset      >> 6] = 0x10A;
    dw_unwind__cfa_control_bits_kind2[DW_CFA_Restore     >> 6] = 0x100;
  }
}

internal U64
dw_unwind_parse_pointer_x64(void *frame_base, Rng1U64 frame_range, DW_EhPtrCtx *ptr_ctx, DW_EhPtrEnc encoding, U64 off, U64 *ptr_out)
{
  // aligned offset
  U64 pointer_off = off;
  if (encoding == DW_EhPtrEnc_Aligned) {
    pointer_off = AlignPow2(off, 8); // TODO: align to 4 bytes when we parse x86 ELF binary
    encoding    = DW_EhPtrEnc_Ptr;
  }
  
  // decode pointer value
  U64 size_param        = 0;
  U64 after_pointer_off = 0;
  U64 raw_pointer       = 0;
  switch (encoding & DW_EhPtrEnc_TypeMask) {
    default:break;
    
    case DW_EhPtrEnc_Ptr   : size_param = 8; goto ufixed;
    case DW_EhPtrEnc_UData2: size_param = 2; goto ufixed;
    case DW_EhPtrEnc_UData4: size_param = 4; goto ufixed;
    case DW_EhPtrEnc_UData8: size_param = 8; goto ufixed;
    ufixed:
    {
      dw_based_range_read(frame_base, frame_range, pointer_off, size_param, &raw_pointer);
      after_pointer_off = pointer_off + size_param;
    } break;
    
    // TODO: Signed is actually just a flag that indicates this int is negavite.
    // There shouldn't be a read for Signed.
    // For instance, (DW_EhPtrEnc_UData2 | DW_EhPtrEnc_Signed) == DW_EhPtrEnc_SData etc.
    case DW_EhPtrEnc_Signed:size_param = 8; goto sfixed; 
    
    case DW_EhPtrEnc_SData2:size_param = 2; goto sfixed;
    case DW_EhPtrEnc_SData4:size_param = 4; goto sfixed;
    case DW_EhPtrEnc_SData8:size_param = 8; goto sfixed;
    sfixed:
    {
      dw_based_range_read(frame_base, frame_range, pointer_off, size_param, &raw_pointer);
      after_pointer_off = pointer_off + size_param;
      // sign extension
      U64 sign_bit = size_param*8 - 1;
      if ((raw_pointer >> sign_bit) != 0) {
        raw_pointer |= (~(1 << sign_bit)) + 1;
      }
    } break;
    
    case DW_EhPtrEnc_ULEB128:
    {
      U64 size = dw_based_range_read_uleb128(frame_base, frame_range, pointer_off, &raw_pointer);
      after_pointer_off = pointer_off + size;
    } break;
    
    case DW_EhPtrEnc_SLEB128:
    {
      U64 size = dw_based_range_read_sleb128(frame_base, frame_range, pointer_off,
                                                    (S64*)&raw_pointer);
      after_pointer_off = pointer_off + size;
    } break;
  }
  
  // apply relative bases
  U64 pointer = raw_pointer;
  if (pointer != 0) {
    switch (encoding & DW_EhPtrEnc_ModifyMask) {
      case DW_EhPtrEnc_PcRel:
      {
        pointer = ptr_ctx->raw_base_vaddr + frame_range.min + off + raw_pointer;
      } break;
      case DW_EhPtrEnc_TextRel:
      {
        pointer = ptr_ctx->text_vaddr + raw_pointer;
      } break;
      case DW_EhPtrEnc_DataRel:
      {
        pointer = ptr_ctx->data_vaddr + raw_pointer;
      } break;
      case DW_EhPtrEnc_FuncRel:
      {
        Assert(!"TODO: need a sample to verify implementation");
        pointer = ptr_ctx->func_vaddr + raw_pointer;
      } break;
    }
  }
  
  // return
  *ptr_out = pointer;
  U64 result = after_pointer_off - off;
  return(result);
}

//- eh_frame parsing

internal void
dw_unwind_parse_cie_x64(void *base, Rng1U64 range, DW_EhPtrCtx *ptr_ctx, U64 off, DW_CIEUnpacked *cie_out)
{
  MemoryZeroStruct(cie_out);
  
  // get version
  U64 version_off = off;
  U8  version     = 0;
  dw_based_range_read(base, range, version_off, 1, &version);
  
  // check version
  if (version == 1 || version == 3) {
    
    // read augmentation
    U64     augmentation_off = version_off + 1;
    String8 augmentation     = dw_based_range_read_string(base, range, augmentation_off);
    
    // read code align
    U64 code_align_factor_off  = augmentation_off + augmentation.size + 1;
    U64 code_align_factor      = 0;
    U64 code_align_factor_size = dw_based_range_read_uleb128(base, range, code_align_factor_off, &code_align_factor);
    
    // read data align
    U64 data_align_factor_off  = code_align_factor_off + code_align_factor_size;
    S64 data_align_factor      = 0;
    U64 data_align_factor_size = dw_based_range_read_sleb128(base, range, data_align_factor_off, &data_align_factor);
    
    // return address register
    U64 ret_addr_reg_off       = data_align_factor_off + data_align_factor_size;
    U64 after_ret_addr_reg_off = 0;
    U64 ret_addr_reg           = 0;
    if (version == 1) {
      dw_based_range_read(base, range, ret_addr_reg_off, 1, &ret_addr_reg);
      after_ret_addr_reg_off = ret_addr_reg_off + 1;
    } else {
      U64 ret_addr_reg_size = dw_based_range_read_uleb128(base, range, ret_addr_reg_off, &ret_addr_reg);
      after_ret_addr_reg_off = ret_addr_reg_off + ret_addr_reg_size;
    }
    
    // TODO: 
    // Handle "eh" param, it indicates presence of EH Data field.
    // On 32bit arch it is a 4-byte and on 64-bit 8-byte value.
    // Reference: https://refspecs.linuxfoundation.org/LSB_3.0.0/LSB-PDA/LSB-PDA/ehframechpt.html
    // Reference doc doesn't clarify structure for EH Data though
    
    // check for augmentation data
    U64 aug_size_off          = after_ret_addr_reg_off;
    U64 after_aug_size_off    = after_ret_addr_reg_off;
    B32 has_augmentation_size = 0;
    U64 augmentation_size     = 0;
    if (augmentation.size > 0 && augmentation.str[0] == 'z') {
      has_augmentation_size = 1;
      U64 aug_size_size = dw_based_range_read_uleb128(base, range, aug_size_off, &augmentation_size);
      after_aug_size_off += aug_size_size;
    }
    
    // read augmentation data
    U64 aug_data_off       = after_aug_size_off;
    U64 after_aug_data_off = after_aug_size_off;
    
    DW_EhPtrEnc lsda_encoding = DW_EhPtrEnc_Omit;
    U64         handler_ip    = 0;
    DW_EhPtrEnc addr_encoding = DW_EhPtrEnc_UData8;
    
    if (has_augmentation_size > 0) {
      U64 aug_data_cursor = aug_data_off;
      for (U8 *ptr = augmentation.str + 1, *opl = augmentation.str + augmentation.size; ptr < opl; ++ptr) {
        switch (*ptr) {
          case 'L': {
            dw_based_range_read_struct(base, range, aug_data_cursor, &lsda_encoding);
            aug_data_cursor += sizeof(lsda_encoding);
          } break;
          case 'P': {
            DW_EhPtrEnc handler_encoding = DW_EhPtrEnc_Omit;
            dw_based_range_read_struct(base, range, aug_data_cursor, &handler_encoding);
            
            U64 ptr_off  = aug_data_cursor + sizeof(handler_encoding);
            U64 ptr_size = dw_unwind_parse_pointer_x64(base, range, ptr_ctx, handler_encoding, ptr_off, &handler_ip);
            aug_data_cursor = ptr_off + ptr_size;
          } break;
          case 'R': {
            dw_based_range_read_struct(base, range, aug_data_cursor, &addr_encoding);
            aug_data_cursor += sizeof(addr_encoding);
          } break;
          default: {
            goto dbl_break_aug;
          } break;
        }
      }
      dbl_break_aug:;
      after_aug_data_off = aug_data_cursor;
    }
    
    // cfi range
    U64 cfi_off  = range.min + after_aug_data_off;
    U64 cfi_size = 0;
    if (range.max > cfi_off) {
      cfi_size = range.max - cfi_off;
    }
    
    // commit values to out
    cie_out->version               = version;
    cie_out->lsda_encoding         = lsda_encoding;
    cie_out->addr_encoding         = addr_encoding;
    cie_out->has_augmentation_size = has_augmentation_size;
    cie_out->augmentation_size     = augmentation_size;
    cie_out->augmentation          = augmentation;
    cie_out->code_align_factor     = code_align_factor;
    cie_out->data_align_factor     = data_align_factor;
    cie_out->ret_addr_reg          = ret_addr_reg;
    cie_out->handler_ip            = handler_ip;
    cie_out->cfi_range.min         = cfi_off;
    cie_out->cfi_range.max         = cfi_off + cfi_size;
  }
}

internal void
dw_unwind_parse_fde_x64(void *base, Rng1U64 range, DW_EhPtrCtx *ptr_ctx, DW_CIEUnpacked *cie, U64 off, DW_FDEUnpacked *fde_out)
{
  // pull out pointer encoding field
  DW_EhPtrEnc ptr_enc = cie->addr_encoding;
  
  // ip first
  U64 ip_first_off  = off;
  U64 ip_first      = 0;
  U64 ip_first_size = dw_unwind_parse_pointer_x64(base, range, ptr_ctx, ptr_enc, ip_first_off, &ip_first);
  
  // ip range size
  U64 ip_range_size_off  = ip_first_off + ip_first_size;
  U64 ip_range_size      = 0;
  U64 ip_range_size_size = dw_unwind_parse_pointer_x64(base, range, ptr_ctx, ptr_enc & DW_EhPtrEnc_TypeMask, ip_range_size_off, &ip_range_size);
  
  // augmentation data
  U64 aug_data_off       = ip_range_size_off + ip_range_size_size;
  U64 after_aug_data_off = aug_data_off;
  U64 lsda_ip            = 0;
  
  if (cie->has_augmentation_size) {
    // augmentation size
    U64 augmentation_size  = 0;
    U64 aug_size_size      = dw_based_range_read_uleb128(base, range, aug_data_off, &augmentation_size);
    U64 after_aug_size_off = aug_data_off + aug_size_size;
    
    // extract lsda (only thing that can actually be in FDE's augmentation data as far as we know)
    DW_EhPtrEnc lsda_encoding = cie->lsda_encoding;
    if (lsda_encoding != DW_EhPtrEnc_Omit) {
      U64 lsda_off = after_aug_size_off;
      dw_unwind_parse_pointer_x64(base, range, ptr_ctx, lsda_encoding, lsda_off, &lsda_ip);
    }
    
    // set offset at end of augmentation data
    after_aug_data_off = after_aug_size_off + augmentation_size;
  }
  
  // cfi range
  U64 cfi_off  = range.min + after_aug_data_off;
  U64 cfi_size = 0;
  if (range.max > cfi_off) {
    cfi_size = range.max - cfi_off;
  }
  
  // commit values to out
  fde_out->ip_voff_range.min = ip_first;
  fde_out->ip_voff_range.max = ip_first + ip_range_size;
  fde_out->lsda_ip           = lsda_ip;
  fde_out->cfi_range.min     = cfi_off;
  fde_out->cfi_range.max     = cfi_off + cfi_size;
}

internal DW_CFIRecords
dw_unwind_eh_frame_cfi_from_ip_slow_x64(String8 raw_eh_frame, DW_EhPtrCtx *ptr_ctx, U64 ip_voff)
{
  Temp scratch = scratch_begin(0, 0);

  DW_CFIRecords result = {0};
  
  DW_CIEUnpackedNode *cie_first = 0;
  DW_CIEUnpackedNode *cie_last  = 0;
  
  U64 cursor = 0;
  for (;;) {
    // CIE/FDE size
    U64 rec_off            = cursor;
    U64 after_rec_size_off = 0;
    U64 rec_size           = 0;
    
    {
      str8_deserial_read(raw_eh_frame, rec_off, &rec_size, 4, 1);
      after_rec_size_off = 4;
      if (rec_size == max_U32) {
        str8_deserial_read(raw_eh_frame, rec_off + 4, &rec_size, 8, 1);
        after_rec_size_off = 12;
      }
    }
    
    // zero size is the end of the loop
    if (rec_size == 0) {
      break;
    }
    
    // compute end offset
    U64 rec_opl = rec_off + after_rec_size_off + rec_size;
    
    // sub-range the rest of the reads
    Rng1U64 rec_range = rng_1u64(rec_off, rec_opl);
    String8 raw_rec   = str8_substr(raw_eh_frame, rec_range);
    

    // discriminator
    U64 discrim_off = after_rec_size_off;
    U32 discrim     = 0;
    str8_deserial_read(raw_rec, discrim_off, &discrim, 4, 1);
    
    U64 after_discrim_off = discrim_off + 4;
    
    // CIE
    if (discrim == 0) {
      DW_CIEUnpacked cie = {0};
      dw_unwind_parse_cie_x64(raw_rec.str, rng_1u64(0, raw_rec.size), ptr_ctx, after_discrim_off, &cie);
      if (cie.version != 0) {
        DW_CIEUnpackedNode *node = push_array(scratch.arena, DW_CIEUnpackedNode, 1);
        node->cie                = cie;
        node->offset             = rec_off;
        SLLQueuePush(cie_first, cie_last, node);
      }
    }
    // FDE
    else {
      // compute cie offset
      U64 cie_offset = rec_range.min + discrim_off - discrim;
      
      // get cie node
      DW_CIEUnpackedNode *cie_node = 0;
      for (DW_CIEUnpackedNode *node = cie_first; node != 0; node = node->next) {
        if (node->offset == cie_offset) {
          cie_node = node;
          break;
        }
      }
      
      // parse fde
      DW_FDEUnpacked fde = {0};
      if (cie_node != 0) {
        dw_unwind_parse_fde_x64(raw_rec.str, rng_1u64(0,raw_rec.size), ptr_ctx, &cie_node->cie, after_discrim_off, &fde);
      }
      
      if (contains_1u64(fde.ip_voff_range, ip_voff)) {
        result.valid = 1;
        result.cie   = cie_node->cie;
        result.fde   = fde;
        break;
      }
    }
    
    // advance cursor
    cursor = rec_opl;
  }
  
  scratch_end(scratch);
  
  return(result);
}

internal U64
dw_search_eh_frame_hdr_linear_x64(String8 raw_eh_frame_hdr, DW_EhPtrCtx *ptr_ctx, U64 location)
{
  // Table contains only addresses for first instruction in a function and we cannot
  // guarantee that result is FDE that corresponds to the input location. 
  // So input location must be cheked against range from FDE header again.
  
  U64 closest_location = max_U64;
  U64 closest_address  = max_U64;
  
  U64 cursor = 0;
  
  U8 version = 0;
  cursor += str8_deserial_read_struct(raw_eh_frame_hdr, cursor, &version);
  
  if (version == 1) {
#if 0
    DW_EhPtrCtx ptr_ctx = {0};
    // Set this to base address of .eh_frame_hdr. Entries are relative
    // to this section for some reason.
    ptr_ctx.data_vaddr = range.min;
    // If input location is VMA then set this to address of .text. 
    // Pointer parsing function will adjust "init_location" to correct VMA.
    ptr_ctx.text_vaddr = 0; 
#endif
    
    DW_EhPtrEnc eh_frame_ptr_enc = 0, fde_count_enc = 0, table_enc = 0;
    cursor += str8_deserial_read_struct(raw_eh_frame_hdr, cursor, &eh_frame_ptr_enc);
    cursor += str8_deserial_read_struct(raw_eh_frame_hdr, cursor, &fde_count_enc);
    cursor += str8_deserial_read_struct(raw_eh_frame_hdr, cursor, &table_enc);
    
    U64 eh_frame_ptr = 0, fde_count = 0;
    cursor += dw_unwind_parse_pointer_x64(raw_eh_frame_hdr.str, rng_1u64(0, raw_eh_frame_hdr.size), ptr_ctx, eh_frame_ptr_enc, cursor, &eh_frame_ptr);
    cursor += dw_unwind_parse_pointer_x64(raw_eh_frame_hdr.str, rng_1u64(0, raw_eh_frame_hdr.size), ptr_ctx, fde_count_enc, cursor, &fde_count);
    
    for (U64 fde_idx = 0; fde_idx < fde_count; ++fde_idx) {
      U64 init_location = 0, address = 0;
      cursor += dw_unwind_parse_pointer_x64(raw_eh_frame_hdr.str, rng_1u64(0, raw_eh_frame_hdr.size), ptr_ctx, table_enc, cursor, &init_location);
      cursor += dw_unwind_parse_pointer_x64(raw_eh_frame_hdr.str, rng_1u64(0, raw_eh_frame_hdr.size), ptr_ctx, table_enc, cursor, &address);
      
      S64 current_delta = (S64)(location - init_location);
      S64 closest_delta = (S64)(location - closest_location);
      if (0 <= current_delta && current_delta < closest_delta) {
        closest_location = init_location;
        closest_address  = address;
      }
    }
  }
  
  // address where to find corresponding FDE, this is an absolute offset
  // into the image file.
  return closest_address;
}

internal DW_CFIRecords
dw_unwind_eh_frame_hdr_from_ip_fast_x64(String8 raw_eh_frame, String8 raw_eh_frame_hdr, DW_EhPtrCtx *ptr_ctx, U64 ip_voff)
{
  DW_CFIRecords result = {0};
  
  // find FDE offset
  void *eh_frame_hdr = raw_eh_frame.str;
  U64   fde_offset   = dw_search_eh_frame_hdr_linear_x64(raw_eh_frame_hdr, ptr_ctx, ip_voff);
  
  B32 is_fde_offset_valid = (fde_offset != max_U64);
  if (is_fde_offset_valid) {
    U64 fde_read_offset = (fde_offset - ptr_ctx->raw_base_vaddr);
    
    // read FDE size
    U64 fde_size = 0;
    fde_read_offset += dw_based_range_read_length(raw_eh_frame.str, rng_1u64(0,raw_eh_frame.size), fde_read_offset, &fde_size);
    
    // read FDE discriminator
    U32 fde_discrim = 0;
    fde_read_offset += str8_deserial_read_struct(raw_eh_frame, fde_read_offset, &fde_discrim);
    
    // compute parent CIE offset
    U64 cie_read_offset = fde_read_offset - (fde_discrim + sizeof(fde_discrim));
    
    // read CIE size
    U64 cie_size = 0;
    cie_read_offset += dw_based_range_read_length(raw_eh_frame.str, rng_1u64(0,raw_eh_frame.size), cie_read_offset, &cie_size);
    
    // read CIE discriminator
    U32 cie_discrim = max_U32;
    cie_read_offset += str8_deserial_read_struct(raw_eh_frame, cie_read_offset, &cie_discrim);
    
    B32 is_fde = (fde_discrim != 0);
    B32 is_cie = (cie_discrim == 0);
    if (is_fde && is_cie) {
      Rng1U64 cie_range = rng_1u64(0, cie_read_offset + (cie_size - sizeof(cie_discrim)));
      Rng1U64 fde_range = rng_1u64(0, fde_read_offset + (fde_size - sizeof(fde_discrim)));
      
      // parse CIE
      DW_CIEUnpacked cie = {0};
      dw_unwind_parse_cie_x64(raw_eh_frame.str, cie_range, ptr_ctx, cie_read_offset, &cie);
      
      // parse FDE
      DW_FDEUnpacked fde = {0};
      dw_unwind_parse_fde_x64(raw_eh_frame.str, fde_range, ptr_ctx, &cie, fde_read_offset, &fde);
      
      // range check instruction pointer
      if (contains_1u64(fde.ip_voff_range, ip_voff)) {
        result.valid = 1;
        result.cie   = cie;
        result.fde   = fde;
      }
    }
  }
  
  return result;
}

//- cfi machine

internal DW_CFIMachine
dw_unwind_make_machine_x64(U64 cells_per_row, DW_CIEUnpacked *cie, DW_EhPtrCtx *ptr_ctx)
{
  DW_CFIMachine result = {0};
  result.cells_per_row = cells_per_row;
  result.cie           = cie;
  result.ptr_ctx       = ptr_ctx;
  return result;
}

internal void
dw_unwind_machine_equip_initial_row_x64(DW_CFIMachine *machine, DW_CFIRow *initial_row)
{
  machine->initial_row = initial_row;
}

internal void
dw_unwind_machine_equip_fde_ip_x64(DW_CFIMachine *machine, U64 fde_ip)
{
  machine->fde_ip = fde_ip;
}

internal DW_CFIRow*
dw_unwind_row_alloc_x64(Arena *arena, U64 cells_per_row)
{
  DW_CFIRow *result = push_array(arena, DW_CFIRow, 1);
  result->cells     = push_array(arena, DW_CFICell, cells_per_row);
  return result;
}

internal void
dw_unwind_row_zero_x64(DW_CFIRow *row, U64 cells_per_row) {
  MemorySet(row->cells, 0, sizeof(*row->cells)*cells_per_row);
  MemoryZeroStruct(&row->cfa_cell);
}

internal void
dw_unwind_row_copy_x64(DW_CFIRow *dst, DW_CFIRow *src, U64 cells_per_row)
{
  MemoryCopy(dst->cells, src->cells, sizeof(*src->cells)*cells_per_row);
  dst->cfa_cell = src->cfa_cell;
}

internal B32
dw_unwind_machine_run_to_ip_x64(void *base, Rng1U64 range, DW_CFIMachine *machine, U64 target_ip, DW_CFIRow *row)
{
  Temp scratch = scratch_begin(0, 0);

  B32 result = 0;
  
  // pull out machine's equipment
  DW_CIEUnpacked *cie           = machine->cie;
  DW_EhPtrCtx    *ptr_ctx       = machine->ptr_ctx;
  U64             cells_per_row = machine->cells_per_row;
  DW_CFIRow      *initial_row   = machine->initial_row;
  
  // start with an empty stack
  DW_CFIRow *stack     = 0;
  DW_CFIRow *free_rows = 0;
  
  // initialize the row
  if (initial_row != 0) {
    dw_unwind_row_copy_x64(row, initial_row, cells_per_row);
  } else {
    dw_unwind_row_zero_x64(row, cells_per_row);
  }
  U64 table_ip = machine->fde_ip;
  
  // loop
  U64 cfi_off = 0;
  for (;;) {
    // op variables
    DW_CFA            opcode       = 0;
    U64               operand0     = 0;
    U64               operand1     = 0;
    U64               operand2     = 0;
    DW_CFAControlBits control_bits = 0;
    
    // decode opcode/operand0
    if (!dw_based_range_read(base, range, cfi_off, 1, &opcode)) {
      result = 1;
      goto done;
    }
    if ((opcode & DW_CFAMask_OpcodeHi) != 0) {
      operand0     = (opcode & DW_CFAMask_Operand);
      opcode       = (opcode & DW_CFAMask_OpcodeHi);
      control_bits = dw_unwind__cfa_control_bits_kind2[opcode >> 6];
    } else {
      if (opcode < DW_CFA_OplKind1) {
        control_bits = dw_unwind__cfa_control_bits_kind1[opcode];
      }
    }
    
    // decode operand1/operand2
    U64 decode_cursor = cfi_off + 1;
    {
      // setup loop ins/outs
      U64 o[2];
      DW_CFADecode dec[2] = {0};
      dec[0]              = (control_bits & 0xF);
      dec[1]              = ((control_bits >> 4) & 0xF);
      
      // loop
      U64 *out = o;
      for (U64 i = 0; i < 2; i += 1, out += 1) {
        DW_CFADecode d = dec[i];
        U64 o_size = 0;
        switch (d) {
          case 0: {
            *out = 0;
          } break;
          default: {
            if (d <= 8) {
              dw_based_range_read(base, range, decode_cursor, d, out);
              o_size = d;
            }
          } break;
          case DW_CFADecode_Address: {
            o_size = dw_unwind_parse_pointer_x64(base, range, ptr_ctx, cie->addr_encoding, decode_cursor, out);
          } break;
          case DW_CFADecode_ULEB128: {
            o_size = dw_based_range_read_uleb128(base, range, decode_cursor, out);
          } break;
          case DW_CFADecode_SLEB128: {
            o_size = dw_based_range_read_sleb128(base, range, decode_cursor, (S64*)out);
          } break;
        }
        decode_cursor += o_size;
      }
      
      // commit out values
      operand1 = o[0];
      operand2 = o[1];
    }
    U64 after_decode_off = decode_cursor;
    
    // register checks
    if (control_bits & DW_CFAControlBits_IsReg0) {
      if (operand0 >= cells_per_row) {
        goto done;
      }
    }
    if (control_bits & DW_CFAControlBits_IsReg1) {
      if (operand1 >= cells_per_row) {
        goto done;
      }
    }
    if (control_bits & DW_CFAControlBits_IsReg2) {
      if (operand2 >= cells_per_row) {
        goto done;
      }
    }
    
    // values for deferred work
    U64 new_table_ip = table_ip;
    
    // step
    U64 step_cursor = after_decode_off;
    switch (opcode) {
      default: goto done;
      case DW_CFA_Nop:break;
      
      //// new row/IP opcodes ////
      
      case DW_CFA_SetLoc: {
        new_table_ip = operand1;
      } break;
      case DW_CFA_AdvanceLoc: {
        new_table_ip = table_ip + operand0*cie->code_align_factor;
      } break;
      case DW_CFA_AdvanceLoc1:
      case DW_CFA_AdvanceLoc2:
      case DW_CFA_AdvanceLoc4: {
        U64 advance = operand1*cie->code_align_factor;
        new_table_ip = table_ip + advance;
      } break;
      
      //// change CFA (canonical frame address) opcodes ////
      
      case DW_CFA_DefCfa: {
        row->cfa_cell.rule = DW_CFI_CFA_Rule_RegOff;
        row->cfa_cell.reg_idx = operand1;
        row->cfa_cell.offset = operand2;
      } break;
      
      case DW_CFA_DefCfaSf: {
        row->cfa_cell.rule = DW_CFI_CFA_Rule_RegOff;
        row->cfa_cell.reg_idx = operand1;
        row->cfa_cell.offset = ((S64)operand2)*cie->data_align_factor;
      } break;
      
      case DW_CFA_DefCfaRegister: {
        // check rule
        if (row->cfa_cell.rule != DW_CFI_CFA_Rule_RegOff) {
          goto done;
        }
        // commit new cfa
        row->cfa_cell.reg_idx = operand1;
      } break;
      
      case DW_CFA_DefCfaOffset: {
        // check rule
        if (row->cfa_cell.rule != DW_CFI_CFA_Rule_RegOff) {
          goto done;
        }
        // commit new cfa
        row->cfa_cell.offset = operand1;
      } break;
      
      case DW_CFA_DefCfaOffsetSf: {
        // check rule
        if (row->cfa_cell.rule != DW_CFI_CFA_Rule_RegOff) {
          goto done;
        }
        // commit new cfa
        row->cfa_cell.offset = ((S64)operand1)*cie->data_align_factor;
      } break;
      
      case DW_CFA_DefCfaExpr: {
        // setup expr range
        U64 expr_first = range.min + after_decode_off;
        U64 expr_size  = operand1;
        step_cursor += expr_size;
        
        // commit new cfa
        row->cfa_cell.rule     = DW_CFI_CFA_Rule_Expr;
        row->cfa_cell.expr.min = expr_first;
        row->cfa_cell.expr.max = expr_first + expr_size;
      } break;
      
      
      //// change register rules ////
      
      case DW_CFA_Undefined: {
        row->cells[operand1].rule = DW_CFIRegisterRule_Undefined;
      } break;
      
      case DW_CFA_SameValue: {
        row->cells[operand1].rule = DW_CFIRegisterRule_SameValue;
      } break;
      
      case DW_CFA_Offset: {
        DW_CFICell *cell = &row->cells[operand0];
        cell->rule       = DW_CFIRegisterRule_Offset;
        cell->n          = operand1*cie->data_align_factor;
      } break;
      
      case DW_CFA_OffsetExt: {
        DW_CFICell *cell = &row->cells[operand1];
        cell->rule       = DW_CFIRegisterRule_Offset;
        cell->n          = operand2*cie->data_align_factor;
      } break;
      
      case DW_CFA_OffsetExtSf: {
        DW_CFICell *cell = &row->cells[operand1];
        cell->rule       = DW_CFIRegisterRule_Offset;
        cell->n          = ((S64)operand2)*cie->data_align_factor;
      } break;
      
      case DW_CFA_ValOffset: {
        DW_CFICell *cell = &row->cells[operand1];
        cell->rule       = DW_CFIRegisterRule_ValOffset;
        cell->n          = operand2*cie->data_align_factor;
      } break;
      
      case DW_CFA_ValOffsetSf: {
        DW_CFICell *cell = &row->cells[operand1];
        cell->rule       = DW_CFIRegisterRule_ValOffset;
        cell->n          = ((S64)operand2)*cie->data_align_factor;
      } break;
      
      case DW_CFA_Register: {
        DW_CFICell *cell = &row->cells[operand1];
        cell->rule = DW_CFIRegisterRule_Register;
        cell->n = operand2;
      } break;
      
      case DW_CFA_Expr: {
        // setup expr range
        U64 expr_first = range.min + after_decode_off;
        U64 expr_size = operand2;
        step_cursor += expr_size;
        
        // commit new rule
        DW_CFICell *cell = &row->cells[operand1];
        cell->rule = DW_CFIRegisterRule_Expression;
        cell->expr.min = expr_first;
        cell->expr.max = expr_first + expr_size;
      } break;
      
      case DW_CFA_ValExpr: {
        // setup expr range
        U64 expr_first = range.min + after_decode_off;
        U64 expr_size = operand2;
        step_cursor += expr_size;
        
        // commit new rule
        DW_CFICell *cell = &row->cells[operand1];
        cell->rule = DW_CFIRegisterRule_ValExpression;
        cell->expr.min = expr_first;
        cell->expr.max = expr_first + expr_size;
      } break;
      
      case DW_CFA_Restore: {
        // check initial row
        if (initial_row == 0) {
          goto done;
        }
        // commit new rule
        row->cells[operand0] = initial_row->cells[operand0];
      } break;
      
      case DW_CFA_RestoreExt: {
        // check initial row
        if (initial_row == 0) {
          goto done;
        }
        // commit new rule
        row->cells[operand1] = initial_row->cells[operand1];
      } break;
      
      
      //// row stack ////
      
      case DW_CFA_RememberState: {
        DW_CFIRow *stack_row = free_rows;
        if (stack_row != 0) {
          SLLStackPop(free_rows);
        } else {
          stack_row = dw_unwind_row_alloc_x64(scratch.arena, cells_per_row);
        }
        dw_unwind_row_copy_x64(stack_row, row, cells_per_row);
        SLLStackPush(stack, stack_row);
      } break;
      
      case DW_CFA_RestoreState: {
        if (stack != 0) {
          DW_CFIRow *stack_row = stack;
          SLLStackPop(stack);
          dw_unwind_row_copy_x64(row, stack_row, cells_per_row);
          SLLStackPush(free_rows, stack_row);
        } else {
          dw_unwind_row_zero_x64(row, cells_per_row);
        }
      } break;
    }
    
    // apply location change
    if (control_bits & DW_CFAControlBits_NewRow) {
      // new ip should always grow the ip
      if (new_table_ip <= table_ip) {
        goto done;
      }
      // stop if this encloses the target ip
      if (table_ip <= target_ip && target_ip < new_table_ip) {
        result = 1;
        goto done;
      }
      // commit new ip
      table_ip = new_table_ip;
    }
    
    // advance
    cfi_off = step_cursor;
  }
  done:;
  
  scratch_end(scratch);
  return result;
}

