#!/usr/bin/env ruby
#
# gpv2nc
# Copyright (C) by GFD-Dennou Club, 2002-2005.  All rights reserved.

def usage
   return <<-EOS

   #{$0}: Converts GPV files into a NetCDF file

   Usage:
      % #{$0} [-ihd] [-r arg_ranges] [-o output_file] [-s stime] [-e etime] gpv_files.. 

      -o output_file : output file name (if omitted, determined internally)
      -h : help (to show this message)
      -d : debug mode (does extra printing)
      -r : to specify packing (compression) using the add_offset and 
           scale_factor attributes. 
           SYNTAX (EXAMPLE): -r u,v,-200..200;temp,-150..100
               This means that variables named u, v, and temp are packed
               into short integer to cover the ranges -200 to 200 (for u
               and v) and -150 to 100 (for temp). Other variables, if any,
               will not be packed, so they will be stored as single-precision
               float.
      -s : start time relative to the beginning of the forecast [hours]
           (data before this time in each input file are excluded from the 
           output file)
      -e : end time relative to the beginning of the forecast [hours]
           (data after this time in each input file are excluded from the
           output file)
      -i : shows info on the input file (does not dump a file, but creates
           a temporarlly file -- if you do not have a write permission of the
           current directory, specify one you have it by the envronmental
           variable TMPDIR. Example: % export TMPDIR=/tmp).
   EOS
end
#############################################################
require 'numru/gpv/bugaifile.rb'
require "getopts"
require "narray"
include NumRu

class Array
  def add_time(sec)
    hour = (sec/3600.0).round
    self.collect{|x| x+hour}
  end
end

cmd_save = ARGV.join(' ')

if( getopts('ihd',"r:","o:gpv.nc","s:","e:") && ! $OPT_h )
  ofilename = $OPT_o
  arg_ranges = $OPT_r
  stime = $OPT_s && $OPT_s.to_i
  etime = $OPT_e && $OPT_e.to_i
  show_info = $OPT_i
  $DEBUG = $OPT_d
else
   raise usage
end

def maxmin2sf_ao(maxmin)
  min = maxmin[0]
  max = maxmin[1]
  sf = (max-min) / 65400
  ao = max - 32700 * sf
  scale_factor = NArray.sfloat(1).fill!(sf)
  add_offset = NArray.sfloat(1).fill!(ao)
  return scale_factor,add_offset
end

ranges = Hash.new
if arg_ranges != nil
  range_ary = arg_ranges.split(/:/).collect{|x| x.split(/,/)}
  range_ary.each{|a| 
    if /(\-?\d*\.?\d*)\.\.(\-?\d*\.?\d*)/ =~ a[-1]
      first = $1.to_f
      last = $2.to_f
    else
      raise "Range expression doesn't match number..number\n" + usage
    end
    scale_factor, add_offset = maxmin2sf_ao( [first,last] )
    a[0..-2].each do |name|
      ranges[name] = [scale_factor, add_offset]
    end
  }
end

outfmt = 'netcdf'

GPVID = %w(institution source grid average)
gpvid = Hash::new
planetype = nil
basetime = []
basetime0 = nil
institution = nil
source = nil

plane = []
time_since_b0 = []
xlist = []
ylist = []
elems = []
istime = 0
ietime = -1

ARGV.each { |fnam|
    fp = BugaiFile::new(fnam, "r")
    fp.each { |rec|
        bull = rec.decode
        raise "currently only DGRB is supported" unless bull.is_a? DGRB
        bull.each { |sec|
            raise "currently only DGRB/GPV is supported" \
		unless sec.is_a? DGRB::StdSection
	    if planetype.nil? then
		print "prescanning ...\n"; $defout.flush
		GPVID.each {|k| gpvid[k] = sec.send(k) }
                institution = sec.institution.to_s
                source = sec.source.to_s
		planetype = sec.planetype
                basetime0 = sec.basetime
                basetime.push( basetime0 )
	   
	        sec.validtime_list.each_with_index{|elem, i|
	          istime = i+1 if stime && elem < stime
	          ietime = i if etime && elem <= etime
	        }
	    else
		GPVID.each {|k|
		    raise "#{k} mismatch (#{gpvid[k]} != #{sec.send(k)})" \
			if not gpvid[k] == sec.send(k)
		}
		raise "planetype mismatch (#{planetype}, #{sec.planetype})" \
		    unless planetype.compatible? sec.planetype
	    end
	    plane = (plane.push sec.plane1).uniq
	    basetime = (basetime.push sec.basetime).uniq
#	    validtime = (validtime + sec.validtime_list).uniq
	    time_since_b0 = ( time_since_b0 + 
                  sec.validtime_list[istime..ietime].add_time(sec.basetime-basetime0) ).uniq
	    xlist = (xlist + sec.xlist).uniq
	    ylist = (ylist + sec.ylist).uniq
            elems = (elems.push sec.format.to_i).uniq
        }
    }
    fp.close
}

