require "json"
require "uuid"

module PG
  alias PGValue = String | Nil | Bool | Int32 | Float32 | Float64 | Time | JSON::Any | PG::Numeric | UUID

  # :nodoc:
  module Decoders
    module Decoder
      abstract def decode(io, bytesize, oid)
      abstract def oids : Array(Int32)
      abstract def type

      macro def_oids(oids)
        OIDS = {{oids}}

        def oids : Array(Int32)
          OIDS
        end
      end

      def read(io, type)
        io.read_bytes(type, IO::ByteFormat::NetworkEndian)
      end

      def read_i16(io)
        read(io, Int16)
      end

      def read_i32(io)
        read(io, Int32)
      end

      def read_i64(io)
        read(io, Int64)
      end

      def read_u32(io)
        read(io, UInt32)
      end

      def read_u64(io)
        read(io, UInt64)
      end

      def read_f32(io)
        read(io, Float32)
      end

      def read_f64(io)
        read(io, Float64)
      end
    end

    struct StringDecoder
      include Decoder

      def_oids [
        19,   # name (internal type)
        25,   # text
        142,  # xml
        705,  # unknown
        1042, # blchar
        1043, # varchar
      ]

      def decode(io, bytesize, oid)
        String.new(bytesize) do |buffer|
          io.read_fully(Slice.new(buffer, bytesize))
          {bytesize, 0}
        end
      end

      def type
        String
      end
    end

    struct UUIDDecoder
      include Decoder

      def_oids [
        2950, # UUID
      ]

      def decode(io, bytesize, oid)
        bytes = uninitialized UInt8[16]

        slice = Bytes.new(bytes.to_unsafe, 16)

        io.read_fully slice

        UUID.new(slice)
      end

      def type
        UUID
      end
    end

    struct CharDecoder
      include Decoder

      def_oids [
        18, # "char" (internal type)
      ]

      def decode(io, bytesize, oid)
        # TODO: can be done without creating an intermediate string
        String.new(bytesize) do |buffer|
          io.read_fully(Slice.new(buffer, bytesize))
          {bytesize, 0}
        end[0]
      end

      def type
        Char
      end
    end

    struct BoolDecoder
      include Decoder

      OIDS = [
        16, # bool
      ]

      def decode(io, bytesize, oid)
        case byte = io.read_byte
        when 0
          false
        when 1
          true
        else
          raise "bad boolean decode: #{byte}"
        end
      end

      def oids : Array(Int32)
        OIDS
      end

      def type
        Bool
      end
    end

    struct Int16Decoder
      include Decoder

      def_oids [
        21, # int2 (smallint)
      ]

      def decode(io, bytesize, oid)
        read_i16(io)
      end

      def type
        Int16
      end
    end

    struct Int32Decoder
      include Decoder

      def_oids [
        23,   # int4 (integer)
        2206, # regtype
      ]

      def decode(io, bytesize, oid)
        read_i32(io)
      end

      def type
        Int32
      end
    end

    struct Int64Decoder
      include Decoder

      def_oids [
        20, # int8 (bigint)
      ]

      def decode(io, bytesize, oid)
        read_i64(io)
      end

      def type
        Int64
      end
    end

    struct UIntDecoder
      include Decoder

      def_oids [
        26, # oid (internal type)
      ]

      def decode(io, bytesize, oid)
        read_u32(io)
      end

      def type
        UInt32
      end
    end

    struct UInt64Decoder
      include Decoder
      def_oids [
        3220, # lsn
      ]

      def decode(io, bytesize, oid)
        read_u64(io)
      end

      def type
        UInt64
      end
    end

    struct Float32Decoder
      include Decoder

      def_oids [
        700, # float4
      ]

      def decode(io, bytesize, oid)
        read_f32(io)
      end

      def type
        Float32
      end
    end

    struct Float64Decoder
      include Decoder

      def_oids [
        701, # float8
      ]

      def decode(io, bytesize, oid)
        read_f64(io)
      end

      def type
        Float64
      end
    end

    struct PointDecoder
      include Decoder

      def_oids [
        600, # point
      ]

      def decode(io, bytesize, oid)
        Geo::Point.new(read_f64(io), read_f64(io))
      end

      def type
        Geo::Point
      end
    end

    struct PathDecoder
      include Decoder

      def_oids [
        602, # path
      ]

      def decode(io, bytesize, oid)
        byte = io.read_byte.not_nil!
        closed = byte == 1_u8
        Geo::Path.new(PolygonDecoder.new.decode(io, bytesize - 1, oid).points, closed)
      end

      def type
        Geo::Path
      end
    end

    struct PolygonDecoder
      include Decoder

      def_oids [
        604, # polygon
      ]

      def decode(io, bytesize, oid)
        c = read_u32(io)
        count = (pointerof(c).as(Int32*)).value
        points = Array.new(count) do |i|
          PointDecoder.new.decode(io, 16, oid)
        end
        Geo::Polygon.new(points)
      end

      def type
        Geo::Polygon
      end
    end

    struct BoxDecoder
      include Decoder

      def_oids [
        603, # box
      ]

      def decode(io, bytesize, oid)
        x2, y2, x1, y1 = read_f64(io), read_f64(io), read_f64(io), read_f64(io)
        Geo::Box.new(x1, y1, x2, y2)
      end

      def type
        Geo::Box
      end
    end

    struct LineSegmentDecoder
      include Decoder

      def_oids [
        601, # lseg
      ]

      def decode(io, bytesize, oid)
        Geo::LineSegment.new(read_f64(io), read_f64(io), read_f64(io), read_f64(io))
      end

      def type
        Geo::LineSegment
      end
    end

    struct LineDecoder
      include Decoder

      def_oids [
        628, # line
      ]

      def decode(io, bytesize, oid)
        Geo::Line.new(read_f64(io), read_f64(io), read_f64(io))
      end

      def type
        Geo::Line
      end
    end

    struct CircleDecoder
      include Decoder

      def_oids [
        718, # circle
      ]

      def decode(io, bytesize, oid)
        Geo::Circle.new(read_f64(io), read_f64(io), read_f64(io))
      end

      def type
        Geo::Circle
      end
    end

    struct JsonDecoder
      include Decoder

      JSONB_OID = 3802

      def_oids [
        114,       # json
        JSONB_OID, # jsonb
      ]

      def decode(io, bytesize, oid)
        if oid == JSONB_OID
          io.read_byte
          bytesize -= 1
        end

        string = String.new(bytesize) do |buffer|
          io.read_fully(Slice.new(buffer, bytesize))
          {bytesize, 0}
        end
        JSON::PullParser.new(string)
      end

      def type
        JSON::PullParser
      end
    end

    struct TimeDecoder
      include Decoder

      enum OID
        DATE        = 1082
        TIMESTAMP   = 1114
        TIMESTAMPTZ = 1184
      end
      JAN_1_2K = Time.utc(2000, 1, 1)

      def_oids OID.values.map(&.value)

      def decode(io, bytesize, oid)
        case oid = OID.new(oid)
        in .date?
          JAN_1_2K + read_i32(io).days
        in .timestamp?
          JAN_1_2K + read_i64(io).microseconds
        in .timestamptz?
          time = JAN_1_2K + read_i64(io).microseconds
          time.in io.connection.time_zone
        end
      end

      def type
        Time
      end
    end

    struct IntervalDecoder
      include Decoder

      def_oids [
        1186,
      ]

      #
      # An Interval consists of
      # * time   (8 bytes)
      # * days   (4 bytes)
      # * months (4 bytes)
      #
      # See: https://github.com/postgres/postgres/blob/a094c8ff53523e88ff9dd28ad467618039e27b58/src/backend/utils/adt/timestamp.c#L1003
      #
      def decode(io, bytesize, oid)
        microseconds = read_i64(io)
        days = read_i32(io)
        months = read_i32(io)

        PG::Interval.new(microseconds, days, months)
      end

      def type
        PG::Interval
      end
    end

    struct ByteaDecoder
      include Decoder

      def_oids [
        17, # bytea
      ]

      def decode(io, bytesize, oid)
        slice = Bytes.new(bytesize)
        io.read_fully(slice)
        slice
      end

      def type
        Bytes
      end
    end

    struct NumericDecoder
      include Decoder

      def_oids [
        1700, # numeric
      ]

      def decode(io, bytesize, oid)
        ndigits = read_i16(io)
        weight = read_i16(io)
        sign = read_i16(io)
        dscale = read_i16(io)
        digits = (0...ndigits).map { |i| read_i16(io) }
        PG::Numeric.new(ndigits, weight, sign, dscale, digits)
      end

      def type
        PG::Numeric
      end
    end

    @@decoders = Hash(Int32, PG::Decoders::Decoder).new(ByteaDecoder.new)

    def self.from_oid(oid)
      @@decoders[oid]
    end

    def self.register_decoder(decoder)
      decoder.oids.each do |oid|
        @@decoders[oid] = decoder
      end
    end

    # https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.h
    register_decoder BoolDecoder.new
    register_decoder ByteaDecoder.new
    register_decoder CharDecoder.new
    register_decoder StringDecoder.new
    register_decoder UUIDDecoder.new
    register_decoder Int16Decoder.new
    register_decoder Int32Decoder.new
    register_decoder Int64Decoder.new
    register_decoder UIntDecoder.new
    register_decoder UInt64Decoder.new
    register_decoder JsonDecoder.new
    register_decoder Float32Decoder.new
    register_decoder Float64Decoder.new
    register_decoder TimeDecoder.new
    register_decoder IntervalDecoder.new
    register_decoder NumericDecoder.new
    register_decoder PointDecoder.new
    register_decoder LineSegmentDecoder.new
    register_decoder PathDecoder.new
    register_decoder BoxDecoder.new
    register_decoder PolygonDecoder.new
    register_decoder LineDecoder.new
    register_decoder CircleDecoder.new
  end
end

require "./decoders/*"
