require "numru/gphys/axis"

=begin
=class NumRU::Grid

A class to handle discretized grids of physical quantities.

==Class Methods
---Grid.new( *axes )
    Constructor.

    RETURN VALUE
    * a Grid

==Instance Methods
---axnames
    Returns the names of the axes

    RETURN VALUE
    * an Array of String

---lost_axes
    Returns info on axes eliminated during operations.

    Useful for annotation in plots, for example (See the code of GGraph
    for an application).

    RETURN VALUE
    * an Array of String

---axis(dim_or_dimname)
    Returns an Axis

    ARGUMENTS
    * dim_or_dimname (String or Integer) to specify an axis.

    RETURN VALUE
    * an Axis

---dim_index(dimname)
    Returns the integer id (count from zero) of the dimension

    ARGUMENT
    * dimname (String or Integer) : this method is trivial if is is an integer

    RETURN VALUE
    * an Integer

---set_axis(dim_or_dimname,ax)

    Sets an axis.

    ARGUMENTS
    * dim_or_dimname (String or Integer) to specify an axis.
    * ax (Axis) the axis

    RETURN VALUE
    * self

---set_lost_axes( lost )

    Sets info on axes eliminated.

    RETURN VALUE
    * self

---add_lost_axes( lost )

    Adds info on axes eliminated to existing ones.

    RETURN VALUE
    * self

---delete_axes( at, deleted_by=nil )

    Delete an axis.

    ARGUMENTS
    * at (String or Integer) to specify an axis.
    * deleted_by (String or nil) if non-nil, it is written in
      the internal lost-axis info. Best if you put the name of the
      method, in which this method is called.

    RETURN VALUE
    * a Grid

---copy
    Makes a deep clone onto memory.

    RETURN VALUE
    * a Grid

---merge(other)
    merge two grids by basically using copies of self's axes but
    using the other's if the length in self is 1 and 
    the length in the other is longer

    ARGUMENTS
    * other (Grid)

    RETURN VALUE
    * a Grid

---shape
    Returns the shape of self.

    RETURN VALUE
    * an Array of Integer

---[] (*slicer)
    Returns a subset.

    ARGUMENTS
    * Same as those for NArray#[], NetCDFVar#[], etc.

    RETURN VALUE
    * a Grid

