require "rsync_command/version"
require "rsync_command/ssh_options"
require "rsync_command/thread_pool"

require 'monitor'

class RsyncCommand
  attr_accessor :failures, :logger

  def initialize(options={})
    @options = options.dup
    @logger = @options.delete(:logger)
    @flags = @options.delete(:flags)
    @failures = []
    @failures.extend(MonitorMixin)
  end

  #
  # takes an Enumerable and iterates each item in the list in parallel.
  #
  def asynchronously(array, &block)
    pool = ThreadPool.new
    array.each do |item|
      pool.schedule(item, &block)
    end
    pool.shutdown
  end

  #
  # runs rsync, recording failures
  #
  def exec(src, dest, options={})
    @failures.synchronize do
      @failures.clear
    end
    rsync_cmd = command(src, dest, options)
    if options[:chdir]
      rsync_cmd = "cd '#{options[:chdir]}'; #{rsync_cmd}"
    end
    @logger.debug rsync_cmd if @logger
    ok = system(rsync_cmd)
    unless ok
      @failures.synchronize do
        @failures << {:source => src, :dest => dest, :options => options.dup}
      end
    end
  end

  #
  # returns true if last exec returned a failure
  #
  def failed?
    @failures && @failures.any?
  end

  #
  # build rsync command
  #
  def command(src, dest, options={})
    src = remote_address(src)
    dest = remote_address(dest)
    options = @options.merge(options)
    flags = []
    flags << @flags if @flags
    flags << options[:flags] if options.has_key?(:flags)
    flags << '--delete' if options[:delete]
    flags << includes(options[:includes]) if options.has_key?(:includes)
    flags << excludes(options[:excludes]) if options.has_key?(:excludes)
    flags << SshOptions.new(options[:ssh]).to_flags if options.has_key?(:ssh)
    "rsync #{flags.compact.join(' ')} #{src} #{dest}"
  end

  private

  #
  # Creates an rsync location if the +address+ is a hash with keys :user, :host, and :path
  # (each component is optional). If +address+ is a string, we just pass it through.
  #
  def remote_address(address)
    if address.is_a? String
      address # assume it is already formatted.
    elsif address.is_a? Hash
      [[address[:user], address[:host]].compact.join('@'), address[:path]].compact.join(':')
    end
  end

  def excludes(patterns)
    [patterns].flatten.compact.map { |p| "--exclude='#{p}'" }
  end

  def includes(patterns)
    [patterns].flatten.compact.map { |p| "--include='#{p}'" }
  end

end