require "numru/gphys/subsetmapping"
require "numru/gphys/attribute"
require "narray_miss"
require "numru/units"
require "numru/gphys/unumeric"
require "rational"    # for VArray#sqrt
require "numru/misc"

module NumRu

=begin
=class NumRu::VArray

VArray is a Virtual Array class, in which a multi-dimensional array data is 
stored on memory (NArray, NArrayMiss) or in file (NetCDFVar etc). 
The in-file data handling is left to subclasses such as VArrayNetCDF,
and this base class handles the following two cases:

(1) Data are stored on memory using NArray
(2) Subset of another VArray (possibly a subclass such as VArrayNetCDF).

Perhaps the latter case needs more explanation. Here, a VArray is defined 
as a subset of another VArray, so the current VArray has only the link
and info on how the subset is mapped to the other VArray.

A VArray behaves just like a NArray, i.e., a numeric multi-dimensional 
array. The important difference is that a VArray has a name and can 
have "attributes" like a NetCDF variable. Therefore, VArray can perfectly
represent a NetCDFVar, which is realized by a sub-class VArrayNetCDF.

NOMENCLATURE

* value(s): The multi-dimensional numeric array contained in the VArray,
  or its contents

* attribute(s): A combination of name and data that describes a VArray.
  Often realized by NumRu::Attribute class (or it is a NetCDFAttr in 
  VArrayNetCDF). The name must be a string, and the type of attribute
  values is restricted to a few classes for compatibility with
  NetCDFAttr (See NumRu::Attribute)

==Class Methods

---VArray.new(narray=nil, attr=nil, name=nil)

    A constructor

    ARGUMENTS
    * narray (NArray or NArrayMiss; default:nil) The array to be held.
      (nil is used to initialize a mapping to another VArray by a protected
      method).
    * attr (NumRu::Attribute; default:nil) Attributes. If nil, an empty 
     attribute object is created and stored.
    * name (String; default nil) Name of the VArray. If nil, it is named "noname".

    RETURN VALUE
    * a VArray

    EXAMPLE

        na = NArray.int(6,3).indgen!
        va1 = VArray.new( na, nil, "test" )

---VArray.new2(ntype, shape, attr=nil, name=nil)

    Another constructor. Uses parameters to initialize a NArray to hold.

    ARGUMENTS
    * ntype (String or NArray constants): Numeric type of the NArray to be
      held (e.g., "sfloat", "float", NArray::SFLOAT, NArray::FLOAT)
    * shape (Array of Integers): shape of the NArray
    * attr (Attribute; default:nil) Attributes. If nil, an empty attribute
      object is created and stored.
    * name (String; default nil) Name of the VArray.

    RETURN VALUE
    * a VArray


==Instance Methods

---val
    Returns the values as a NArray (or NArrayMiss).

    This is the case even when the VArray is a mapping to another. Also,
    this method is to be redefined in subclasses to do the same thing.

    ARGUMENTS -- none

    RETURN VALUE
    * a NArray (or NArrayMiss)

---val=(narray)

    Set values.

    The whole values are set. If you want to set partly, use ((<[]=>)).
    In this method, values are read from narray and set into the internal
    value holder. Thus, for exampled, the numeric type is not changed
    regardress the numeric type of narray. Use ((<replace_val>)) to
    replace entirely with narray.

    ARGUMENTS
    * narray (NArray or NArrayMiss or Numeric): If Numeric, the whole
      values are set to be equal to it. If NArray (or NArrayMiss), its
      shape must agree with the shape of the VArray.

---replace_val(narray)

    Replace the internal array data with the object narray.
    Use ((<val=>)) if you want to copy the values of narray.

    ARGUMENTS
    * narray (NArray or NArrayMiss): as pxlained above.
      Its shape must agree with the shape of the VArray (self).

    RETURN VALUE
    * if (self.class == VArray), self;
      otherwise (if self.class is a subclass of VArray), a new VArray.

---[]
    Get a subset. Its usage is the same as NArray#[]

