class Clive

  class Parser

    class MissingArgumentError < Error
      reason 'missing argument for #0, found #1, needed #2'
    end

    class MissingOptionError < Error
      reason 'option could not be found: #0'
    end

    DEFAULTS = {
      :state => ::Clive::StructHash
    }

    # @param base [Command]
    #
    # @param config [Hash]
    # @option config [.new, #[], #[]=, #alias] :state
    #   What class the state should be
    def initialize(base, config)
      @base = base
      @config = DEFAULTS.merge(config)
    end

    # The parser should work how you expect. It allows you to put global options
    # before and after a command section (if it exists, which it doesn't), so
    # you have something like.
    #
    #    app [global] [command] [global]
    #
    # Where the [global] sections are made of options and arguments and
    # [command] is made of
    #
    #    [command] [options/args]
    #
    # Only one command can be run, if you attempt to use two the other will be
    # caught as an argument.
    #
    # @param argv [Array]
    #   The input to parse from the command line, usually ARGV.
    #
    # @param pre_state [Hash]
    #   A pre-populated state to be used.
    #
    def parse(argv, pre_state)
      @argv = argv
      @i    = 0

      @state = @config[:state].new(pre_state)
      @state.store :args, []

      # Pull out 'help' command immediately if found
      if @argv[0] == 'help'
        if @argv[1]
          if @base.has?(@argv[1])
            command = @base.find(@argv[1])
            command.run_block({})
            puts command.help
          else
            puts "Error: command #{@argv[1]} could not be found. Try `help` to see the available commands."
          end
        else
          puts @base.help
        end
        Kernel.exit 0
      end

      until ended?
        # does +curr+ exist? (and also check that if it is a command a command hasn't been run yet
        if @base.has?(curr) && ((@base.find(curr).kind_of?(Command) && !command_ran?) || @base.find(curr).kind_of?(Option))

          found = @base.find(curr)

          # is it a command?
          if found.kind_of?(Command)
            @command_ran = true
            @state.store found.names, found.run_block(@config[:state].new)

            inc
            args = []

            until ended?
              # it's a no- option
              if curr[0..4] == '--no-' && found.has?("--#{curr[5..-1]}") && found.find("--#{curr[5..-1]}").config[:boolean] == true
                found.find("--#{curr[5..-1]}").run @state, [false], found

              # it's one (or more) short options
              elsif curr[0..0] == '-' && curr.size > 2 && found.has?("-#{curr[1..1]}")
                currs = curr[1..-1].split('').map {|i| "-#{i}" }

                currs.each do |c|
                  opt = found.find(c)
                  raise MissingOptionError.new(c) unless opt

                  if c == currs.last
                    run_option opt, found
                  else
                    # can't take any arguments as an option is next to it
                    if opt.args.min > 0
                      raise MissingArgumentError.new(opt, [], opt.args)
                    else
                      opt.run @state, [true], found
                    end
                  end
                end

              elsif found.has?(curr)
                run_option found.find(curr), found

              else
                break unless found.args.possible?(args + [curr])
                args << curr
              end
              inc
            end
            dec

            found.run @state, validate_arguments(found, args), found

          # otherwise it is an option
          else
            run_option found
          end

        # it's a no- option
        elsif curr[0..4] == '--no-' && @base.has?("--#{curr[5..-1]}") && @base.find("--#{curr[5..-1]}").config[:boolean] == true
          @base.find("--#{curr[5..-1]}").run @state, [false]

        # it's one (or more) short options
        elsif curr[0..0] == '-' && curr.size > 2 && @base.has?("-#{curr[1..1]}")
          currs = curr[1..-1].split('').map {|i| "-#{i}" }

          currs.each do |c|
            opt = @base.find(c)
            raise MissingOptionError.new(c) unless opt

            if c == currs.last
              run_option opt
            else
              # can't take any arguments as an option is next to it
              if opt.args.min > 0
                raise MissingArgumentError.new(opt, [], opt.args)
              else
                opt.run @state, [true]
              end
            end
          end

        # otherwise it is an argument
        else
          @state.args << curr
        end

        inc
      end

      @state
    end


    private

    def run_option(opt, within=nil)
      args = opt.args.max > 0 ? do_arguments_for(opt) : [true]
      opt.run @state, args, within
    end

    # Increment the index
    def inc
      @i += 1
    end

    # Decrement the index
    def dec
      @i -= 1
    end

    # @return [String] The current token
    def curr
      @argv[@i]
    end

    # Whether the index is at the end of the argv
    def ended?
      @i >= @argv.size
    end

    def command_ran?
      @command_ran || false
    end

    # Returns the finished argument list for +opt+ which can then be pushed to the state.
    def do_arguments_for(opt)
      arg_list = collect_arguments(opt)
      arg_list = validate_arguments(opt, arg_list)

      arg_list
    end

    # Collects the arguments for +opt+.
    def collect_arguments(opt)
      inc
      arg_list = []
      while !ended? && arg_list.size < opt.args.max
        break unless opt.args.possible?(arg_list + [curr])
        arg_list << curr
        inc
      end
      dec
      arg_list
    end

    # Makes sure the found list of arguments is valid, if not raises
    # MissingArgumentError. Returns the valid argument list with the arguments
    # as the correct type, in the correct positions and with default values
    # inserted if necessary.
    def validate_arguments(opt, arg_list)
      # If we don't have enough args
      unless opt.args.valid?(arg_list)
        raise MissingArgumentError.new(opt, arg_list, opt.args.to_s)
      end

      opt.args.create_valid(arg_list)
    end

  end
end
