# = ArgVector
#
# == Synopsis
#
# ArgVector provides a very simple means of parsing
# command line arguments.
#
# Unlike other more complex libs this provides only
# the most basic and standard parsing functionality.
# In many cases that's all one really needs.
#
# Usage is straight foward. Simply instantiate the
# class and query it for the particular "views" of
# the command line you want.
#
#   cargs = ArgVector.new("-a foo -b=2")
#
#   cargs.parameters    #=> [['foo'],{'a'=>true,'b'=>'2'}]
#   cargs.flags         #=> ['a']
#   cargs.preoptions    #=> {'a'=>true}
#   cargs.preflags      #=> ['a']
#   cargs.subcommand    #=> ['foo',{'b'=>'2'}]
#
# == Authors
#
# * Thomas Sawyer
#
# == Copying
#
# Copyright (c) 2006 Thomas Sawyer
#
# Ruby License
#
# This module is free software. You may use, modify, and/or
# redistribute this software under the same terms as Ruby.
#
# This program is distributed in the hope that it will be
# useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
# PURPOSE.

require 'shellwords'

module CLI

  # = CLI::Arguments
  #
  # CLI::Arguments provides a very simple means of parsing
  # command line arguments.
  #
  # Unlike other more complex libs this provides only
  # the most basic and standard parsing functionality.
  # In many cases that's all one really needs.
  #
  # Usage is straight foward. Simply instantiate the
  # class and query it for the particular "views" of
  # the command line you want.
  #
  #   cargs = CLI::Arguments.new("-a foo -b=2")
  #
  #   cargs.parameters    #=> [['foo'],{'a'=>true,'b'=>'2'}]
  #   cargs.flags         #=> ['a']
  #   cargs.preoptions    #=> {'a'=>true}
  #   cargs.preflags      #=> ['a']
  #   cargs.subcommand    #=> ['foo',{'b'=>'2'}]
  #
  class Arguments

    def self.parameters(*args)
      new.parameters(*args)
    end

    attr :line
    attr :argv
    attr :arity
    #attr :opts

    #alias :flags :options

    # Takes the command line string (or array) and options.
    # Options have flags and end with a hash of option arity.
    #
    def initialize(line=nil, arity=nil)
      @line, @argv  = parse_line(line)
      @arity = parse_arity(arity||{})
      parse
    end

    public

    # Returns operand array.

    def operands
      @operands
    end
    
    # deprecate alias ?
    alias arguments operands

    # Returns options hash.

    def options
      @options
    end

    # Returns [operands, options], which is good for plugging
    # directly into a method.

    def parameters
      return @operands, @options
    end

    # Return flags, which are true options.

    def flags
      f = []
      @options.each do |k, v|
        if TrueClass===v or FalseClass===v  # not that it's ever false
          f << k
        end
      end
      return f
    end

    # Assumes the first operand is a "subcommand" and
    # returns it and the argments following it as
    # parameters.

    def subcommand_with_parameters
      opts, args = *parse_preoptions(argv)
      cmd = args.shift
      subargs = self.class.new(args, @arity)
      return [cmd, *subargs.parameters]
    end

    alias subcommand subcommand_with_parameters

    #
    def subcommand_with_preoptions
      pre, args = *parse_preoptions(argv)
      cmd = args.shift
      subargs = self.class.new(args, @arity)
      args, opts = *subargs.parameters
      return [cmd, args, pre.merge(opts)]
    end

    # Assumes the first operand is a "subcommand" and
    # returns it and the argments following it as
    # another Arguments object.
    #
    # TODO: This probably should be called 'subcommand'.
    #
    def subcommand_with_arguments
      opts, args = *parse_preoptions(argv)
      cmd = args.shift
      subargs = self.class.new(args, @arity)
      return cmd, subargs
    end

    # Returns a hash of options that occur before
    # the first operand. This works well with
    # subcommand to get the main command's options.
    #
    #   line = "--trace stamp --file VERSION"
    #   cargs = CLI::Arguments.new(line)
    #   opts = cargs.preoptions
    #   opts #=> {"trace"=>true}

    def preoptions
      preopts, remainder = *parse_preoptions(argv)
      return preopts
    end

    # Same as +flags+ but only returns flags in the
    # preoptions.

    def preflags
      preopts, remainder = *parse_preoptions(argv)
      f = []
      preopts.each do |k, v|
        if TrueClass===v or FalseClass===v  # not that it's ever false
          f << k
        end
      end
      return f
    end

    # Like parameters but without allowing for duplicate options.

    def parameters_without_duplicates
      opts = {}
      @options.each do |k,v|
        if Array===v
          opts[k] = v[0]
        else
          opts[k] =  v
        end
      end
      return @operands, opts
    end

    private # ------------------

    # Basic parser partitions the command line into options and
    # operands. Options are converted to a hash and the two 
    # parts are returned.
    #
    #   line = "--trace stamp --file=VERSION"
    #
    #   args, keys = *parse_command(line)
    #
    #   args #=> ["stamp"]
    #   keys #=> {"trace"=>true, "file"=>"VERSION"}
    #
    def parse
      args = assoc_options(argv) #, *opts_arity)

      opts, opds = args.partition{ |a| Array === a }

      @operands = opds
      @options  = format_options(opts)

      return @operands, @options
    end

    # First pass parser to split the command line into an
    # array using Shellwords, if not already so divided.

    def parse_line(line=nil)
      if line
        case line
        when String
          argv = Shellwords.shellwords(line)
        else
          argv = line.to_ary.dup
          line = argv.join(' ')
        end
      else
        argv = ARGV.dup
        line = argv.join(' ')
      end
      return line, argv
    end

    # Ensure opts are a uniform.
    #
    #def clean_opts( opts )
    #  opts2 = opts.collect{ |o| o.to_sym }
    #  opts2 = opts2 & [:simple, :repeat]  # valid options only
    #  return opts2
    #end

    # Ensure arity is uniform.

    def parse_arity(arity)
      arity2 = {}
      arity.each{ |k,v| arity2[k.to_s] = v.to_i }
      return arity2
    end

    # Parse preoptions. A "preoption" is one that
    # occurs before the first operans (if any).

    def parse_preoptions(args)
      #args = args.dup
      args = multi_flag(args) #unless opts.include?(:simple)

      flags = []
      while args.first =~ /^-/
        key = args.shift
        key.sub!(/^-{1,2}/,'')
        if key.index('=')
          key, val = key.split('=')
        elsif a = arity[key]
          val = args.slice!(0,a)
          val = val.first if a == 1
        else
          val = true
        end
        flags << [key, val]
      end

      flags = format_options(flags)

      return flags, args
    end

    # Parse flags takes the command line and
    # transforms it such that flags (eg. -x and --x)
    # are elemental associative arrays.
    #
    #   line = "--foo hello --try=this"
    #
    #   parse_flags(line) #=> [ [foo,true], hello, ["try","this"] ]
    #
    def assoc_options(args)
      #args = args.dup
      args = multi_flag(args) #unless opts.include?(:simple)

      i = 0
      while i < args.size
        arg = args[i]
        case arg
        when /^-/
          arg = arg.sub(/^-{1,2}/,'')
          if arg.index('=')
            key, val = arg.split('=')
            args[i] = [key, val||true]
          elsif arity.key?(arg)
            cnt = arity[arg]
            key = arg
            val = args[i+1,cnt]
            args[i,cnt+1] = [[key, *val]]
            i += (cnt - 1)
          else
            key = arg
            args[i] = [key,true]
          end
        end
        i += 1
      end
      return args
    end

    # Split single letter option groupings into separate options.
    # ie. -xyz => -x -y -z

    def multi_flag(args=nil)
      args ||= argv
      args.collect { |arg|
        if md = /^-(\w{2,})/.match( arg )
          md[1].split(//).collect { |c| "-#{c}" }
        else
          arg.dup
        end
      }.flatten
    end

    # Format flag options. This converts the associative array of
    # options/flags into a hash. Repeat options will be placed in arrays.

    def format_options(assoc_options)
      opts = {}
      assoc_options.each do |k,v|
        if opts.key?(k)
          opts[k] = [opts[k]].flatten << v
        else
          opts[k] = v
        end
      end
      return opts
    end

  end

end

# For backward compatibiltiy
module Console #:nodoc:
  Arguments = CLI::Arguments
end


