require "numru/gphys"
require "no_rdb_base"
require "vizshot_gfdnavi"

class AnalysisColumn < ActiveRecord::ConnectionAdapters::Column

  def type_cast_code(var_name)
    case type
    when :array
      "#{self.class.name}.value_to_array(#{var_name})"
    when :array_int
      "#{self.class.name}.value_to_array(#{var_name}, :int)"
    when :array_float
      "#{self.class.name}.value_to_array(#{var_name}, :float)"
    when :model
      "#{self.class.name}.check_model(#{var_name})"
    else
      super("(#{var_name}=='NULL' ? nil : #{var_name})")
    end
  end

  def self.value_to_array(value, val_type=nil)
    if Array === value
      ary = value
    elsif Hash === value
      ary = Array.new
      value.each{|k,v|
        ary[k.to_i] = v
      }
    elsif String === value
      ary = value.split(/,/)
    else
      ary = [value]
    end
    case val_type
    when :int
      ary.collect!{|v| v.to_i}
    when :float
      ary.collect!{|v| v.to_f}
    end
    return ary
  end

  def self.check_model(value)
    if ActiveRecord::Base === value
      return value
    else
      raise "a model is expected"
    end
  end

  def default
    Array===@default ? @default.dup : @default
  end
    

  private
  def simplified_type(type)
    case type
    when /array_int/i
      :array_int
    when /array_float/i
      :array_float
    when /array/i
      :array
    when /model/i
      :model
    else
      super(type)
    end
  end

end