---[]=
    Set a subset. Its usage is the same as NArray#[]=

---attr
    To be undefined.

---ntype

    Returns the numeric type.

    ARGUMENTS -- none

    RETURN VALUE
    * a String ("byte", "sint", "int", "sfloat", "float", "scomplex"
      "complex", or "obj"). It can be used in NArray.new to initialize
      another NArray.

---rank
    Returns the rank (number of dimensions)

---shape
    Returns the shape

---shape_current
    aliased to ((<shape>)).

---length
    Returns the length of the VArray

---typecode
    Returns the NArray typecode

---name
    Returns the name

    RETURN VALUE
    * (String) name of the VArray

---name=(nm)
    Changes the name.

    ARGUMENTS
    * nm(String): the new name.

    RETURN VALUE
    * nm

---rename!(nm)
    Changes the name (Same as ((<name=>)), but returns self)

    ARGUMENTS
    * nm(String): the new name.

    RETURN VALUE
    * self

---rename(nm)
    Same as rename! but duplicate the VArray object and set its name.

    This method may not be supported in sub-classes, since it is sometimes
    problematic not to change the original.

---copy(to=nil)
    Copy a VArray. If to is nil, works as the deep cloning (duplication of the entire object).

    Both the values and the attributes are copied.

    ARGUMENTS
    * to (VArray (possibly a subclass) or nil): The VArray to which the 
      copying is made.

---reshape!( *shape )
    Changes the shape without changing the total size. May not be available in subclasses.

    ARGUMENTS
    * shape (Array of Integer): new shape

    RETURN VALUE
    * self

    EXAMPLE
       vary = VArray.new2( "float", [10,2])
       vary.reshape!(5,4)   # changes the vary 
       vary.copy.reshape!(2,2,5)  # make a deep clone and change it
             # This always works with a VArray subclass, since vary.copy
             # makes a deep clone to VArray with NArray.

---file
    Returns a file object if the data of the VArray is in a file, nil if it is on memory

    ARGUMENTS
    * none

    RETURN VALUE
    * an object representing the file in which data are stored. Its class
      depends on the file type. nil is returned if the data is not in a file.

---coerce(other)
    For Numeric operators. (If you do not know it, see a manual or book of Ruby)

==Methods compatible with NArray

VArray is a numeric multi-dimensional array, so it supports most of the
methods and operators in NArray. Here, the name of those methods are just 
quoted. See the documentation of NArray for their usage.

=== Math functions

====sqrt, exp, log, log10, log2, sin, cos, tan, sinh, cosh, tanh, asin, acos, atan, asinh, acosh, atanh, csc, sec, cot, csch, sech, coth, acsc, asec, acot, acsch, asech, acoth

=== Binary operators

====-, +, *, /, %, **, .add!, .sub!, .mul!, .div!, mod!, >, >=, <, <=, &, |, ^, .eq, .ne, .gt, .ge, .lt, .le, .and, .or, .xor, .not

=== Unary operators

====~ - +

=== Mean etc

====mean, sum, stddev, min, max, median

=== Other methods

These methods returns a NArray (not a VArray).

====all?, any?, none?, where, where2, floor, ceil, round, to_f, to_i, to_a

