# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with this
# work for additional information regarding copyright ownership.  The ASF
# licenses this file to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
# License for the specific language governing permissions and limitations under
# the License.

autoload :Archive, 'archive/tar/minitar'
autoload :Zlib, 'zlib'

module Buildr #:nodoc:

  # The TarTask creates a new Tar file. You can include any number of files and and directories,
  # use exclusion patterns, and include files into specific directories.
  #
  # To create a GZipped Tar, either set the gzip option to true, or use the .tgz or .gz suffix.
  #
  # For example:
  #   tar("test.tgz").tap do |task|
  #     task.include "srcs"
  #     task.include "README", "LICENSE"
  #   end
  #
  # See Buildr#tar and ArchiveTask.
  class TarTask < ArchiveTask

    # To create a GZipped Tar, either set this option to true, or use the .tgz/.gz suffix.
    attr_accessor :gzip
    # Permission mode for files contained in the Tar.  Defaults to 0755.
    attr_accessor :mode

    def initialize(*args, &block) #:nodoc:
      super
      self.gzip = name =~ /\.t?gz$/
      self.mode = '0755'
    end

    # :call-seq:
    #   entry(name) => Entry
    #
    # Returns a Tar file entry. You can use this to check if the entry exists and its contents,
    # for example:
    #   package(:tar).entry("src/LICENSE").should contain(/Apache Software License/)
    def entry(entry_name)
      Buildr::TarEntry.new(self, entry_name)
    end

    def entries() #:nodoc:
      tar_entries = nil
      with_uncompressed_tar { |tar| tar_entries = tar.entries }
      tar_entries
    end

    # :call-seq:
    #   with_uncompressed_tar { |tar_entries| ... }
    #
    # Yields an Archive::Tar::Minitar::Input object to the provided block.
    # Opening, closing and Gzip-decompressing is automatically taken care of.
    def with_uncompressed_tar &block
      if gzip
        Zlib::GzipReader.open(name) { |tar| Archive::Tar::Minitar.open(tar, &block) }
      else
        Archive::Tar::Minitar.open(name, &block)
      end
    end

    private

    def create_from(file_map, transform_map)
      if gzip
        StringIO.new.tap do |io|
          create_tar io, file_map, transform_map
          io.seek 0
          Zlib::GzipWriter.open(name) { |gzip| gzip.write io.read }
        end
      else
        File.open(name, 'wb') { |file| create_tar file, file_map, transform_map }
      end
    end

    def create_tar(out, file_map, transform_map)
      Archive::Tar::Minitar::Writer.open(out) do |tar|
        options = { :mode=>mode || '0755', :mtime=>Time.now }

        file_map.each do |path, contents|
          to_transform = []
          transform = transform_map.key?(path)
          if contents.nil?
          elsif File.directory?(contents.to_s)
            stat = File.stat(contents.to_s)
            tar.mkdir(path, options.merge(:mode=>stat.mode, :mtime=>stat.mtime, :uid=>stat.uid, :gid=>stat.gid))
          else
            contents = [contents].flatten

            combined_options = options
            if File.exists?(contents.first.to_s)
              stat = File.stat(contents.first.to_s)
              combined_options = options.merge(:mode=> stat.mode, :mtime=> stat.mtime, :uid=>stat.uid, :gid=> stat.gid)
            elsif contents.first.respond_to?(:mode)
              combined_options = combined_options.merge(:mode => contents.first.mode)
            end


            tar.add_file path, combined_options do |os, opts|
              [contents].flatten.each do |content|
                if content.respond_to?(:call)
                  if transform
                    output = StringIO.new
                    content.call output
                    to_transform << output.string
                  else
                    content.call os
                  end
                else
                  File.open content.to_s, 'rb' do |is|
                    if transform
                      output = StringIO.new
                      while data = is.read(4096)
                        output << data
                      end
                      to_transform << output.string
                    else
                      while data = is.read(4096)
                        os.write(data)
                      end
                    end
                  end
                end
              end
              if transform_map.key?(path)
                os.write(transform_map[path].call(to_transform))
              end
            end
          end

        end
      end
    end

  end


  class TarEntry #:nodoc:

    def initialize(tar_task, entry_name)
      @tar_task = tar_task
      @entry_name = entry_name
    end

    # :call-seq:
    #   contain?(*patterns) => boolean
    #
    # Returns true if this Tar file entry matches against all the arguments. An argument may be
    # a string or regular expression.
    def contain?(*patterns)
      content = read_content_from_tar
      patterns.map { |pattern| Regexp === pattern ? pattern : Regexp.new(Regexp.escape(pattern.to_s)) }.
      all? { |pattern| content =~ pattern }
    end

    # :call-seq:
    #   empty?() => boolean
    #
    # Returns true if this entry is empty.
    def empty?()
      read_content_from_tar.nil?
    end

    # :call-seq:
    #   exist() => boolean
    #
    # Returns true if this entry exists.
    def exist?()
      exist = false
      @tar_task.with_uncompressed_tar { |tar| exist = tar.any? { |entry| entry.name == @entry_name } }
      exist
    end

    def to_s #:nodoc:
      @entry_name
    end

    private

    def read_content_from_tar
      content = Errno::ENOENT.new("No such file or directory - #{@entry_name}")
      @tar_task.with_uncompressed_tar do |tar|
        content = tar.inject(content) { |content, entry| entry.name == @entry_name ? entry.read : content }
      end
      raise content if Exception === content
      content
    end
  end

end


# :call-seq:
#    tar(file) => TarTask
#
# The TarTask creates a new Tar file. You can include any number of files and
# and directories, use exclusion patterns, and include files into specific
# directories.
#
# To create a GZipped Tar, either set the gzip option to true, or use the .tgz or .gz suffix.
#
# For example:
#   tar("test.tgz").tap do |tgz|
#     tgz.include "srcs"
#     tgz.include "README", "LICENSE"
#   end
def tar(file)
  TarTask.define_task(file)
end
