require "vizshot_gfdnavi"
require "file_gfdnavi"

class VirtualNode

  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]]

  @@draw_options = {
    "x_axis" => {:type => "string"},
    "y_axis" => {:type => "string"},
    "projection" => {:default => 1, :type => "int"},
    "region" => {:default => {}, :type => "hash"},
    "pileup" => {:default => false, :type => "boolean"},
    "size" => {:default => [400,400], :type => "array_int"},
    "anim" => {:default => false, :type => "boolean", :optional => true},
    "anim_dim" => {:type => "string", :optional => true},
    "viewport" => {:default => [0.2, 0.8, 0.2, 0.8], :type => "array_float", :optional => true},
    "map" => {:default => false, :type => "boolean", :optional => true}
  }



  attr_accessor :original_nodes, :functions, :draw_method

  def initialize(nodes)
    nodes = [nodes] unless Array === nodes
    nodes.each{|node|
      if Node === node
        unless node.variable?
          raise "node must be variable"
        end
      elsif ! VirtialNode === node
        raise "node is invalid"
      end
    }
    @original_nodes = nodes
    @num_vars = nodes.length
    @functions = Array.new
    @draw_method = nil
  end

  def function(func,args=nil)
    unless Function === func
      return [false, "function is invalid"]
    end
    if @draw_method
      return [false, "cannot apply function after drawing"]
    end
    unless @num_vars%func.nvars == 0
      return [false, "wrong number of variables (#{@num_vars}%#{func.nvars} != 0)"]
    end
    if args && args.length > (al = func.function_arguments.length)
      return [false, "wrong number of arguments (#{args.length} for #{al})"]
    end
    @functions.push( {:type => :func, :func => func, :args => args} )
    @num_vars = func.function_outputs.length * (@num_vars/func.nvars)
    return true
  end

  def [](*ind)
    if ind.length == 0
      return [false, "index is invalid"]
    end
    ind.each{|i|
      unless Integer === i
        return [false, "indices must be integer"]
      end
    }
    if ind.max >= @num_vars
      return [false, "index out of range"]
    end
    @functions.push( {:type => :index, :index => ind} )
    @num_vars = ind.length
    return true
  end

  def cut!(*val)
    if val.length == 0
      return [false, "value is invalid"]
    end
    val.each{|i|
      unless Numeric === i || Range === i || TrueClass === i || FalseClass === i
        return [false, "value is invalid"]
      end
    }
    @functions.push( {:type => :cut, :value => val} )
    return true
  end

  def draw(dm,opts=nil)
    unless DrawMethod === dm
      raise "draw method is invalid"
    end
    unless @num_vars%dm.nvars == 0
      return [false, "wrong number of variables (#{@num_vars}%#{dm.nvars}!=0)"]
    end
    opts_new = Hash.new
    dm.draw_method_attributes.each{|dma|
      on = dma.name
      if ( val = opts.delete(on) )
        opts_new[on] = val
      end
    }
    @@draw_options.each{|on,v|
      if ( val = opts.delete(on) )
        opts_new[on] = val
      end
    }
    unless opts.empty?
      return [false, "wrong options were specified: #{opts.keys.join(", ")}"]
    end
    @draw_method = [dm,opts_new]
    return true
  end


  def type
    @draw_method ? "draw" : "analysis"
  end

  def get(path)
    fname = File.temp_name(path,"_001.png")
    dirname = File.dirname(fname)
    basename = File.basename(fname,"_001.png")
    res,code = get_code(basename)
    if res
    #    File.open(File.join(dirname,basename)+".rb","w"){|file| file.print code} # for debug
      msg = execute(code,dirname)
    else
      msg = code
    end
    if msg
      files = nil
    else
      files = Dir[File.join(dirname,basename)+"_*.png"].sort
    end
    return [files, msg]
  end

  def path
    str = @original_nodes.collect{|on| on.path}.join(",")
    @functions.each{|f|
      case f[:type]
      when :func
        str += "/analysis"
        str += f[:func].path
        hash = Hash.new
        if ( args = f[:args] )
          args.each_with_index{|arg,i|
            hash["argv[#{i}]"] = arg
          }
          str += options_to_str(hash).gsub(/\?/,"%3F")
        end
      when :index
        str += "/[#{f[:index].join(',')}]"
      when :cut
        str += "/#{f[:value].join(',')}"
      else
        raise "invalid type"
      end
    }
    if @draw_method
      str += "/draw"
      str += @draw_method[0].path
      str += options_to_str(@draw_method[1])
    end
    return str
  end

  def options_to_str(opts)
    unless opts && Hash === opts && !opts.empty?
      return ""
    end
    ary = Array.new
    opts.sort.each{|k,v|
      case v
      when String, Numric
        ary.push "#{k.to_s}=#{v}"
      when Array
        ary.push "#{k.to_s}=#{v.join(',')}"
      else
        raise "not supported type"
      end
    }
    return "?" + ary.join("&")
  end

  def to_xml(arg={})
    uri_prefix = arg.delete(:uri_prefix)
    nodes = Array.new
    @original_nodes.each{|on|
      case on
      when Node
        path = on.path
        nodes.push( {"path" => path, "uri" => File.join(uri_prefix, "data", path+".xml")} )
      when VirtualNode
        nodes.push node
      end
    }
    funcs = Array.new
    @functions.each{|func|
      case func[:type]
      when :func
        path = func[:func].path
        hash = {"path" => path, "uri" => File.join(uri_prefix, "data", path+".xml")}
        hash["arguments"] = func[:args] if func[:args]
        funcs.push hash
      when :index
        funcs.push( {"indices" => func[:index]} )
      when :cut
        funcs.push( {"cut" => func[:value]} )
      end
    }
    hash = {"nodes" => nodes, "functions" => funcs}
    if @draw_method
      path = @draw_method[0].path
      hash["draw_method"] = {"path" => @draw_method[0].path, "uri" => File.join(uri_prefix, "data", path+".xml")}
      hash["draw_method"]["options"] = @draw_method[1] if @draw_method[1]
    end
    return hash.to_xml(*arg)
  end


  protected
  def get_code(basename)
    if @draw_method
      dm, opts = @draw_method
      vars = Array.new
      script = gen_code(dm.nvars,vars)
      plot = Hash.new
      plot[:method] = dm.vizshot_method.to_sym

      size = opts["size"]
      if size
        size = size.split(",").collect{|c| c.to_i}
        unless size.length == 2
          return [false, "option size must be array of length 2"]
        end
      else
        size = @@draw_options["size"][:default]
      end
      projection = opts["projection"]
      if projection
        projection = projection.to_i
        unless DRAW_PROJECTION.has_key?(projection)
          return [false, "option projection number is invalid"]
        end
      else
        projection = @@draw_options["projection"][:default]
      end
      viewport = opts["viewport"]
      if viewport
        viewport = viewport.split(",").collect{|c| c.to_f}
        if viewport.min < 0 || viewport.max > 1 || viewport[0] >= viewport[1] || viewport[2] >= viewport[3]
          return [false, "option viewport is invalid"]
        end
      else
        viewport = @@draw_options["viewport"][:default]
      end

      code = ""
      (@num_vars/dm.nvars).times{|i|
        viz = NumRu::VizShot.new(:iwidth => size[0], :iheight => size[1], :basename => basename)
        viz.set_fig("itr" => projection, "viewport" => viewport)
        viz.set_tone("tonf" => true) unless projection == 5
        plot[:variables] = vars[i].collect{|var| var.path}
        plot[:script] = script
        viz.plot(plot.dup)
        code += viz.gen_code(false, {:image_dump => true}, false)
      }
    else
      vars = Array.new
      script = self.gen_code(nil, vars)
      code = <<"EOF"
