require ‘zip/zip’ require ‘sprout/version’ require ‘archive/tar/minitar’

## # Given a source, destination and archive type (or ability to infer it), # unpack the provided archive. # # unpacker = Sprout::ArchiveUnpacker.new # unpacker.unpack “Foo.zip”, “unpacked/” # class Sprout::ArchiveUnpacker

##
# Unpack the provided +archive+ into the provided +destination+.
# 
# If a +type+ is not provided, a type will be inferred from the file name suffix.
# 
# @param archive [File] Path to the archive that will be unpacked (or copied)
# @param destination [Path] Path to the folder where unpacked files should be placed (or copied).
# @param type [Symbol] The type of the archive in cases where it can't be inferred
#     from the name. Acceptable values are: :zip, :tgz, :swc, :exe or :rb
# @param clobber [Boolean] If the destination already contains the expected file(s), 
#     the unpacker will not run unless +clobber+ is true.
# @return [String] path to the unpacked files (usually same as destination).
# @raise Sprout::Errors::UnknownArchiveType If the archive type cannot be inferred and a valid type is not provided.
def unpack archive, destination, type=nil, clobber=nil
  return unpack_zip(archive, destination, clobber) if is_zip?(archive, type)
  return unpack_tgz(archive, destination, clobber) if is_tgz?(archive, type)

  # This is definitely debatable, should we copy the file even if it's
  # not an archive that we're about to unpack?
  # If so, why would we only do this with some subset of file types?
  # Opinions welcome here...
  return copy_file(archive, destination, clobber)  if is_copyable?(archive)

  raise Sprout::Errors::UnknownArchiveType.new("Unsupported or unknown archive type encountered with: #{archive}")
end

##
# Unpack zip archives on any platform using whatever strategy is most
# efficient and reliable.
#
# @param archive [File] Path to the archive that will be unpacked.
# @param destination [Path] Path to the folder where unpacked files should be placed.
# @param clobber [Boolean] If the destination already contains the expected file(s), 
#     the unpacker will not run unless +clobber+ is true.
# @return [File] the file or directory that was created.
def unpack_zip archive, destination, clobber=nil
  validate archive, destination

  ##
  # As it turns out, the Rubyzip library corrupts
  # binary files (like the Flash Player) on OSX and is also
  # horribly slow for large archives (like the ~120MB Flex SDK)
  # on all platforms.
  if is_darwin?
    unpack_zip_on_darwin archive, destination, clobber
  else
    Zip::ZipFile.open archive do |zipfile|
      zipfile.each do |entry|
        next if entry.name =~ /__MACOSX/ or entry.name =~ /\.DS_Store/
          unpack_zip_entry entry, destination, clobber
      end
    end
  end
end

##
# Return true if we're on a Darwin native system (OSX).
# @return [Boolean]
def is_darwin?
  Sprout.current_system.is_a?(Sprout::System::OSXSystem)
end

