{% unless flag?(:win32) %}
  require "c/dlfcn"
{% end %}
require "c/stdio"
require "c/string"
require "../lib_unwind"

{% if flag?(:darwin) || flag?(:bsd) || flag?(:linux) || flag?(:solaris) || flag?(:win32) %}
  require "./dwarf"
{% else %}
  require "./null"
{% end %}

struct Exception::CallStack
  skip(__FILE__)

  {% if flag?(:gnu) && flag?(:i386) %}
    # This is only used for the workaround described in `Exception.unwind`
    @@makecontext_range : Range(Void*, Void*)?

    def self.makecontext_range
      @@makecontext_range ||= begin
        makecontext_start = makecontext_end = LibC.dlsym(LibC::RTLD_DEFAULT, "makecontext")

        while true
          ret = LibC.dladdr(makecontext_end, out info)
          break if ret == 0 || info.dli_sname.null?
          break unless LibC.strcmp(info.dli_sname, "makecontext") == 0
          makecontext_end += 1
        end

        (makecontext_start...makecontext_end)
      end
    end
  {% end %}

  def self.setup_crash_handler
    {% if flag?(:win32) %}
      Crystal::System::Signal.setup_seh_handler
    {% else %}
      Crystal::System::Signal.setup_segfault_handler
    {% end %}
  end

  {% if flag?(:interpreted) %} @[Primitive(:interpreter_call_stack_unwind)] {% end %}
  protected def self.unwind : Array(Void*)
    callstack = Array(Void*).new(32)
    backtrace_fn = ->(context : LibUnwind::Context, data : Void*) do
      bt = data.as(typeof(callstack))

      ip = {% if flag?(:arm) %}
             Pointer(Void).new(__crystal_unwind_get_ip(context))
           {% else %}
             Pointer(Void).new(LibUnwind.get_ip(context))
           {% end %}
      bt << ip

      {% if flag?(:gnu) && flag?(:i386) %}
        # This is a workaround for glibc bug: https://sourceware.org/bugzilla/show_bug.cgi?id=18635
        # The unwind info is corrupted when `makecontext` is used.
        # Stop the backtrace here. There is nothing interest beyond this point anyway.
        if CallStack.makecontext_range.includes?(ip)
          return LibUnwind::ReasonCode::END_OF_STACK
        end
      {% end %}

      LibUnwind::ReasonCode::NO_REASON
    end

    LibUnwind.backtrace(backtrace_fn, callstack.as(Void*))
    callstack
  end

  struct RepeatedFrame
    getter ip : Void*, count : Int32

    def initialize(@ip : Void*)
      @count = 0
    end

    def incr
      @count += 1
    end
  end

  def self.print_backtrace : Nil
    backtrace_fn = ->(context : LibUnwind::Context, data : Void*) do
      last_frame = data.as(RepeatedFrame*)

      ip = {% if flag?(:arm) %}
             Pointer(Void).new(__crystal_unwind_get_ip(context))
           {% else %}
             Pointer(Void).new(LibUnwind.get_ip(context))
           {% end %}

      if last_frame.value.ip == ip
        last_frame.value.incr
      else
        print_frame(last_frame.value) unless last_frame.value.ip.address == 0
        last_frame.value = RepeatedFrame.new ip
      end
      LibUnwind::ReasonCode::NO_REASON
    end

    rf = RepeatedFrame.new(Pointer(Void).null)
    LibUnwind.backtrace(backtrace_fn, pointerof(rf).as(Void*))
    print_frame(rf)
  end

  private def self.print_frame(repeated_frame)
    Crystal::System.print_error "[%p] ", repeated_frame.ip
    print_frame_location(repeated_frame)
    Crystal::System.print_error " (%d times)", repeated_frame.count + 1 unless repeated_frame.count == 0
    Crystal::System.print_error "\n"
  end

  private def self.print_frame_location(repeated_frame)
    {% if flag?(:debug) %}
      if @@loaded
        pc = CallStack.decode_address(repeated_frame.ip)
        if name = decode_function_name(pc)
          file, line, column = Exception::CallStack.decode_line_number(pc)
          if file && file != "??"
            Crystal::System.print_error "%s at %s:%d:%d", name, file, line, column
            return
          end
        end
      end
    {% end %}

    unsafe_decode_frame(repeated_frame.ip) do |offset, sname, fname|
      Crystal::System.print_error "%s +%lld in %s", sname, offset.to_i64, fname
      return
    end

    Crystal::System.print_error "???"
  end

  protected def self.decode_frame(ip)
    decode_frame(ip) do |offset, symbol, file|
      symbol = symbol ? String.new(symbol) : "??"
      file = file ? String.new(file) : "??"
      {offset, symbol, file}
    end
  end

  # variant of `.decode_frame` that returns the C strings directly instead of
  # wrapping them in `String.new`, since the SIGSEGV handler cannot allocate
  # memory via the GC
  protected def self.unsafe_decode_frame(ip, &)
    decode_frame(ip) do |offset, symbol, file|
      symbol ||= "??".to_unsafe
      file ||= "??".to_unsafe
      yield offset, symbol, file
    end
  end

  private def self.decode_frame(ip, &)
    original_ip = ip
    while true
      retry = dladdr(ip) do |file, symbol, address|
        offset = original_ip - address
        if offset == 0
          ip -= 1
          true
        elsif symbol.null? && file.null?
          false
        else
          return yield offset, symbol, file
        end
      end
      break unless retry
    end
  end

  {% if flag?(:win32) %}
    def self.dladdr(ip, &)
      if LibC.GetModuleHandleExW(LibC::GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT | LibC::GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, ip.as(LibC::LPWSTR), out hmodule) != 0
        symbol, address = internal_symbol(hmodule, ip) || external_symbol(hmodule, ip) || return

        utf16_file = uninitialized LibC::WCHAR[LibC::MAX_PATH]
        len = LibC.GetModuleFileNameW(hmodule, utf16_file, utf16_file.size)
        if 0 < len < utf16_file.size
          utf8_file = uninitialized UInt8[sizeof(UInt8[LibC::MAX_PATH][3])]
          file = utf8_file.to_unsafe
          appender = file.appender
          String.each_utf16_char(utf16_file.to_slice[0, len + 1]) do |ch|
            ch.each_byte { |b| appender << b }
          end
        else
          file = Pointer(UInt8).null
        end

        yield file, symbol, address
      end
    end

    private def self.internal_symbol(hmodule, ip)
      if coff_symbols = @@coff_symbols
        if LibC.GetModuleHandleExW(LibC::GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, nil, out this_hmodule) != 0 && this_hmodule == hmodule
          section_base, section_index = lookup_section(hmodule, ip) || return
          offset = ip - section_base
          section_coff_symbols = coff_symbols[section_index]? || return
          next_sym = section_coff_symbols.bsearch_index { |sym| offset < sym.offset } || return
          sym = section_coff_symbols[next_sym - 1]? || return

          {sym.name.to_unsafe, section_base + sym.offset}
        end
      end
    end

    private def self.external_symbol(hmodule, ip)
      if dir = data_directory(hmodule, LibC::IMAGE_DIRECTORY_ENTRY_EXPORT)
        exports = dir.to_unsafe.as(LibC::IMAGE_EXPORT_DIRECTORY*).value

        found_address = Pointer(Void).null
        found_index = -1

        func_address_offsets = (hmodule + exports.addressOfFunctions).as(LibC::DWORD*).to_slice(exports.numberOfFunctions)
        func_address_offsets.each_with_index do |offset, i|
          address = hmodule + offset
          if found_address < address <= ip
            found_address, found_index = address, i
          end
        end

        return unless found_address

        func_name_ordinals = (hmodule + exports.addressOfNameOrdinals).as(LibC::WORD*).to_slice(exports.numberOfNames)
        if ordinal_index = func_name_ordinals.index(&.== found_index)
          symbol = (hmodule + (hmodule + exports.addressOfNames).as(LibC::DWORD*)[ordinal_index]).as(UInt8*)
          {symbol, found_address}
        end
      end
    end

    private def self.lookup_section(hmodule, ip)
      dos_header = hmodule.as(LibC::IMAGE_DOS_HEADER*)
      return unless dos_header.value.e_magic == 0x5A4D # MZ

      nt_header = (hmodule + dos_header.value.e_lfanew).as(LibC::IMAGE_NT_HEADERS*)
      return unless nt_header.value.signature == 0x00004550 # PE\0\0

      section_headers = (nt_header + 1).as(LibC::IMAGE_SECTION_HEADER*).to_slice(nt_header.value.fileHeader.numberOfSections)
      section_headers.each_with_index do |header, i|
        base = hmodule + header.virtualAddress
        if base <= ip < base + header.virtualSize
          return base, i
        end
      end
    end

    private def self.data_directory(hmodule, index)
      dos_header = hmodule.as(LibC::IMAGE_DOS_HEADER*)
      return unless dos_header.value.e_magic == 0x5A4D # MZ

      nt_header = (hmodule + dos_header.value.e_lfanew).as(LibC::IMAGE_NT_HEADERS*)
      return unless nt_header.value.signature == 0x00004550 # PE\0\0
      return unless nt_header.value.optionalHeader.magic == {{ flag?(:bits64) ? 0x20b : 0x10b }}
      return unless index.in?(0...{16, nt_header.value.optionalHeader.numberOfRvaAndSizes}.min)

      directory = nt_header.value.optionalHeader.dataDirectory.to_unsafe[index]
      if directory.virtualAddress != 0
        Bytes.new(hmodule.as(UInt8*) + directory.virtualAddress, directory.size, read_only: true)
      end
    end
  {% else %}
    private def self.dladdr(ip, &)
      if LibC.dladdr(ip, out info) != 0
        yield info.dli_fname, info.dli_sname, info.dli_saddr
      end
    end
  {% end %}
end