$SAFE = 3

NumRu::GPhys::read_size_limit_2 = #{GPHYS_READ_SIZE_LIMIT_2.inspect}
NumRu::GPhys::read_size_limit_1 = #{GPHYS_READ_SIZE_LIMIT_1.inspect}

gphyses = Array.new
EOF
      vars.each_with_index{|var,i|
        code += "gphyses[#{i}] = NumRu::GPhys::IO.open('#{var.fname}','#{var.vname}')\n"
      }
      code += <<"EOF"
#{script}

ofname = ARGV.shift || "output.nc"
file = NumRu::NetCDF.create(ofname)
begin
  $SAFE=1
  gphyses.each{|gphys|
    NumRu::GPhys::IO.write(file,gphys)
  }
ensure
  file.close
end

EOF
    end

    return true, code

  end


  def gen_code(nvars,vars,id=0)
    if id == 0 && nvars
      vars_org = vars
      vars = Array.new
    end
    script = "gphyses#{id} = Array.new\n"
    @original_nodes.each_with_index{|node,i|
      case node
      when Node
        script += "gphyses#{id}.push gphyses[#{vars.length}]\n"
        vars.push node.entity
      when VirtualNode
        id2 = id*10+i
        script += node.gen_code(var,id2)
        script += "gphyses#{id} += proc#{id2}.call\n"
      else
        raise "[BUG] node is invalid"
      end
      if id == 0
        if nvars
          if vars.length == nvars
            vars_org.push vars
            vars = Array.new
          elsif vars.length > nvars
            raise "[BUG} vars.length is invalid"
          end
        end
      end
    }
    script += "proc#{id} = Proc.new{\n"
    @functions.each{|func|
      case func[:type]
      when :func
        argv = func[:args] || Array.new
        func = func[:func]
        nvars = func.nvars
        fargs = func.function_arguments
        ary = Array.new
        nvars.times{|i| ary << "gphys#{i}"}
        args ||= Array.new
        fargs.each_with_index{|fa,i|
          ary << "arg#{i}"
          args[i] = (argv[i] || YAML.load(fa.default)).inspect
        }
        script +=<<"EOF"
          new_gphyses = Array.new
          proc = Proc.new{|#{ary.join(",")}|
              #{func.script}
          }
          (gphyses#{id}.length/#{nvars}).times{|i|
             new_gphyses += proc.call(#{s=Array.new;nvars.times{|i|s.push("gphyses"+id.to_s+"["+(i*nvars).to_s+"+i]")};args.each{|arg|s.push arg};s.join(",")})
          }
          gphyses#{id} = new_gphyses
EOF
      when :index
        script += "gphyses#{id} = gphyes#{id}[#{func[:index].join(",")}]\n"
      when :cut
        script += "gphyses#{id}.collect!{|gphys| gphys.cut(#{func[:value].join(",")})}\n"
      end
    }
    script +=<<"EOF"
       return gphyses#{id}
     }
