# = stage.rb
#
# TODO Currently stages to temp folder, make it an option?

require 'shellwords'
require 'tmpdir'

module FileUtils

  # Staging provides a means of easily transfering
  # files from one location to another based on a set
  # of transfer rules.

  module Stage

    #module_function

    # Stage and execute block in the staged location.

    def staged(options) #:yield:
      stage_folder = stage(options)
      if block_given?
        Dir.chdir(stage_folder) do
          yield
        end
      end
      stage_folder
    end

    # There is one parameter that deserves additional attention. This is the setting
    # called 'rules'. The rules setting allows you to define how files are
    # copied into the distribution package, so instead of a one to one copy
    # of the included files, you can actually have a file placed in a different
    # location within the distribution. This can be very handy if you wish to
    # develop your project with one layout, but need to distribute it with another.
    #
    # The rules parameter is a literal string that consists of one rule per line.
    # A line consists three space separated entries.
    #
    #   from_path file_glob to_path
    #
    # If no 'to_path' is given then it is considered the same as the 'from_path'.
    # Each entry also supports two substitution variables: $name and $version,
    # Here is a possible example:
    #
    #   rules: |
    #     lib **/* lib/$name/$version
    #
    # This will move any file under lib/ to the equivalent location under lib/$name/$version/.
    # The default set of rules is a mirror image transfer, spelled out it would be:
    #
    #   rules: |
    #     bin  *
    #     ext  **/*
    #     lib  **/*
    #     data **/*
    #     conf **/*
    #
    # If your using the Library class against a normal project folder the alterntive is to create
    # versioned paths, basically as follows:
    #
    #   rules: |
    #     ext/$name   **/*  ext/$name/$version
    #     lib/$name   **/*  lib/$name/$version
    #     data/$name  **/*  ext/$name/$version
    #     conf/$name  **/*  ext/$name/$version
    #
    # But this is just example. To version tier you package, simply use the tier option.
    #
    # Please note that the rules parameter is a new feature and is still considered beta.

    def stage( keys )
      keys = keys.rekey(:to_s)

      name    = keys['name']   || File.basename(Dir.pwd)
      files   = keys['files']  || '**/*'
      rules   = keys['rules']  || ''
      source  = keys['source'] || Dir.pwd
      vars    = keys['vars']   || {}

      raise ArgumentError, "missing argument -- name" unless name

      files = [files].flatten.compact

      # Work folder where all work on this package is being done.
      work_folder = File.join(Dir.tmpdir, $0.gsub(/W/,''), 'stage')

      # Where files are copied recreating an image of the local package.
      stage_folder ||= File.join(work_folder, name)

      # allocate temporary work space
      if FileTest.directory?(work_folder)
        rm_r(work_folder) #unless $DRYRUN
      end
      mkdir_p(work_folder)

      # source (typically the current directory).
      #source_folder = File.expand_path( source.chomp('/') )

      # TODO Should dirs include all subfiles? Currently yes.
      files = Dir.chdir(source) do
        Dir.multiglob_recurse(*files)
      end

      # preformat transfer rules
      trans = transfer_rules(source, rules, vars)

      # create staging package
      transfer_files(source, files, trans, stage_folder)

      return stage_folder
    end

    # Generate staging ground. Since there are sometimes special transfer
    # rules we need to create a separate temporary location for the package.
    # Also, some package formats, like Debian's, need a complete install "image".
    # So a staging ground is often a neccessity in any case.

    def transfer_files( source, files, trans, stage_folder )
      Dir.chdir(source) do
        #trans = transfer_rules( rules )
        list = files

        # build transfer table
        folders, files = [], {}
        list.each do |from|
          to = trans.key?(from) ? trans[from] : from
          #to = File.join( stage_folder, to )
          if File.directory?( from )
            folders << to
          else
            files[ from ] = to
            # ensure creation of files' folder
            files_folder = File.dirname( to )
            folders << files_folder unless files_folder == '.'
          end
        end

        folders.delete('')
        folders.uniq!

        # make staging folder
        mkdir_p(stage_folder)

        # create folders
        folders.each do |to|
          to = File.join(stage_folder, to)
          mkdir_p( to )
        end

        # copy files to staging "mirror" folder
        files.each do |from, to|
          to = File.join( stage_folder, to )
          cp( from, to )
          #safe_ln( from, to )
        end
      end
    end

    # Remove the temporary folder now.

    def cleanup_stage( work_folder )
      rm_r(work_folder)
    end

    # Determine transfer rules.
    #
    # Rules should proceed from the most general to the most specific.

    def transfer_rules(source,rules,subs=nil)
      trans={}
      Dir.chdir(source) do
        parse_rules(rules,subs).each do |rule|
          from,glob,to=*rule
          to=from unless to #???
          files=Dir.glob(File.join(from,glob))
          files.each do |file|
            trans[file]=File.join(to,file.sub(from,''))
          end
        end
      end
      return trans
    end

    # Parse individual transfer rule.

    def parse_rules( spec, subs=nil )
      return [] unless spec
      return [] if spec.strip == ''
      rules = []
      lines = spec.strip.split("\n")
      lines.each { |line|
        #words = line.strip.split(/\s+/)
        words = Shellwords.shellwords(line)
        from, glob, to = *words
        rules << [ rule_sub(from), rule_sub(glob), rule_sub(to) ]
      }
      rules
    end

    # Substitue package information for rule variables. (eg. $name).

    def rule_sub( str, subs=nil )
      return nil if str.nil?
      str = str.dup
      subs ||= {}
      subs.each do |k,v|
        str.gsub!( "${k}", v.to_s )
      end
      return str
    end

  end

  include Stage

  #module_function
  #def stage(*a) Stage.stage(*a) end
  #def staged(*a,&y) Stage.staged(*a,&y) end

  # DryRun rendition

  module DryRun
    include Stage
    #module_function
    #def stage(*a) Stage.stage(*a) end
    #def staged(*a,&y) Stage.staged(*a,&y) end
  end

end
