=begin
= class NumRu::Misc::KeywordOpt

== Overview

A class to facilitate optional keyword arguments. More specifically,
it helps the use of a Hash to mimic the keyword argument system.

== 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/keywordopt'
  include NumRu

  class Foo
    @@opt_hoge = 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 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, defualt 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. If you want to show the help message,
use

   foo.hoge( x, y, 'help'=>true )

Tails of options names can be shortened as long as unambiguous:

   foo.hoge( x, y, 'fla'=>true, 'num'=>10 )

This will cause an exception raised with the following help message such as:

  \** 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: ** show help message and raise **
          from (irb):78:in `hoge'
          from (irb):83


== Class methods

---KeywordOpt.new( *args )

    Constructor.

    ARGUMENTS
    * args : arrays of two or three elements: [option name, defualt value,
      description ], or [option name, defualt value] if you don't want to
      write descriptions. Option names and descriptions must be String.

    RETURN VALUE
    * a KeywordOpt object

== Methods
---interpret(hash)

    Interprets a hash that specifies option values.

    ARGUMENTS
    * hash (Hash) : a hash with string keys matching option names (initialized
      when 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').

    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 ambigous

---help

    Returns a help message

    RETURN VALUE
    * a String describing the option names, defualt values, and descriptions

=end

module NumRu

   class HelpMessagingException < StandardError
   end

   class KeywordOpt
      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|
	    @keys.push(x[0])
	    @val[x[0]]=x[1]
	    @description[x[0]]= ( (x.length>=3) ? x[2] : '' )
	 }
	 @keys_sort = @keys.sort
      end

      def interpret(hash)
	 return @val if !hash
	 ##
	 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]
		     raise "Ambiguous key specification '#{key}'." 
		  end
		  out[@keys_sort[im]]=hash[key]
		  break
	       end
	       im += 1
	       raise "'#{key}' does not match any of the keys." if im==len
	    end
	 end
	 out
      end

      def help
	 "** Description of options **\n" +
	 "  option name => default value (description), ..:\n{" +
         @keys.collect{|k| "  #{k.inspect} => #{@val[k].inspect} (#{@description[k]})"}.join(",\n") +
	 '}'
      end
   end

   module DCL_Ext

      module_function

      #< def sgset sgstx udset udstx etc. >

      # USAGE EAXPLE: usset('lxinv'=>true, 'xfac'=>10, 'cxttl'=>'longitude')

      %w!gl sl sg sw uc ud ue ug ul um us uu uz!.each do |pkg|
	 %w!set stx!.each do |set|
	    eval <<-EOS
	       def #{pkg}#{set}( hash )
		  hash.each{|k,v|
		     DCL.#{pkg}p#{set}(k,v)
		  }
	       end
	    EOS
	 end
      end

      # < contour/tone level handling >

      @@opt_levels = KeywordOpt.new(
	 ['levels',nil, 'levels'],
	 ['mjmn',  nil, 'whether major or minor  ex.[false,true,false,...]'],
	 ['cmin',  nil, 'minimum level'],
 	 ['cmax',  nil, 'maximum level'],
 	 ['cint',  nil, 'interval'],
         ['nlev',  10,  'number of levels'],

         ['help',  nil, 'show help message']
      )
      def ud_set_levels(z,options=nil)
	 # < interpret options >
	 opt = @@opt_levels.interpret(options) 
	 if opt['help']
	    puts @@opt_levels.help
	    puts ' Current values='+opt.inspect
	    return
	 end
	 if levels=opt['levels']
	    icycle = DCL.udiget('icycle')
	    indxmj = DCL.udiget('indxmj')
	    indxmn = DCL.udiget('indxmn')
	    idash = DCL.udiget('idash')
	    ldash = DCL.udlget('ldash')
	    label = DCL.udlget('label')
	    isolid = DCL.udiget('isolid')
	    rsizel = DCL.udrget('rsizel')
	    nlev=levels.length
	    if (mjmn=opt['mjmn'])
	       if mjmn.length != levels.length
		  raise ArgumentError,"lengths of levels and mjmn do not agree"
 	       end
	    else
	       if levels.include?(0)
		  idx = levels.index(0)
		  ioffset = icycle*idx - idx
	       else
		  ioffset = 1
	       end
	       mjmn = (0...nlev).collect{|x| ((x+ioffset)%icycle)==0}
	    end
	    indices = mjmn.collect{|x| (x ? indxmj : indxmn)}
	    DCL.udiclv
	    (0...nlev).each{ |i|
	       lev = levels[i]
	       clev = DCL.udlabl(lev)
	       ityp = ( ldash && lev<0 ) ? idash : isolid
	       ht = ( mjmn[i] && label ) ? rsizel : 0.0
	       DCL.udsclv(lev,indices[i],ityp,clev,ht)
	    }
	 elsif  opt['cmin'] || opt['cmax'] || opt['cint'] || opt['nlev']
 	    cmin=opt['cmin']
	    cmax=opt['cmax'] 
	    cint=opt['cint']
	    nlev=opt['nlev']
	    dx = (  cint || -nlev )
	    if cmin || cmax
	       # if one or two of [cmin,cmax] is/are defined:
	       cmin = z.min if !cmin
	       cmax = z.max if !cmax
	       DCL.udgcla(cmin,cmax,dx)
	    else
	       DCL.udgclb(z,dx)
	    end
	 else
	    levels = nil
	 end
      end
   end
end

#################################################
if $0 == __FILE__
   require "narray"
   require "numru/dcl"
   include NumRu
   include Math

   nx = 19
   ny = 19
   xmin = 0
   xmax = 360
   ymin = -90
   ymax = 90
   drad = PI/180
   dz = 0.05
   p = NArray.sfloat(nx, ny)

   #-- data ---
   for j in 0..ny-1
      for i in 0..nx-1
	 alon = (xmin + (xmax-xmin)*i/(nx-1)) * drad
	 alat = (ymin + (ymax-ymin)*j/(ny-1)) * drad
	 slat = sin(alat)
	 p[i,j] = cos(alon) * (1-slat**2) * sin(2*PI*slat) + dz
      end
   end

   #-- graph ---
   iws = (ARGV[0] || (puts ' WORKSTATION ID (I)  ? ;'; DCL::sgpwsn; gets)).to_i
   DCL::gropn iws

   DCL::sldiv('y',2,1)
   DCL::grfrm
   DCL::grswnd(xmin, xmax, ymin, ymax)
   DCL::grsvpt(0.2, 0.8, 0.2, 0.8)
   DCL::grstrn(1)
   DCL::grstrf
   DCL::usdaxs
   DCL_Ext.udset('icycle'=>3,'idash'=>4)
   DCL_Ext.ud_set_levels(p, 'nlev'=>8, 'help'=>true)
   DCL_Ext.ud_set_levels(p,{'nlev'=>8, 'cmin'=>-0.2})
   DCL::udcntr(p)

   DCL::grfrm
   DCL::grswnd(xmin, xmax, ymin, ymax)
   DCL::grsvpt(0.2, 0.8, 0.2, 0.8)
   DCL::grstrn(1)
   DCL::grstrf
   DCL::usdaxs
   DCL_Ext.ud_set_levels(p,{'lev'=>[-0.3,-0.1,0,0.1,0.3]})
   DCL::udcntr(p)
   DCL::grcls

end
