require "ffi"

module Archive
  module C
    def self.attach_function_maybe(*args)
      attach_function(*args)
    rescue FFI::NotFoundError # rubocop:disable Lint/HandleExceptions
    end

    extend FFI::Library
    ffi_lib %w{libarchive.so.13 libarchive.13 libarchive-13 libarchive.so libarchive archive}

    attach_function :archive_version_number, [], :int
    attach_function :archive_version_string, [], :string
    attach_function :archive_error_string, [:pointer], :string
    attach_function :archive_errno, [:pointer], :int

    attach_function :archive_read_new, [], :pointer
    attach_function :archive_read_open_filename, %i{pointer string size_t}, :int
    attach_function :archive_read_open_memory, %i{pointer pointer size_t}, :int
    attach_function :archive_read_open1, [:pointer], :int
    attach_function :archive_read_support_compression_program, %i{pointer string}, :int
    attach_function :archive_read_support_compression_all, [:pointer], :int

    callback :archive_read_callback, %i{pointer pointer pointer}, :int
    callback :archive_skip_callback, %i{pointer pointer int64}, :int
    callback :archive_seek_callback, %i{pointer pointer int64 int}, :int
    attach_function :archive_read_set_read_callback, %i{pointer archive_read_callback}, :int
    attach_function :archive_read_set_callback_data, %i{pointer pointer}, :int
    attach_function :archive_read_set_skip_callback, %i{pointer archive_skip_callback}, :int
    attach_function :archive_read_set_seek_callback, %i{pointer archive_seek_callback}, :int

    attach_function_maybe :archive_read_set_format, %i{pointer int}, :int
    attach_function_maybe :archive_read_append_filter, %i{pointer int}, :int
    attach_function_maybe :archive_read_append_filter_program, %i{pointer pointer}, :int
    attach_function_maybe :archive_read_append_filter_program_signature, %i{pointer string pointer size_t}, :int

    attach_function_maybe :archive_read_support_filter_all, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_bzip2, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_compress, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_gzip, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_grzip, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_lrzip, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_lz4, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_lzip, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_lzma, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_lzop, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_none, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_program, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_program_signature, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_rpm, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_uu, [:pointer], :int
    attach_function_maybe :archive_read_support_filter_xz, [:pointer], :int

    attach_function_maybe :archive_read_support_format_all, [:pointer], :int
    attach_function_maybe :archive_read_support_format_7zip, [:pointer], :int
    attach_function_maybe :archive_read_support_format_ar, [:pointer], :int
    attach_function_maybe :archive_read_support_format_by_code, [:pointer], :int
    attach_function_maybe :archive_read_support_format_cab, [:pointer], :int
    attach_function_maybe :archive_read_support_format_cpio, [:pointer], :int
    attach_function_maybe :archive_read_support_format_empty, [:pointer], :int
    attach_function_maybe :archive_read_support_format_gnutar, [:pointer], :int
    attach_function_maybe :archive_read_support_format_iso9660, [:pointer], :int
    attach_function_maybe :archive_read_support_format_lha, [:pointer], :int
    attach_function_maybe :archive_read_support_format_mtree, [:pointer], :int
    attach_function_maybe :archive_read_support_format_rar, [:pointer], :int
    attach_function_maybe :archive_read_support_format_raw, [:pointer], :int
    attach_function_maybe :archive_read_support_format_tar, [:pointer], :int
    attach_function_maybe :archive_read_support_format_warc, [:pointer], :int
    attach_function_maybe :archive_read_support_format_xar, [:pointer], :int
    attach_function_maybe :archive_read_support_format_zip, [:pointer], :int
    attach_function_maybe :archive_read_support_format_zip_streamable, [:pointer], :int
    attach_function_maybe :archive_read_support_format_zip_seekable, [:pointer], :int

    attach_function :archive_read_finish, [:pointer], :int
    attach_function :archive_read_extract, %i{pointer pointer int}, :int
    attach_function :archive_read_header_position, [:pointer], :int
    attach_function :archive_read_next_header, %i{pointer pointer}, :int
    attach_function :archive_read_data, %i{pointer pointer size_t}, :size_t
    attach_function :archive_read_data_into_fd, %i{pointer int}, :int

    attach_function :archive_write_new, [], :pointer
    attach_function :archive_write_open_filename, %i{pointer string}, :int
    callback :archive_open_callback, %i{pointer pointer}, :int
    callback :archive_write_callback, %i{pointer pointer pointer size_t}, :int
    callback :archive_close_callback, %i{pointer pointer}, :int
    attach_function :archive_write_open, %i{pointer pointer pointer archive_write_callback pointer}, :int
    attach_function :archive_write_set_compression_none, [:pointer], :int
    attach_function_maybe :archive_write_set_compression_gzip, [:pointer], :int
    attach_function_maybe :archive_write_set_compression_bzip2, [:pointer], :int
    attach_function_maybe :archive_write_set_compression_deflate, [:pointer], :int
    attach_function_maybe :archive_write_set_compression_compress, [:pointer], :int
    attach_function_maybe :archive_write_set_compression_lzma, [:pointer], :int
    attach_function_maybe :archive_write_set_compression_xz, [:pointer], :int
    attach_function :archive_write_set_compression_program, %i{pointer string}, :int

    def self.archive_write_set_compression(archive, compression)
      case compression
      when String
        archive_write_set_compression_program archive, compression
      when COMPRESSION_BZIP2
        archive_write_set_compression_bzip2 archive
      when COMPRESSION_GZIP
        archive_write_set_compression_gzip archive
      when COMPRESSION_LZMA
        archive_write_set_compression_lzma archive
      when COMPRESSION_XZ
        archive_write_set_compression_xz archive
      when COMPRESSION_COMPRESS
        archive_write_set_compression_compress archive
      when COMPRESSION_NONE
        archive_write_set_compression_none archive
      else
        raise "Unknown compression type: #{compression}"
      end
    end

    attach_function :archive_write_set_format, %i{pointer int}, :int
    attach_function :archive_write_data, %i{pointer pointer size_t}, :ssize_t
    attach_function :archive_write_header, %i{pointer pointer}, :int
    attach_function :archive_write_finish, [:pointer], :void
    attach_function :archive_write_get_bytes_in_last_block, [:pointer], :int
    attach_function :archive_write_set_bytes_in_last_block, %i{pointer int}, :int

    attach_function :archive_entry_new, [], :pointer
    attach_function :archive_entry_free, [:pointer], :void
    attach_function :archive_entry_atime, [:pointer], :time_t
    attach_function :archive_entry_atime_nsec, %i{pointer time_t long}, :void
    attach_function_maybe :archive_entry_atime_is_set, [:pointer], :int
    attach_function :archive_entry_set_atime, %i{pointer time_t long}, :int
    attach_function_maybe :archive_entry_unset_atime, [:pointer], :int
    attach_function_maybe :archive_entry_birthtime, [:pointer], :time_t
    attach_function_maybe :archive_entry_birthtime_nsec, %i{pointer time_t long}, :void
    attach_function_maybe :archive_entry_birthtime_is_set, [:pointer], :int
    attach_function_maybe :archive_entry_set_birthtime, %i{pointer time_t long}, :int
    attach_function_maybe :archive_entry_unset_birthtime, [:pointer], :int
    attach_function :archive_entry_ctime, [:pointer], :time_t
    attach_function :archive_entry_ctime_nsec, %i{pointer time_t long}, :void
    attach_function_maybe :archive_entry_ctime_is_set, [:pointer], :int
    attach_function :archive_entry_set_ctime, %i{pointer time_t long}, :int
    attach_function_maybe :archive_entry_unset_ctime, [:pointer], :int
    attach_function :archive_entry_mtime, [:pointer], :time_t
    attach_function :archive_entry_mtime_nsec, %i{pointer time_t long}, :void
    attach_function_maybe :archive_entry_mtime_is_set, [:pointer], :int
    attach_function :archive_entry_set_mtime, %i{pointer time_t long}, :int
    attach_function_maybe :archive_entry_unset_mtime, [:pointer], :int
    attach_function :archive_entry_dev, [:pointer], :dev_t
    attach_function :archive_entry_set_dev, %i{pointer dev_t}, :void
    attach_function :archive_entry_devmajor, [:pointer], :dev_t
    attach_function :archive_entry_set_devmajor, %i{pointer dev_t}, :void
    attach_function :archive_entry_devminor, [:pointer], :dev_t
    attach_function :archive_entry_set_devminor, %i{pointer dev_t}, :void
    attach_function :archive_entry_filetype, [:pointer], :mode_t
    attach_function :archive_entry_set_filetype, %i{pointer mode_t}, :void
    attach_function :archive_entry_fflags, %i{pointer pointer pointer}, :void
    attach_function :archive_entry_set_fflags, %i{pointer ulong ulong}, :void
    attach_function :archive_entry_fflags_text, [:pointer], :string
    attach_function :archive_entry_gid, [:pointer], :gid_t
    attach_function :archive_entry_set_gid, %i{pointer gid_t}, :void
    attach_function :archive_entry_gname, [:pointer], :string
    attach_function :archive_entry_set_gname, %i{pointer string}, :void
    attach_function :archive_entry_hardlink, [:pointer], :string
    attach_function :archive_entry_set_hardlink, %i{pointer string}, :void
    attach_function :archive_entry_set_link, %i{pointer string}, :void
    attach_function :archive_entry_ino, [:pointer], :ino_t
    attach_function :archive_entry_set_ino, %i{pointer ino_t}, :void
    attach_function :archive_entry_mode, [:pointer], :mode_t
    attach_function :archive_entry_set_mode, %i{pointer mode_t}, :void
    attach_function :archive_entry_set_perm, %i{pointer mode_t}, :void
    attach_function :archive_entry_nlink, [:pointer], :uint
    attach_function :archive_entry_set_nlink, %i{pointer uint}, :void
    attach_function :archive_entry_pathname, [:pointer], :string
    attach_function :archive_entry_set_pathname, %i{pointer string}, :void
    attach_function :archive_entry_rdev, [:pointer], :dev_t
    attach_function :archive_entry_set_rdev, %i{pointer dev_t}, :void
    attach_function :archive_entry_rdevmajor, [:pointer], :dev_t
    attach_function :archive_entry_set_rdevmajor, %i{pointer dev_t}, :void
    attach_function :archive_entry_rdevminor, [:pointer], :dev_t
    attach_function :archive_entry_set_rdevminor, %i{pointer dev_t}, :void
    attach_function :archive_entry_size, [:pointer], :int64_t
    attach_function :archive_entry_set_size, %i{pointer int64_t}, :void
    attach_function_maybe :archive_entry_unset_size, [:pointer], :void
    attach_function_maybe :archive_entry_size_is_set, [:pointer], :int
    attach_function :archive_entry_sourcepath, [:pointer], :string
    attach_function :archive_entry_strmode, [:pointer], :string
    attach_function :archive_entry_symlink, [:pointer], :string
    attach_function :archive_entry_set_symlink, %i{pointer string}, :void
    attach_function :archive_entry_uid, [:pointer], :uid_t
    attach_function :archive_entry_set_uid, %i{pointer uid_t}, :void
    attach_function :archive_entry_uname, [:pointer], :string
    attach_function :archive_entry_set_uname, %i{pointer string}, :void
    attach_function :archive_entry_copy_stat, %i{pointer pointer}, :void
    attach_function :archive_entry_copy_fflags_text, %i{pointer string}, :string
    attach_function :archive_entry_copy_gname, %i{pointer string}, :string
    attach_function :archive_entry_copy_uname, %i{pointer string}, :string
    attach_function :archive_entry_copy_hardlink, %i{pointer string}, :string
    attach_function :archive_entry_copy_link, %i{pointer string}, :string
    attach_function :archive_entry_copy_symlink, %i{pointer string}, :string
    attach_function :archive_entry_copy_sourcepath, %i{pointer string}, :string
    attach_function :archive_entry_copy_pathname, %i{pointer string}, :string
    attach_function :archive_entry_xattr_clear, [:pointer], :void
    attach_function :archive_entry_xattr_add_entry, %i{pointer string pointer size_t}, :void
    attach_function :archive_entry_xattr_count, [:pointer], :int
    attach_function :archive_entry_xattr_reset, [:pointer], :int
    attach_function :archive_entry_xattr_next, %i{pointer pointer pointer pointer}, :int

    EOF    = 1
    OK     = 0
    RETRY  = -10
    WARN   = -20
    FAILED = -25
    FATAL  = -30

    DATA_BUFFER_SIZE = 2**16
  end

  COMPRESSION_NONE     = 0
  COMPRESSION_GZIP     = 1
  COMPRESSION_BZIP2    = 2
  COMPRESSION_COMPRESS = 3
  COMPRESSION_PROGRAM  = 4
  COMPRESSION_LZMA     = 5
  COMPRESSION_XZ       = 6
  COMPRESSION_UU       = 7
  COMPRESSION_RPM      = 8
  COMPRESSION_LZIP     = 9
  COMPRESSION_LRZIP    = 10
  COMPRESSION_LZOP     = 11
  COMPRESSION_GRZIP    = 12
  COMPRESSION_LZ4      = 13

  FORMAT_BASE_MASK           = 0xff0000
  FORMAT_CPIO                = 0x10000
  FORMAT_CPIO_POSIX          = (FORMAT_CPIO | 1)
  FORMAT_CPIO_BIN_LE         = (FORMAT_CPIO | 2)
  FORMAT_CPIO_BIN_BE         = (FORMAT_CPIO | 3)
  FORMAT_CPIO_SVR4_NOCRC     = (FORMAT_CPIO | 4)
  FORMAT_CPIO_SVR4_CRC       = (FORMAT_CPIO | 5)
  FORMAT_SHAR                = 0x20000
  FORMAT_SHAR_BASE           = (FORMAT_SHAR | 1)
  FORMAT_SHAR_DUMP           = (FORMAT_SHAR | 2)
  FORMAT_TAR                 = 0x30000
  FORMAT_TAR_USTAR           = (FORMAT_TAR | 1)
  FORMAT_TAR_PAX_INTERCHANGE = (FORMAT_TAR | 2)
  FORMAT_TAR_PAX_RESTRICTED  = (FORMAT_TAR | 3)
  FORMAT_TAR_GNUTAR          = (FORMAT_TAR | 4)
  FORMAT_ISO9660             = 0x40000
  FORMAT_ISO9660_ROCKRIDGE   = (FORMAT_ISO9660 | 1)
  FORMAT_ZIP                 = 0x50000
  FORMAT_EMPTY               = 0x60000
  FORMAT_AR                  = 0x70000
  FORMAT_AR_GNU              = (FORMAT_AR | 1)
  FORMAT_AR_BSD              = (FORMAT_AR | 2)
  FORMAT_MTREE               = 0x80000
  FORMAT_RAW                 = 0x90000
  FORMAT_XAR                 = 0xA0000
  FORMAT_LHA                 = 0xB0000
  FORMAT_CAB                 = 0xC0000
  FORMAT_RAR                 = 0xD0000
  FORMAT_7ZIP                = 0xE0000
  FORMAT_WARC                = 0xF0000

  EXTRACT_OWNER              = 0x0001
  EXTRACT_PERM               = 0x0002
  EXTRACT_TIME               = 0x0004
  EXTRACT_NO_OVERWRITE       = 0x0008
  EXTRACT_UNLINK             = 0x0010
  EXTRACT_ACL                = 0x0020
  EXTRACT_FFLAGS             = 0x0040
  EXTRACT_XATTR              = 0x0080
  EXTRACT_SECURE_SYMLINKS    = 0x0100
  EXTRACT_SECURE_NODOTDOT    = 0x0200
  EXTRACT_NO_AUTODIR         = 0x0400
  EXTRACT_NO_OVERWRITE_NEWER = 0x0800
  EXTRACT_SPARSE             = 0x1000
  EXTRACT_MAC_METADATA       = 0x2000
  EXTRACT_NO_HFS_COMPRESSION = 0x4000
  EXTRACT_HFS_COMPRESSION_FORCED = 0x8000
  EXTRACT_SECURE_NOABSOLUTEPATHS = 0x10000
  EXTRACT_CLEAR_NOCHANGE_FFLAGS = 0x20000

  def self.read_open_filename(file_name, command = nil, &block)
    Reader.open_filename file_name, command, &block
  end

  def self.read_open_memory(string, command = nil, &block)
    Reader.open_memory string, command, &block
  end

  def self.read_open_stream(reader, &block)
    Reader.open_stream reader, &block
  end

  def self.write_open_filename(file_name, compression, format, &block)
    Writer.open_filename file_name, compression, format, &block
  end

  def self.write_open_memory(string, compression, format, &block)
    Writer.open_memory string, compression, format, &block
  end

  def self.version_number
    C.archive_version_number
  end

  def self.version_string
    C.archive_version_string
  end

  class Error < StandardError
    def initialize(archive)
      if archive.is_a? String
        super archive
      else
        super C.archive_error_string(archive).to_s
      end
    end
  end

  class BaseArchive
    def initialize(alloc, free)
      @archive = nil
      @archive_free = nil
      @archive = alloc.call
      @archive_free = [nil]
      raise Error, @archive unless @archive

      @archive_free[0] = free
      ObjectSpace.define_finalizer(self, BaseArchive.finalizer(@archive, @archive_free))
    end

    def self.finalizer(archive, archive_free)
      proc do |*_args|
        archive_free[0].call(archive) if archive_free[0]
      end
    end

    def close
      # TODO: do we need synchronization here?
      if @archive
        # TODO: Error check?
        @archive_free[0].call(@archive)
      end
    ensure
      @archive = nil
      @archive_free[0] = nil
      @data = nil
    end

    def archive
      raise Error, "No archive open" unless @archive

      @archive
    end
    protected :archive

    def error_string
      C.archive_error_string(@archive)
    end

    def errno
      C.archive_errno(@archive)
    end
  end
end