---cut(*args)
    Similar to ((<[]>)), but the subset is specified by physical coordinate.

    ARGUMENTS
    * pattern 1: similar to those for ((<[]>)), where the first
      argument specifies a subset for the first dimension.
    * pattern 2: by a Hash, in which keys are axis names.

    EXAMPLES
    * Pattern 1
       gphys.cut(135.5,0..20.5,false)
    * Pattern 2
       gphys.cut({'lon'=>135.5,'lat'=>0..20})

    RETURN VALUE
    * an Array : [a Grid, slicer], where slicer is an array
      to be used to make a subset of an corresponding varray
      (to be used in GPhys#cut).
   
---cut_rank_conserving(*args)
    Similar to ((<cut>)), but the rank is conserved by not eliminating
    any dimension (whose length could be one).

---exclude(dim_or_dimname)
    Returns a Grid in which an axis is eliminated from self.

    ARGUMENTS
    * dim_or_dimname (String or Integer) to specify an axis.

    RETURN VALUE
    * a Grid

---change_axis(dim, axis)
    Replaces an axis. (Returns a new object)

    ARGUMENTS
    * dim_or_dimname (String or Integer) to specify an axis.
    * axis (Axis)

    RETURN VALUE
    * a Grid

---change_axis!(dim_or_dimname, axis)
    Replaces an axis. (overwrites self)

    ARGUMENTS
    * dim_or_dimname (String or Integer) to specify an axis.
    * axis (Axis)

    RETURN VALUE
    * self

---insert_axis(dim_or_dimname, axis)
    Inserts an axis. (Returns a new object)

    ARGUMENTS
    * dim_or_dimname (String or Integer) to specify an axis.
    * axis (Axis)

    RETURN VALUE
    * a Grid

---insert_axis!(dim_or_dimname, axis)
    Inserts an axis. (overwrites self)

    ARGUMENTS
    * dim_or_dimname (String or Integer) to specify an axis.
    * axis (Axis)

    RETURN VALUE
    * self

---transpose( *dims )
    Transpose.

    ARGUMENTS
    * dims (integers) : for example, [1,0] to transpose a 2D object.
      For 3D objects, [1,0,2], [2,1,0], etc.etc.

    RETURN VALUE
    * a Grid

=end

module NumRu

   class Grid

      def initialize( *axes )
	 @axes = Array.new
	 axes.each{|ag|
	    if ag.is_a?(Axis)
	       @axes.push(ag)
	    else
	       raise ArgumentError, "each argument must be an Axis"
	    end
	 }
	 @lost_axes = Array.new   # Array of String
	 @rank = @axes.length
	 @axnames = Array.new
	 __check_and_set_axnames
      end

      def inspect
	 "<#{rank}D grid #{@axes.collect{|ax| ax.inspect}.join("\n\t")}>"
      end

      def __check_and_set_axnames
	 @axnames.clear
	 @axes.each{|ax| 
	    nm=ax.name
	    if @axnames.include?(nm)
	       raise "Two or more axes share a name: #{nm}"
	    end
	    @axnames.push(nm)
	 }
      end
      private :__check_and_set_axnames

      attr_reader :rank

      def axnames
	@axnames.dup
      end
      def lost_axes
	@lost_axes.dup
      end

#      def axis(i)
#	 @axes[i]
#      end

      def axis(dim_or_dimname)
	 ax_dim(dim_or_dimname)[0]
      end

      alias get_axis axis

      def dim_index(dimname)
	 ax_dim(dimname)[1]
      end

      def set_axis(dim_or_dimname,ax)
	 @axes[ ax_dim(dim_or_dimname)[1] ] = ax
	 self
      end

      def set_lost_axes( lost )
	 @lost_axes = lost   # Array of String
         self
      end
      def add_lost_axes( lost )
	 @lost_axes = @lost_axes + lost   # Array of String
         self
      end

      def delete_axes( at, deleted_by=nil )
	case at
        when String
          at = [dim_index(at)]
	when Numeric
	  at = [at]
	when Array
          at = at.collect{|x|
            case x
            when String
              dim_index(x)
            when Integer
              x
            else
              raise ArgumentError,"'at' must consist of Integer and/or String"
            end
          }
	else
	  raise TypeError, "1st arg not an Array"
	end

	at.collect!{|pos| 
	  if pos < 0
	    pos + a.length
	  else
	    pos
	  end
	}
	at.sort!
	newaxes = @axes.dup
	at.reverse.each{|pos| 
	  del = newaxes.delete_at(pos)
	  if !del
	    raise ArgumentError, "dimension #{pos} does not exist in a #{rank}D Grid"
	  end
	}

	newgrid = self.class.new( *newaxes )
	newgrid.set_lost_axes( @lost_axes.dup )

	if !deleted_by 
	  msg = '(deleted) '
	else
	  raise TypeError, "2nd arg not a String" if !deleted_by.is_a?(String)
	  msg = '('+deleted_by+') '
	end
	lost = at.collect{|pos|
	  mn = sprintf("%g", ( UNumeric===(a=@axes[pos].pos.min) ? a.val : a) )
	  mx = sprintf("%g", ( UNumeric===(a=@axes[pos].pos.max) ? a.val : a) )
	  msg + 
	  "#{@axes[pos].name}:#{mn}..#{mx}"
	}

	newgrid.add_lost_axes( lost )
	newgrid
      end

      def copy
	 # deep clone onto memory
	 out = self.class.new( *@axes.collect{|ax| ax.copy} )
	 out.set_lost_axes( @lost_axes.dup )
	 out
      end

      def merge(other)
	# merge two grids by basically using copies of self's axes but
	# using the other's if the length in self is 1 and 
        # the length in the other is longer
	if self.rank != other.rank
	  raise "ranks do not agree (self:#{self.rank} vs other:#{other.rank})"
	end
	axes = Array.new
	for i in 0...self.rank
	  if @axes[i].length == 1 and other.axis(i).length > 1
	    axes[i] = other.axis(i)
	  else
	    axes[i] = @axes[i]
	  end
	end
	out = self.class.new( *axes )
	out.set_lost_axes( (@lost_axes.dup + other.lost_axes).uniq )
	out
      end

      def shape
	 @axes.collect{|ax| ax.length}
      end
      alias shape_current shape

      def [] (*slicer)
	 if slicer.length == 0
	    # make a clone
	    axes = Array.new
	    (0...rank).each{ |i| axes.push( @axes[i][0..-1] ) }
	    self.class.new( *axes )
	 else
	    slicer = __rubber_expansion(slicer)
	    if slicer.length != rank
	       raise ArgumentError,"# of the args does not agree with the rank"
	    end
	    axes = Array.new
	    lost = self.lost_axes  #Array.new
	    for i in 0...rank
	       ax = @axes[i][slicer[i]]
	       if ax.is_a?(Axis)      # else its rank became zero (lost)
		  axes.push( ax )
               else
		  lost.push( ax )
	       end
	    end
	    grid = self.class.new( *axes )
	    grid.set_lost_axes( lost ) if lost.length != 0
	    grid
	 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
      private :__rubber_expansion

      def cut(*args)
	_cut_(false, *args)
      end
      def cut_rank_conserving(*args)
	_cut_(true, *args)
      end

      def _cut_(conserve_rank, *args)

	# assume that the coordinates are monotonic (without checking)

	if args.length==1 && args[0].is_a?(Hash)
	  # specification by axis names
	  spec = args[0]
	  if (spec.keys - axnames).length > 0
	    raise ArgumentError,"One or more of the hash keys "+
	      "(#{spec.keys.inspect}) are not found in the axis names "+
              "(#{axnames.inspect})."
	  end
	  args = axnames.collect{|ax| spec[ax] || true}
	end

	args = __rubber_expansion(args)

	if rank != args.length
	  raise ArgumentError, "# of dims doesn't agree with the rank(#{rank})"
	end

	slicer = Array.new

	for dim in 0...rank
	  ax = @axes[dim]
	  if conserve_rank
	    dummy, slicer[dim] = ax.cut_rank_conserving(args[dim])
	  else
	    dummy, slicer[dim] = ax.cut(args[dim])
	  end
	end

	[ self[*slicer], slicer ]
      end
      private :_cut_

      def exclude(dim_or_dimname)
	 dim = dim_index(dim_or_dimname)
	 axes = @axes.dup
	 axes.delete_at(dim)
	 self.class.new( *axes )
      end

      def change_axis(dim, axis)
	 axes = @axes.dup
	 lost = @lost_axes.dup
	 if axis.is_a?(Axis)
	    axes[dim] = axis
	 else
	    lost.push(axis) if axis.is_a?(String)
	    axes.delete_at(dim)
	 end
	 self.class.new( *axes ).add_lost_axes( lost )
      end

      def change_axis!(dim_or_dimname, axis)
	 if axis.is_a?(Axis)
	    @axes[ ax_dim(dim_or_dimname)[1] ] = axis
	 else
	    @lost_axes.push(axis) if axis.is_a?(String)
	    @axes.delete_at( ax_dim(dim_or_dimname)[1] )
	 end
	 @rank = @axes.length
	 __check_and_set_axnames
	 self
      end

      def insert_axis(dim_or_dimname, axis)
	 dim = ax_dim(dim_or_dimname)[1]
	 axes = @axes.dup
	 if axis.is_a?(Axis)
	    axes[dim+1,0] = axis    # axes.insert(dim, axis) if ruby 1.7
	 else
	    # do nothing
	 end
	 self.class.new( *axes )
      end

      def insert_axis!(dim_or_dimname, axis)
	 dim = ax_dim(dim_or_dimname)[1]
	 if axis == nil
	    # do nothing
	 else
	    @axes[dim+1,0] = axis    # @axes.insert(dim, axis) if ruby 1.7
	    @rank = @axes.length
	    __check_and_set_axnames
	 end
	 self
      end

      def transpose( *dims )
	if dims.sort != NArray.int(rank).indgen!.to_a
	  raise ArgumentError, 
            "Args must a permutation of 0..rank-1 (eg, if 3D 2,1,0; 1,0,2;etc)"
	end
	axes = Array.new
	for i in 0...rank
	  axes[i] = @axes[dims[i]]
	end
	grid = self.class.new(*axes)
	grid.set_lost_axes( lost_axes )
	grid
      end

      # Define operations along each axis --- The following defines
      # instance methods such as "average" and "integrate":

      Axis.defined_operations.each do |method|
      	 eval <<-EOS, nil, __FILE__, __LINE__+1
	 def #{method}(vary, dim_or_dimname, *extra_args)
	    ax, dim = self.ax_dim(dim_or_dimname)
	    va, new_ax = ax.#{method}(vary, dim, *extra_args)
            if va.is_a?(Numeric) || va.is_a?(UNumeric)
              va
	    else
              [va, self.change_axis(dim, new_ax)]
	    end
	 end
	 EOS
      end

      ######### < protected methods > ###########

      protected

      def ax_dim(dim_or_dimname)
	 if dim_or_dimname.is_a?(Integer)
	    dim = dim_or_dimname
	    if dim < -rank || dim >= rank
	       raise ArgumentError,"rank=#{rank}: #{dim}th grid does not exist"
	    end
	    dim += rank if dim < 0
	 else
	    dim = @axnames.index(dim_or_dimname)
	    if !dim
	       raise ArgumentError, "Axis #{dim_or_dimname} is not contained"
	    end
	 end
	 [@axes[dim], dim]
      end


   end

end

######################################################
## < test >
if $0 == __FILE__
   include NumRu
   vx = VArray.new( NArray.float(10).indgen! + 0.5 ).rename("x")
   vy = VArray.new( NArray.float(6).indgen! ).rename("y")
   xax = Axis.new().set_pos(vx)
   yax = Axis.new(true).set_cell_guess_bounds(vy).set_pos_to_center
   grid = Grid.new(xax, yax)

   z = VArray.new( NArray.float(vx.length, vy.length).indgen! )
   p z.val
   p "average along x-axis:", grid.average(z,0)[0].val, 
     grid.average(z,"x")[0].val
   p "average along y-axis:", grid.average(z,1)[0].val, 
     grid.average(z,"y")[0].val
   p "grid set by an operation:", (g = grid.average(z,1)[1]).rank, g.shape

   p grid.shape, grid.axis(0).pos.val, grid.axis(1).pos.val
   subgrid = grid[1..3,1..2]
   p subgrid.shape, subgrid.axis(0).pos.val, subgrid.axis(1).pos.val
   p grid[3,2].lost_axes

   p grid

   gr,slice = grid.cut(1.0..4.0, 3.2)
   p "%%",gr.copy,slice,gr.lost_axes
   gr,slice = grid.cut_rank_conserving(-10,false)
   p "%%",gr.copy,slice,gr.lost_axes

   p grid[0,0]

   p Grid.new(xax).average(vx,0)  # --> scalar

   p "+++++"
   p grid.delete_axes(0).lost_axes
   p grid.delete_axes([0,1]).lost_axes
   p grid.delete_axes([0,1], 'mean').lost_axes

   p grid, grid.transpose(1,0)

end