EOF
    if id==0
      script += "gphyses = proc#{id}.call\n"
    end
    return script
  end


  def execute(code,dir=nil,nice=19)
    pipe =  /linux/ =~ Config::CONFIG["arch"]
    if pipe
      rr,rw = IO.pipe
      er,ew = IO.pipe
    end
    pid = fork {
      if pipe
        rr.close
        er.close
        STDERR.reopen(ew)
      end
      begin
        Process.setpriority(Process::PRIO_PROCESS, 0, nice)
      rescue Errno::EACCES
      end
      Dir.chdir(dir) if dir
      eval code
      if pipe
        rw.close
        ew.close
      end
    }
    if pipe
      rw.close
      ew.close
    end
    pid, status = Process.wait2(pid)
    case
    when status.signaled?
      msg = ["Killed by signal #{status.termsig}\n"]
    when status.exited?
      if (es = status.exitstatus) == 0
        msg = nil
      else
        msg = ["error occured with status #{es}\n"]
      end
    else
      msg = ["stoped with unknown status (#{status.to_i})\n"]
    end
    if pipe
      els = er.readlines
      rr.close
      er.close
      $stderr.print els.join("") if els.length > 0
      if msg
        msg += els
        msg = msg.delete_if{|line| /^\s*from / =~ line}.collect{|line| line.sub(/[\/\w\.]*\.rb:\d*:in /, "") }
      end
    end
    msg = msg.join("") if msg
    return msg
  end


end

