# dgrb.rb
# Copyright (C) by GFD-Dennou Club, 2002.  All rights reserved.

require 'numru/gpv/stringfile'
require "numru/multibitnums"
require "narray"

module NumRu
  class Units

    def initialize (factor, units = nil)
	@factor, @units = factor, units
    end

    attr_accessor :factor, :units

    def to_s
	if @units.nil? then
	    factor.to_s
	else
	    "#{factor} #{units}"
	end
    end
    def inspect; to_s; end

  end

  class Enum
    def initialize i
        raise "#{self.class}: bad value #{i}" unless rule.has_key? i
	@i = i
    end
    def meaning; rule[@i]; end
    alias :to_s :meaning
    def to_i; @i; end
    def inspect
	"#{@i} {#{to_s}}"
    end
    def == other
	to_i == other.to_i
    end
  end

  class DGRB

    class FormatError < StandardError; end

    @@locale = :E  # or :J
    def DGRB.locale=(locale)
      case locale
      when :E, :J
	@@locale = locale
      else
	raise "locale must be :E or :J (a Symbol)"
      end
    end
    def DGRB.locale
      @@locale
    end

    class Institution < Enum
        # 面倒なのでとりあえず英語版は全部 JMA にしちゃう(2005/06/16 horinout)
        RULE = {
         0 => ['数値予報課デコード','JMA'], 
         1 => ['数値予報ルーチン','JMA'],
	 2 => ['数値予報課短時間予報ルーチン','JMA'], 
	 10 => ['通信課','JMA'],
	 11 => ['情報システム課','JMA'], 
	 12 => ['予報課','JMA'], 
	 13 => ['長期予報課','JMA'],
	 31 => ['海上気象課','JMA'], 
	 32 => ['海洋課','JMA'], 
	 33 => ['気候情報課','JMA'],
	 100 => ['札幌管区気象台','JMA'], 110 => ['仙台管区気象台','JMA'],
	 121 => ['新潟地方気象台','JMA'], 122 => ['名古屋地方気象台','JMA'],
	 130 => ['大阪管区気象台','JMA'], 131 => ['広島地方気象台','JMA'],
	 132 => ['高松地方気象台','JMA'], 140 => ['福岡管区気象台','JMA'],
	 141 => ['鹿児島地方気象台','JMA'], 150 => ['沖縄気象台','JMA']
        }
        def rule; RULE; end
	def meaning
	  if DGRB.locale == :J
	    rule[@i][0]
	  else
	    rule[@i][1]
	  end
	end
	alias :to_s :meaning
    end

    class Source < Enum
        RULE = {
	 0x0000 => ['数値予報課デコード','Decoded by Numerical Forecast Section'],
	 0x0101 => ['日本域モデル JSM', 'Japan Spectral Model JSM'],
	 0x0102 => ['アジア域モデル ASM', 'Asian Spectral Model ASM'],
	 0x0103 => ['全球モデル GSM', 'Global Scale Model GSM'],
	 0x0104 => ['領域モデル RSM', 'Regional Scale Model RSM'],
	 0x0105 => ['メソ数値予報モデル MSM', 'Mesoscale Model MSM'],
	 0x0106 => ['週間アンサンブルモデル GSMT106', 'Weekly ensemble model GSMT106'],
	 0x0201 => ['レーダーエコー合成 SEC', 'Radar echo synthesis SEC'],
	 0x0202 => ['レーダーアメダス解析雨量 SRA', 'Synthetic Radar-Amedas SRA'],
	 0x0203 => ['降水短時間予報 SRF', 'Short range forecast SRF'],
	 0x0204 => ['土壌雨量指数実況', 'Soil water anaysis'],
	 0x0204 => ['土壌雨量指数予想', 'Sil water forecast'],
        }
        def rule; RULE; end
	def meaning
	  if DGRB.locale == :J
	    rule[@i][0]
	  else
	    rule[@i][1]
	  end
	end
	alias :to_s :meaning
    end

    class Grid < Enum
	RULE = {
	    # key => [projection, dy, dx, lat0, lon0, j0, i0]
	    101 => ['LL',  -20,  20, 'arcmin', 90, 0, 0, 0],
	    102 => ['LL',  -40,  40, 'arcmin', 90, 0, 0, 0],
            120 => ['LL',   -6,   6, 'arcmin', 90, 0, 0, 0],
            121 => ['LL',  -12,  15, 'arcmin', 90, 0, 0, 0],
            122 => ['LL',  -24,  30, 'arcmin', 90, 0, 0, 0],
            123 => ['LL',   -6, 7.5, 'arcmin', 90, 0, 0, 0],
	    201 => ['OLM',  -5,   5, 'km', '35:21:26', '138:43:50', 0, 0,
	        '56:11:31.3', '82:44:17.4'],
	    301 => ['PS', -120, 120, 'km', 30, 140, 22, 22, 60, 140],
	    401 => ['EQD',  -5,   5, 'km', '43:08:11', '141:00:48', 49.5, 49.5],
	}
 	def rule; RULE; end
	def to_s; meaning.inspect; end
	def lon (i, j = nil)
            m = meaning
            raise 'currently only LL projection supported' \
		if not m.first == 'LL'
	    if m[3] == 'arcmin' then
                m[5] + m[2] / 60.0 * (i - m[7])
            else
		raise
	    end
        end
	def lat (j, i = nil)
            m = meaning
            raise 'currently only LL projection supported' \
		if not m.first == 'LL'
	    if m[3] == 'arcmin' then
                m[4] + m[1] / 60.0 * (j - m[6])
            else
		raise
	    end
	end
	def lon_list (a); a.collect { |i| lon(i) }; end
	def lat_list (a); a.collect { |j| lat(j) }; end
    end

    class Parameter < Enum
	RULE = {
	    1 => ['気圧', 'hPa', 'psea', 'sea level pressure', 'air_pressure'],
	    4 => ['気温', 'celcius', 'temp', 'temperature', 'air_temperature'],
	    13 => ['相対湿度', '%', 'rh', 'relative humidity', 'relative_humidity'],
	    22 => ['風向風速', '5 deg, m/s', 'wind', 'wind direction/speed', nil],
	    23 => ['風U成分', 'm/s', 'u', 'eastward component of wind', 'eastward_wind'],
	    24 => ['風V成分', 'm/s', 'v', 'northward component of wind', 'northward_wind'],
	    42 => ['鉛直速度(↓)', 'hPa/h', 'omega', 'vertical velocity in p', 'lagrangian_tendency_of_air_pressure'],
	    49 => ['1時間降水量', 'mm/h', 'r1h', 'rainfall in 1 hour', 'rainfall_rate'],
	    102 => ['ジオポテンシャル高度', 'm', 'z', 'geopotential height', 'geopotential_height'],
	    225 => ['数値予報雲量', "", 'ncld', 'nwp cloudiness', nil],
	}
 	def rule; RULE; end
	def to_s; meaning.join(' '); end
	def title; meaning[0]; end
	def units; meaning[1]; end
	def altname; meaning[2]; end
	def alttitle; meaning[3]; end
	def cf_standard_name; meaning[4]; end
    end

    class PlaneType < Enum
	RULE = {
	    1 => ['地表面', 0, nil, 'Surface'],
	    2 => ['雲底面', 0, nil, 'Cloud base'],
	    3 => ['雲頂面', 0, nil, 'Cloud top'],
	    100 => ['等圧面', 1, 'hPa', 'Isobaric level'],
	    101 => ['等圧面間の層', 2, 'kPa', 'Intermediate isobaric level'],
	    102 => ['平均海面', 0, nil, 'Mean sea level'],
	}
	def rule; RULE; end
	def title
	  if DGRB.locale == :J
	    meaning[0]
	  else
	    meaning[3]
	  end
	end
	def byteuse; meaning[1]; end
	def units; Units::new(meaning[2]); end
	def to_s
	    title + (meaning[2] ? " [#{units}]" : "")
        end
        def canonical
            i = to_i
	    if (i == 102) then 1 else i end
	end
        def compatible? other
	    self.canonical == other.canonical
	end
    end

    class Compress < Enum
	RULE = {
	    0 => '圧縮なし',
	    1 => '連長圧縮',
	    2 => '差分圧縮',
	}
	def rule; RULE; end
    end

    class TimeUnit < Enum
	RULE = {
	    0 => 'min',
	    1 => 'hour',
	    2 => 'day',
	    3 => 'mon',
	    4 => 'year',
	    5 => '10 year',
	    6 => '30 year',
	    7 => '100 year',
	    250 => '10 min',
	    254 => '1 sec',
	}
	def rule; RULE; end
    end

    class TimeMode < Enum
	RULE = {
	    0 => [1, 0, '瞬間', nil],
	    1 => [1, 0, '初期', nil],
	    2 => [1, 2, '期間', nil],
	    3 => [1, 2, '平均', nil],
	    4 => [1, 2, '積算', nil],
	    5 => [1, 2, '変化', nil],
	    10 => [1, 1, '瞬間', nil],
	    201 => [1, 2, '瞬間', 1],
	    202 => [1, 2, '瞬間', 2],
	    203 => [1, 2, '瞬間', 3],
	    204 => [1, 2, '瞬間', 4],
	    206 => [1, 2, '瞬間', 6],
	    208 => [1, 2, '瞬間', 8],
	    212 => [1, 2, '瞬間', 12],
	    224 => [1, 2, '平均', 24],
	    225 => [-1, 2, '平均', nil],
	}
	def rule; RULE; end
	def t1factor; meaning[0]; end
	def b2usage; meaning[1]; end
        def span; meaning[3]; end
        def title (units = "")
            if span.nil? then
                meaning[2]
            else
                "#{span} #{units} 毎の#{meaning[2]}値"
            end
        end
    end

    class ValidTime
	def initialize (t4, basetime)
	    @units = TimeUnit::new(t4[0])
	    @tmode = TimeMode::new(t4[3])
	    @basetime = basetime
	    @t1 = t4[1] * @tmode.t1factor
	    case @tmode.b2usage
	    when 1
		@t1 = @t1 * 256 + t4[2]
		@t2 = nil
	    else
		@t2 = t4[2]
	    end
	end
        def n_vt
            if @tmode.span.nil? then nil else (@t2 - @t1) / @tmode.span + 1 end
        end
	def getlist
	    if @tmode.span.nil? then
	        [@t1]
	    else
	    	(0 ... n_vt).collect {|i| i * @tmode.span + @t1 }
	    end
	end
        def aspect
            if @tmode.span.nil? then
                @tmode.title
            else
                "#{n_vt} 個の #{@tmode.title @units}"
            end
        end
	def to_s
	    "<ValidTime #{@t1} -- #{@t2} #{@units} #{aspect}>"
	end
	def inspect; to_s; end
    end

    def String::float_unpack(str)
	nega = str[0][7]
        expo = (str[0] & 0x7F)
	mant = (((str[1] * 0x100) + str[2]) * 0x100 + str[3])
	r = ((16.0 ** (expo - 70)) * mant)
	r = -r if nega.nonzero?
