require "../pg/geo"

module PQ
  # :nodoc:
  record Param, slice : Slice(UInt8), size : Int32, format : Int16 do
    delegate to_unsafe, to: slice

    #  Internal wrapper to represent an encoded parameter

    def self.encode(val : Nil)
      binary Pointer(UInt8).null.to_slice(0), -1
    end

    def self.encode(val : Slice)
      binary val, val.size
    end

    def self.encode(val : Array)
      text encode_array(val)
    end

    def self.encode(val : Time)
      text Time::Format::RFC_3339.format(val, fraction_digits: 9)
    end

    def self.encode(val : Enum)
      encode val.value
    end

    def self.encode(val : PG::Geo::Point)
      text "(#{val.x},#{val.y})"
    end

    def self.encode(val : PG::Geo::Line)
      text "{#{val.a},#{val.b},#{val.c}}"
    end

    def self.encode(val : PG::Geo::Circle)
      text "<(#{val.x},#{val.y}),#{val.radius}>"
    end

    def self.encode(val : PG::Geo::LineSegment)
      text "((#{val.x1},#{val.y1}),(#{val.x2},#{val.y2}))"
    end

    def self.encode(val : PG::Geo::Box)
      text "((#{val.x1},#{val.y1}),(#{val.x2},#{val.y2}))"
    end

    def self.encode(val : PG::Geo::Path)
      if val.closed?
        encode_points "(", val.points, ")"
      else
        encode_points "[", val.points, "]"
      end
    end

    def self.encode(val : PG::Geo::Polygon)
      encode_points "(", val.points, ")"
    end

    private def self.encode_points(left, points, right)
      string = String.build do |io|
        io << left
        points.each_with_index do |point, i|
          io << "," if i > 0
          io << "(" << point.x << "," << point.y << ")"
        end
        io << right
      end

      text string
    end

    def self.encode(val : PG::Interval)
      # https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-INTERVAL-INPUT
      text "#{val.months} months #{val.days} days #{val.microseconds} microseconds"
    end

    def self.encode(val)
      text val.to_s
    end

    def self.binary(slice, size)
      new slice, size, 1_i16
    end

    def self.text(string : String)
      text string.to_slice
    end

    def self.text(slice : Bytes)
      new slice, slice.size, 0_i16
    end

    def self.encode_array(array)
      String.build(array.size + 2) do |io|
        encode_array(io, array)
      end
    end

    def self.encode_array(io, value : Array)
      io << "{"
      value.join(io, ",") do |item|
        encode_array(io, item)
      end

      io << "}"
    end

    def self.encode_array(io, value)
      io << value
    end

    def self.encode_array(io, value : Nil)
      io << "NULL"
    end

    def self.encode_array(io, value : Bool)
      io << (value ? 't' : 'f')
    end

    def self.encode_array(io, value : Bytes)
      io << %{"\\\\x}
      value.each do |byte|
        byte.to_s io, base: 16, precision: 2
      end
      io << '"'
    end

    def self.encode_array(io, value : String)
      io << '"'
      if value.ascii_only?
        special_chars = {'"'.ord.to_u8, '\\'.ord.to_u8}
        last_index = 0
        value.to_slice.each_with_index do |byte, index|
          if special_chars.includes?(byte)
            io.write value.unsafe_byte_slice(last_index, index - last_index)
            last_index = index
            io << '\\'
          end
        end

        io.write value.unsafe_byte_slice(last_index)
      else
        last_index = 0
        reader = Char::Reader.new(value)
        while reader.has_next?
          char = reader.current_char
          if {'"', '\\'}.includes?(char)
            io.write value.unsafe_byte_slice(last_index, reader.pos - last_index)
            last_index = reader.pos
            io << '\\'
          end
          reader.next_char
        end

        io.write value.unsafe_byte_slice(last_index)
      end

      io << '"'
    end
  end
end
