module Pkg
  class Tar
    require 'fileutils'
    require 'pathname'
    include FileUtils

    attr_accessor :files, :project, :version, :excludes, :target, :templates

    def initialize
      @tar      = Pkg::Util::Tool.find_tool('tar', :required => true)
      @project  = Pkg::Config.project
      @version  = Pkg::Config.version
      @files    = Pkg::Config.files
      @target   = File.join(Pkg::Config.project_root, "pkg", "#{@project}-#{@version}.tar.gz")

      # If the user did not specify any files, then archive the entire working directory
      # instead
      @files ||= Dir.glob('*')

      # We require that the excludes list be a string (which is space
      # separated, we hope)(deprecated) or an array.
      #
      if Pkg::Config.tar_excludes
        if Pkg::Config.tar_excludes.is_a?(String)
          warn "warning: `tar_excludes` should be an array, not a string"
          @excludes = Pkg::Config.tar_excludes.split(' ')
        elsif Pkg::Config.tar_excludes.is_a?(Array)
          @excludes = Pkg::Config.tar_excludes
        else
          fail "Tarball excludes must either be an array or a string, not #{@excludes.class}"
        end
      else
        @excludes = []
      end

      # If the user has specified things to exclude via config file, they will be
      # honored by the tar class, but we also always exclude the packaging repo
      # and 'pkg' directory.
      @excludes += ['pkg', 'ext/packaging']

      # On the other hand, support for explicit templates started with Arrays,
      # so that's all we support.
      #
      if Pkg::Config.templates
        @templates = Pkg::Config.templates.dup
        fail "templates must be an array" unless @templates.is_a?(Array)
        expand_templates
      end
    end

    def install_files_to(workdir)
      # It is nice to use arrays in YAML to represent array content, but we used
      # to support a mode where a space-separated string was used.  Support both
      # to allow a gentle migration to a modern style...
      patterns =
        case @files
        when String
          warn "warning: `files` should be an array, not a string"
          @files.split(' ')
        when Array
          @files
        else
          raise "`files` must be a string or an array!"
        end

      Pkg::Util::File.install_files_into_dir(patterns, workdir)
    end

    # The templates of a project can include globs, which may expand to an
    # arbitrary number of files. This method expands all of the templates using
    # Dir.glob and then filters out any templates that live in the packaging
    # tools themselves. If the template is a source/target combination, it is
    # returned to the array untouched.
    def expand_templates
      @templates.map! do |tempfile|
        if tempfile.is_a?(String)
          # Expand possible globs to all matching entries
          Dir.glob(File.join(Pkg::Config::project_root, tempfile))
        elsif tempfile.is_a?(Hash)
          tempfile
        end
      end
      @templates.flatten!

      # Reject matches that are templates from packaging itself. These will contain the packaging root.
      # These tend to come from the current tar.rake implementation.
      @templates.reject! { |temp| temp.is_a?(String) && temp.match(/#{Pkg::Config::packaging_root}/) }
    end

    # Given the tar object's template files (assumed to be in Pkg::Config.project_root), transform
    # them, removing the originals. If workdir is passed, assume Pkg::Config.project_root
    # exists in workdir
    def template(workdir = nil)
      workdir ||= Pkg::Config.project_root
      root = Pathname.new(Pkg::Config.project_root)

      # Templates can be either a string or a hash of source and target. If it
      # is a string, the target is assumed to be the same path as the
      # source,with the extension removed. If it is a hash, we assume nothing
      # and use the provided source and target.
      @templates.each do |cur_template|
        if cur_template.is_a?(String)
          template_file = File.expand_path(cur_template)
          target_file = template_file.sub(File.extname(template_file), "")
        elsif cur_template.is_a?(Hash)
          template_file = File.expand_path(cur_template["source"])
          target_file = File.expand_path(cur_template["target"])
        end

        #   We construct paths to the erb template and its proposed target file
        #   relative to the project root, *not* fully qualified. This allows us
        #   to, given a temporary workdir containing a copy of the project,
        #   construct the full path to the erb and target file inside the
        #   temporary workdir.
        #
        rel_path_to_template = Pathname.new(template_file).relative_path_from(root).to_s
        rel_path_to_target = Pathname.new(target_file).relative_path_from(root).to_s

        #   What we pass to Pkg::util::File.erb_file are the paths to the erb
        #   and target inside of a temporary project directory. We are, in
        #   essence, templating "in place." This is why we remove the original
        #   files - they're not the originals in the authoritative project
        #   directory, but the originals in the temporary working copy.
        if File.exist?(File.join(workdir, rel_path_to_template))
          mkpath(File.dirname(File.join(workdir, rel_path_to_target)), :verbose => false)
          Pkg::Util::File.erb_file(File.join(workdir, rel_path_to_template), File.join(workdir, rel_path_to_target), true, :binding => Pkg::Config.get_binding)
        elsif File.exist?(File.join(root, rel_path_to_template))
          mkpath(File.dirname(File.join(workdir, rel_path_to_target)), :verbose => false)
          Pkg::Util::File.erb_file(File.join(root, rel_path_to_template), File.join(workdir, rel_path_to_target), false, :binding => Pkg::Config.get_binding)
        else
          fail "Expected to find #{template_file} in #{root} for templating. But it was not there. Maybe you deleted it?"
        end
      end
    end

    def tar(target, source)
      mkpath File.dirname(target)
      cd File.dirname(source) do
        %x(#{@tar} #{@excludes.map { |x| " --exclude #{x} " }.join if @excludes} -zcf '#{File.basename(target)}' '#{File.basename(source)}')
        unless $?.success?
          fail "Failed to create .tar.gz archive with #{@tar}. Please ensure the tar command in your path accepts the flags '-c', '-z', and '-f'"
        end
        mv File.basename(target), target
      end
    end

    def clean_up(workdir)
      rm_rf workdir
    end

    def pkg!
      workdir = File.join(Pkg::Util::File.mktemp, "#{@project}-#{@version}")
      mkpath workdir
      self.install_files_to workdir
      self.template(workdir)
      self.tar(@target, workdir)
      self.clean_up workdir
    end
  end
end