# = arguments.rb
#
# == 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.
#
# == Author(s)
#
# * Thomas Sawyer
#
# == Developer Notes
#
#   TODO Move to console/arguments.rb, but I'm not sure yet
#        if adding subdirectories to more/ is a good idea.

# CREDIT Thomas Sawyer

# Author::    Thomas Sawyer
# Copyright:: Copyright (c) 2005 Thomas Sawyer
# License::   Ruby License

#require 'facets/core/array/index'
require 'shellwords'

module Console; end

# = Console Arguments
#
# Console Arguments provaide a simpel and convient measn
# of parsing arguments passed to via commandline.
#
# Unlike other more complex libs (like Facets' own Console::Command)
# Arguments provides only the basic standard parsing functionality.
# In many casses that's all one really needs.

class Console::Arguments

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

  def initialize( line=nil, *opts_arity )
    @line, @argv = parse_line(line)

    @opts, @arity = clean(*opts_arity)

    # caches
    #@words    = Hash.new #{|h,k| h[k]=[]}
    #@flags    = Hash.new #{|h,k| h[k]=[]}
    #@preflags = Hash.new #{|h,k| h[k]=[]}
  end

  private

  # 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

  public

  # Parse off the front flags of a command line.
  #
  #   line = "--trace stamp --file VERSION"
  #
  #   argv,keys = *parse_front_flags(line)
  #
  #   argv #=> ["stamp", "--file", "VERSION"]
  #   keys #=> [["trace", true]]
  #
  # Provided an arity hash if any flags take free standing
  # parameters.
  #
  #   line = "--level 4 stamp --file VERSION"
  #
  #   argv,keys = *parse_front_flags(line, :trace => 1)
  #
  #   argv #=> ["stamp", "--file", "VERSION"]
  #   keys #=> [["level", 4]]
  #

  def preflags #( *opts_arity )
    #opts, arity  = clean(*opts_arity)

    @preflags ||= (
      preflags, args = *parse_preflags(argv) #, *opts_arity)
      preflags
    )
  end

  # Like preflags but will remove preflags from
  # the argumuments array (argv).

  def preflags! #( *opts_arity )
    #opts, arity  = clean(*opts_arity)

    @preflags ||= (
      preflags, args = *parse_preflags(argv) #, *opts_arity)
      @argv = args
      preflags
    )
  end

  #

  def words( *opts_arity )
    #opts, arity  = clean(*opts_arity)

    @words ||= (
      parse #( opts, arity )
      @words
    )
  end

  #

  def flags( *opts_arity )
    #opts, arity  = clean(*opts_arity)

    @flags ||= (
      parse #( opts, arity )
      @flags
    )
  end

  # Returns the parsed command line as an array + hash collection
  # of parameters, like what a Ruby method accepts. Because of the
  # use of the hash for flags and options parameters, repeat entries
  # can be used with this, in that case use #parse directly.

  def parameters
    args, keys = parse
    args.flatten << keys unless keys.empty?
    return *args
  end

  # Basic parser partitions the command line into flags and
  # arguments. Flags are converted to a associative array
  # and then the two parts are returned.
  #
  #   line = "--trace stamp --file=VERSION"
  #
  #   args,keys = *parse_command(line)
  #
  #   args #=> ["stamp"]
  #   keys #=> [["trace",true], ["file","VERSION"]]
  #

  def parse #( *opts_arity )
    #opts, arity  = clean(*opts_arity)

    args = associate_flags(argv) #, *opts_arity)

    flags, words = args.partition{ |a| Array === a }

    flags = format_flags(flags) unless opts.include?(:repeat)

    @words = words
    @flags = flags

    return words, flags
  end

  # Multi-command.
  #
  # Parses a chain of commands from a single command line.
  # This assumes commands take no free standing arguments,
  # and rather only utilize option flags.
  #
  #   line = "--trace rubyforge --groupid=2014 publish --copy='**/*'
  #
  # This method does not support flag arity since each command in
  # the chain could have it's own arity settings. So '=' must be
  # used to set a flag parameter.

  def multi_command #( *opts_arity )
    #opts, arity  = clean(*opts_arity)

    pflags = preflags #(*opts_arity)

    args = argv.dup
    args = multi_flag(args) unless opts.include?(:simple)

    cmds = []

    f = args.find{ |e| e !~ /^-/ }
    i = f ? args.index(f) : -1
    until i < 0
      args = args[i..-1]  # chain command
      cmd = args.shift
      f = args.find{ |e| e !~ /^-/ }
      if f
        i = args.index(f)
        subopts = args[0...i]
      else
        i = -1
        subopts = args[0..-1]
      end
      keys = format_flags(associate_flags(subopts)) #, *opts_arity))
      cmds << [cmd, keys]
    end

    return cmds, pflags
  end

=begin
  # Subcommand.
  #
  # Subcommand parsing assumes that the first non-flag
  # entry is a subcommand, followed by subcommand flags.
  #

  def parse_subcommand( line=nil, arity={} )
    argv = parse_line(line)
    argv = multi_flag(argv)

    argv, glbl = parse_front_flags( argv, arity )

    cmd = argv.shift

    args, keys = parse_command(argv, arity)

    return cmd, args, keys, glbl
  end
