# frozen_string_literal: true

##
# Encodes and decodes locations from Morton-coded "quad tile" strings. Each
# variable-length string encodes to a precision of one pixel per tile (roughly,
# since this computation is done in lat/lon coordinates, not mercator).
# Each character encodes 3 bits of x and 3 of y, so there are extra characters
# tacked on the end to make the zoom levels "work".
module ShortLink
  # array of 64 chars to encode 6 bits. this is almost like base64 encoding, but
  # the symbolic chars are different, as base64's + and / aren't very
  # URL-friendly.
  ARRAY = ("A".."Z").to_a + ("a".."z").to_a + ("0".."9").to_a + ["_", "~"]

  class << self
    ##
    # Given a string encoding a location, returns the [lon, lat, z] tuple of that
    # location.
    def decode(str)
      x = 0
      y = 0
      z = 0
      z_offset = 0

      # keep support for old shortlinks which use the @ character, now
      # replaced by the ~ character because twitter is horribly broken
      # and we can't have that.
      str = str.tr("@", "~")

      str.each_char do |c|
        t = ARRAY.index c
        if t.nil?
          z_offset -= 1
        else
          3.times do
            x <<= 1
            x |= 1 unless t.nobits?(32)
            t <<= 1

            y <<= 1
            y |= 1 unless t.nobits?(32)
            t <<= 1
          end
          z += 3
        end
      end
      # pack the coordinates out to their original 32 bits.
      x <<= (32 - z)
      y <<= (32 - z)

      # project the parameters back to their coordinate ranges.
      [(x * 360.0 / (2**32)) - 180.0,
       (y * 180.0 / (2**32)) - 90.0,
       z - 8 - (z_offset % 3)]
    end

    ##
    # given a location and zoom, return a short string representing it.
    def encode(lon, lat, z)
      code = interleave_bits(((lon + 180.0) * (2**32) / 360.0).to_i,
                             ((lat + 90.0) * (2**32) / 180.0).to_i)
      str = +""
      # add eight to the zoom level, which approximates an accuracy of
      # one pixel in a tile.
      ((z + 8) / 3.0).ceil.times do |i|
        digit = (code >> (58 - (6 * i))) & 0x3f
        str << ARRAY[digit]
      end
      # append characters onto the end of the string to represent
      # partial zoom levels (characters themselves have a granularity
      # of 3 zoom levels).
      ((z + 8) % 3).times { str << "-" }

      str
    end

    private

    ##
    # interleaves the bits of two 32-bit numbers. the result is known
    # as a Morton code.
    def interleave_bits(x, y)
      c = 0
      31.downto(0) do |i|
        c = (c << 1) | ((x >> i) & 1)
        c = (c << 1) | ((y >> i) & 1)
      end
      c
    end
  end
end
