# encoding: utf-8

require 'rbconfig'
require 'tty/command/version'
require 'tty/command/cmd'
require 'tty/command/exit_error'
require 'tty/command/dry_runner'
require 'tty/command/process_runner'
require 'tty/command/printers/null'
require 'tty/command/printers/pretty'
require 'tty/command/printers/progress'
require 'tty/command/printers/quiet'

module TTY
  class Command
    ExecuteError = Class.new(StandardError)

    TimeoutExceeded = Class.new(StandardError)

    # Path to the current Ruby
    RUBY = ENV['RUBY'] || File.join(
      RbConfig::CONFIG['bindir'],
      RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT'])

    def self.record_separator
      @record_separator ||= $/
    end

    def self.record_separator=(sep)
      @record_separator = sep
    end

    attr_reader :printer

    # Initialize a Command object
    #
    # @param [Hash] options
    # @option options [IO] :output
    #   the stream to which printer prints, defaults to stdout
    # @option options [Symbol] :printer
    #   the printer to use for output logging, defaults to :pretty
    # @option options [Symbol] :dry_run
    #   the mode for executing command
    #
    # @api public
    def initialize(options = {})
      @output = options.fetch(:output) { $stdout }
      @color   = options.fetch(:color) { true }
      @uuid    = options.fetch(:uuid) { true }
      @printer_name = options.fetch(:printer) { :pretty }
      @dry_run = options.fetch(:dry_run) { false }
      @printer = use_printer(@printer_name, color: @color, uuid: @uuid)
    end

    # Start external executable in a child process
    #
    # @example
    #   cmd.run(command, [argv1, ..., argvN], [options])
    #
    # @example
    #   cmd.run(command, ...) do |result|
    #     ...
    #   end
    #
    # @param [String] command
    #   the command to run
    #
    # @param [Array[String]] argv
    #   an array of string arguments
    #
    # @param [Hash] options
    #   hash of operations to perform
    #
    # @option options [String] :chdir
    #   The current directory.
    #
    # @option options [Integer] :timeout
    #   Maximum number of seconds to allow the process
    #   to run before aborting with a TimeoutExceeded
    #   exception.
    #
    # @raise [ExitError]
    #   raised when command exits with non-zero code
    #
    # @api public
    def run(*args)
      cmd = command(*args)
      yield(cmd) if block_given?
      result = execute_command(cmd)
      if result && result.failure?
        raise ExitError.new(cmd.to_command, result)
      end
      result
    end

    # Start external executable without raising ExitError
    #
    # @example
    #   cmd.run!(command, [argv1, ..., argvN], [options])
    #
    # @api public
    def run!(*args)
      cmd = command(*args)
      yield(cmd) if block_given?
      execute_command(cmd)
    end

    # Execute shell test command
    #
    # @api public
    def test(*args)
      run!(:test, *args).success?
    end

    # Run Ruby interperter with the given arguments
    #
    # @example
    #   ruby %q{-e "puts 'Hello world'"}
    #
    # @api public
    def ruby(*args, &block)
      options = args.last.is_a?(Hash) ? args.pop : {}
      if args.length > 1
        run(*([RUBY] + args + [options]), &block)
      else
        run("#{RUBY} #{args.first}", options, &block)
      end
    end

    # Check if in dry mode
    #
    # @return [Boolean]
    #
    # @public
    def dry_run?
      @dry_run
    end

    private

    # @api private
    def command(*args)
      Cmd.new(*args)
    end

    # @api private
    def execute_command(cmd)
      mutex = Mutex.new
      dry_run = @dry_run || cmd.options[:dry_run] || false
      @runner = select_runner(@printer, dry_run)
      mutex.synchronize { @runner.run(cmd) }
    end

    # @api private
    def use_printer(class_or_name, options)
      if class_or_name.is_a?(TTY::Command::Printers::Abstract)
        return class_or_name
      end

      if class_or_name.is_a?(Class)
        class_or_name
      else
        find_printer_class(class_or_name)
      end.new(@output, options)
    end

    # Find printer class or fail
    #
    # @raise [ArgumentError]
    #
    # @api private
    def find_printer_class(name)
      const_name = name.to_s.capitalize.to_sym
      if const_name.empty? || !TTY::Command::Printers.const_defined?(const_name)
        fail ArgumentError, %(Unknown printer type "#{name}")
      end
      TTY::Command::Printers.const_get(const_name)
    end

    # @api private
    def select_runner(printer, dry_run)
      if dry_run
        DryRunner.new(printer)
      else
        ProcessRunner.new(printer)
      end
    end
  end # Command
end # TTY