defmodule Archive.Setup do
  @moduledoc false
  @ext (case :os.type() do
          {:win32, _} -> ".dll"
          {:unix, :darwin} -> ".dylib"
          {:unix, _} -> ".so"
        end)

  @base_path "#{:code.priv_dir(:archive)}/#{Mix.target()}"
  @include_path "#{@base_path}/include/"
  @library_dir "#{@base_path}/lib/"
  @link_path "#{@library_dir}libarchive#{@ext}"

  defmacro __using__(_opts) do
    quote do
      use Zig,
        otp_app: :archive,
        c: [
          include_dirs: [unquote(@include_path)],
          link_lib: unquote(@link_path),
          library_dirs: [unquote(@library_dir)]
        ],
        resources: [:ArchiveReaderResource, :ArchiveWriterResource, :ArchiveEntryResource],
        dump_build_zig: true
    end
  end
end

defmodule Archive.Nif do
  @moduledoc """
  > #### Proceed With Caution {: .error}
  >
  > The `Archive.Nif` module contains the functions that directly interact with `libarchive`.
  > It is generally discouraged to interact with this library directly. If you find you need
  > functionality not supported in the higher-level APIs, consider opening an issue first
  > if the feature would be generally applicable or desireable.

  `Archive.Nif` provides direct `Elixir` bindings to the `libarchive` C API. Most of the functions
  in this module are 1-to-1 mappings to the C API equivalent functions.

  These functions work directly with references, rather than `Archive` or `Archive.Entry` structs.

  As long as you use the `ArchiveReaderResource` and `ArchiveEntryResource` reference types, they will
  be managed and garbage-collected by this module once created.

  Most functions in this module may raise `ErlangError` on failure. You should invoke **most** of
  these functions using `Archive.Nif.safe_call/2` to catch the errors and return them as `:ok` or
  an `{:error, reason}` tuple

  ## `libarchive` APIs

  `libarchive` distinguishes the following APIs:

  <!-- tabs-open -->
  ### `archive_read`
  > #### Reading an Archive {: .info}
  >
  > Although `Archive`'s high-level API takes care of all of the resource management for you, it can still be useful to understand how it works:
  >   1) Create new archive reader object
  >   2) Update any global reader properties as appropriate. These properties determine supported compressions, formats, etc.
  >   3) Open the archive
  >   4) Repeatedly call archive_read_next_header to get information about
  >       successive archive entries.  Call archive_read_data to extract
  >      data for entries of interest.
  >   5) Cleanup archive reader object

  ### `archive-write`
  > #### Creating an Archive {: .info}
  > To create an archive:
  >   1) Ask archive_write_new for an archive writer object.
  >   2) Set any global properties.  In particular, you should set
  >      the compression and format to use.
  >   3) Call archive_write_open to open the file (most people
  >       will use archive_write_open_file or archive_write_open_fd,
  >       which provide convenient canned I/O callbacks for you).
  >   4) For each entry:
  >      - construct an appropriate struct archive_entry structure
  >      - archive_write_header to write the header
  >      - archive_write_data to write the entry data
  >   5) archive_write_close to close the output
  >   6) archive_write_free to cleanup the writer and release resources

  ### `archive_write_disk`
  > #### Writing to Disk {: .info}
  To create objects on disk:
  >   1) Ask archive_write_disk_new for a new archive_write_disk object.
  >   2) Set any global properties.  In particular, you probably
  >      want to set the options.
  >   3) For each entry:
  >      - construct an appropriate struct archive_entry structure
  >      - archive_write_header to create the file/dir/etc on disk
  >      - archive_write_data to write the entry data
  >   4) archive_write_free to cleanup the writer and release resources
  >s
  > In particular, you can use this in conjunction with archive_read()
  > to pull entries out of an archive and create them on disk.
  <!-- tabs-close -->

  It also has `archive_read_extract` functions that combine reading with writing to disk.
  """
  require Logger
  use Archive.Setup

  ~Z"""
  const std = @import("std");
  const c = @cImport({
      if (@import("builtin").abi == .musl) {
          @cDefine("__DEFINED_struct_timespec", "");
      }
      @cInclude("archive.h");
      @cInclude("archive_entry.h");
  });
  const ArchiveError = error{ ArchiveFailed, ArchiveRetry, ArchiveWarn, ArchiveFatal, ArchiveEof };
  const CAllocError = error{ archiveEntry, archive };
  const beam = @import("beam");
  const enif = @import("erl_nif");
  const root = @import("root");

  pub const ArchiveReaderResource = beam.Resource(*c.archive, root, .{ .Callbacks = ArchiveReaderResourceCallbacks });
  pub const ArchiveWriterResource = beam.Resource(*c.archive, root, .{ .Callbacks = ArchiveWriterResourceCallbacks });
  pub const ArchiveEntryResource = beam.Resource(*c.archive_entry, root, .{ .Callbacks = ArchiveEntryResourceCallbacks });

  pub const ArchiveFilter = enum(c_int) {
      none = c.ARCHIVE_FILTER_NONE,
      gzip = c.ARCHIVE_FILTER_GZIP,
      bzip2 = c.ARCHIVE_FILTER_BZIP2,
      compress = c.ARCHIVE_FILTER_COMPRESS,
      program = c.ARCHIVE_FILTER_PROGRAM,
      lzma = c.ARCHIVE_FILTER_LZMA,
      xz = c.ARCHIVE_FILTER_XZ,
      uu = c.ARCHIVE_FILTER_UU,
      rpm = c.ARCHIVE_FILTER_RPM,
      lzip = c.ARCHIVE_FILTER_LZIP,
      lrzip = c.ARCHIVE_FILTER_LRZIP,
      lzop = c.ARCHIVE_FILTER_LZOP,
      grzip = c.ARCHIVE_FILTER_GRZIP,
      lz4 = c.ARCHIVE_FILTER_LZ4,
      zstd = c.ARCHIVE_FILTER_ZSTD,
  };

  pub const ArchiveFormat = enum(c_int) {
      cpio = c.ARCHIVE_FORMAT_CPIO,
      cpio_posix = c.ARCHIVE_FORMAT_CPIO_POSIX,
      cpio_bin_le = c.ARCHIVE_FORMAT_CPIO_BIN_LE,
      cpio_bin_be = c.ARCHIVE_FORMAT_CPIO_BIN_BE,
      cpio_svr4_nocrc = c.ARCHIVE_FORMAT_CPIO_SVR4_NOCRC,
      cpio_svr4_crc = c.ARCHIVE_FORMAT_CPIO_SVR4_CRC,
      cpio_afio_large = c.ARCHIVE_FORMAT_CPIO_AFIO_LARGE,
      cpio_pwb = c.ARCHIVE_FORMAT_CPIO_PWB,
      shar = c.ARCHIVE_FORMAT_SHAR,
      shar_base = c.ARCHIVE_FORMAT_SHAR_BASE,
      shar_dump = c.ARCHIVE_FORMAT_SHAR_DUMP,
      tar = c.ARCHIVE_FORMAT_TAR,
      tar_ustar = c.ARCHIVE_FORMAT_TAR_USTAR,
      tar_pax_interchange = c.ARCHIVE_FORMAT_TAR_PAX_INTERCHANGE,
      tar_pax_restricted = c.ARCHIVE_FORMAT_TAR_PAX_RESTRICTED,
      tar_gnutar = c.ARCHIVE_FORMAT_TAR_GNUTAR,
      iso9660 = c.ARCHIVE_FORMAT_ISO9660,
      iso9660_rockridge = c.ARCHIVE_FORMAT_ISO9660_ROCKRIDGE,
      zip = c.ARCHIVE_FORMAT_ZIP,
      empty = c.ARCHIVE_FORMAT_EMPTY,
      ar = c.ARCHIVE_FORMAT_AR,
      ar_gnu = c.ARCHIVE_FORMAT_AR_GNU,
      ar_bsd = c.ARCHIVE_FORMAT_AR_BSD,
      mtree = c.ARCHIVE_FORMAT_MTREE,
      raw = c.ARCHIVE_FORMAT_RAW,
      xar = c.ARCHIVE_FORMAT_XAR,
      lha = c.ARCHIVE_FORMAT_LHA,
      cab = c.ARCHIVE_FORMAT_CAB,
      rar = c.ARCHIVE_FORMAT_RAR,
      sevenz = c.ARCHIVE_FORMAT_7ZIP,
      warc = c.ARCHIVE_FORMAT_WARC,
      rar_v5 = c.ARCHIVE_FORMAT_RAR_V5,
  };

  pub fn isSubFormatOf(self: ArchiveFormat, parent: ArchiveFormat) bool {
      return (@intFromEnum(self) & c.ARCHIVE_FORMAT_BASE_MASK) == @intFromEnum(parent);
  }

  pub const ExtractFlag = enum(c_int) {
      owner = c.ARCHIVE_EXTRACT_OWNER,
      perm = c.ARCHIVE_EXTRACT_PERM,
      time = c.ARCHIVE_EXTRACT_TIME,
      no_overwrite = c.ARCHIVE_EXTRACT_NO_OVERWRITE,
      unlink = c.ARCHIVE_EXTRACT_UNLINK,
      acl = c.ARCHIVE_EXTRACT_ACL,
      fflags = c.ARCHIVE_EXTRACT_FFLAGS,
      xattr = c.ARCHIVE_EXTRACT_XATTR,
      secure_symlinks = c.ARCHIVE_EXTRACT_SECURE_SYMLINKS,
      secure_nodotdot = c.ARCHIVE_EXTRACT_SECURE_NODOTDOT,
      no_autodir = c.ARCHIVE_EXTRACT_NO_AUTODIR,
      no_overwrite_newer = c.ARCHIVE_EXTRACT_NO_OVERWRITE_NEWER,
      sparse = c.ARCHIVE_EXTRACT_SPARSE,
      mac_metadata = c.ARCHIVE_EXTRACT_MAC_METADATA,
      no_hfs_compression = c.ARCHIVE_EXTRACT_NO_HFS_COMPRESSION,
      hfs_compression_forced = c.ARCHIVE_EXTRACT_HFS_COMPRESSION_FORCED,
      secure_noabsolutepaths = c.ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS,
      clear_nochange_fflags = c.ARCHIVE_EXTRACT_CLEAR_NOCHANGE_FFLAGS,
      safe_writes = c.ARCHIVE_EXTRACT_SAFE_WRITES,
  };

  const ExtractFlagDocs = struct {
      flag: ExtractFlag,
      default_doc: []const u8,
  };

  const extractFlagDocumentation = [_]ExtractFlagDocs{
      .{ .flag = .owner, .default_doc = "Do not try to set owner/group." },
      .{ .flag = .perm, .default_doc = "Do obey umask, do not restore SUID/SGID/SVTX bits." },
      .{ .flag = .time, .default_doc = "Do not restore mtime/atime." },
      .{ .flag = .no_overwrite, .default_doc = "Replace existing files." },
      .{ .flag = .unlink, .default_doc = "Try create first, unlink only if create fails with EEXIST." },
      .{ .flag = .acl, .default_doc = "Do not restore ACLs." },
      .{ .flag = .fflags, .default_doc = "Do not restore fflags." },
      .{ .flag = .xattr, .default_doc = "Do not restore xattrs." },
      .{ .flag = .secure_symlinks, .default_doc = "Do not try to guard against extracts redirected by symlinks. Note: With ARCHIVE_EXTRACT_UNLINK, will remove any intermediate symlink." },
      .{ .flag = .secure_nodotdot, .default_doc = "Do not reject entries with '..' as path elements." },
      .{ .flag = .no_autodir, .default_doc = "Create parent directories as needed." },
      .{ .flag = .no_overwrite_newer, .default_doc = "Overwrite files, even if one on disk is newer." },
      .{ .flag = .sparse, .default_doc = "Detect blocks of 0 and write holes instead." },
      .{ .flag = .mac_metadata, .default_doc = "Do not restore Mac extended metadata. This has no effect except on Mac OS." },
      .{ .flag = .no_hfs_compression, .default_doc = "Use HFS+ compression if it was compressed. This has no effect except on Mac OS v10.6 or later." },
      .{ .flag = .hfs_compression_forced, .default_doc = "Do not use HFS+ compression if it was not compressed. This has no effect except on Mac OS v10.6 or later." },
      .{ .flag = .secure_noabsolutepaths, .default_doc = "Do not reject entries with absolute paths" },
      .{ .flag = .clear_nochange_fflags, .default_doc = "Do not clear no-change flags when unlinking object" },
      .{ .flag = .safe_writes, .default_doc = "Do not extract atomically (using rename)" },
  };

  pub const Allow = enum {
      Read,
      Write,
      ReadWrite,
  };

  pub const ArchiveFormatInfo = struct {
      format: ArchiveFormat,
      allow: Allow,
  };

  pub const ArchiveFilterInfo = struct {
      filter: ArchiveFilter,
      allow: Allow,
  };

  pub const archive_format_info = [_]ArchiveFormatInfo{
      .{ .format = .cpio, .allow = .ReadWrite },
      .{ .format = .cpio_posix, .allow = .Write },
      .{ .format = .cpio_bin_le, .allow = .Write },
      .{ .format = .cpio_bin_be, .allow = .Write },
      .{ .format = .cpio_svr4_nocrc, .allow = .Write },
      .{ .format = .cpio_svr4_crc, .allow = .Write },
      .{ .format = .cpio_afio_large, .allow = .Write },
      .{ .format = .cpio_pwb, .allow = .Write },
      .{ .format = .shar, .allow = .Write },
      .{ .format = .shar_base, .allow = .Write },
      .{ .format = .shar_dump, .allow = .Write },
      .{ .format = .tar, .allow = .ReadWrite },
      .{ .format = .tar_ustar, .allow = .Write },
      .{ .format = .tar_pax_interchange, .allow = .Write },
      .{ .format = .tar_pax_restricted, .allow = .Write },
      .{ .format = .tar_gnutar, .allow = .Write },
      .{ .format = .iso9660, .allow = .ReadWrite },
      .{ .format = .iso9660_rockridge, .allow = .Read },
      .{ .format = .zip, .allow = .ReadWrite },
      .{ .format = .empty, .allow = .Read },
      .{ .format = .ar, .allow = .Read },
      .{ .format = .ar_gnu, .allow = .Read },
      .{ .format = .ar_bsd, .allow = .Read },
      .{ .format = .mtree, .allow = .ReadWrite },
      .{ .format = .raw, .allow = .ReadWrite },
      .{ .format = .xar, .allow = .ReadWrite },
      .{ .format = .lha, .allow = .Read },
      .{ .format = .cab, .allow = .Read },
      .{ .format = .rar, .allow = .Read },
      .{ .format = .sevenz, .allow = .ReadWrite },
      .{ .format = .warc, .allow = .ReadWrite },
      .{ .format = .rar_v5, .allow = .Read },
  };

  pub const archive_filter_info = [_]ArchiveFilterInfo{
      .{ .filter = .none, .allow = .ReadWrite },
      .{ .filter = .gzip, .allow = .ReadWrite },
      .{ .filter = .bzip2, .allow = .ReadWrite },
      .{ .filter = .compress, .allow = .ReadWrite },
      .{ .filter = .lzma, .allow = .ReadWrite },
      .{ .filter = .xz, .allow = .ReadWrite },
      .{ .filter = .uu, .allow = .ReadWrite },
      .{ .filter = .lzip, .allow = .ReadWrite },
      .{ .filter = .lrzip, .allow = .ReadWrite },
      .{ .filter = .lzop, .allow = .ReadWrite },
      .{ .filter = .grzip, .allow = .ReadWrite },
      .{ .filter = .lz4, .allow = .ReadWrite },
      .{ .filter = .zstd, .allow = .ReadWrite },
      .{ .filter = .rpm, .allow = .Read },
  };

  fn TableNames(comptime table: anytype, comptime getFormat: fn (info: @TypeOf(table[0])) []const u8, comptime condition: fn (info: @TypeOf(table[0])) bool) []const []const u8 {
      @setEvalBranchQuota(10000); // Increase if needed for larger arrays

      return comptime blk: {
          var result: []const []const u8 = &[_][]const u8{};
          for (table) |info| {
              if (condition(info)) {
                  result = result ++ [_][]const u8{getFormat(info)};
              }
          }
          break :blk result;
      };
  }

  pub const format_readable_names = TableNames(archive_format_info, struct {
      fn getFormat(info: @TypeOf(archive_format_info[0])) []const u8 {
          return @tagName(info.format);
      }
  }.getFormat, struct {
      fn isReadable(info: @TypeOf(archive_format_info[0])) bool {
          return info.allow == .Read or info.allow == .ReadWrite;
      }
  }.isReadable);

  pub const format_writable_names = TableNames(archive_format_info, struct {
      fn getFormat(info: @TypeOf(archive_format_info[0])) []const u8 {
          return @tagName(info.format);
      }
  }.getFormat, struct {
      fn isWritable(info: @TypeOf(archive_format_info[0])) bool {
          return info.allow == .Write or info.allow == .ReadWrite;
      }
  }.isWritable);

  pub const filter_readable_names = TableNames(archive_filter_info, struct {
      fn getFormat(info: @TypeOf(archive_filter_info[0])) []const u8 {
          return @tagName(info.filter);
      }
  }.getFormat, struct {
      fn isReadable(info: @TypeOf(archive_filter_info[0])) bool {
          return info.allow == .Read or info.allow == .ReadWrite;
      }
  }.isReadable);

  pub const filter_writable_names = TableNames(archive_filter_info, struct {
      fn getFormat(info: @TypeOf(archive_filter_info[0])) []const u8 {
          return @tagName(info.filter);
      }
  }.getFormat, struct {
      fn isWritable(info: @TypeOf(archive_filter_info[0])) bool {
          return info.allow == .Write or info.allow == .ReadWrite;
      }
  }.isWritable);

  pub fn listReadableFormats() []const []const u8 {
      return format_readable_names;
  }

  pub fn listReadableFilters() []const []const u8 {
      return filter_readable_names;
  }

  pub fn listWritableFormats() []const []const u8 {
      return format_writable_names;
  }

  pub fn listWritableFilters() []const []const u8 {
      return filter_writable_names;
  }

  pub const ArchiveReaderResourceCallbacks = struct {
      pub fn dtor(a: **c.archive) void {
          _ = c.archive_read_free(a.*);
      }
  };

  pub const ArchiveWriterResourceCallbacks = struct {
      pub fn dtor(a: **c.archive) void {
          _ = c.archive_write_free(a.*);
      }
  };

  pub const ArchiveEntryResourceCallbacks = struct {
      pub fn dtor(a: **c.archive_entry) void {
          _ = c.archive_entry_free(a.*);
      }
  };

  pub fn fileKinds() struct {
      mask: c_uint,
      block_device: c_uint,
      character_device: c_uint,
      directory: c_uint,
      named_pipe: c_uint,
      sym_link: c_uint,
      file: c_uint,
      unix_domain_socket: c_uint,
      unknown: c_uint,
  } {
      return .{
          .mask = c.S_IFMT,
          .block_device = c.S_IFBLK,
          .character_device = c.S_IFCHR,
          .directory = c.S_IFDIR,
          .named_pipe = c.S_IFIFO,
          .sym_link = c.S_IFLNK,
          .file = c.S_IFREG,
          .unix_domain_socket = c.S_IFSOCK,
          .unknown = 0,
      };
  }

  const FileMode = enum(c_uint) {
      irwxu = c.S_IRWXU,
      irusr = c.S_IRUSR,
      iwusr = c.S_IWUSR,
      ixusr = c.S_IXUSR,
      irwxg = c.S_IRWXG,
      irgrp = c.S_IRGRP,
      iwgrp = c.S_IWGRP,
      ixgrp = c.S_IXGRP,
      irwxo = c.S_IRWXO,
      iroth = c.S_IROTH,
      iwoth = c.S_IWOTH,
      ixoth = c.S_IXOTH,
      isuid = c.S_ISUID,
      isgid = c.S_ISGID,
      isvtx = c.S_ISVTX,
  };

  pub fn archive_entry_copy_stat(e: ArchiveEntryResource, stat: std.c.Stat) !void {
      const c_ino: c.la_int64_t = @intCast(stat.ino);
      c.archive_entry_set_ino(e.unpack(), c_ino);
      c.archive_entry_set_size(e.unpack(), stat.size);
      c.archive_entry_set_mode(e.unpack(), stat.mode);
      const atime = stat.atime();
      const mtime = stat.mtime();
      const ctime = stat.ctime();
      if (@hasField(@TypeOf(atime), "tv_sec") and @hasField(@TypeOf(atime), "tv_nsec")) {
          c.archive_entry_set_atime(e.unpack(), atime.tv_sec, atime.tv_nsec);
          c.archive_entry_set_mtime(e.unpack(), mtime.tv_sec, mtime.tv_nsec);
          c.archive_entry_set_ctime(e.unpack(), ctime.tv_sec, ctime.tv_nsec);
      } else if (@hasField(@TypeOf(atime), "sec") and @hasField(@TypeOf(atime), "nsec")) {
          c.archive_entry_set_atime(e.unpack(), atime.sec, atime.nsec);
          c.archive_entry_set_mtime(e.unpack(), mtime.sec, mtime.nsec);
          c.archive_entry_set_ctime(e.unpack(), ctime.sec, ctime.nsec);
      }

      const c_nlink: u32 = @intCast(stat.ino);
      c.archive_entry_set_nlink(e.unpack(), c_nlink);
      c.archive_entry_set_dev(e.unpack(), stat.dev);
      c.archive_entry_set_rdev(e.unpack(), stat.rdev);
      c.archive_entry_set_gid(e.unpack(), stat.gid);
      c.archive_entry_set_uid(e.unpack(), stat.uid);
  }

  pub fn archive_entry_stat(e: ArchiveEntryResource) !std.c.Stat {
      const c_stat = c.archive_entry_stat(e.unpack());
      if (c_stat == null) {
          return error.StatError;
      }
      var stat: std.c.Stat = @bitCast(c_stat.*);
      const nlinks: @TypeOf(stat.nlink) = @intCast(c.archive_entry_nlink(e.unpack()));
      const newino: @TypeOf(stat.ino) = @intCast(c.archive_entry_ino(e.unpack()));
      const newgid: @TypeOf(stat.gid) = @intCast(c.archive_entry_gid(e.unpack()));
      const newuid: @TypeOf(stat.uid) = @intCast(c.archive_entry_uid(e.unpack()));
      stat.ino = newino;
      stat.nlink = nlinks;
      stat.dev = c.archive_entry_dev(e.unpack());
      stat.gid = newgid;
      stat.uid = newuid;

      return stat;
  }

  pub fn archiveFilterToInt(filter: ArchiveFilter) c_int {
      return @intFromEnum(filter);
  }

  pub fn archiveFilterToAtom(filter: i32) ArchiveFilter {
      return @enumFromInt(filter);
  }

  pub fn archiveFormatToInt(format: ArchiveFormat) c_int {
      return @intFromEnum(format);
  }

  pub fn archiveFormatToAtom(format: i32) ArchiveFormat {
      return @enumFromInt(format);
  }

  fn checkArchiveResult(result: c_int) !void {
      switch (result) {
          c.ARCHIVE_OK => return,
          c.ARCHIVE_RETRY => return ArchiveError.ArchiveRetry,
          c.ARCHIVE_WARN => return ArchiveError.ArchiveWarn,
          c.ARCHIVE_FATAL => return ArchiveError.ArchiveFatal,
          c.ARCHIVE_EOF => return ArchiveError.ArchiveEof,
          else => return ArchiveError.ArchiveFailed,
      }
  }

  pub fn archive_read_new() !ArchiveReaderResource {
      const a: ?*c.archive = c.archive_read_new();
      return if (a) |non_null_a|
          ArchiveReaderResource.create(non_null_a, .{})
      else
          error.archiveEntry;
  }

  pub fn archive_write_new() !ArchiveWriterResource {
      const a: ?*c.archive = c.archive_write_new();
      return if (a) |non_null_a|
          ArchiveWriterResource.create(non_null_a, .{})
      else
          error.archiveEntry;
  }

  pub fn archive_read_support_filter_all(a: ArchiveReaderResource) !void {
      try checkArchiveResult(c.archive_read_support_filter_all(a.unpack()));
  }

  pub fn archive_read_support_format_all(a: ArchiveReaderResource) !void {
      try checkArchiveResult(c.archive_read_support_format_all(a.unpack()));
  }

  pub fn archive_read_open_filename(a: ArchiveReaderResource, filename: []u8, block_size: usize) !void {
      //   Have to manually null-terminate this for the C API
      // Zig's []u8 hold length information, so they aren't necessarily null-terminated
      var slice = try beam.allocator.alloc(u8, filename.len + 1);
      defer beam.allocator.free(slice);
      @memcpy(slice[0..filename.len], filename);
      slice[filename.len] = 0;
      const result = c.archive_read_open_filename(a.unpack(), slice.ptr, block_size);
      try checkArchiveResult(result);
  }

  pub fn archive_write_open_filename(a: ArchiveWriterResource, filename: []u8) !void {
      var slice = try beam.allocator.alloc(u8, filename.len + 1);
      defer beam.allocator.free(slice);
      @memcpy(slice[0..filename.len], filename);
      slice[filename.len] = 0;
      const result = c.archive_write_open_filename(a.unpack(), slice.ptr);
      try checkArchiveResult(result);
  }

  pub fn archive_read_open_memory(a: ArchiveReaderResource, buf: []const u8) !void {
      const result = c.archive_read_open_memory(a.unpack(), buf.ptr, buf.len);
      try checkArchiveResult(result);
  }

  pub fn archive_format_name(a: ArchiveReaderResource) [*c]u8 {
      return @constCast(c.archive_format_name(a.unpack()));
  }

  pub fn archive_file_count(a: ArchiveReaderResource) i32 {
      return c.archive_file_count(a.unpack());
  }

  pub fn archive_entry_new() !ArchiveEntryResource {
      const e: ?*c.archive_entry = c.archive_entry_new();
      return if (e) |non_null_e|
          ArchiveEntryResource.create(non_null_e, .{})
      else
          error.archiveEntry;
  }

  pub fn archive_read_next_header(a: ArchiveReaderResource, e: ArchiveEntryResource) !void {
      try checkArchiveResult(c.archive_read_next_header2(a.unpack(), e.unpack()));
  }

  pub fn archive_write_header(a: ArchiveWriterResource, e: ArchiveEntryResource) !void {
      try checkArchiveResult(c.archive_write_header(a.unpack(), e.unpack()));
  }
  pub fn archive_entry_pathname(e: ArchiveEntryResource) [*c]u8 {
      return @constCast(c.archive_entry_pathname(e.unpack()));
  }

  pub fn archive_entry_set_pathname(e: ArchiveEntryResource, pathname: [*c]const u8) void {
      return c.archive_entry_set_pathname(e.unpack(), pathname);
  }

  fn getArchive(term: beam.term) !*c.archive {
      if (beam.get(ArchiveReaderResource, term, .{})) |reader| {
          return reader.unpack();
      } else |_| {
          if (beam.get(ArchiveWriterResource, term, .{})) |writer| {
              return writer.unpack();
          } else |_| {
              return error.InvalidArchiveType;
          }
      }
  }

  pub fn archive_error_string(term: beam.term) ![*c]u8 {
      const archive = try getArchive(term);
      return @constCast(c.archive_error_string(archive));
  }

  pub fn archive_clear_error(term: beam.term) !void {
      const archive = try getArchive(term);
      c.archive_clear_error(archive);
  }

  pub fn archive_format(a: ArchiveReaderResource) i32 {
      return c.archive_format(a.unpack());
  }

  pub fn archive_entry_size(e: ArchiveEntryResource) i64 {
      return c.archive_entry_size(e.unpack());
  }

  pub fn archive_read_close(a: ArchiveReaderResource) !void {
      try checkArchiveResult(c.archive_read_close(a.unpack()));
  }

  pub fn archive_write_close(a: ArchiveWriterResource) !void {
      try checkArchiveResult(c.archive_write_close(a.unpack()));
  }

  pub fn archive_read_support_format_raw(a: ArchiveReaderResource) !void {
      try checkArchiveResult(c.archive_read_support_format_raw(a.unpack()));
  }

  pub fn archive_read_support_compression_all(a: ArchiveReaderResource) !void {
      try checkArchiveResult(c.archive_read_support_compression_all(a.unpack()));
  }

  pub fn archive_read_data(a: ArchiveReaderResource, size: usize) !beam.term {
      const data = try beam.allocator.alloc(u8, size);
      defer beam.allocator.free(data);

      const num_read: isize = c.archive_read_data(a.unpack(), data.ptr, size);
      if (size != num_read) {
          return CAllocError.archive;
      } else {
          return beam.make(data, .{});
      }
  }

  pub fn archive_read_support_format_by_code(a: ArchiveReaderResource, code: c_int) !void {
      try checkArchiveResult(c.archive_read_support_format_by_code(a.unpack(), code));
  }

  pub fn archive_read_support_filter_by_code(a: ArchiveReaderResource, code: c_int) !void {
      try checkArchiveResult(c.archive_read_support_filter_by_code(a.unpack(), code));
  }

  pub fn archive_read_format_capabilities(a: ArchiveReaderResource) i32 {
      return c.archive_read_format_capabilities(a.unpack());
  }

  pub fn archive_write_add_filter(a: ArchiveWriterResource, code: c_int) !void {
      try checkArchiveResult(c.archive_write_add_filter(a.unpack(), code));
  }

  pub fn archive_write_set_format(a: ArchiveWriterResource, code: c_int) !void {
      try checkArchiveResult(c.archive_write_set_format(a.unpack(), code));
  }

  pub fn archive_entry_clear(e: ArchiveEntryResource) !void {
      const orig = e.unpack();
      const cleared = c.archive_entry_clear(orig) orelse return error.ArchiveEntryClearFailed;
      e.update(cleared);
  }

  pub fn archive_read_refresh(a: ArchiveReaderResource) !void {
      const orig = a.unpack();
      _ = c.archive_read_free(orig);
      const new_archive = c.archive_read_new() orelse return error.ArchiveCreationFailed;
      a.update(new_archive);
  }

  pub fn archive_write_refresh(a: ArchiveWriterResource) !void {
      const orig = a.unpack();
      _ = c.archive_write_free(orig);
      const new_archive = c.archive_write_new() orelse return error.ArchiveCreationFailed;
      a.update(new_archive);
  }

  pub fn archive_compression(a: ArchiveReaderResource) i32 {
      return c.archive_compression(a.unpack());
  }

  pub fn archive_compression_name(a: ArchiveReaderResource) [*c]u8 {
      return @constCast(c.archive_compression_name(a.unpack()));
  }

  pub fn archive_version_string() [*c]u8 {
      return @constCast(c.archive_version_string());
  }

  pub fn archive_version_details() [*c]u8 {
      return @constCast(c.archive_version_details());
  }
  pub fn archive_zlib_version() [*c]u8 {
      return @constCast(c.archive_zlib_version());
  }
  pub fn archive_liblzma_version() [*c]u8 {
      return @constCast(c.archive_liblzma_version());
  }
  pub fn archive_bzlib_version() [*c]u8 {
      return @constCast(c.archive_bzlib_version());
  }
  pub fn archive_liblz4_version() [*c]u8 {
      return @constCast(c.archive_liblz4_version());
  }
  pub fn archive_libzstd_version() [*c]u8 {
      return @constCast(c.archive_libzstd_version());
  }
  pub fn archive_read_extract(a: ArchiveReaderResource, e: ArchiveEntryResource, flags: i32) !void {
      try checkArchiveResult(c.archive_read_extract(a.unpack(), e.unpack(), flags));
  }

  pub fn getExtractInfo() [extractFlagDocumentation.len]ExtractFlagDocs {
      return extractFlagDocumentation;
  }

  pub fn extractFlagToInt(f: ExtractFlag) i32 {
      return @intFromEnum(f);
  }
  """

  @errors [:ArchiveEof, :ArchiveFailed, :ArchiveWarn, :ArchiveFatal]

  def get_error_string(ref) when is_reference(ref) do
    err_string = archive_error_string(ref)

    err_string =
      unless is_nil(err_string) do
        err_string
        |> :binary.bin_to_list()
        |> Enum.filter(&(&1 < 128))
        |> List.to_string()
      end

    archive_clear_error(ref)

    err_string
  end

  def safe_call(fun, ref \\ nil) do
    try do
      case fun.() do
        :ok -> :ok
        {:error, reason} -> {:error, reason}
        other -> {:ok, other}
      end
    rescue
      e in [ErlangError] ->
        error_string =
          if ref do
            get_error_string(ref)
          end

        case e do
          %{original: :ArchiveWarn} ->
            if error_string, do: Logger.info(error_string)
            :ok

          %{original: error} when error in @errors ->
            {:error, error_string || error}

          %{reason: reason} ->
            {:error, error_string || reason}

          %{message: message} ->
            {:error, error_string || message}
        end
    end
  end

  defmacro call(func) do
    quote do
      safe_call(fn -> unquote(func) end)
    end
  end

  defmacro call(func, ref) do
    quote do
      safe_call(fn -> unquote(func) end, unquote(ref))
    end
  end

  @doc false
  def unwrap!(:ok), do: :ok
  def unwrap!({:ok, value}), do: value
  def unwrap!({:error, reason}), do: raise(reason)
  def unwrap(_), do: raise("Bad value")

  def list_all(:formats, :read), do: listReadableFormats() |> Enum.map(&String.to_atom/1)
  def list_all(:filters, :read), do: listReadableFilters() |> Enum.map(&String.to_atom/1)
  def list_all(:formats, :write), do: listWritableFormats() |> Enum.map(&String.to_atom/1)
  def list_all(:filters, :write), do: listWritableFilters() |> Enum.map(&String.to_atom/1)

  defmacro get_file_kinds() do
    file_types = fileKinds()

    quote do
      unquote(Macro.escape(file_types))
    end
  end

  defmacro __using__(_opts) do
    read_formats = list_all(:formats, :read)
    read_filters = list_all(:filters, :read)
    write_formats = list_all(:formats, :write)
    write_filters = list_all(:filters, :write)
    extract_info = getExtractInfo()

    quote do
      alias Archive.Nif
      import Archive.Nif, only: [unwrap!: 1, call: 1, call: 2, safe_call: 2, safe_call: 1]
      @read_formats unquote(read_formats)
      @read_filters unquote(read_filters)
      @write_formats unquote(write_formats)
      @write_filters unquote(write_filters)
      @extract_info unquote(Macro.escape(extract_info))
    end
  end
end