p "plane list", plane if $DEBUG
p "basetime list", basetime if $DEBUG
p "time list", time_since_b0 if $DEBUG
p "i grid number", xlist if $DEBUG
p "j grid number", ylist if $DEBUG
p "element list", elems if $DEBUG

rv_plane = Hash.new
rv_time_since_b0 = Hash.new
rv_xlist = Hash.new
rv_ylist = Hash.new
plane.each_with_index{|x,i| rv_plane[x]=i}
time_since_b0.each_with_index{|x,i| rv_time_since_b0[x]=i}
xlist.each_with_index{|x,i| rv_xlist[x]=i}
ylist.each_with_index{|x,i| rv_ylist[x]=i}


if outfmt == 'netcdf' then

    require 'numru/netcdf'
    if (show_info)
        output = NumRu::NetCDF::create_tmp
    else
        output = NumRu::NetCDF::create(ofilename)
      print "file #{ofilename} created ...\n"; $defout.flush
    end

    output.put_att('Conventions', 'CF-1.0')
    output.put_att('source', source)
    output.put_att('institution', institution)
    output.put_att('history', 
           "#{Time.now.strftime("%Y-%m-%d")}: #{File.basename($0)} " +
           cmd_save)

    dimx = output.def_dim('lon', xlist.size)
    dimy = output.def_dim('lat', ylist.size)
    dimz = output.def_dim('p', plane.size) if not plane.first.nil?
    dimt = output.def_dim('time', time_since_b0.size)
    dimrt = output.def_dim('ref_time', basetime.size)

    lon = output.def_var('lon', "sfloat", [dimx])
    lon.put_att('long_name', 'longitude')
    lon.put_att('standard_name', 'longitude')
    lon.put_att('units', 'degrees_east')
    lat = output.def_var('lat', "sfloat", [dimy])
    lat.put_att('long_name', 'latitude')
    lat.put_att('standard_name', 'latitude')
    lat.put_att('units', 'degrees_north')
    if not plane.first.nil?
        prs = output.def_var('p', "sfloat", [dimz]) 
        prs.put_att('long_name', 'pressure level')
        prs.put_att('standard_name', 'air_pressure')
        prs.put_att('units', planetype.units.to_s)
    end
    tm = output.def_var('time', "sfloat", [dimt])
    tm.put_att('long_name', 'time')
    tm.put_att('standard_name', 'time')
    units = "hours since " + basetime0.getgm.strftime("%Y-%m-%d %H:%M:%S+00:00")
    tm.put_att('units', units)
    rtm = output.def_var('ref_time', "sfloat", [dimrt])
    rtm.put_att('long_name', 'forecaset reference time')
    rtm.put_att('standard_name', 'forecaset_reference_time')
    rtm.put_att('units', units)

  elems.each { |ie|
    e = DGRB::Parameter::new(ie)
    varname = e.altname
    if varname == 'ncld'
      # special treatment for the nwp cloudiness 
      # --> divid into upper, mid, and low levels
      var = output.def_var('ncld_upper', "byte", [dimx, dimy, dimt])
      var.put_att('long_name', 'upper-level cloudiness')
      var.put_att('units', '1')    # non-dimension
      var.put_att('comment', '9 represents 9 or 10 (full cloud coverage)')
      var = output.def_var('ncld_mid', "byte", [dimx, dimy, dimt])
      var.put_att('long_name', 'mid-level cloudiness')
      var.put_att('units', '1')    # non-dimension
      var.put_att('comment', '9 represents 9 or 10 (full cloud coverage)')
      var = output.def_var('ncld_low', "byte", [dimx, dimy, dimt])
      var.put_att('long_name', 'low-level cloudiness')
      var.put_att('units', '1')    # non-dimension
      var.put_att('comment', '9 represents 9 or 10 (full cloud coverage)')
    elsif ! (plane.first.nil?)
      if !ranges[varname]
        var = output.def_var(varname, "sfloat", [dimx, dimy, dimz, dimt])
      else
        var = output.def_var(varname, "sint", [dimx, dimy, dimz, dimt])
        var.put_att('scale_factor', ranges[varname].first)
        var.put_att('add_offset', ranges[varname].last)
      end
      var.put_att('long_name', e.alttitle)
      var.put_att('units', e.units)
    else
      if !ranges[varname]
        var = output.def_var(varname, "sfloat", [dimx, dimy, dimt])
      else
        var = output.def_var(varname, "sint", [dimx, dimy, dimt])
        var.put_att('scale_factor', ranges[varname].first)
        var.put_att('add_offset', ranges[varname].last)
      end
      var.put_att('long_name', e.alttitle)
      var.put_att('units', e.units)
    end
    (sn = e.cf_standard_name) && var.put_att('standard_name', sn)
  }   
  
  output.enddef

    output.var('lon').put gpvid['grid'].lon_list(xlist)
    output.var('lat').put gpvid['grid'].lat_list(ylist)
    if not plane.first.nil?
        output.var('p').put plane
    end
    output.var('time').put(time_since_b0.collect(){|e| e.to_i })
    output.var('ref_time').put(basetime.collect{|bt| ((bt-basetime0)/3600).round})

    if (show_info)
        output.close
        p output.path if $DEBUG
        print `ncdump -c #{output.path}`
        exit
    end

    ARGV.each { |fnam|
	fp = BugaiFile::new(fnam, "r")
	fp.each { |rec|
	    bull = rec.decode
	    raise "currently only DGRB is supported" unless bull.is_a? DGRB
	    bull.each { |sec|
	        vtlist = sec.validtime_list
	        tlength = vtlist.length
		tlist = vtlist[istime..ietime].add_time(sec.basetime-basetime0)
                ip = rv_plane[ sec.plane1 ]
		iv = rv_time_since_b0[ tlist.first ]
		ix = rv_xlist[ sec.xlist.first ]
		iy = rv_ylist[ sec.ylist.first ]
                varname = sec.format.altname
                kx = rv_xlist[ sec.xlist.last ]
                ky = rv_ylist[ sec.ylist.last ]
		kv = rv_time_since_b0[ tlist.last ]
	        shape = [ (kx-ix+1)*(ky-iy+1), tlength] 
		if (tlist.size > 1) then
		    sv = (kv - iv) / (tlist.size - 1)
		else
		    sv = 1
		end
	        vals = sec.get_vals.reshape!(*shape)[true,istime..ietime]
	        if varname == 'ncld'
                  # special treatment for the nwp cloudiness
		  vals = vals.to_i
		  vals_upper = (vals/100) % 10
		  vals_mid = (vals/10) % 10
		  vals_low = vals % 10
	          output.var('ncld_upper').put( vals_upper, 
                        "start" => [ix, iy, iv], "end" => [kx, ky, kv],
		        "stride" => [1, 1, sv])
	          output.var('ncld_mid').put( vals_mid, 
                        "start" => [ix, iy, iv], "end" => [kx, ky, kv],
		        "stride" => [1, 1, sv])
	          output.var('ncld_low').put( vals_low, 
                        "start" => [ix, iy, iv], "end" => [kx, ky, kv],
		        "stride" => [1, 1, sv])
                elsif ! (plane.first.nil?)
		  output.var(varname).scaled_put( vals, 
			"start" => [ix, iy, ip, iv], "end" => [kx, ky, ip, kv],
		        "stride" => [1, 1, 1, sv])
		else
	          output.var(varname).scaled_put( vals, 
                        "start" => [ix, iy, iv], "end" => [kx, ky, kv],
		        "stride" => [1, 1, sv])
		end
            }
        }
    }

    output.close

end
