require 'English'

require File.join(File.dirname(__FILE__), "platform.rb")

module CommandLine
  unless defined?(QUOTE_REPLACEMENT)
    QUOTE_REPLACEMENT = (Platform.family == "mswin32") ? '"' : '\\"'
  end
  unless defined?(LESS_THAN_REPLACEMENT)
    LESS_THAN_REPLACEMENT = (Platform.family == "mswin32") ? '<' : '\\<'
  end

  class OptionError < StandardError;
  end
  
  class ExecutionError < StandardError
    attr_reader :cmd, :dir, :exitstatus, :stderr

    def initialize(cmd, full_cmd, dir, exitstatus, stderr)
      @cmd, @full_cmd, @dir, @exitstatus, @stderr = cmd, full_cmd, dir, exitstatus, stderr
    end

    def to_s
      "\ndir : #{@dir}\n" +
          "command : #{@full_cmd}\n" +
          "exitstatus: #{@exitstatus}\n" +
          (@stderr ? "STDERR TAIL START\n#{@stderr}\nSTDERR TAIL END\n" : '')
    end
  end

 

  def execute(cmd, options={}, &proc)
    cmd = cmd.join(" ") if cmd.class == Array
    raise "Can't have newline in cmd" if cmd =~ /\n/
    options = {
        :dir => Dir.pwd,
        :env => options[:envs] && !options[:envs].empty? ? options[:envs] : {},
        :mode => 'r',
        :exitstatus => 0}.merge(options)

    options[:stdout] = escape(File.expand_path(options[:stdout])) if options[:stdout]
    options[:stderr] = escape(File.expand_path(options[:stderr])) if options[:stderr]

    Dir.chdir(options[:dir]) do
      return exec(cmd, options, &proc)
    end
  end

  module_function :execute

  private

  def exec(cmd, options, &proc)
    full_cmd = full_cmd(cmd, options, &proc)

    options[:env].each { |k, v| ENV[k]=v.to_s if k && !k.strip.empty? } if options[:env]
    begin

      result = IO.popen(full_cmd, options[:mode]) do |io|
        if proc
          proc.call(io)
        else
          io.each_line do |line|
            STDOUT.puts line if options[:stdout].nil?
          end
        end
      end
      exit_status = $CHILD_STATUS
      raise "$CHILD_STATUS is nil" unless exit_status
      verify_exit_code(exit_status, cmd, full_cmd, options)
      return result
    rescue Errno::ENOENT => e
      if options[:stderr]
        File.open(options[:stderr], "a") { |io| io.write("Command line exec error: " + e.message) }
      else
        STDERR.puts e.message
        STDERR.puts e.backtrace.map { |line| "    #{line}" }
      end
      raise ExecutionError.new(cmd, full_cmd, options[:dir], nil, e.message)
    end
  end

  module_function :exec

  def full_cmd(cmd, options, &proc)
    stdout_opt, stderr_opt = redirects(options)

    capture_info_command = (block_given? && options[:stdout]) ?
        "echo [output captured and therefore not logged] >> #{options[:stdout]} && " :
        ''

    stdout_prompt_command = options[:stdout] ?
        "echo #{Platform.prompt} #{escape_and_concatenate(cmd)} >> #{options[:stdout]} && " :
        ''

    stderr_prompt_command = options[:stderr] && options[:stderr] != options[:stdout] ?
        "echo #{Platform.prompt} #{escape_and_concatenate(cmd)} >> #{options[:stderr]} && " :
        ''

    cmd = escape_and_concatenate(cmd) unless cmd.is_a? String

    redirected_command = block_given? ? "#{cmd} #{stderr_opt}" : "#{cmd} #{stdout_opt} #{stderr_opt}"

    stdout_prompt_command + capture_info_command + stderr_prompt_command + redirected_command
  end

  module_function :full_cmd

  def verify_exit_code(exit_status, cmd, full_cmd, options)
    if exit_status.exitstatus != options[:exitstatus]
      if options[:stderr]
        if File.exist?(options[:stderr])
          File.open(options[:stderr]) do |errio|
            begin
              errio.seek(-1200, IO::SEEK_END)
            rescue Errno::EINVAL

            end
            error_message = errio.read
          end
        else
          error_message = "#{options[:stderr]} doesn't exist"
        end
      else
        error_message = nil
      end
      raise ExecutionError.new(cmd, full_cmd, options[:dir] || Dir.pwd, exit_status.exitstatus, error_message)
    end
  end

  module_function :verify_exit_code

  def redirects(options)
    stdout_opt = options[:stdout] ? ">> #{options[:stdout]}" : ""


    stderr_opt =
        case (options[:stderr])
          when nil then
            ''
          when options[:stdout] then
            '2>&1'
          else
            "2>> #{options[:stderr]}"
        end


    if Platform.family == 'mswin32'
      stdout_opt.gsub!('/', '\\')
      stderr_opt.gsub!('/', '\\')
    end

    [stdout_opt, stderr_opt]
  end

  module_function :redirects

  def escape_and_concatenate(cmd)
    if cmd.is_a?(String)
      escape(cmd)
    else
      cmd.map { |item| escape(item) }.join(' ')
    end
  end

  module_function :escape_and_concatenate

  def escape(item)
    if Platform.family == 'mswin32' 
      escaped_characters = /\\|&|\||>|<|\^/
      escape_symbol = '^'
      quote_argument = (item =~ /\s/)
    else
      escaped_characters = /"|'|<|>| |&|\||\(|\)|\\|;/
      escape_symbol = '\\'
      quote_argument_with_spaces = false
    end
    escaped_value = item.to_s.gsub(escaped_characters) { |match| "#{escape_symbol}#{match}" }
    if quote_argument
      '"' + escaped_value + '"'
    else
      escaped_value
    end
  end

  module_function :escape


  def format_for_printing(command)
    if command.is_a? String
      command
    else
      command.join(' ')
    end
  end

  module_function :format_for_printing
end