=end

   class Units
      # for automatic operator generation in VArray
      def mul!(other); self*other; end
      def div!(other); self/other; end
   end    # class Units

   class VArray

      ### < basic parts to be redefined in subclasses > ###

      def initialize(narray=nil, attr=nil, name=nil)
	 # initialize with an actual array --- initialization by subset
	 # mapping is made with VArray.new.initialize_mapping(...)
	 @name = ( name || "noname" )
	 @mapping = nil
	 @varray = nil
	 @ary = __check_ary_class(narray)
	 case attr
	 when Attribute
	    @attr = attr
	 when VArray
	    vary = attr
	    @attr = vary.attr.copy
	 when Hash
	    @attr = NumRu::Attribute.new
	    attr.each{|key,val| @attr[key]=val}
	 when nil
	    @attr = NumRu::Attribute.new
	 else
	   raise TypeErroor, "#{attr.class} is unsupported for the 2nd arg"
	 end
      end

      attr_reader :mapping, :varray, :ary
      protected :mapping, :varray, :ary

      def inspect
	 if !@mapping
            "<'#{name}' #{ntype}#{shape.inspect} val=[#{(0...(4<length ? 4 : length)).collect do |i| @ary[i].to_s+',' end}#{'...' if 4<length}]>"
	 else
	    "<'#{name}' shape=#{shape.inspect}  subset of a #{@varray.class}>"
	 end
      end

      def VArray.new2(ntype, shape, attr=nil, name=nil)
	 ary = NArray.new(ntype, *shape)
	 VArray.new(ary, attr, name)
      end

      def val
	 if @mapping
	    @varray.ary[*@mapping.slicer]  # 1-step mapping is assumed
	 else
	    @ary.dup
	 end
      end

      def val=(narray)
	 if @mapping
	    @varray.ary[*@mapping.slicer] = __check_ary_class2(narray)
	 else
	    @ary[] = __check_ary_class2(narray)
	 end
	 narray
      end

      def replace_val(narray)
	narray = __check_ary_class(narray)
	if narray.shape != shape
	  raise "shape of the argument (#{narray.shape.inspect}) !="+
          " self.shape (#{shape.inspect})"
	end
	if self.class == VArray
	  @ary = narray
	  self
	else
	  # subclass --> VArray
	  VArray.new(narray, attr.copy, name)
	end
      end

      def ntype
	 if !@mapping
	    __ntype(@ary)
	 else
	    __ntype(@varray.ary)
	 end
      end

      def name=(nm)
	 raise ArgumentError, "name should be a String" if ! nm.is_a?(String)
	 if @mapping
	    @varray.name = nm
	 else
	    @name = nm
	 end
	 nm
      end
      def rename!(nm)
	 self.name=nm
	 self
      end
      def rename(nm)
	self.dup.rename!(nm)
      end

      def reshape!( *shape )
	if @mapping
	  raise "Cannot reshape an VArray mapped to another. Use copy first to make it independent"
	else
	  @ary.reshape!( *shape )
	end
	self
      end

      def file
	if @mapping
	  @varray.file
	else
	  raise nil
	end
      end

      ### < basic parts invariant in subclasses > ###

      def copy(to=nil)
	 attr = self.attr.copy( (to ? to.attr : to) )
	 val = self.val
	 if self.class==VArray && !self.mapped? && (to==nil || to.class==VArray)
	    val = val.dup
	 end
	 if to
	    to.val = val
	    to
	 else
	    VArray.new(val, attr, self.name)
	 end
      end

      #def reshape( *shape )
      #	 # reshape method that returns a new entire copy (deep one).
      #	 # ToDo :: prepare another reshape method that does not make the
      #	 # entire copy (you could use NArray.refer, but be careful not 
      #	 # to make it public, because it's easily misused)
      #	 newobj = self.copy
      #	 newobj.ary.reshape!( *shape )
      #	 newobj
      #end

      def mapped?
	 @mapping ? true : false
      end

      def initialize_mapping(mapping, varray)
	 # protected method
	 raise ArgumentError if ! mapping.is_a?(SubsetMapping)
	 raise ArgumentError if ! varray.is_a?(VArray)
	 if ! varray.mapping
	    @mapping = mapping
	    @varray = varray
	 else
	    # To keep the mapping within one step
	    @mapping = varray.mapping.composite(mapping)
	    @varray = varray.varray
	 end
	 @attr = NumRu::Attribute.new
	 @ary = nil
	 self
      end
      protected :initialize_mapping

      def attr
	 if @mapping
	    @varray.attr
	 else
	    @attr
	 end
      end
      protected :attr

      def att_names
	attr.keys
      end
      def get_att(name)
	attr[name]
      end
      def set_att(name, val)
	attr[name]=val
	self
      end
      def del_att(name)
	attr.delete(name)
      end
      alias put_att set_att

      def units
	str_units = attr['units']
	if !str_units || str_units==''
	  str_units = '1'              # represents non-dimension
	end
	Units.new( str_units )
      end

      def units=(units)
	attr['units'] = units.to_s
	units
      end

      def [](*slicer)
	 mapping = SubsetMapping.new(self.shape_current, slicer)
	 VArray.new.initialize_mapping(mapping, self)
      end

      def []=(*args)
	 val = args.pop
	 slicer = args
	 if val.is_a?(VArray)
	    val = val.val
	 else
	    val = __check_ary_class2(val)
	 end
	 if @mapping
	    sl= @mapping.composite(SubsetMapping.new(self.shape,slicer)).slicer
	    @varray[*sl]=val
	 else
	    @ary[*slicer]=val
	 end
	 val
      end

      def name
	 if @mapping
	    @varray.name
	 else
	    @name.dup
	 end
      end

      def transpose(*dims)
	VArray.new( val.transpose(*dims), @attr.copy, name )
      end

      ### < NArray methods > ###

      ## ToDo: implement units handling
      ## ToDo: coerce

      def coerce(other)
	oattr = self.attr.copy
	case other
	when UNumeric
	  oattr['units'] = other.units.to_s
	  na_other, = NArray.new(self.typecode, 1).coerce(other.val)   # scalar
	  c_other = VArray.new(na_other, oattr, self.name)
	else
	  oattr['units'] = '1'    # Assume the other to be non-dimensional
	  case other
	  when Numeric
	    na_other, = NArray.new(self.typecode, 1).coerce(other)   # scalar
	    c_other = VArray.new(na_other, oattr, self.name)
	  when Array
	    na = NArray.to_na(other)
	    c_other = VArray.new(na, oattr, self.name)
	  when NArray
	    c_other = VArray.new(other, oattr, self.name)
	  else
	    raise "Cannot coerse #{other.class}"
	  end
	end
	[c_other, self]
      end

      Math_funcs_nondim = ["exp","log","log10","log2","sin","cos","tan",
	    "sinh","cosh","tanh","asinh","acosh",
	    "atanh","csc","sec","cot","csch","sech","coth",
	    "acsch","asech","acoth"]
      Math_funcs_radian = ["asin","acos","atan","atan2","acsc","asec","acot"]
      Math_funcs = Math_funcs_nondim + Math_funcs_radian + ["sqrt"]

      Binary_operators_Uop   = ["*","/","**", ".mul!",".div!"]
      Binary_operators_Uconv = ["+","-",".add!",".sub!"]
      Binary_operators_Unone = ["%",".mod!",".imag="]
      Binary_operators = Binary_operators_Uop + 
	                 Binary_operators_Uconv +
	                 Binary_operators_Unone

      Binary_operatorsL_comp = [">",">=","<","<=",
	                 ".eq",".ne",".gt",".ge",".lt",".le"]
      Binary_operatorsL_other = ["&","|","^",".and",".or",".xor",".not"]
      Binary_operatorsL = Binary_operatorsL_comp +
                          Binary_operatorsL_other

      Unary_operators = ["-@","~"]

      # type1 methods: returns a VArray with the same shape
      # type2 methods: returns the result directly
      NArray_type1_methods = ["sort", "sort_index", 
	    "floor","ceil","round","to_f","to_i","to_type","abs",
	    "real","im","imag","angle","arg","conj","conjugate","cumsum",
	    "indgen","random"]

      NArray_type2_methods1 = ["all?","any?","none?","where","where2",
            "to_a", "to_string"]
      NArray_type2_methods2 = ["rank", "shape", "total","length"]
      NArray_type2_methods3 = ["typecode"]
      NArray_type3_methods = ["mean","sum","stddev","min","max","median"]
      # remaining: "transpose"

      NArray_type2_methods = Array.new.push(*NArray_type2_methods1).
	                         push(*NArray_type2_methods2).
	                         push(*NArray_type2_methods3)

      for f in Math_funcs_nondim
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f}(*arg)
            newattr = self.attr.copy
            newattr['units'] = '1'
	    case arg.length
	    when 0
	      VArray.new( Misc::EMath.#{f}(self.val), newattr, self.name )
	    #when 1  # for atan2
	    #  ar = arg[0].respond_to?(:val) ? arg[0].val : arg[0]
	    #  VArray.new( Misc::EMath.#{f}(self.val, ar), newattr, self.name )
	    else
	      raise ArgumentError, "# of args must be 0 or 1"
	    end
	 end
         EOS
      end
      for f in Math_funcs_radian
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f}(*arg)
            newattr = self.attr.copy
            newattr['units'] = 'rad'
	    case arg.length
	    when 0
	      VArray.new( Misc::EMath.#{f}(self.val), newattr, self.name )
	    when 1  # for atan2
	      ar = arg[0].respond_to?(:val) ? arg[0].val : arg[0]
	      ## ar = ar.to_f   # NMath.atan2 does not work with NArray.int
	      VArray.new( Misc::EMath.#{f}(self.val, ar), newattr, self.name )
	    else
	      raise ArgumentError, "# of args must be 0 or 1"
	    end
	 end
         EOS
      end
      def sqrt
	va = VArray.new( Misc::EMath.sqrt(self.val), self.attr.copy, self.name )
	va.units = units**Rational(1,2)
	va
      end

      for f in Binary_operators_Uop
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f.delete(".")}(other)
	    case other
            when VArray, UNumeric
               ary = self.val#{f}(other.val)
	       va = VArray.new( ary, self.attr.copy, self.name )
	       va.units= self.units#{f}(other.units)
               va
	    when Numeric, NArray, NArrayMiss, Array
	       ary = self.val#{f}(other)
	       va = VArray.new( ary, self.attr.copy, self.name )
	       if "#{f}" == "**" 
		 va.units= self.units#{f}(other)
               end
               va
            else
               c_me, c_other = other.coerce(self)
               c_me#{f}(c_other)
            end
	 end
	 EOS
      end
      for f in Binary_operators_Uconv
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f.delete(".")}(other)
	    case other
            when VArray, UNumeric
               ary = self.val#{f}( other.units.convert2(other.val,units) )
	       VArray.new( ary, self.attr.copy, self.name )
	    when Numeric, NArray, NArrayMiss, Array
	       ary = self.val#{f}(other)
	       VArray.new( ary, self.attr.copy, self.name )
            else
               c_me, c_other = other.coerce(self)
               c_me#{f}(c_other)
            end
	 end
	 EOS
      end
      for f in Binary_operators_Unone
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f.delete(".")}(other)
	    case other
            when VArray, UNumeric
               ary = self.val#{f}(other.val)
	       VArray.new( ary, self.attr.copy, self.name )
	    when Numeric, NArray, NArrayMiss, Array
	       ary = self.val#{f}(other)
	       VArray.new( ary, self.attr.copy, self.name )
            else
               c_me, c_other = other.coerce(self)
               c_me#{f}(c_other)
            end
	 end
	 EOS
      end
      for f in Binary_operatorsL_comp
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f.delete(".")}(other)
            # returns NArray
	    case other
            when VArray, UNumeric
               self.val#{f}( other.units.convert2(other.val,units) )
	    when Numeric, NArray, NArrayMiss, Array
               self.val#{f}(other)
            else
               c_me, c_other = other.coerce(self)
               self#{f}(other)
            end
	 end
	 EOS
      end
      for f in Binary_operatorsL_other
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f.delete(".")}(other)
            # returns NArray
	    case other
            when VArray, UNumeric
               self.val#{f}(other.val)
	    when Numeric, NArray, NArrayMiss, Array
               self.val#{f}(other)
            else
               c_me, c_other = other.coerce(self)
               self#{f}(other)
            end
	 end
	 EOS
      end
      for f in Unary_operators
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f}
            ary = #{f.delete("@")} self.val
	    VArray.new( ary, self.attr.copy, self.name )
	 end
	 EOS
      end
      def +@
	self
      end
      for f in NArray_type1_methods
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f}(*args)
            newattr = self.attr.copy
            newattr['units'] = '1' if "#{f}"=="angle" || "#{f}"=="arg"
	    VArray.new(self.val.#{f}(*args), newattr, self.name )
	 end
         EOS
      end
      for f in NArray_type2_methods1
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f}(*args)
	    self.val.#{f}(*args)
	 end
	 EOS
      end
      for f in NArray_type2_methods2
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f}
            if @mapping
	       @mapping.#{f}
	    else
	       @ary.#{f}
            end
	 end
	 EOS
      end
      for f in NArray_type2_methods3
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f}
            if @mapping
	       @varray.ary.#{f}
	    else
	       @ary.#{f}
            end
	 end
	 EOS
      end
      for f in NArray_type3_methods
	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{f}(*args)
            result = self.val.#{f}(*args)
            if result.is_a?(NArray) || result.is_a?(NArrayMiss)
	       VArray.new(result , self.attr.copy, self.name )
            else
	       UNumeric[result, units]    # used to be 'result' (not UNumeric)
            end
	 end
         EOS
      end

      alias shape_current shape

      ## < private methods >
      private
      def __check_ary_class(narray)
	 case narray
	 when NArray, NArrayMiss, nil
	 else
	    raise ArgumentError, "Invalid array class: #{narray.class}" 
	 end
	 narray
      end
      def __check_ary_class2(narray)
	 case narray
	 when NArray, NArrayMiss, nil, Numeric
	 else
	    raise ArgumentError, "Invalid array class: #{narray.class}" 
	 end
	 narray
      end
      def __ntype(na)
	 case na.typecode
	 when NArray::BYTE
	    "byte"
	 when NArray::SINT
	    "sint"
	 when NArray::LINT
	    "int"
	 when NArray::SFLOAT
	    "sfloat"
	 when NArray::DFLOAT
	    "float"
	 when NArray::SCOMPLEX
	    "scomplex"
	 when NArray::DCOMPLEX
	    "complex"
	 when NArray::ROBJ
	    "obj"
	 end
      end

   end    # class VArray

