require 'optparse'

module Commander
  class Runner
    
    #--
    # Exceptions
    #++

    class CommandError < StandardError; end
    class InvalidCommandError < CommandError; end
    
    ##
    # Commands within the runner.
    
    attr_reader :commands
    
    ##
    # Global options.
    
    attr_reader :options
    
    ##
    # Initialize a new command runner.
    
    def initialize args = ARGV
      @args = args
      @commands, @options, @program = {}, {}, { 
        :help_formatter => Commander::HelpFormatter::Terminal,
        :int_message => "\nProcess interrupted",
      }
      create_default_commands
      parse_global_options
    end
    
    ##
    # Run the command parsing and execution process immediately.
    
    def run!
      %w[ name version description ].each { |k| ensure_program_key_set k.to_sym }
      case 
      when options[:version] : $terminal.say "#{@program[:name]} #{@program[:version]}" 
      when options[:help] : get_command(:help).run
      else active_command.run args_without_command
      end
    rescue InvalidCommandError
      $terminal.say "invalid command. Use --help for more information"
    rescue OptionParser::InvalidOption, 
      OptionParser::InvalidArgument,
      OptionParser::MissingArgument => e
      $terminal.say e
    end
    
    ##
    # Assign program information.
    #
    # === Examples:
    #    
    #    # Set data
    #    program :name, 'Commander'
    #    program :version, Commander::VERSION
    #    program :description, 'Commander utility program.'
    #    program :help, 'Copyright', '2008 TJ Holowaychuk'
    #    program :help, 'Anything', 'You want'
    #    program :int_message 'Bye bye!'
    #
    #    # Get data
    #    program :name # => 'Commander'
    #
    # === Keys:
    #
    #    :name            (required) Program name
    #    :version         (required) Program version triple, ex: '0.0.1'
    #    :description     (required) Program description
    #    :help_formatter  Defaults to Commander::HelpFormatter::Terminal
    #    :help            Allows addition of arbitrary global help blocks
    #    :int_message     Message to display when interrupted (CTRL + C)
    #
    
    def program key, *args
      if key == :help and !args.empty?
        @program[:help] ||= {}
        @program[:help][args.first] = args[1]
      else
        @program[key] = *args unless args.empty?
        @program[key] if args.empty?
      end
    end
    
    ##
    # Generate a command object instance using a block
    # evaluated with the command as its scope.
    #
    # === Examples:
    #    
    #    command :my_command do |c|
    #      c.when_called do |args|
    #        # Code
    #      end
    #    end
    #
    # === See:
    #
    # * Commander::Command
    # * Commander::Runner#add_command
    #
    
    def command name, &block
      command = Commander::Command.new(name) and yield command
      add_command command
    end
    
    ##
    # Add a command object to this runner.
    
    def add_command command
      @commands[command.name] = command
    end
    
    ##
    # Get a command object if available or nil.
    
    def get_command name
      @commands[name.to_s] or raise InvalidCommandError, "invalid command '#{ name || 'nil' }'", caller
    end
    
    ##
    # Check if a command exists.
    
    def command_exists? name
      @commands[name.to_s]
    end
    
    ##
    # Get active command within arguments passed to this runner.
    #
    # === See:
    #
    # * Commander::Runner#parse_global_options
    #
    
    def active_command
      @_active_command ||= get_command(command_name_from_args)
    end
    
    ##
    # Attemps to locate command from @args. Supports multi-word
    # command names. 
    
    def command_name_from_args
      @_command_name_from_args ||= @args.delete_switches.inject do |name, arg|
        return name if command_exists? name
        name += " #{arg}"
      end
    end
    
    ##
    # Help formatter instance.
    
    def help_formatter
      @_help_formatter ||= @program[:help_formatter].new self
    end
    
    ##
    # Return arguments without the command name.
    
    def args_without_command
      @args.dup - command_name_from_args.split
    end
            
    private
    
    ##
    # Creates default commands such as 'help' which is 
    # essentially the same as using the --help switch.
    
    def create_default_commands
      command :help do |c|
        c.syntax = "command help <sub_command>"
        c.summary = "Display help documentation"
        c.description = "Display help documentation for the global or sub commands"
        c.example "Display global help", "command help"
        c.example "Display help for 'foo'", "command help foo"
        c.when_called do |args, options|
          gen = help_formatter
          if args.empty?
            $terminal.say gen.render 
          else
            $terminal.say gen.render_command(get_command(args.join(' ')))
          end
        end
      end
    end
            
    ##
    # Parse global command options.
    #
    # These options are used by commander itself 
    # as well as allowing your program to specify 
    # global commands such as '--verbose'.
    #
    # TODO: allow 'option' method for global program
    #
    
    def parse_global_options
      opts = OptionParser.new
      opts.on("--help") { @options[:help] = true }
      opts.on("--version") { @options[:version] = true }
      opts.parse! @args.dup
    rescue OptionParser::InvalidOption
      # Ignore invalid options since options will be further 
      # parsed by our sub commands.
    end
    
    ##
    # Raises a CommandError when the program +key+ is not present, or is empty.
        
    def ensure_program_key_set key 
      raise CommandError, "Program #{key} required (use #program method)" if (@program[key].nil? || @program[key].empty?)
    end
    
  end
end