module GLI
  # Parses the command-line options using an actual +OptionParser+
  class GLIOptionParser
    attr_accessor :options

    DEFAULT_OPTIONS = {
      :default_command => nil,
      :autocomplete => true,
      :subcommand_option_handling_strategy => :legacy,
      :argument_handling_strategy => :loose
    }

    def initialize(commands,flags,switches,accepts, options={})
      self.options = DEFAULT_OPTIONS.merge(options)

      command_finder       = CommandFinder.new(commands,
                                               :default_command => (options[:default_command] || :help),
                                               :autocomplete => options[:autocomplete])
      @global_option_parser = GlobalOptionParser.new(OptionParserFactory.new(flags,switches,accepts),command_finder,flags)
      @accepts              = accepts
      if options[:argument_handling_strategy] == :strict && options[:subcommand_option_handling_strategy] != :normal
        raise ArgumentError, "To use strict argument handling, you must enable normal subcommand_option_handling, e.g. subcommand_option_handling :normal"
      end
    end

    # Given the command-line argument array, returns an OptionParsingResult
    def parse_options(args) # :nodoc:
      option_parser_class = self.class.const_get("#{options[:subcommand_option_handling_strategy].to_s.capitalize}CommandOptionParser")
      OptionParsingResult.new.tap { |parsing_result|
        parsing_result.arguments = args
        parsing_result = @global_option_parser.parse!(parsing_result)
        option_parser_class.new(@accepts).parse!(parsing_result, options[:argument_handling_strategy])
      }
    end

  private

    class GlobalOptionParser
      def initialize(option_parser_factory,command_finder,flags)
        @option_parser_factory = option_parser_factory
        @command_finder        = command_finder
        @flags                 = flags
      end

      def parse!(parsing_result)
        parsing_result.arguments      = GLIOptionBlockParser.new(@option_parser_factory,UnknownGlobalArgument).parse!(parsing_result.arguments)
        parsing_result.global_options = @option_parser_factory.options_hash_with_defaults_set!
        command_name = if parsing_result.global_options[:help]
                         "help"
                       else
                         parsing_result.arguments.shift
                       end
        parsing_result.command        = @command_finder.find_command(command_name)
        unless command_name == 'help'
          verify_required_options!(@flags, parsing_result.command, parsing_result.global_options)
        end
        parsing_result
      end

    protected
      def verify_arguments!(arguments, command)
        # Lets assume that if the user sets a 'arg_name' for the command it is for a complex scenario
        # and we should not validate the arguments
        return unless command.arguments_description.empty?

        # Go through all declared arguments for the command, counting the min and max number
        # of arguments
        min_number_of_arguments = 0
        max_number_of_arguments = 0
        command.arguments.each do |arg|
          if arg.optional?
            max_number_of_arguments = max_number_of_arguments + 1
          else
            min_number_of_arguments = min_number_of_arguments + 1
            max_number_of_arguments = max_number_of_arguments + 1
          end

          # Special case, as soon as we have a 'multiple' arguments, all bets are off for the
          # maximum number of arguments !
          if arg.multiple?
            max_number_of_arguments = 99999
          end
        end

        # Now validate the number of arguments
        if arguments.size < min_number_of_arguments
          raise MissingRequiredArgumentsException.new("Not enough arguments for command", command)
        end
        if arguments.size > max_number_of_arguments
          raise MissingRequiredArgumentsException.new("Too many arguments for command", command)
        end
      end

      def verify_required_options!(flags, command, options)
        missing_required_options = flags.values.
          select(&:required?).
          reject { |option|
            options[option.name] != nil
        }
        unless missing_required_options.empty?
          missing_required_options.sort!
          raise MissingRequiredArgumentsException.new(missing_required_options.map { |option|
            "#{option.name} is required"
          }.join(', '), command)
        end
      end
    end

    class NormalCommandOptionParser < GlobalOptionParser
      def initialize(accepts)
        @accepts = accepts
      end

      def error_handler
        lambda { |message,extra_error_context|
          raise UnknownCommandArgument.new(message,extra_error_context)
        }
      end

      def parse!(parsing_result,argument_handling_strategy)
        parsed_command_options = {}
        command = parsing_result.command
        arguments = nil

        loop do
          option_parser_factory       = OptionParserFactory.for_command(command,@accepts)
          option_block_parser         = CommandOptionBlockParser.new(option_parser_factory, self.error_handler)
          option_block_parser.command = command
          arguments                   = parsing_result.arguments

          arguments = option_block_parser.parse!(arguments)

          parsed_command_options[command] = option_parser_factory.options_hash_with_defaults_set!
          command_finder                  = CommandFinder.new(command.commands, :default_command => command.get_default_command)
          next_command_name               = arguments.shift

          verify_required_options!(command.flags, command, parsed_command_options[command])

          begin
            command = command_finder.find_command(next_command_name)
          rescue AmbiguousCommand
            arguments.unshift(next_command_name)
            break
          rescue UnknownCommand
            arguments.unshift(next_command_name)
            # Although command finder could certainy know if it should use
            # the default command, it has no way to put the "unknown command"
            # back into the argument stack.  UGH.
            unless command.get_default_command.nil?
              command = command_finder.find_command(command.get_default_command)
            end
            break
          end
        end
        parsed_command_options[command] ||= {}
        command_options = parsed_command_options[command]

        this_command          = command.parent
        child_command_options = command_options

        while this_command.kind_of?(command.class)
          this_command_options = parsed_command_options[this_command] || {}
          child_command_options[GLI::Command::PARENT] = this_command_options
          this_command = this_command.parent
          child_command_options = this_command_options
        end

        parsing_result.command_options = command_options
        parsing_result.command = command
        parsing_result.arguments = Array(arguments.compact)

        # Lets validate the arguments now that we know for sure the command that is invoked
        verify_arguments!(parsing_result.arguments, parsing_result.command) if argument_handling_strategy == :strict

        parsing_result
      end

    end

    class LegacyCommandOptionParser < NormalCommandOptionParser
      def parse!(parsing_result,argument_handling_strategy)
        command                     = parsing_result.command
        option_parser_factory       = OptionParserFactory.for_command(command,@accepts)
        option_block_parser         = LegacyCommandOptionBlockParser.new(option_parser_factory, self.error_handler)
        option_block_parser.command = command

        parsing_result.arguments       = option_block_parser.parse!(parsing_result.arguments)
        parsing_result.command_options = option_parser_factory.options_hash_with_defaults_set!

        subcommand,args                = find_subcommand(command,parsing_result.arguments)
        parsing_result.command         = subcommand
        parsing_result.arguments       = args
        verify_required_options!(command.flags, parsing_result.command, parsing_result.command_options)
      end

    private

      def find_subcommand(command,arguments)
        arguments = Array(arguments)
        command_name = if arguments.empty?
                         nil
                       else
                         arguments.first
                       end

        default_command = command.get_default_command
        finder = CommandFinder.new(command.commands, :default_command => default_command.to_s)

        begin
          results = [finder.find_command(command_name),arguments[1..-1]]
          find_subcommand(results[0],results[1])
        rescue UnknownCommand, AmbiguousCommand
          begin
            results = [finder.find_command(default_command.to_s),arguments]
            find_subcommand(results[0],results[1])
          rescue UnknownCommand, AmbiguousCommand
            [command,arguments]
          end
        end
      end
    end
  end
end
