require "numru/gphys/varray"

=begin
=class NumRu::VArrayComposite < NumRu::VArray

a VArray that consists of multiple VArrays tiled regularly
(possibly in multi-dimension). Except for the constructer
"new" and ((<attr_update>)), the usage of this class is the same as VArray.

Note that the name and the attributes of a VArrayComposite is 
borrowed form one of the VArrays contained (actually, the first one
is used). Currently, no check is made 
regarding whether the names and attributes are the same among the 
VArrays contained. If you rename a VArrayComposite, the change will
be made with all the VArrays contained. However, currently, 
change of the attributes are only reflected only in the first 
VArray. You have to call ((<attr_update>)) separately.

==Class methods
---VArrayComposite.new(varrays)
    Constructor

    ARGUMENTS
    * varrays (NArray of VArray) : VArrays to be included, gridded 
      regularly in a NArray.

    RETURN VALUE
    * a VArrayComposite

    EXAMPLES
    * Suppose that you have VArrays va00, va01, va10, va00,
      with va00 and va10 having a same 0th dimension length,
      va01 and va11 having a same 0th dimension length,
      and va00 and va01 having a same 1st dimention length.

        varrays = NArray[ [ va00, va01 ],
                          [ va10, va11 ] ]
        vac = VArrayComposite.new(varrays)

      This will create a composite VArray tiled two-dimensionally.

    * You can create a VArray that lacks one or more VArrays as
      long as the shape is unambiguous:

        vac = VArrayComposite.new(  NArray[ [ va00, va01 ],
                                            [ va10, nil ] ] )
      is allowed, but 
        vac = VArrayComposite.new(  NArray[ [ va00, nil ],
                                            [ va10, nil ] ] )
      is prohibited.

      NOTICE: At this moment, handling of such nil-contianing 
      VArrayComposite is very limited, and many methods do not work.

    * Suppose that you have 3D VArrays va0, va1, va2, va3,
      and you want to concatenate them with the 3rd dimension.

        varrays = NArray[ va0,va1,va2,va3 ].newdim(0,0)

      This will create a 3D NArray with a shape of [1,1,4].
      Then you can make the composite as follows:

        vac = VArrayComposite.new(varrays)

Usage of all the other class methods are the same as in VArray.

=end