class Analysis < ActiveRecord::NoRdbBase

  ACTION_TYPE = %w( draw analysis )

  DRAW_PROJECTION = {1 => "rectangular uniform coordinate",
                     2 => "semi-logarithmic coordinate (y axis)",
                     3 => "semi-logarithmic coordinate (x axis)",
                     4 => "logarithmic coordinate",
                     5 => "polar coordinate",
                     6 => "bipolar coordinate",
#                     7 => "elliptic coordinate",
                     10 => "equidistant cylindrical projection",
                     11 => "Mercator's projection",
                     12 => "Mollweide's projection",
                     13 => "Hammer's projection",
                     14 => "Eckert VI projection",
                     15 => "Kitada's elliptic projection",
                     20 => "equidistant conical projection",
                     21 => "Lambert's equal-area conical projection",
                     22 => "Lambert's conformal conical projection",
                     23 => "Bonne's projection",
                     30 => "orthographic projection",
                     31 => "polar stereo projection",
                     32 => "azimuthal equidistant projection",
                     33 => "Lambert's azimuthal equal-area projection"
                    }

  DRAW_SIZE = [[700,700], [550,550], [400,400], [250,250]]


  common_attrs =
    [
     {:name => "variables", :default => [], :type => "array"},
     {:name => "action_type", :default => ACTION_TYPE.index("draw"), :type => "int"}
    ]
  draw_attrs =
    [
     {:name => "draw_share", :default => true, :type => "boolean"},
     {:name => "draw_method", :type => "model"},
     {:name => "x_axis", :type => "string"},
     {:name => "y_axis", :type => "string"},
     {:name => "draw_projection", :default => 1, :type => "int"},
     {:name => "region", :default => {}, :type => "hash"},
     {:name => "draw_pileup", :default => false, :type => "boolean"},
     {:name => "draw_keep", :default => false, :type => "boolean"},
     {:name => "draw_size", :default => 1, :type => "int"},
     {:name => "anim", :default => false, :type => "boolean", :optional => true},
     {:name => "anim_dim", :type => "string", :optional => true},
     {:name => "viewport", :default => "0.2, 0.8, 0.2, 0.8", :type => "array_float", :optional => true},
     {:name => "draw_variables_order", :default => [], :type => "array", :optional => true}
    ]
  function_attrs =
    [
     {:name => "function", :type => "model"},
     {:name => "function_arguments", :default => [], :type => "array"},
     {:name => "function_variables_order", :default => [], :type => "array"}
    ]

  @@minmaxs =
    {
    "action_type" => [0, ACTION_TYPE.length-1],
    "draw_size" => [0, DRAW_SIZE.length-1]
  }

  push_column AnalysisColumn.new("user", nil, "model", true)
  (common_attrs +
   draw_attrs +
   function_attrs
   ).each{ |hash|
    push_column AnalysisColumn.new(hash[:name], hash[:default], hash[:type], hash[:optional]==true )
  }
  DrawMethod.find(:all, :user=>:all).each{|dm|
    dm.draw_method_attributes.each{|dma|
      push_column AnalysisColumn.new( "#{dm.name}_#{dma.name}", dma.default=="NULL"?nil : dma.default, dma.value_type.name, dma.optional)
    }
  }

  def write_attribute(attr_name, value)
    case attr_name.to_s
    when "function"
      unless Function === value
        value = Function.find(:first, :conditions=>["name=?",value], :user=>user)
      end
    when "draw_method"
      unless DrawMethod === value
        value = DrawMethod.find(:first, :conditions=>["name=?",value], :user=>user)
      end
    when "draw_variables_order"
      value = variables_order(value)
    when "function_variables_order"
      value = variables_order(value)
    end

    value = nil if value == "NULL"

    super

    if (minmax = @@minmaxs[attr_name.to_s])
      val = read_attribute(attr_name)
      if val < minmax[0]
        write_attribute(attr_name, minmax[0].to_s)
      elsif val > minmax[1]
        write_attribute(attr_name, minmax[1].to_s)
      end
    end
    xa = read_attribute("x_axis")
    ya = read_attribute("y_axis")
    ad = read_attribute("anim") && read_attribute("anim_dim")
    case attr_name.to_s
    when "x_axis"
      if xa
        if xa == ya
          write_attribute("y_axis", nil)
        elsif xa == ad
          write_attribute("anim_dim", nil)
        end
      end
    when "y_axis"
      if ya
        if ya == xa
          write_attribute("x_axis", nil)
        elsif ya == ad
          write_attribute("anim_dim", nil)
        end
      end
    when "anim_dim"
      if ad
        if ad == xa
          write_attribute("x_axis", nil)
        elsif ad == ya
          write_attribute("y_axis", nil)
        end
      end
    when "draw_projection"
      unless DRAW_PROJECTION[value.to_i]
        write_attribute("draw_projection", "1")
      end
    when "viewport"
      unless viewport.length==4
        write_attribute("viewport", Analysis.columns_hash["viewport"].default)
      end
    end
  end

  def variables
    vars = read_attribute("variables")
    vars.each{|var|
      next if var.new_record?
      unless var.other_mode==4 || ( user && (var.rgroups & user.groups) )
        raise "an invalide variable was set"
      end
    }
    return vars
  end

  def draw_methods
    dm = read_attribute("draw_method")
    return dm if dm.nil?
    unless dm == DrawMethod.find(:first, :conditions=>["id=>",dm.node.id], :user=>user)
      raise "an invalid draw method was set"
    end
    return dm
  end

  def functions
    func = read_attribute("function")
    return func if func.nil?
    unless func ==  Function.find(:first, :conditions=>["id=?",func.id], :user=>user)
      raise "an invalid function was set"
    end
    return func
  end


  def variable_clear
    @dimensions = nil
    %w( variables region x_axis y_axis draw_variables_order function_variables_order ).each{|name|
      column = column_for_attribute(name)
      default = column.default
      self[name] = default
    }
  end

  def file_and_variable_names
    variables.collect do |var|
      [fname, vname]
    end
  end

  def gphyses
    variables.collect do |var|
      var.gphys
    end
  end

  def dimensions
    unless @dimensions
      axes = Hash.new
      @dimensions = Array.new
      self.gphyses.each{|gp|
        gp.rank.times do |i|
          ax = gp.axis(i)
          ax_name = ax.name
          if axes[ax_name]
            ind, units, ary = axes[ax_name]
            pos = ax.pos
            ary2 = pos.val
            unless pos.units == units
              begin
                factor,offset = Units.new(units).factor_and_offset(Units.new(pos.units))
                ary2 = ary*factor + offset
              rescue
              end
            end
            ary += ary2.to_a
            ary.sort!.uniq!
            axes[ax_name] = [ind, units, ary]
          else
            pos = ax.pos
            axes[ax_name] = [axes.length, pos.units, pos.val.to_a]
          end
        end
        axes.each do |name, ary|
          @dimensions[ary[0]] = {:name => name, :units => ary[1], :ary => ary[2]}
        end
      }
    end
    return @dimensions
  end

  def index
    dims = region
    if region.length == 0
      dims = Hash.new
      dimensions.each{|dim|
        dims[dim[:name]] = {"min"=>dim[:ary][0], "max"=>dim[:ary][-1]}
      }
    end
    twoD = dimensions.length > 1
    dim0 = dimensions[0][:name]
    dim1 = dimensions[1][:name] if twoD
    unless x_axis
      self.x_axis = (twoD && y_axis==dim0) ? dim1 : dim0
    end
    if twoD && !y_axis
      self.y_axis = x_axis==dim1 ? dim0 : dim1
    end
    hash = Hash.new
    dims.each do |name,range|
      if ACTION_TYPE[action_type]=="analysis" || (name==x_axis || (draw_method.ndims>1&&name==y_axis))
        min = range["min"].to_f
        max = range["max"].to_f
        hash[name] = min==max ? min : min..max
      else
        hash[name] = range["min"].to_f
      end
    end
    return hash
  end

  def get_vizshots(basename)
    options = Hash.new
    vars = variables
    nvars = variables.length
    if nvars == 0
      return [false, "at least one variable must be selected"]
    end
    dm_nvars = draw_method.nvars
    if nvars%dm_nvars != 0
      return [false, "number of variable must be multiples of #{dm_nvars}"]
    end
    ix = get_index_from_name(x_axis)
    iy = get_index_from_name(y_axis)

    options[:method] = draw_method.vizshot_method.to_sym
    options.update( get_vizshot_options )
    options["transpose"] = true if draw_method.ndims>1 && ix && iy && ix > iy
    options[:cut] = index

    size = Analysis::DRAW_SIZE[draw_size]
    vizs = Array.new
    nfig = nvars/dm_nvars
    nfig.times{|n|
      if dm_nvars == 2
        options[:variable] = vars[draw_variables_order[n][0]]
        options[:variable2] = vars[draw_variables_order[n][1]]
      else
        options[:variable] = vars[n]
      end
      if draw_pileup && vizs[0]
        viz = vizs[0]
      else
        viz = NumRu::VizShot.new(:iwidth => size[0], :iheight => size[1], :basename => basename)
        viz.set_fig('itr' => draw_projection, 'viewport' => viewport)
        viz.set_tone('tonf' => true )
        vizs.push viz
      end
      viz.plot( options.dup )
    }
    return [true, vizs]
  end




  def diagram_label
    label = draw_method.name
    label += ", #{region[x_axis][:name]}(#{region[x_axis]["min"]}:#{region[x_axis]["max"]})"
    if draw_method.ndims > 1
      label += " vs #{region[y_axis][:name]}(#{region[y_axis]["min"]}:#{region[y_axis]["max"]})"
    end
    tmp = Array.new
    dimensions.each{|dim|
      name = dim[:name]
      next if name==x_axis
      next if draw_method.ndims>1 && name==y_axis
      tmp.push "#{name}=#{region[name]["min"]}" if region[name]
    }
    label += " @ "+tmp.join(",") unless tmp.length==0
    return label
  end


  def self.from_vizshot(viz)
    if viz && (par = parse_vizshot(viz))
      vars, params = par
      analysis = Analysis.new
      vars.each{|var|
        analysis.variables.push var
      }
      analysis.action_type = ACTION_TYPE.index("draw")
      params.each{|name,val|
        analysis.send(name+"=", val)
      }
      return analysis
    else
      return nil
    end
  end


  def self.uri_params_from_vizshot(viz)
    if viz && (par = parse_vizshot(viz))
      vars, params = par
      ary = Array.new
      vars.each{|var|
        ary.push "variables[#{var.path}]=1"
      }
      ary.push "action_type=draw"
      params.each{|name,val|
        ary += param_to_str("analysis[#{name}]",val)
      }
      #    params.collect{|str| str.gsub(/[/,"%5B").gsub(/]/,"%5D")}.join("&")
      URI.escape( ary.join("&") )
    else
      return nil
    end
  end



  private
  def self.parse_vizshot(viz)
    plots = viz.get_plots
    if viz.get_variables.length == 0
      return nil
    end

    plots.each{|plot|
      plot.each{|k,v|
        plot[k.to_sym] = v if String === k
      }
    }
    vars = Array.new
    viz.get_variables.each{|var|
      # return nil unless var.id   # commented out by horinout on 2007-06-28
      vars.push var
    }

    params = Array.new

    cut = plots[0][:cut]
    plots.each{|plot|
      unless cut == plot[:cut]
        raise "different cuts exist"
      end
    }
    if Array === cut
      gphys = NumRu::GPhys::IO.open(plots[0][:file], plots[0][:var])
      cut_ary = Array.new
      gphys.rank.times{|i|
        name = gphys.coord(i).name
        cut_ary.push  [name, cut[i]]
      }
    else Hash === cut
      gphys = NumRu::GPhys::IO.open(plots[0][:file], plots[0][:var])
      cut_ary = Array.new
      gphys.rank.times{|i|
        name = gphys.coord(i).name
        cut_ary.push  [name, cut[name]]
      }
    end
    reg = Hash.new
    axes = Array.new

    cut_ary.each{|name,range|
      case range
      when Range
        reg[name] = {"min"=>range.first,"max"=>range.last}
        axes.push name
      when Numeric
        reg[name] = {"min"=>range}
      else
        raise "[BUG]"
      end
    }

    params.push ["region", reg]

    method = plots[0][:method].to_s
    plots.each{|plot|
      unless method == plot[:method].to_s
        raise "different methods exist"
      end
    }
    draw_method = Node.find_draw_methods(:first, :conditions => ["vizshot_method=?",method]).entity
    unless draw_method.ndims == axes.length
      raise "[BUG]"
    end

    params.push ["draw_method", draw_method.name]

    axes = [axes[1],axes[0]] if draw_method.ndims>1 && plots[0][:transpose]
    params.push ["x_axis", axes[0]]
    params.push ["y_axis", axes[1]] if draw_method.ndims > 1
    params.push ["draw_projection", viz.get_itr]
    params.push ["viewport", viz.get_viewport]

    isize = DRAW_SIZE.index(viz.get_size)
    unless isize
      raise "[BUG]"
    end
    params.push ["draw_size", isize]

    params += get_params_from_vizshot_plot(draw_method,plots[0])
    if draw_method.ndims == 2
      params.push ["draw_variables_order", {0 => {0=>0,1=>1}}]
    end

    return [vars, params]
  end


  def self.param_to_str(name, val)
    ary = Array.new
    case val
    when String
      ary.push "#{name}=#{val}"
    when Numeric
      val = val.to_i if Float === val && val.to_i == val
      ary.push "#{name}=#{val}"
    when TrueClass
      ary.push "#{name}=1"
    when FalseClass
      ary.push "#{name}=0"
    when Array
      ary.push "#{name}=#{val.join(',')}"
    when Hash
      val = val.sort{|a,b|
        if String===a
          if a=="min"
            return -1
          elsif a=="max"
            return 1
          end
        end
        a <=> b
      }
      val.each{|k,v|
        ary += param_to_str("#{name}[#{k}]", v)
      }
    else
      ary.push "#{name}=#{val.inspect}"
    end
    return ary
  end


  def self.get_params_from_vizshot_plot(draw_method,plot)
    ary = Array.new
    draw_method.draw_method_attributes.each{|attr|
      case attr.parser
      when :vizshot, "vizshot"
        name = attr.name.to_sym
      when :ggraph, "ggraph"
        name = attr.name
      else
        next
      end
      val = plot[name]
      ary.push(["#{draw_method.name}_#{attr.name}",val]) if val
    }
    return ary
  end



  def variables_order(val)
    unless Hash === val
      return nil
    end
    variables_order = Hash.new
    val.each{|k,v|
      if Hash === v
        tmp = Array.new
        v.each{|k1,v1|
          tmp[k1.to_i] = v1.to_i
        }
        variables_order[k.to_i] = tmp
      else
        return nil
      end
    }
    ary = variables_order.values.flatten
    unless ary.max == ary.length-1
      return nil
    end
    ary.length.times{|i|
      unless ary.include?(i)
        return nil
      end
    }
    return variables_order
  end


  def get_vizshot_options
    hash = Hash.new
    draw_method.draw_method_attributes.each{|attr|
      case attr.parser
      when :vizshot, "vizshot"
        name = attr.name.to_sym
      when :ggraph, "ggraph"
        name = attr.name
      else
        next
      end
      val = self.send("#{draw_method.name}_#{attr.name}")
      if !val.nil? && val!=[] && val!=""
        hash[name] = val
      elsif !attr.optional
        raise "#{attr.name} of #{draw_method.name} cannot not be omitted"
      end
    }
    return hash
  end

  def get_index_from_name(name)
    dimensions.each_with_index{|dim,i|
      if dim[:name] == name
        return i
      end
    }
    return nil
  end



end
