#!/usr/bin/ruby

require 'json'
require 'zlib'
require_relative '../asn1/asn1'

ASN1 = GPS_PVT::ASN1

upl = ASN1::resolve_tree(JSON::parse(
    Zlib::GzipReader::open(File::join(File::dirname(__FILE__), 'upl.json.gz')).read,
    {:symbolize_names => true}))

# RRLP payload conversion
ASN1::dig(upl, :ULP, :"ULP-PDU", :message, :msSUPLPOS, :posPayLoad, :rrlpPayload)[:type][1].merge!({
  :hook_encode => proc{|data|
    next data unless data.kind_of?(Hash)
    ASN1::encode_per(upl[:"RRLP-Messages"][:PDU], data).scan(/.{1,8}/).tap{|buf|
      buf[-1] += "0" * (8 - buf[-1].length)
    }.collect{|str| str.to_i(2)}
  },
  :hook_decode => proc{|data|
    data.define_singleton_method(:decode){
      ASN1::decode_per(upl[:"RRLP-Messages"][:PDU], self.collect{|v| "%08b" % [v]}.join)
    }
    data
  },
})

ASN1::dig(upl, :"MAP-LCS-DataTypes", :"Ext-GeographicalInformation")[:type][1].merge!({
  :hook_decode => proc{
    gen_ut = proc{|c, x| proc{|k| ((x + 1) ** k - 1) * c} }
    tbl_task = {
      :lat => [3, proc{|v| Rational((v & 0x7FFFFF) * 90, 1 << 23).to_f * (((v >> 23) == 1) ? -1 : 1)}],
      :lng => [3, proc{|v| Rational((v >= (1 << 23) ? v - (1 << 24) : v) * 180, 1 << 23).to_f}],
      :ucode => 1,
      :usmaj => [1, gen_ut.call(10, 0.1)],
      :usmin => [1, gen_ut.call(10, 0.1)],
      :omaj => 1,
      :cnf => 1,
      :alt => [2, proc{|v| (v & 0x7FFF) * (((v >> 15) == 1) ? -1 : 1)}],
      :ualt => [1, gen_ut.call(45, 0.025)],
      :irad => [2, proc{|v| v * 5}],
      :urad => [1, gen_ut.call(10, 0.1)],
      :oang => [1, proc{|v| v * 2}],
      :iang => [1, proc{|v| (v + 1) * 2}],
    }
    tbl_item = { # 7.2 Table 2a in 3GPP TS 23.032
      # (a) 7.3.2 Ellipsoid point with uncertainty Circle
      1 => [8, :lat, :lng, :ucode],
      # (b) 7.3.3 Ellipsoid point with uncertainty Ellipse
      3 => [11, :lat, :lng, :usmaj, :usmin, :omaj, :cnf],
      # (c) 7.3.6 Ellipsoid point with altitude and uncertainty Ellipsoid
      9 => [14, :lat, :lng, :alt, :usmaj, :usmin, :omaj, :ualt, :cnf],
      # (d) 7.3.7 Ellipsoid Arc 
      10 => [13, :lat, :lng, :irad, :urad, :oang, :iang, :cnf],
      # (e) 7.3.1 Ellipsoid Point
      0 => [7, :lat, :lng],
    }
    proc{|data|
      data.define_singleton_method(:decode){
        len, *items = tbl_item[self[0] >> 4]
        next nil unless self.length == len
        offset = 1
        Hash[*(items.collect{|k|
          len2, task = tbl_task[k]
          v = self.slice(offset, len2).inject(0){|res, v2| (res << 8) + v2}
          v = task.call(v) if task
          offset += len2
          [k, v]
        }.flatten(1))]
      }
      data
    }
  }.call,
})

# LPP payload conversion
ASN1::dig(upl, :ULP, :"ULP-PDU", :message, :msSUPLPOS, :posPayLoad, :"ver2-PosPayLoad-extension", :lPPPayload)[:type][1].merge!({
  :hook_encode => proc{|data|
    next data unless data.kind_of?(Hash)
    ASN1::encode_per(upl[:"LPP-PDU-Definitions"][:"LPP-Message"], data).scan(/.{1,8}/).tap{|buf|
      buf[-1] += "0" * (8 - buf[-1].length)
    }.collect{|str| str.to_i(2)}.scan(/.{1,60000}/)
  },
  :hook_decode => proc{|data|
    data.define_singleton_method(:decode){
      ASN1::decode_per(upl[:"LPP-PDU-Definitions"][:"LPP-Message"], self.flatten.collect{|v| "%08b" % [v]}.join)
    }
    data
  },
})

# BCD String
[:msisdn, :mdn, :imsi, :"ver2-imei"].each{|k|
  elm = ASN1::dig(upl, :ULP, :"ULP-PDU", :sessionID, :setSessionID, :setId, k)
  next unless elm
  elm[:type][1].merge!({
    :hook_encode => proc{|data|
      next data unless data.kind_of?(String)
      (("0" * (16 - data.size)) + data).scan(/\d{2}/).collect{|v| v.to_i(16)}
    },
    :hook_decode => proc{|data|
      data.collect{|v| "%02X"%[v]}.join
    },
  })
}

=begin
UPL: User Plane Location
ULP: UserPlane Location Protocol
=end

GPS_PVT::UPL = Module::new{
define_method(:generate_skeleton){|k_cmd, *args|
  opts = args[0] || {}
  ver = case [2, 0xFF, 0xFF].zip([opts[:version]].flatten || []) \
      .inject(0){|sum, (v1, v2)| (sum * 0x100) + (v2 || v1)}
    when 0x020006..0x02FFFF; [2, 0, 6]
    #when 0x020000..0x02FFFF; [2, 0, 0]
    else; raise
  end
  res = ASN1::generate_skeleton(upl[:ULP][:"ULP-PDU"])
  [:maj, :min, :servind].zip(ver).each{|k, v|
    res[:version][k] = v
  }
  res[:message].reject!{|k, v|
    "ms#{k_cmd}".to_sym != k
  }
  res.define_singleton_method(:all_keys){
    (iter = proc{|hash|
      hash.collect{|k, v|
        v.kind_of?(Hash) ? {k => iter.call(v)} : k
      }
    }).call(self)
  }
  res
}
define_method(:encode){|cmd|
  buf = ASN1::encode_per(upl[:ULP][:"ULP-PDU"], cmd).scan(/.{1,8}/)
  buf[-1] += "0" * (8 - buf[-1].length)
  (buf.length.divmod(1 << 8) + buf[2..-1].collect{|str| str.to_i(2)}).pack("C*")
} 
define_method(:decode){|str|
  ASN1::decode_per(upl[:ULP][:"ULP-PDU"], str.unpack("C*").collect{|v| "%08b" % [v]}.join)
}
module_function(:generate_skeleton, :encode, :decode)
}