

module NumRu

  module Misc
    class HelpMessagingException < StandardError
    end

    # == Overview
    # 
    # A class to facilitate optional keyword arguments. More specifically,
    # it helps the use of a Hash to mimic the keyword argument system.
    # With this, you can set default values and description to each 
    # keyword argument.
    # 
    # == Classes defined supplementarilly
    # 
    # === class NumRu::Misc::HelpMessagingException < StandardError
    # 
    # This is for your convenience. See the usage example below.
    # 
    # == Usage example
    # 
    # Suppose that you introduce keyword arguments "flag" and "number"
    # to the method "hoge" in a class/module Foo. It can be done as 
    # follows:
    # 
    #   require 'numru/misc'  # or, specifically, require 'numru/misc/keywordopt'
    #   include NumRu
    # 
    #   class Foo
    #     @@opt_hoge = Misc::KeywordOpt.new(
    #       ['flag',   false, 'whether or not ...'],
    #       ['number', 1,     'number of ...'],
    #       ['help',   false, 'show help message']
    #     )
    #     def hoge(regular_arg1, regular_arg2, options=nil)
    #       opt = @@opt_hoge.interpret(options)
    #       if opt['help']
    #         puts @@opt_hoge.help
    #         puts ' Current values='+opt.inspect
    #         raise Misc::HelpMessagingException, '** show help message and raise **'
    #       end
    #       # do what you want below 
    #       # (options are set in the Hash opt: opt['flag'] and opt['number'])
    #     end
    #   end
    # 
    # Here, the options are defined in the class variable @@opt_hoge
    # with option names, default values, and descriptions (for help 
    # messaging). One can use the method hoge as follows:
    # 
    #   foo = Foo.new
    #   ...
    #   x = ...
    #   y = ...
    #   ...
    #   foo.hoge( x, y, {'flag'=>true, 'number'=>10} )
    # 
    # Or equivalently,
    # 
    #   foo.hoge( x, y, 'flag'=>true, 'number'=>10 )
    # 
    # because '{}' can be omitted here. 
    # 
    # Tails of options names can be shortened as long as unambiguous:
    # 
    #   foo.hoge( x, y, 'fla'=>true, 'num'=>10 )
    # 
    # 
    # To show the help message, call
    # 
    #   foo.hoge( x, y, 'help'=>true )
    # 
    # This will cause the following help message printed with the 
    # exception HelpMessagingException raised.
    # 
    #    << Description of options >>
    #     option name => default value   description:
    #      "flag" =>     false   whether or not ...
    #      "number" =>   1       number of ...
    #      "help" =>     false   show help message
    #    Current values={"help"=>true, "number"=>1, "flag"=>false}
    #    NumRu::Misc::HelpMessagingException: ** help messaging done **
    #           from (irb):78:in "hoge"
    #           from (irb):83
    # 
    # Do not affraid to write long descriptions. The help method  
    # breaks lines nicely if they are long.
    # 
    class KeywordOpt

      #   Constructor.
      # 
      #   ARGUMENTS
      #   * args : (case 1) arrays of two or three elements: [option name, 
      #     default value, description ], or [option name, default value]
      #     if you do not want to write descriptions. Option names and 
      #     descriptions must be String. (case 2) another KeywordOpt.
      #     Cases 1 and 2 can be mixed. 
      # 
      #     When case 2, a link to the other KeywordOpt is kept. Thus, change
      #     of values in it is reflected to the current one. However,
      #     the link is deleted if values are changed by <b>#set</b>.
      # 
      #   RETURN VALUE
      #   * a KeywordOpt object
      # 
      #   EXAMPLE
      #   * case 1
      #       opt = Misc::KeywordOpt.new(
      #         ['flag',   false, 'whether or not ...'],
      #         ['help',   false, 'show help message']
      #       )
      #   * case 2
      #       opt = Misc::KeywordOpt.new( optA, optB )
      #   * case 1 & 2
      #       opt = Misc::KeywordOpt.new(
      #         ['flag',   false, 'whether or not ...'],
      #         optA
      #       )
      def initialize(*args)
	# USAGE:
	#    KeywordOpt.new([key,val,description],[key,val,description],..)
	#    where key is a String, and description can be omitted.
	@val=Hash.new
	@description=Hash.new
	@keys = []
	args.each{ |x|
	  case x
	  when Array
	    unless (x[0]=='help') && @keys.include?(x[0])
	      #^only 'help' can overwrap in the arguments
	      @keys.push(x[0])
	      @val[x[0]] = x[1]
	      @description[x[0]] = ( (x.length>=3) ? x[2] : '' )
	    end
	  when KeywordOpt
	    x.keys.each{|k|
	      unless k=='help' && @keys.include?(k)
	        #^only 'help' can overwrap in the arguments
		@keys.push(k)
		@val[k] = x  #.val[k]
		@description[k] = x.description[k]
	      end
	    }
	    def @val.[](k)
	      val = super(k)
	      val.is_a?(KeywordOpt) ? val[k] : val
	    end
	    def @val.dup
	      out = Hash.new
	      each{|k,val| out[k] = (val.is_a?(KeywordOpt) ? val[k] : val)}
	      out
	    end
	  else
	    raise ArgumentError, "invalid argument: #{x.inspect}"
	  end
	}
	@keys_sort = @keys.sort
	if @keys_sort.length != @keys_sort.uniq.length
	  raise ArgumentError, "keys are not unique"
	end
      end

      #   Interprets a hash that specifies option values.
    # 
      #   ARGUMENTS
      #   * hash (Hash or nil) : a hash with string keys matching option names 
      #     (initializedwhen constructed). The matching is case sensitive and done
      #     such that the tail of a option name can be omitted as long as 
      #     unambiguous (for example, 'num' for 'number'). 
      #     If the argument is nil, the current values are returned.
      #     If there are two options like 'max' and 'maxval', to use 
      #     a key 'max' (identical to the former paramer) is allowed, although
      #     it matches 'maxval' as well. (Again 'ma' is regarded ambiguous.)
    # 
      #   RETURN VALUE
      #   * a Hash containing the option values (default values overwritten
      #     with hash).
    # 
      #   POSSIBLE EXCEPTION
      #   * hash has a key that does not match any of the option names.
      #   * hash has a key that is ambiguous
      def interpret(hash)
	return @val.dup if hash.nil?
	##
	len = @val.length
	im = 0
	out = @val.dup
	hash.keys.sort.each do |key|
	  rkey = /^#{key}/
	  loop do
	    if rkey =~ @keys_sort[im]
	      if im<len-1 && rkey=~@keys_sort[im+1] &&
		 key != @keys_sort[im]   # not identical
		raise ArgumentError, "Ambiguous key specification '#{key}'." 
	      end
	      out[@keys_sort[im]]=hash[key]
	      break
	    end
	    im += 1
	    if im==len
	      raise ArgumentError, "'#{key}' does not match any of the keys."
	    end
	  end
	end
	out
      end

      #   Copies hash_or_keys, exclude ones that are not included in the option
      #   (by comparing keys), and returns it. I.e. select only the ones 
      #   exsitent. 
      # 
      #   NOTE: ambiguity is not checked, so the resultant value is not 
      #   necessarily accepted by <b>#interpret</b>.
      # 
      #   ARGUMENTS
      #   * hash_or_keys (Hash or Array)
      # 
      #   RETURN VALUE
      #   * a Hash or Array depending on the class of the argument hash_or_keys
      def select_existent(hash_or_keys)
	hash_or_keys = hash_or_keys.dup         # not to alter the original
	len = @val.length
	im = 0
	kys = ( Array === hash_or_keys ? hash_or_keys : hash_or_keys.keys )
	kys.sort.each do |key|
	  rkey = /^#{key}/
	  loop do
	    break if rkey =~ @keys_sort[im]
	    im += 1
	    if im==len
	      hash_or_keys.delete(key)
	      im = 0           # rewind
              break
	    end
	  end
	end
	hash_or_keys
      end

      #   Similar to <b>#interpret</b> but changes internal values.
      # 
      #   ARGUMENTS
      #   * hash (Hash) : see <b>#interpret</b>. (Here, nil is not permitted though)
      # 
      #   RETURN VALUE
      #   * a Hash containing the values replaced (the ones before calling this 
      #     method)
      # 
      #   POSSIBLE EXCEPTION
      #   * the argument is not a Hash
      #   * others are same as in <b>#interpret</b>
      def set(hash)
	raise ArgumentError, "not a hash" if !hash.is_a?(Hash)
	##
	replaced = Hash.new
	len = @val.length
	im = 0
	hash.keys.sort.each do |key|
	  rkey = /^#{key}/
	  loop do
	    if rkey =~ @keys_sort[im]
	      if im<len-1 && rkey=~@keys_sort[im+1]
		raise "Ambiguous key specification '#{key}'." 
	      end
	      replaced[@keys_sort[im]] = @val[@keys_sort[im]]
	      @val[@keys_sort[im]]=hash[key]
	      break
	    end
	    im += 1
	    raise "'#{key}' does not match any of the keys." if im==len
	  end
	end
	replaced
      end

      def __line_feed(str, len)
	if str.length >= len
	  idx = str[0...len].rindex(/\s/)
	  if idx
	    str = str[0...idx] + "\n\t\t\t# " + __line_feed(str[(idx+1)..-1],50)
	  end
	end
	str
      end
      private :__line_feed

      #   Returns a help message
      # 
      #   RETURN VALUE
      #   * a String describing the option names, default values, and descriptions
      def help
	 "  option name\tdefault value\t# description:\n" +
	 @keys.collect{|k| 
	   __line_feed("  #{k.inspect}\t#{@val[k].inspect}\t# #{@description[k]}", 66)
	 }.join("\n")
      end

      # Returns a value associated with the key (exact matching unlike interpret)
      def [](k)
	v = @val[k]
	if v.is_a?(KeywordOpt)
	  v = v.val[k]
	end
	v
      end

      # Returns the keys.
      def keys
	@keys.dup
      end

      ##### protected method

      protected
      attr_reader :val, :description
    end

    ##################################################

    # 
    # class NumRu::Misc::KeywordOptAutoHelp < NumRu::Misc::KeywordOpt
    # 
    # Same as <b>class NumRu::Misc::KeywordOpt</b>, but the method <b>#interpret</b>
    # shows a help message and raise an exception if option 'help' is provided
    # as an argument and is not nil or false 
    # (NumRu::Misc::HelpMessagingException < StandardError 
    # or if the arguments cannot be interpreted correctly (ArgumentError).
    # Option 'help' is automatically defined, so you do not have to define it
    # yourself.
    class KeywordOptAutoHelp < KeywordOpt
      def initialize(*args)
	args.push(['help',  false, 'show help message if true'])
	super(*args)
      end

      def interpret(hash)
	begin
	  out = super(hash)
	rescue
	  raise $!.inspect + "\n  Available parameters are:\n" + help
	end
	if out['help']
	  puts 	"<< Description of options >>\n" + help
	  puts ' Current values=' + out.inspect
	  raise Misc::HelpMessagingException, '** help messaging done **'
	end
	out
      end

      def set(hash)
	raise ArgumentError, "not a hash" if !hash.is_a?(Hash)
	if hash['help']
	  puts 	"<< Description of options >>\n" + help
	  raise Misc::HelpMessagingException, '** help messaging done **'
	end
	super
      end
    end

  end