=end

  private

  # Parse and normalize options and arity.

  def clean( *opts_arity )
    case opts_arity.last
    when Hash
      opts  = opts_arity[0...-1]
      arity = opts_arity.last
    else
      opts  = opts_arity
      arity = {}
    end
    opts  = clean_opts(opts)
    arity = clean_arity(arity)
    return opts, arity
  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 clean_arity( arity )
    arity2 = {}
    arity.each{ |k,v| arity2[k.to_s] = v.to_i }
    return arity2
  end

  # Parse preflags.

  private
  def parse_preflags( args, *opts_arity )
    opts, arity  = clean(*opts_arity)

    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)
      else
        val = true
      end
      flags << [key, val]
    end

    flags = format_flags(flags) unless opts.include?(:repeat)

    return flags, args
  end

  # Parse flags takes the command line and
  # transforms it such that flags (eg. -x and --x)
  # are elemetnal associative arrays.
  #
  #   line = "--foo hello --try=this"
  #
  #   parse_flags(line) #=> [ [foo,true], hello, ["try","this"] ]
  #

  def associate_flags( args ) #, *opts_arity )
    #opts, arity  = clean(*opts_arity)

    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
        else
          key = arg
          args[i] = [key,true]
        end
      end
      i += 1
    end
    return args
  end

=begin
  # Parse out flags across the entire command line.
  # Flags parsed are the front flags and any specified in the
  # the +flags+ parameter. The +flags+ parameters can be either
  # a list of flag names or a name => arity hash.
  #
  # Returns the remaining split line and the global flags.

  def parse_global_flags( line=nil, flags={} )
    argv = parse_line(line)
    argv = multi_flag(argv)

    keys = []
    flags.each do |name, arity|
      while i = argv.index{ |e| e =~ /^-{1,2}#{name}/ }
        arg = argv[i]
        if arg.index('=')
          key, val = arg.split('=')
          keys << [name,val]
        elsif arity
          val = argv.slice!(i+1,arity)
          keys << [name,val]
        else
          keys << [name,true]
        end
        argv.delete(i)
      end
    end

    i = argv.index{ |e| e !~ /^-/ }
    glbl = argv[0...i]  # main options
    argv = argv[i..-1]  # rest of line

    glbl.each do |key|
      name = key.sub(/^-{1,2}/,'')
      keys.unshift [name, true]
    end

    keys = format_flags(keys)

    return argv, keys
  end
=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 flags into
  # hash. This prevents repate option flags. If you need repeat options flags
  # you can deacitvate this formatting by using the submodule RepeatFlag.

  def format_flags(assoc_array)
    Hash[*assoc_array.flatten]
  end

end


#  _____         _
# |_   _|__  ___| |_
#   | |/ _ \/ __| __|
#   | |  __/\__ \ |_
#   |_|\___||___/\__|
#

=begin test

  require 'test/unit'

  class TestArguments < Test::Unit::TestCase

    include Console

    def test_simple
      line = "-x baz --foo=8 bar"
      cargs = Arguments.new(line)
      args, keys = cargs.parse
      assert_equal(['baz','bar'],args)
      assert_equal({'foo'=>'8','x'=>true},keys)
    end

    def test_repeat
      line = "-x baz --foo=9 bar"
      cargs = Arguments.new(line, :repeat)
      args, keys = cargs.parse
      assert_equal(['baz','bar'],args)
      assert_equal([['x',true],['foo','9']],keys)
    end

    def test_preflags
      line = "-x --foo=7 baz bar"
      cargs = Arguments.new(line)
      flags = cargs.preflags
      assert_equal({'x'=>true,'foo'=>'7'},flags)
    end

    def test_preflags!
      line = "-x baz --foo=7 bar"
      cargs = Arguments.new(line)
      glbl = cargs.preflags!
      words, keys = cargs.parse
      cmd, *args = *words
      assert_equal({'x'=>true},glbl)
      assert_equal('baz',cmd)
      assert_equal(['bar'],args)
      assert_equal({'foo'=>'7'},keys)
    end

    def test_multi_command
      line = "-x baz --foo=6 bar --zoo='xx'"
      cargs = Arguments.new(line)
      cmds, preflags = cargs.multi_command

      assert_equal({'x'=>true},preflags)

      cmd, keys = *cmds[0]
      assert_equal('baz',cmd)
      assert_equal({'foo'=>'6'},keys)

      cmd, keys = *cmds[1]
      assert_equal('bar',cmd)
      assert_equal({'zoo'=>'xx'},keys)
    end

    def test_with_arity
      line = "-q baz --aq 5 bar"
      cargs = Arguments.new(line,'aq'=>1)
      words, flags = cargs.parse
      assert_equal(['baz','bar'],words)
      assert_equal({'q'=>true,'aq'=>'5'},flags)
    end

  end

=end