module NumRu
  class VArrayComposite < VArray

    def initialize( varrays )

      if !varrays.is_a?(NArray) || varrays.typecode != NArray::OBJECT
	raise ArgumentError, "argument must be a NArray of VArray (or nil)"
      end

      nvas = varrays.shape
      vrank = nvas.length
      rank=0
      varrays.each{|va|
	if va.is_a?(VArray)
	  @first_vary = va
	  @attr = @first_vary.attr
	  @name = @first_vary.name
	  @rank = va.rank
	  break
	end
      }
      if vrank > @rank
	raise ArgumentError, "rank of varrays > rank of the VArray"
      elsif vrank < @rank
	(@rank - vrank).times{
	  varrays = varrays.newdim(vrank)
	  nvas.push(1)
	}
	vrank = @rank
      end

      @bound_idx = Array.new     # will be Array of Array
      for dim in 0...vrank
	@bound_idx[dim] = [0]    # the frst element is always 0
	for i in 0...nvas[dim]
	  idx = [true]*dim + [i..i,false]
	  set=false
	  len=0
	  varrays[*idx].each{|va|
	    if !set && va
	      if !va.is_a?(VArray)
		raise ArgumentError,"Not a VArray: #{va.inspect}"
	      end
	      len = va.shape_current[dim]
	      @bound_idx[dim][i+1] = @bound_idx[dim][i] + len
	      set=true
	    elsif va
	      if va.shape_current[dim] != len
		raise ArgumentError,"Non-uniformity in the #{i}th element"+
                   " of the #{dim}th dimension (#{va.shape_current[dim]} for #{len})"
	      end
	    end
	  }
	  if !set
	    raise "No VArray is found in the #{i}th elem of the #{dim}th dim"
	  end
	end
      end
      #< finish >
      @varrays = varrays.dup
      @shape = @bound_idx.collect{|a| a[-1]}
      @length = 1
      @shape.each{|l| @length *= l}
      # p @bound_idx, @varrays
    end

    undef initialize_mapping

    def set_att(name,val)
      @varrays.each{|va|
	va.set_att(name,val)
      }
      self
    end

    undef :mapping, :varray, :ary

    def inspect
      "<#{self.class.to_s} shape=#{shape.inspect} #_of_tiles=#{@bound_idx.collect{|b| b.length-1}.inspect}>"
      # "bounds=#{@bound_idx.collect{|b| b[1..-2]}.inspect}"
    end

    def ntype
      @first_vary.ntype
    end

    def shape
      @shape
    end
    alias shape_current shape
    def rank
      @rank
    end
    def length
      @length
    end
    alias total length

    def val
      val = nil
      loop_multi_dim_index( @varrays.shape ){|index|
	vidx = Array.new
	for d in 0...@rank
	  vidx[d] = (@bound_idx[d][index[d]])..(@bound_idx[d][index[d]+1]-1)
	end
	v = @varrays[*index].val
	if !val
	  if NArray===v
	    val = NArray.new(self.ntype,*@shape)
	  elsif NArrayMiss===v
	    val = NArrayMiss.new(self.ntype,*@shape)
	  else
	    raise TypeError, "Unexpected type #{v.class}"
	  end
	end
	val[*vidx] = @varrays[*index].val
      }
      val
    end

    def val=(narray)
      loop_multi_dim_index( @varrays.shape ){|index|
	if narray.is_a?(Numeric)
	  sub = narray
	else
	  __check_ary_class(narray)
	  vidx = Array.new
	  for d in 0...@rank
	    vidx[d] = (@bound_idx[d][index[d]])..(@bound_idx[d][index[d]+1]-1)
	  end
	  sub=narray[*vidx]
	end
	@varrays[*index].val = sub
      }
      narray
    end

    def [](*slicer)
      slicer = __rubber_expansion(slicer)
      varrays = _div_idx(*slicer)
      if varrays.length == 1
	varrays[0]
      else
	VArrayComposite.new( varrays )
      end
    end

    def []=(*args)
      args = __rubber_expansion(args)
      val = args.pop
      slicer = args
      slicer.collect!{|i| (i.is_a?(Numeric)) ? i..i : i }
      subva = self[*slicer]
      val = val.val if val.is_a?(VArray)
      subva.val= val
      val
    end

    def ntype
      __ntype(@first_vary)
    end

    def typecode
      @first_vary.typecode
    end

    def file
      @varrays.collect{|va| va.file}
    end

    def name=(nm)
      raise ArgumentError, "name should be a String" if ! nm.is_a?(String)
      @name = nm
      @varrays.each{|va| va.name=nm}
      nm
    end
    def rename(nm)
      raise "method rename is not vailable. (But rename! and name= are available)"
      ## self.dup.set_name_shallow(nm)
    end

