require "./lib_llvm"

@[Experimental("The C API wrapped by this type is marked as experimental by LLVM.")]
struct LLVM::DIBuilder
  private DW_TAG_structure_type = 19

  private def initialize(@unwrap : LibLLVM::DIBuilderRef, @llvm_module : Module)
  end

  def self.new(mod : LLVM::Module)
    new(LibLLVM.create_di_builder(mod), mod)
  end

  def dispose
    LibLLVM.dispose_di_builder(self)
  end

  def context
    @llvm_module.context
  end

  def create_compile_unit(lang : DwarfSourceLanguage, file, dir, producer, optimized, flags, runtime_version)
    file = create_file(file, dir)
    {% if LibLLVM::IS_LT_110 %}
      LibLLVM.di_builder_create_compile_unit(self,
        lang, file, producer, producer.bytesize, optimized ? 1 : 0, flags, flags.bytesize, runtime_version,
        split_name: nil, split_name_len: 0, kind: LibLLVM::DWARFEmissionKind::Full, dwo_id: 0,
        split_debug_inlining: 1, debug_info_for_profiling: 0,
      )
    {% else %}
      LibLLVM.di_builder_create_compile_unit(self,
        lang, file, producer, producer.bytesize, optimized ? 1 : 0, flags, flags.bytesize, runtime_version,
        split_name: nil, split_name_len: 0, kind: LibLLVM::DWARFEmissionKind::Full, dwo_id: 0,
        split_debug_inlining: 1, debug_info_for_profiling: 0, sys_root: nil, sys_root_len: 0, sdk: nil, sdk_len: 0,
      )
    {% end %}
  end

  @[Deprecated("Pass an `LLVM::DwarfSourceLanguage` for `lang` instead")]
  def create_compile_unit(lang cpp_lang_code, file, dir, producer, optimized, flags, runtime_version)
    # map the c++ values from `llvm::dwarf::SourceLanguage` to the c values from `LLVMDWARFSourceLanguage`
    c_lang_code =
      case cpp_lang_code
      when 0x8001; DwarfSourceLanguage::Mips_Assembler
      when 0x8e57; DwarfSourceLanguage::GOOGLE_RenderScript
      when 0xb000; DwarfSourceLanguage::BORLAND_Delphi
      else         DwarfSourceLanguage.new(lang - 1)
      end

    create_compile_unit(c_lang_code, file, dir, producer, optimized, flags, runtime_version)
  end

  def create_basic_type(name, size_in_bits, align_in_bits, encoding)
    LibLLVM.di_builder_create_basic_type(self, name, name.bytesize, size_in_bits, encoding.value, DIFlags::Zero)
  end

  def get_or_create_type_array(types : Array(LibLLVM::MetadataRef))
    LibLLVM.di_builder_get_or_create_type_array(self, types, types.size)
  end

  def create_subroutine_type(file, parameter_types)
    LibLLVM.di_builder_create_subroutine_type(self, file, parameter_types, parameter_types.size, DIFlags::Zero)
  end

  def create_file(file, dir)
    LibLLVM.di_builder_create_file(self, file, file.bytesize, dir, dir.bytesize)
  end

  def create_lexical_block(scope, file_scope, line, column)
    LibLLVM.di_builder_create_lexical_block(self, scope, file_scope, line, column)
  end

  def create_lexical_block_file(scope, file_scope, discriminator = 0)
    LibLLVM.di_builder_create_lexical_block_file(self, scope, file_scope, discriminator)
  end

  def create_function(scope, name, linkage_name, file, line, composite_type, is_local_to_unit, is_definition,
                      scope_line, flags, is_optimized, func)
    sub = LibLLVM.di_builder_create_function(self, scope, name, name.bytesize,
      linkage_name, linkage_name.bytesize, file, line, composite_type, is_local_to_unit ? 1 : 0,
      is_definition ? 1 : 0, scope_line, flags, is_optimized ? 1 : 0)
    LibLLVM.set_subprogram(func, sub)
    sub
  end

  def create_auto_variable(scope, name, file, line, type, align_in_bits, flags = DIFlags::Zero)
    LibLLVM.di_builder_create_auto_variable(self, scope, name, name.bytesize, file, line, type, 1, flags, align_in_bits)
  end

  def create_parameter_variable(scope, name, argno, file, line, type, flags = DIFlags::Zero)
    LibLLVM.di_builder_create_parameter_variable(self, scope, name, name.bytesize, argno, file, line, type, 1, flags)
  end

  def create_expression(addr, length)
    LibLLVM.di_builder_create_expression(self, addr, length)
  end

  def insert_declare_at_end(storage, var_info, expr, dl : LibLLVM::MetadataRef, block)
    {% if LibLLVM::IS_LT_190 %}
      LibLLVM.di_builder_insert_declare_at_end(self, storage, var_info, expr, dl, block)
    {% else %}
      LibLLVM.di_builder_insert_declare_record_at_end(self, storage, var_info, expr, dl, block)
    {% end %}
  end

  def get_or_create_array(elements : Array(LibLLVM::MetadataRef))
    LibLLVM.di_builder_get_or_create_array(self, elements, elements.size)
  end

  def create_enumerator(name, value)
    is_unsigned = value.is_a?(Int::Unsigned) ? 1 : 0

    {% unless LibLLVM::IS_LT_210 %}
      if value.is_a?(Int128) || value.is_a?(UInt128)
        encoded_value = UInt64[value & UInt64::MAX, (value >> 64) & UInt64::MAX]
        return LibLLVM.di_builder_create_enumerator_of_arbitrary_precision(
          self, name, name.bytesize, encoded_value.size * 64, encoded_value, is_unsigned)
      end
    {% end %}

    {{ LibLLVM::IS_LT_90 ? LibLLVMExt : LibLLVM }}.di_builder_create_enumerator(
      self, name, name.bytesize, value.to_i64!, is_unsigned)
  end

  def create_enumeration_type(scope, name, file, line_number, size_in_bits, align_in_bits, elements, underlying_type)
    LibLLVM.di_builder_create_enumeration_type(self, scope, name, name.bytesize, file, line_number,
      size_in_bits, align_in_bits, elements, elements.size, underlying_type)
  end

  def create_struct_type(scope, name, file, line, size_in_bits, align_in_bits, flags, derived_from, element_types)
    LibLLVM.di_builder_create_struct_type(self, scope, name, name.bytesize, file, line,
      size_in_bits, align_in_bits, flags, derived_from, element_types, element_types.size, 0, nil, nil, 0)
  end

  def create_union_type(scope, name, file, line, size_in_bits, align_in_bits, flags, element_types)
    LibLLVM.di_builder_create_union_type(self, scope, name, name.bytesize, file, line,
      size_in_bits, align_in_bits, flags, element_types, element_types.size, 0, nil, 0)
  end

  def create_array_type(size_in_bits, align_in_bits, type, subs)
    LibLLVM.di_builder_create_array_type(self, size_in_bits, align_in_bits, type, subs, subs.size)
  end

  def create_member_type(scope, name, file, line, size_in_bits, align_in_bits, offset_in_bits, flags, ty)
    LibLLVM.di_builder_create_member_type(self, scope, name, name.bytesize, file, line, size_in_bits, align_in_bits,
      offset_in_bits, flags, ty)
  end

  def create_pointer_type(pointee, size_in_bits, align_in_bits, name)
    LibLLVM.di_builder_create_pointer_type(self, pointee, size_in_bits, align_in_bits, 0, name, name.bytesize)
  end

  def create_replaceable_composite_type(scope, name, file, line)
    LibLLVM.di_builder_create_replaceable_composite_type(self, DW_TAG_structure_type, name, name.bytesize,
      scope, file, line, 0, 0, 0, DIFlags::FwdDecl, nil, 0)
  end

  def replace_temporary(from, to)
    LibLLVM.metadata_replace_all_uses_with(from, to)
  end

  def create_unspecified_type(name : String)
    LibLLVM.di_builder_create_unspecified_type(self, name, name.bytesize)
  end

  def get_or_create_array_subrange(lo, count)
    LibLLVM.di_builder_get_or_create_subrange(self, lo, count)
  end

  def create_debug_location(line, column, scope, inlined_at = nil)
    LibLLVM.di_builder_create_debug_location(context, line, column, scope, inlined_at)
  end

  def end
    LibLLVM.di_builder_finalize(self)
  end

  def to_unsafe
    @unwrap
  end

  @[Deprecated("Use a `LibLLVM::MetadataRef` for `dl` instead")]
  def insert_declare_at_end(storage, var_info, expr, dl : LibLLVM::ValueRef | LLVM::Value, block)
    dl = dl.to_unsafe unless dl.is_a?(LibLLVM::ValueRef)
    insert_declare_at_end(storage, var_info, expr, LibLLVM.value_as_metadata(dl), block)
  end

  @[Deprecated("Pass an array for `parameter_types` directly")]
  def create_subroutine_type(file, parameter_types : LibLLVM::MetadataRef)
    create_subroutine_type(file, extract_metadata_array(parameter_types))
  end

  @[Deprecated("Pass an array for `elements` directly")]
  def create_enumeration_type(scope, name, file, line_number, size_in_bits, align_in_bits, elements : LibLLVM::MetadataRef, underlying_type)
    create_enumeration_type(scope, name, file, line_number, size_in_bits, align_in_bits, extract_metadata_array(elements), underlying_type)
  end

  @[Deprecated("Pass an array for `element_types` directly")]
  def create_struct_type(scope, name, file, line, size_in_bits, align_in_bits, flags, derived_from, element_types : LibLLVM::MetadataRef)
    create_struct_type(scope, name, file, line, size_in_bits, align_in_bits, flags, derived_from, extract_metadata_array(element_types))
  end

  @[Deprecated("Pass an array for `element_types` directly")]
  def create_union_type(scope, name, file, line, size_in_bits, align_in_bits, flags, element_types : LibLLVM::MetadataRef)
    create_union_type(scope, name, file, line, size_in_bits, align_in_bits, flags, extract_metadata_array(element_types))
  end

  @[Deprecated("Pass an array for `subs` directly")]
  def create_array_type(size_in_bits, align_in_bits, type, subs : LibLLVM::MetadataRef)
    create_array_type(size_in_bits, align_in_bits, type, extract_metadata_array(subs))
  end

  private def extract_metadata_array(metadata : LibLLVM::MetadataRef)
    metadata_as_value = LibLLVM.metadata_as_value(context, metadata)
    operand_count = LibLLVM.get_md_node_num_operands(metadata_as_value).to_i
    operands = Pointer(LibLLVM::ValueRef).malloc(operand_count)
    LibLLVM.get_md_node_operands(metadata_as_value, operands)
    Slice.new(operand_count) { |i| LibLLVM.value_as_metadata(operands[i]) }
  end
end