end

##################################
### < test > ###

if $0 == __FILE__
   include NumRu
   p va = VArray.new( NArray.int(6,2,3).indgen!, nil, 'va' )
   va.set_att("units","m")
   vs = va[2..4,0,0..1]
   p "@@@",vs.rank,vs.shape,vs.total,vs.val,vs.get_att("name")
   vs.val=999
   p "*1*",va
   co,=va.coerce(UNumeric.new(1,Units.new("rad")))
   p '*coerce*',co, co.units
   p "*2*",vt = vs/9, vs + vt
   p "*3*",vt.log10
   p "*4*",(vt < vs)
   vt.name='vvvttt'
   p "*5*",(3+vt), vt.sin, vt.cos
   vc = vt.copy
   p 'atan2'
   vv = VArray.new( NArray.sfloat(5).indgen!, nil, 'vv' )
   p vv.atan2(vv).val
   p "eq",vc.eq(vt),vc.equal?(vt)

   vd = VArray.new( NArray.int(6).indgen!+10 )
   p "+++",vd[1],vd[1].rank,vd[1].val
   p va.val
   p vs
   p va.sort.val
   p vs.to_a, vs.to_string, vs.to_f, vs.to_type(NArray::SINT)
   p "@@@",va.max, va.max(0), va.max(0,1)

   vkg = VArray.new( NArray.float(4,3).indgen!, nil, 'vkg' )
   vkg.units = 'kg'
   vg = vkg.copy
   vg.units = 'g'
   vmul = vkg*vg
   p '##', vkg.val, vmul.get_att('units'), vmul.units
   p '##', (vkg + vg).val, (vg + vkg).val, vkg > vg

end
