# Ntrip (Networked Transport of RTCM via Internet Protocol)

require 'net/http'
require 'uri'
require_relative 'version'

module GPS_PVT
class Ntrip < Net::HTTP
  Net::HTTPResponse.class_eval{
    orig = singleton_method(:read_new)
    define_singleton_method(:read_new){|sock|
      # handle Ntrip(rev1), which does not comply with HTTP
      unless sock.respond_to?(:ntrip) then
        orig.call(sock)
      else
        str = sock.readline
        case str
        when /\A(?:(?:HTTP(?:\/(\d+\.\d+))?|([^\d\s]+))\s+)?(\d\d\d)(?:\s+(.*))?\z/in
          res = response_class($3).new($1 || '1.1', $3, $2 || $4)
          each_response_header(sock){|k,v|
            res.add_field k, v
          } unless res.message == 'ICY' # or 'SOURCETABLE' for Ntrip(rev1)
          res
        else
          raise Net::HTTPBadResponse, "wrong status line: #{str.dump}"
        end
      end
    }
  }
  def on_connect
    super
    @socket.define_singleton_method(:ntrip){true}
  end
  
  SOURCE_TBL_ITEMS = {
    :STR => [
        :type, :mountpoint, :identifier, :format, :format_details,
        :carrier, :nav_system, :network, :country, :latitude, :longitude,
        :nmea, :solution, :generator, :compression, :authentication, :fee, :bitrate],
    :CAS => [
        :type, :host, :port, :identifier, :operator,
        :nmea, :country, :latitude, :longitude, :fallback_host, :fallback_ip],
    :NET => [
        :type, :identifier, :operator, :authentication, :fee,
        :web_net, :web_str, :web_reg],
  }
  module MountPoints
    [:select, :reject, :merge, :clone, :dup].each{|f|
      define_method(f){|*args, &b| super(*args, &b).extend(MountPoints)}
    }
    D2R = Math::PI / 180
    def near_from(lat_deg, lng_deg)
      require 'gps_pvt/Coordinate'
      llh0 = Coordinate::LLH::new(D2R * lat_deg, D2R * lng_deg, 0)
      collect{|pt, prop|
        llh = Coordinate::LLH::new(*([:latitude, :longitude].collect{|k| D2R * prop[k].to_f} + [0]))
        [llh0.xyz.dist(llh.xyz), prop]
      }.sort{|a, b| a[0] <=> b[0]} # return [distance, property]
    end
  end
  def Ntrip.parse_source_table(str)
    res = {}
    str.lines.each{|line|
      values = line.chomp.split(/\s*;\s*/)
      type = values[0].to_sym
      next unless keys = SOURCE_TBL_ITEMS[type]
      next unless (values.size >= keys.size)
      entry = Hash[*(keys.zip(values).flatten(1))]
      entry[:misc] = values[(keys.size)..-1] if values.size > keys.size
      (res[type] ||= []) << entry
    }
    res.define_singleton_method(:mount_points, lambda{
      Hash[*((self[:STR] || []).collect{|entry|
        [entry[:mountpoint], entry]
      }.flatten(1))].extend(MountPoints)
    })
    res
  end
  def generate_request(path, header)
    req = Net::HTTP::Get.new(path, {
      'User-Agent' => "GPS_PVT NTRIP client/#{GPS_PVT::VERSION}",
      'Accept' => '*/*',
      'Ntrip-Version' => 'Ntrip/2.0',
    }.merge(header.select{|k, v| k.kind_of?(String)}))
    header.each{|k, v|
      next unless k.kind_of?(Symbol)
      req.send(k, *v)
    }
    req
  end
  def get_source_table(header = {}, &b)
    (b || proc{|str| Ntrip.parse_source_table(str)}).call(request(generate_request('/', header)).read_body)
  end
  def get_data(mount_point, header = {}, &b)
    request(generate_request("/#{mount_point}", header)){|res|
      res.read_body(&b)
    }
  end
end
end

require 'open-uri'

OpenURI.class_eval{
  check_options_orig = singleton_method(:check_options)
  define_singleton_method(:check_options){|options|
    uri = options.delete(:uri)
    case uri
    when URI::Ntrip
      options[:basic_auth] ||=
          options.delete(:http_basic_authentication) ||
          ([:user, :password].collect{|k|
            URI::decode_www_form_component(uri.send(k))
          } rescue nil)
      options['Ntrip-Version'] ||= "Ntrip/%3.1f"%[options.delete(:version)] if options[:version]
      options['User-Agent'] ||= options[:user_agent]
      options.select!{|k, v| v} #compact! Ruby >= 2.4.0
      true
    else
      check_options_orig.call(options)
    end
  }
  open_uri_orig = singleton_method(:open_uri)
  define_singleton_method(:open_uri){|name, *rest, &b|
    uri = URI::Generic === name ? name : URI.parse(name)
    (rest[-1].kind_of?(Hash) ? rest : (rest << {}))[-1][:uri] = uri
    open_uri_orig.call(uri, *rest, &b)
  }
  def OpenURI.open_ntrip(buf, target, proxy, options) # :nodoc:
    GPS_PVT::Ntrip.start(target.host, target.port){|ntrip|
      # get source table
      tbl = ntrip.get_source_table(options){|str| str}
      if target.root? then
        buf << tbl
        buf.io.rewind
        next
      end
      tbl = GPS_PVT::Ntrip::parse_source_table(tbl)
      
      # check mount point
      mnt_pt = target.mount_point
      prop = tbl.mount_points[mnt_pt]
      raise Net::ProtocolError::new("Mount point(#{mnt_pt}) not found") unless prop
      
      # set stream
      buf.instance_eval{
        @io, w = IO::pipe
        @io.define_singleton_method(:property){prop}
        Thread::new{
          begin
            ntrip.get_data(mnt_pt, options){|data| w << data}
          rescue Errno::EPIPE;
          rescue; raise
          ensure; w.close
          end
        }
      }
    }
  end
}
module URI
  class Ntrip < HTTP
    def root
      res = self.clone
      res.path = '/'
      res
    end
    def root?; self.path == '/'; end
    def mount_point
      self.path.sub(%r|^/|, '')
    end

    def buffer_open(buf, proxy, options)
      OpenURI.open_ntrip(buf, self, proxy, options)
    end
    include OpenURI::OpenRead
    def read_source_table(options = {})
      GPS_PVT::Ntrip::parse_source_table(self.root.read(options))
    end
  end
  if respond_to?(:register_scheme) then
    register_scheme('NTRIP', Ntrip)
  else
    @@schemes['NTRIP'] = Ntrip
  end
end