# frozen_string_literal: true

module Onebox
  module Engine
    class GoogleMapsOnebox
      include Engine

      class << self
        def ===(other)
          if other.kind_of? URI
            @@matchers && @@matchers.any? { |m| other.to_s =~ m[:regexp] }
          else
            super
          end
        end

        private

        def matches_regexp(key, regexp)
          (@@matchers ||= []) << { key: key, regexp: regexp }
        end
      end

      always_https

      # Matches shortened Google Maps URLs
      matches_regexp :short,      %r"^(https?:)?//goo\.gl/maps/"

      # Matches URLs for custom-created maps
      matches_regexp :custom,     %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps/d/(?:edit|viewer|embed)\?mid=.+$"

      # Matches URLs with streetview data
      matches_regexp :streetview, %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps[^@]+@(?<lon>-?[\d.]+),(?<lat>-?[\d.]+),(?:\d+)a,(?<zoom>[\d.]+)y,(?<heading>[\d.]+)h,(?<pitch>[\d.]+)t.+?data=.*?!1s(?<pano>[^!]{22})"

      # Matches "normal" Google Maps URLs with arbitrary data
      matches_regexp :standard,   %r"^(?:https?:)?//www\.google(?:\.(?:\w{2,}))+/maps"

      # Matches URLs for the old Google Maps domain which we occasionally get redirected to
      matches_regexp :canonical,  %r"^(?:https?:)?//maps\.google(?:\.(?:\w{2,}))+/maps\?"

      def initialize(url, timeout = nil)
        super
        resolve_url!
      rescue Net::HTTPServerException, Timeout::Error, Net::HTTPError, Errno::ECONNREFUSED, RuntimeError => err
        raise ArgumentError, "malformed url or unresolveable: #{err.message}"
      end

      def streetview?
        !!@streetview
      end

      def to_html
        "<div class='maps-onebox'><iframe src=\"#{link}\" width=\"690\" height=\"400\" frameborder=\"0\" style=\"border:0\">#{placeholder_html}</iframe></div>"
      end

      def placeholder_html
        width  = @placeholder_width  || 690
        height = @placeholder_height || 400
        "<img src='#{@placeholder}' width='#{width}' height='#{height}'/>"
      end

      private

      def data
        { link: url, title: url }
      end

      def resolve_url!
        @streetview = false
        type, match = match_url

        # Resolve shortened URL, if necessary
        if type == :short
          follow_redirect!
          type, match = match_url
        end

        # Try to get the old-maps URI, it is far easier to embed.
        if type == :standard
          retry_count = 10
          while (retry_count -= 1) > 0
            follow_redirect!
            type, match = match_url
            break if type != :standard
            sleep 0.1
          end
        end

        case type
        when :standard
          # Fallback for map URLs that don't resolve into an easily embeddable old-style URI
          # Roadmaps use a "z" zoomlevel, satellite maps use "m" the horizontal width in meters
          # TODO: tilted satellite maps using "a,y,t"
          match = @url.match(/@(?<lon>[\d.-]+),(?<lat>[\d.-]+),(?<zoom>\d+)(?<mz>[mz])/)
          raise "unexpected standard url #{@url}" unless match
          zoom = match[:mz] == "z" ? match[:zoom] : Math.log2(57280048.0 / match[:zoom].to_f).round
          location = "#{match[:lon]},#{match[:lat]}"
          url = "https://maps.google.com/maps?ll=#{location}&z=#{zoom}&output=embed&dg=ntvb"
          url += "&q=#{$1}" if match = @url.match(/\/place\/([^\/\?]+)/)
          url += "&cid=#{($1 + $2).to_i(16)}" if @url.match(/!3m1!1s0x(\h{16}):0x(\h{16})/)
          @url = url
          @placeholder = "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap&center=#{location}&zoom=#{zoom}&size=690x400&sensor=false"

        when :custom
          url = @url.dup
          @url = rewrite_custom_url(url, "embed")
          @placeholder = rewrite_custom_url(url, "thumbnail")
          @placeholder_height = @placeholder_width = 120

        when :streetview
          @streetview = true
          panoid = match[:pano]
          lon = match[:lon].to_f.to_s
          lat = match[:lat].to_f.to_s
          heading = match[:heading].to_f.round(4).to_s
          pitch = (match[:pitch].to_f / 10.0).round(4).to_s
          fov = (match[:zoom].to_f / 100.0).round(4).to_s
          zoom = match[:zoom].to_f.round
          @url = "https://www.google.com/maps/embed?pb=!3m2!2sen!4v0!6m8!1m7!1s#{panoid}!2m2!1d#{lon}!2d#{lat}!3f#{heading}!4f#{pitch}!5f#{fov}"
          @placeholder = "https://maps.googleapis.com/maps/api/streetview?size=690x400&location=#{lon},#{lat}&pano=#{panoid}&fov=#{zoom}&heading=#{heading}&pitch=#{pitch}&sensor=false"

        when :canonical
          uri = URI(@url)

          query = URI::decode_www_form(uri.query).to_h
          if !query.has_key?("ll")
            raise ArgumentError, "canonical url lacks location argument" unless query.has_key?("sll")
            query["ll"] = query["sll"]
            @url += "&ll=#{query["sll"]}"
          end
          location = query["ll"]
          if !query.has_key?("z")
            raise ArgumentError, "canonical url has incomplete query arguments" unless query.has_key?("spn") || query.has_key?("sspn")
            if !query.has_key?("spn")
              query["spn"] = query["sspn"]
              @url += "&spn=#{query["sspn"]}"
            end
            angle = query["spn"].split(",").first.to_f
            zoom = (Math.log(690.0 * 360.0 / angle / 256.0) / Math.log(2)).round
          else
            zoom = query["z"]
          end
          @url = @url.sub('output=classic', 'output=embed')
          @placeholder = "https://maps.googleapis.com/maps/api/staticmap?maptype=roadmap&size=690x400&sensor=false&center=#{location}&zoom=#{zoom}"

        else
          raise "unexpected url type #{type.inspect}"
        end
      end

      def match_url
        @@matchers.each do |matcher|
          if m = matcher[:regexp].match(@url)
            return matcher[:key], m
          end
        end
        raise ArgumentError, "\"#{@url}\" does not match any known pattern"
      end

      def rewrite_custom_url(url, target)
        uri = URI(url)
        uri.path = uri.path.sub(/(?<=^\/maps\/d\/)\w+$/, target)
        uri.to_s
      end

      def follow_redirect!
        uri = URI(@url)
        begin
          http = Net::HTTP.start(uri.host, uri.port,
            use_ssl: uri.scheme == 'https', open_timeout: timeout, read_timeout: timeout)
          response = http.head(uri.path)

          raise "unexpected response code #{response.code}" unless %w(200 301 302).include?(response.code)
          @url = response.code == "200" ? uri.to_s : response["Location"]
        ensure
          http.finish rescue nil
        end
      end

    end
  end
end