end

if __FILE__ == $0
  include NumRu

  class Foo   # :nodoc:
    @@opt_hoge = Misc::KeywordOpt.new(
      ['flag',   false, 'whether or not ...'],
      ['number', 1,     'number of ...'],
      ['fff',    1,     'fff...'],
      ['help',  false, 'show help message']
    )
    def self.change_default(hash)
      @@opt_hoge.set(hash)
    end
    def hoge(regular_arg1, regular_arg2, options=nil)
      opt = @@opt_hoge.interpret(options)
      if opt['help']
        puts "* Description of options:\n" + @@opt_hoge.help
        puts ' Current values='+opt.inspect
        raise Misc::HelpMessagingException, '** show help message and raise **'
      end
      # do what you want below 
      # (options are set in the Hash opt: opt['flag'] and opt['number'])
      p opt
    end
  end

  foo = Foo.new
  x = 1
  y = 1
  print "### 0 ###\n"
  foo.hoge( x, y, {'flag'=>true, 'number'=>10} )
  foo.hoge( x, y )
  print "### 1 ###\n"
  foo.hoge( x, y, 'fla'=>true, 'num'=>10 )
  print "### 2 ###\n"
  begin
    foo.hoge( x, y, 'help'=>true )
  rescue
    puts $!
  end
  print "### 3 ###\n"
  Foo.change_default( {'number'=>3} )
  begin
    foo.hoge( x, y, 'fla'=>true, 'num'=>10, 'help'=>true)
  rescue
    puts $!
  end
  print "### 4 ###\n"
  begin
    foo.hoge( x, y, 'dummy'=>nil)
  rescue
    puts $!
  end
  print "### 5 ###\n"
  begin
    foo.hoge( x, y, 'f'=>nil)
  rescue
    puts $!
  end

  print "\n######  test of KeywordOptAutoHelp ######\n"
  opt = Misc::KeywordOptAutoHelp.new(
    ['flag',   false, 'whether or not ...'],
    ['number', 1,     'number of ...']
  )
  print "### 11 ###\n"
  begin
    opt.interpret('flag'=>10,'help'=>true)
  rescue
    puts $!
  end
  print "### 12 ###\n"
  begin
    opt.interpret('nnn'=>10)
  rescue
    puts $!
  end

  print "### 13 ###\n"
  opt2 = Misc::KeywordOptAutoHelp.new(
    ['flafla',   false, 'whether or not ...']
  )
  opt3 = Misc::KeywordOptAutoHelp.new( opt, opt2 )
  p opt3.interpret('flag'=>true)
  begin
    opt3.interpret('help'=>true)
  rescue
    puts $!
  end

  print "### 14 ###\n"
  p opt2.keys, opt.keys
  p opt.select_existent({"flag"=>99, "num"=>88, 'acb'=>333})
  p opt.select_existent(["flag", "num", 'acb'])
end
