defmodule Bolt.Sips.Internals.PackStream.DecoderImplV2 do
  alias Bolt.Sips.Types.{TimeWithTZOffset, DateTimeWithTZOffset, Duration, Point}

  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)

      @last_version Bolt.Sips.Internals.BoltVersionHelper.last()

      # Null
      @null_marker 0xC0

      # Boolean
      @true_marker 0xC3
      @false_marker 0xC2

      # String
      @tiny_bitstring_marker 0x8
      @bitstring8_marker 0xD0
      @bitstring16_marker 0xD1
      @bitstring32_marker 0xD2

      # Integer
      @int8_marker 0xC8
      @int16_marker 0xC9
      @int32_marker 0xCA
      @int64_marker 0xCB

      # Float
      @float_marker 0xC1

      # List
      @tiny_list_marker 0x9
      @list8_marker 0xD4
      @list16_marker 0xD5
      @list32_marker 0xD6

      # Map
      @tiny_map_marker 0xA
      @map8_marker 0xD8
      @map16_marker 0xD9
      @map32_marker 0xDA

      # Structure
      @tiny_struct_marker 0xB
      @struct8_marker 0xDC
      @struct16_marker 0xDD

      # Node
      @node_marker 0x4E

      # Relationship
      @relationship_marker 0x52

      # Unbounded relationship
      @unbounded_relationship_marker 0x72

      # Path
      @path_marker 0x50

      # Local Time
      @local_time_signature 0x74
      @local_time_struct_size 1

      # Time With TZ Offset
      @time_with_tz_signature 0x54
      @time_with_tz_struct_size 2

      # Date
      @date_signature 0x44
      @date_struct_size 1

      # Local DateTime
      @local_datetime_signature 0x64
      @local_datetime_struct_size 2

      # Datetime with TZ offset
      @datetime_with_zone_offset_signature 0x46
      @datetime_with_zone_offset_struct_size 3

      # Datetime with TZ id
      @datetime_with_zone_id_signature 0x66
      @datetime_with_zone_id_struct_size 3

      # Duration
      @duration_signature 0x45
      @duration_struct_size 4

      # Point 2D
      @point2d_signature 0x58
      @point2d_struct_size 3

      # Point 3D
      @point3d_signature 0x59
      @point3d_struct_size 4

      # Local Date
      def decode({@date_signature, struct, @date_struct_size}, bolt_version)
          when bolt_version >= 2 and bolt_version <= @last_version do
        {[date], rest} = decode_struct(struct, @date_struct_size, bolt_version)
        [Date.add(~D[1970-01-01], date) | rest]
      end

      # Local Time
      def decode({@local_time_signature, struct, @local_time_struct_size}, bolt_version)
          when bolt_version >= 2 and bolt_version <= @last_version do
        {[time], rest} = decode_struct(struct, @local_time_struct_size, bolt_version)

        [Time.add(~T[00:00:00.000000], time, :nanosecond) | rest]
      end

      # Local DateTime
      def decode({@local_datetime_signature, struct, @local_datetime_struct_size}, bolt_version)
          when bolt_version >= 2 and bolt_version <= @last_version do
        {[seconds, nanoseconds], rest} =
          decode_struct(struct, @local_datetime_struct_size, bolt_version)

        ndt =
          NaiveDateTime.add(
            ~N[1970-01-01 00:00:00.000000000],
            seconds * 1_000_000_000 + nanoseconds,
            :nanosecond
          )

        [ndt | rest]
      end

      # Time with Zone Offset
      def decode({@time_with_tz_signature, struct, @time_with_tz_struct_size}, bolt_version)
          when bolt_version >= 2 and bolt_version <= @last_version do
        {[time, offset], rest} = decode_struct(struct, @time_with_tz_struct_size, bolt_version)

        t = TimeWithTZOffset.create(Time.add(~T[00:00:00.000000], time, :nanosecond), offset)
        [t | rest]
      end

      # Datetime with zone Id
      def decode(
            {@datetime_with_zone_id_signature, struct, @datetime_with_zone_id_struct_size},
            bolt_version
          )
          when bolt_version >= 2 and bolt_version <= @last_version do
        {[seconds, nanoseconds, zone_id], rest} =
          decode_struct(struct, @datetime_with_zone_id_struct_size, bolt_version)

        naive_dt =
          NaiveDateTime.add(
            ~N[1970-01-01 00:00:00.000000],
            seconds * 1_000_000_000 + nanoseconds,
            :nanosecond
          )

        dt = Bolt.Sips.TypesHelper.datetime_with_micro(naive_dt, zone_id)
        [dt | rest]
      end

      # Datetime with zone offset
      def decode(
            {@datetime_with_zone_offset_signature, struct,
             @datetime_with_zone_offset_struct_size},
            bolt_version
          )
          when bolt_version >= 2 and bolt_version <= @last_version do
        {[seconds, nanoseconds, zone_offset], rest} =
          decode_struct(struct, @datetime_with_zone_id_struct_size, bolt_version)

        naive_dt =
          NaiveDateTime.add(
            ~N[1970-01-01 00:00:00.000000],
            seconds * 1_000_000_000 + nanoseconds,
            :nanosecond
          )

        dt = DateTimeWithTZOffset.create(naive_dt, zone_offset)
        [dt | rest]
      end

      # Duration
      def decode({@duration_signature, struct, @duration_struct_size}, bolt_version)
          when bolt_version >= 2 and bolt_version <= @last_version do
        {[months, days, seconds, nanoseconds], rest} =
          decode_struct(struct, @duration_struct_size, bolt_version)

        duration = Duration.create(months, days, seconds, nanoseconds)
        [duration | rest]
      end

      # Point2D
      def decode({@point2d_signature, struct, @point2d_struct_size}, bolt_version)
          when bolt_version >= 2 and bolt_version <= @last_version do
        {[srid, x, y], rest} = decode_struct(struct, @point2d_struct_size, bolt_version)
        point = Point.create(srid, x, y)

        [point | rest]
      end

      # Point3D
      def decode({@point3d_signature, struct, @point3d_struct_size}, bolt_version)
          when bolt_version >= 2 and bolt_version <= @last_version do
        {[srid, x, y, z], rest} = decode_struct(struct, @point3d_struct_size, bolt_version)
        point = Point.create(srid, x, y, z)

        [point | rest]
      end
    end
  end
end
