require "../numeric"

module PG
  module Decoders
    # Generic Array decoder: decodes to a recursive array type
    struct ArrayDecoder(T, D)
      include Decoder

      getter oids : Array(Int32)

      def self.new(oid : Int32)
        new([oid])
      end

      def initialize(@oids : Array(Int32))
      end

      def decode(io, bytesize, oid)
        header = Decoders.decode_array_header(io)

        if header.dimensions == 0
          ([] of T).as(T)
        elsif header.dimensions == 1 && header.dim_info.first[:lbound] == 1
          # allow casting down to unnested crystal arrays
          build_simple_array(io, header.dim_info.first[:dim], header.oid).as(T)
        else
          if header.dim_info.any? { |di| di[:lbound] < 1 }
            raise PG::RuntimeError.new("Only lower-bounds >= 1 are supported")
          end

          # recursively build nested array
          get_element(io, header.dim_info, header.oid).as(T)
        end
      end

      def build_simple_array(io, size, oid)
        Array(T).new(size) { get_next(io, oid) }
      end

      def get_element(io, dim_info, oid)
        if dim_info.size == 1
          lbound = dim_info.first[:lbound] - 1 # in lower-bound is not 1
          Array(T).new(dim_info.first[:dim] + lbound) do |i|
            i < lbound ? nil : get_next(io, oid)
          end
        else
          Array(T).new(dim_info.first[:dim]) do |i|
            get_element(io, dim_info[1..-1], oid)
          end
        end
      end

      def get_next(io, oid)
        bytesize = read_i32(io)
        if bytesize == -1
          nil
        else
          D.new.decode(io, bytesize, oid)
        end
      end

      def type
        T
      end
    end

    # Specific array decoder method: decodes to exactly Array(T).
    # Used when invoking, for example `rs.read(Array(Int32))`.
    def self.decode_array(io, bytesize, t : Array(T).class) forall T
      header = decode_array_header(io)

      decoder = array_decoder(T)
      unless decoder.oids.includes?(header.oid)
        correct_decoder = Decoders.from_oid(header.oid)

        raise PG::RuntimeError.new("Can't read column of type Array(#{correct_decoder.type}) as Array(#{flatten_type(T)})")
      end

      if header.dimensions == 0
        return [] of T
      end

      decode_array_element(io, t, header.dim_info, decoder, header.oid)
    end

    def self.decode_array_element(io, t : Array(T).class, dim_info, decoder, oid) forall T
      size = dim_info.first[:dim]
      rest = dim_info[1..-1]

      Array(T).new(size) do
        decode_array_element(io, T, rest, decoder, oid)
      end
    end

    def self.decode_array_element(io, t : T.class, dim_info, decoder, oid) forall T
      bytesize = read_i32(io)
      if bytesize == -1
        {% if T.nilable? %}
          nil
        {% else %}
          raise PG::RuntimeError.new("unexpected NULL")
        {% end %}
      else
        decoder.decode(io, bytesize, oid)
      end
    end

    def self.array_decoder(t : Array(T).class) forall T
      array_decoder(T)
    end

    {% for type in %w(Bool Char Int16 Int32 String Int64 Float32 Float64 Numeric Time UUID).map(&.id) %}
      def self.array_decoder(t : {{type}}?.class)
        {{type}}Decoder.new
      end

      def self.array_decoder(t : {{type}}.class)
        {{type}}Decoder.new
      end
    {% end %}

    def self.flatten_type(t : Array(T).class) forall T
      flatten_type(T)
    end

    def self.flatten_type(t : T?.class) forall T
      T
    end

    def self.flatten_type(t : T.class) forall T
      T
    end

    record ArrayHeader,
      dimensions : Int32,
      oid : Int32,
      dim_info : Array({dim: Int32, lbound: Int32})

    def self.decode_array_header(io)
      dimensions = read_i32(io)
      has_null = read_i32(io) == 1 # unused
      oid = read_i32(io)           # unused but in header
      dim_info = Array({dim: Int32, lbound: Int32}).new(dimensions) do |i|
        {
          dim:    read_i32(io),
          lbound: read_i32(io),
        }
      end

      ArrayHeader.new(dimensions, oid, dim_info)
    end

    def self.read_i32(io)
      io.read_bytes(Int32, IO::ByteFormat::NetworkEndian)
    end
  end

  macro array_type(t, oid)
    alias {{t}}Array = {{t}}? | Array({{t}}Array)

    module Decoders
      register_decoder ArrayDecoder({{t}}Array, {{t}}Decoder).new({{oid}})
    end
  end

  array_type Bool, 1000
  array_type Char, 1002
  array_type Int16, 1005
  array_type Int32, 1007
  array_type Int64, 1016
  array_type Float32, 1021
  array_type Float64, 1022
  array_type String, 1009
  array_type Numeric, 1231
  array_type Time, [1115, 1182]
  array_type UUID, 2951
end