## STDERR << "#{str.unpack('B*')} -> #{nega} #{expo} #{mant} -> #{r}\n"
	r
    end

    class StdSection
	def initialize (probe, input)
	    @size = probe.unpack('n').first
	    head = input.read(40)
	    @body = input.read(@size - 44)
	    @institution = Institution::new(head[0])
	    @source = Source::new(*head.unpack('n'))
	    geom = head.unpack('x2n').first
	    if (geom & 0x8000).nonzero? then
	        @grid = nil
		@format = [(geom & 0x7FFF), head[4]]
	    else
	        @grid = Grid::new(geom)
		@format = Parameter::new(head[4])
	    end
	    @planetype = PlaneType::new(head[5])
	    case @planetype.byteuse
	    when 2
		@plane1 = head[6]
		@plane2 = head[7]
	    when 1
		@plane1 = ((head[6] << 8) + head[7])
		@plane2 = nil
	    else
		@plane1 = nil
		@plane2 = nil
	    end
	    clock = head.unpack('x8C5')
	    # in 2080 AD this will ... forgive me!
	    if (clock[0] > 80) then
		clock[0] += 1900
	    else
		clock[0] += 2000
	    end
	    @basetime = Time::utc(*clock)
	    @validtime = ValidTime::new(head.unpack('x13C4'), @basetime)
	    @average = head.unpack('x17n').first
	    @compress = Compress::new(head[19])
	    @region = head.unpack('x20n4').pack('S4').unpack('s4')
	    width = head.unpack('x28n').first
	    if (head[28][7].nonzero?) then
		@width = head[29] >> 4
		@width2 = head[29] & 0x0F0
	    else
		@width = head.unpack('x28n').first
		@width2 = nil
            end
	    @scale = head.unpack('x30n').first
	    if (@scale & 0x8000).nonzero? then
		@scale = -(@scale & 0x7FFF)
	    end
	    @refvalue = String.float_unpack(head[32, 4])
	    @refvalue = Units::new(@refvalue, @format.units) unless @grid.nil?
	    @maxv = head[36]
	    @shape = [ @region[2]-@region[0]+1, @region[3]-@region[1]+1,
		       @validtime.n_vt ]
	end

	Attr = %w(size institution source grid format planetype
	    plane1 plane2 basetime validtime 
	    average compress region width width2 scale refvalue maxv)
        attr_reader *Attr

	def data_length
	    @shape[0] * @shape[1] * @shape[2]
	end
	def pos_index( c )
            # returns [i,j,n]
	    [ @region[0] + (c / @shape[0]) % @shape[1], 
	      @region[1] + (c % @shape[0]), 
              c/@shape[0]/@shape[1] ]
	end

	def validtime_list; @validtime.getlist; end
	def xlist; (@region[0] .. @region[2]).collect; end
	def ylist; (@region[1] .. @region[3]).collect; end

	def get_vals
	    mb = MultiBitNums::new(@body, @width, data_length)
	    x = NArray.to_na(mb.to_int32str, NArray::INT)
	    val = x.to_type(NArray::FLOAT) * (2 ** @scale) + @refvalue.factor
	end

        def each
	    c = 0
	    get_vals.each{ |val|
		yield val, *pos_index( c )   # == yield val, i, j, n
		c += 1
	    }
	end

	def inspect
	    r = "<StdSection>\n"
	    Attr.each {|a|
		r += "#{a}=#{send(a).inspect}\n"
	    }
	    each { |v, i, j, n|
		r += "#{n}, #{i}, #{j}, #{v}\n"
	    }
	    r += "</StdSection>"
	end
    end

    class GuidanceSection
	def initialize (probe, input)
	    head = probe + input.read(20)
	    @basetime = Time::utc(*head.unpack('n4'))    
	    @n_part, @part, n_station, n_elemtime = head.unpack('x8n4')
	    @n_elem, @institution, @id = head.unpack('x16n2a4')
	    if (@part == 1) then
		@station = input.read(4 * n_station).unpack('N*')
	    else
		@station = nil
	    end
	    @elemtime = input.read(4 * n_elemtime).unpack('N*')
	    @data = []
	    (0 ... n_station).each {|is|
		@data[is] = []
		(0 ... n_elemtime).each {|ie|
		     @data[is][ie] = input.read(2).unpack('n').first
		}
	    }
	end
    end

    def initialize str
	if str[0, 4] == "DGRB" then
	    @body = StringFile::new(str.sub(/^DGRB/, ""))
	else
            @body = StringFile::new(str)
	end
	@size = @body.read(2).unpack('n').first
	raise FormatError, "too short data" if @size > @body.size
	raise FormatError, "section 0 octet 3--4 should be zero" \
	    unless @body.read(2) == "\x00\x00"
    end

    def get_section
    	# section 1 has at least 24 bytes.
	return nil if @body.pos + 24 > @body.size
        probe = @body.read(4)
	if (probe[2] == 0xFF) then
	    StdSection::new(probe, @body)
	else
            GuidanceSection::new(probe, @body)
	end
    end

    def each
        while sec = get_section
	    yield sec
	end
    end

    def inspect
        r = "<DGRB size=#{@size}>"
	each { |sec|
	    r += "\n"
	    r += sec.inspect
	}
	r += "</DGRB>"
    end

  end
end