##
# Optimization for zip files on OSX. Uses the native
# 'unzip' utility which is much faster (and more reliable) 
# than Ruby for large archives (like the Flex SDK) and 
# binaries that Ruby corrupts (like the Flash Player).
#
# @return [File] the file or directory that was created.
def unpack_zip_on_darwin archive, destination, clobber
  # Unzipping on OS X
  FileUtils.makedirs destination
  zip_dir  = File.expand_path File.dirname(archive)
  zip_name = File.basename archive
  output   = File.expand_path destination
  # puts ">> zip_dir: #{zip_dir} zip_name: #{zip_name} output: #{output}"
  %x(cd #{zip_dir};unzip #{zip_name} -d #{output})
end

##
# Unpack tar.gz or .tgz files on any platform.
#
# @return [File] the file or directory that was created.
def unpack_tgz archive, destination, clobber=nil
  validate archive, destination

  tar = Zlib::GzipReader.new(File.open(archive, 'rb'))
  if(!should_unpack_tgz?(destination, clobber))
    raise Sprout::Errors::DestinationExistsError.new "Unable to unpack #{archive} into #{destination} without explicit :clobber argument"
  end

  Archive::Tar::Minitar.unpack(tar, destination)

  # Recurse and unpack gzipped children (Adobe did this double 
  # gzip with the Linux FlashPlayer for some weird reason)
  ["#{destination}/**/*.tgz", "#{destination}/**/*.tar.gz"].each do |pattern|
    Dir.glob(pattern).each do |child|
      if(child != archive && dir != File.dirname(child))
        unpack_tgz(child, File.dirname(child))
      end
    end
  end
end

##
# Rather than unpacking, safely copy the file from one location
# to another.
# This method is generally used when .exe files are downloaded
# directly.
#
# @return [File] the file or directory that was created.
def copy_file file, destination, clobber=nil
  validate file, destination
  target = File.expand_path( File.join(destination, File.basename(file)) )
  if(File.exists?(target) && clobber != :clobber)
    raise Sprout::Errors::DestinationExistsError.new "Unable to copy #{file} to #{target} because target already exists and we were not asked to :clobber it"
  end
  FileUtils.mkdir_p destination
  FileUtils.cp_r file, destination

  destination
end

##
# Returns true if the provided file name looks like a zip file or the +type+ argument is +:zip+.
# @return [Boolean]
def is_zip? archive, type=nil
  type == :zip || !archive.match(/\.zip$/).nil?
end

##
# Return true if the provided file name looks like a tar.gz file or the +type+ argument is +:tgz+.
# @return [Boolean]
def is_tgz? archive, type=nil
  type == :tgz || !archive.match(/\.tgz$/).nil? || !archive.match(/\.tar.gz$/).nil?
end

##
# Return true if the downloaded archive is a .exe file or the +type+ argument is +:exe+.
# @return [Boolean]
def is_exe? archive, type=nil
  type == :exe || !archive.match(/\.exe$/).nil?
end

##
# Return true if the downloaded archive is a .swc file or the +type+ argument is +:swc+.
# @return [Boolean]
def is_swc? archive, type=nil
  type == :swc || !archive.match(/\.swc$/).nil?
end

## 
# Return true if the downloaded archive is a .rb file or the +type+ argument is +:rb+.
# @return [Boolean]
def is_rb? archive, type=nil
  type == :rb || !archive.match(/\.rb$/).nil?
end

private

##
# Return true if the provided archive should be copied as-is, rather
# than being unpacked first.
# @return [Boolean]
def is_copyable? archive
  (is_exe?(archive) || is_swc?(archive) || is_rb?(archive))
end

##
# Return true if the tgz should be unpacked.
# @return [Boolean]
def should_unpack_tgz? dir, clobber=nil
  return !directory_has_children?(dir) || clobber == :clobber

end

##
# Return true if the provided directory has one or more chidren.
# @return [Boolean]
def directory_has_children? dir
  (Dir.entries(dir) - ['.', '..']).size > 0
end

##
# @return [Boolean] true if the +archive+ and +destination+ exist.
# @raise Sprout::Errors::ArchiveUnpackerError if the +archive+ or +destination+ don't exist.
def validate archive, destination
  archive_message = "Archive could not be found at: #{archive}"
  raise Sprout::Errors::ArchiveUnpackerError.new(archive_message) unless valid_path?(archive)
  destination_message = "Destination could not be found at: #{destination}"
  raise Sprout::Errors::ArchiveUnpackerError.new(destination_message) unless valid_path?(destination)
  true
end

##
# @return [Boolean] true if the provided +path+ is not nil and exists on disk.
def valid_path? path
  !path.nil? && File.exists?(path)
end

##
# Unpack an entry from a zip archive. This is an _inconvenience_ method
# thanks to the way Ruby zip handles zip archives.
def unpack_zip_entry entry, destination, clobber
  # Ensure hidden mac files don't get written to disk:
  path = File.join destination, entry.name

  if entry.directory?
    # If an archive has empty directories:
    FileUtils.mkdir_p path
  elsif entry.file?
    # On Windows, we don't get the entry for
    # each parent directory:
    FileUtils.mkdir_p File.dirname(path)
    begin
      entry.extract path
    rescue Zip::ZipDestinationFileExistsError => zip_dest_error
      if(clobber == :clobber)
        FileUtils.rm_rf path
        entry.extract path
      else
        raise Sprout::Errors::DestinationExistsError.new zip_dest_error.message
      end
    end
  end
end

end