#    def set_name_shallow(nm)
#      raise ArgumentError, "name should be a String" if ! nm.is_a?(String)
#      @name = nm
#      self
#    end
#    protected set_name_shallow

    #def reshape!( *shape )
    #  raise "cannot reshape a #{class}. Use copy first to make it a VArray with NArray"
    #end
    undef reshape!

    ## Not needed, so far:
    #def convention
    #  @first_vary.convention
    #end

    ################# private #####################
    private

    def loop_multi_dim_index(shape)
      cumshape=[1]
      rank = shape.length
      (1..rank).each{|i| cumshape[i] = shape[i-1]*cumshape[i-1]}
      len = cumshape[-1]
      for i in 0...len
	index = (0...rank).collect{|d| (i/cumshape[d])%shape[d]}
	yield(index)
      end
    end

    def _div_idx(*idx)
      if idx.length != @rank
	raise ArgumentError, "length of args != rank"
      end
      imask = Array.new
      isub = Array.new
      idx.each_with_index{ |ix, dim|
	size = @bound_idx[dim][-1]
	imask[dim] = Array.new
	isub[dim] = Array.new
	for j in 0...(@bound_idx[dim].length)
	  match, isb = _imatch( size, ix, @bound_idx[dim][j..j+1] )
	  if match
	    imask[dim].push(j)
	    isub[dim].push(isb) 
	  end
	end
      }
      varrays = @varrays[ *imask ]
      loop_multi_dim_index( varrays.shape ){ |index|
	slice = Array.new
	isub.each_with_index{|is, d| slice[d]=is[index[d]]}
	varrays[*index] = varrays[*index][*slice]
      }
      shape = varrays.shape
      (idx.length-1).downto(0){ |d|
	if idx[d].is_a?(Integer)
	  shape.delete_at(d)
	end
      }
      varrays.reshape!(*shape)
      varrays
    end

    def _imatch( size, index, bounds )
      first = bounds[0]
      last = bounds[-1]-1
      case index
      when Integer
	index += size if index < 0
	if first <= index && index <= last
	  return [true, index-first ]
	else
	  return [false, nil]
	end
      when Range, Hash, true
	if true === index
	  fst,lst = 0,size-1
	  stp = 1
	elsif Range === index
	  fst = index.first
	  fst += size if fst < 0
	  lst = index.exclude_end? ? index.last-1 : index.last
	  lst += size if lst < 0
	  stp = 1
	else # Hash
	  range, stp = index.to_a[0]
	  range = 0..-1 if range==true
	  fst = range.first
	  fst += size if fst < 0
	  lst = range.last
	  lst += size if lst < 0
	  if range.exclude_end?
	    stp>=0 ? lst-=1 : lst+=1
	  end
	end
	if fst <= last && lst >= first
	  a = ( (fst >= first) ? fst-first : (fst - first).modulo(stp) )
	  b = ( (lst <= last) ? lst-first : last-first )
	  if stp==1
	    return [true, a..b ]
	  else
	    return [true, {a..b,stp} ]
	  end
	else
	  return [false, nil]
	end
      else
	raise "Unsupported type for indexing: #{index.class}"
      end
    end

    def __rubber_expansion( args )
      if (id = args.index(false))  # substitution into id
        # false is incuded
	alen = args.length
	if args.rindex(false) != id
	  raise ArguemntError,"only one rubber dimension is permitted"
	elsif alen > rank+1
	  raise ArgumentError, "too many args"
	end
	ar = ( id!=0 ? args[0..id-1] : [] )
	args = ar + [true]*(rank-alen+1) + args[id+1..-1]
      end
      args
    end

  end
end

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

if __FILE__ == $0
  include NumRu
  va00 = VArray.new( NArray.float(2,3,2).indgen!,    nil, 'TEST' )
  va01 = VArray.new( NArray.float(2,4,2).indgen!+100 )
  va10 = VArray.new( NArray.float(5,3,2).indgen!+200 )
  va11 = VArray.new( NArray.float(5,4,2).indgen!+300 )
  varrays = NArray[ [ va00, va10 ],
                    [ va01, va11 ] ]
  p vac = VArrayComposite.new(varrays)
  varrays = NArray[ [ va00.copy, nil       ],
                    [ va01.copy, va11.copy ] ]
  p vac2 = VArrayComposite.new(varrays)
  p vac.ntype, vac.typecode
  p vac.val
  vacs = vac[1..3,0..3,true]
  p vacs.val
  p vac[false,1]
  vacs.val *= -1
  p vac.val
  vacs.val = 999.0
  p vac.val
  vac[-1, 0..1, true] = 0.0
  p vac.val
  p( (vac*100.0).val, vac.sin.val )
  vac2[0, 0..1, true] = 1000.0
  p vac2[0..1, 0..1, true].val
  p vac.name
  p vac.name='test'
  p va01.name
  vac.set_att('long_name', 'test data')
  p vac.get_att('long_name')
  p va01.get_att('long_name')
end
