defmodule Bolt.Sips.Internals.PackStream.EncoderV2 do
  @moduledoc false
  use Bolt.Sips.Internals.PackStream.Markers
  alias Bolt.Sips.Internals.PackStream.EncoderHelper
  alias Bolt.Sips.Types.{TimeWithTZOffset, DateTimeWithTZOffset, Duration, Point}

  @doc """
  Encode a Time (represented by Time) into Bolt binary format.
  Encoded in a structure.

  Signature: `0x74`

  Encoding:
  `Marker` `Size` `Signature` ` Content`

  where `Content` is:
  `Nanoseconds_from_00:00:00`

  ## Example

      iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_local_time(~T[06:54:32.453], 2))
      <<0xB1, 0x74, 0xCB, 0x0, 0x0, 0x16, 0x9F, 0x11, 0xB9, 0xCB, 0x40>>
  """
  @spec encode_local_time(Time.t(), integer()) :: Bolt.Sips.Internals.PackStream.value()
  def encode_local_time(local_time, bolt_version), do: EncoderHelper.call_encode(:local_time, local_time, bolt_version)

  @doc """
  Encode a TIME WITH TIMEZONE OFFSET (represented by TimeWithTZOffset) into Bolt binary format.
  Encoded in a structure.

  Signature: `0x54`

  Encoding:
  `Marker` `Size` `Signature` ` Content`

  where `Content` is:
  `Nanoseconds_from_00:00:00` `Offset_in_seconds`

  ## Example

      iex> time_with_tz = Bolt.Sips.Types.TimeWithTZOffset.create(~T[06:54:32.453], 3600)
      iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_time_with_tz(time_with_tz, 2))
      <<0xB2, 0x54, 0xCB, 0x0, 0x0, 0x16, 0x9F, 0x11, 0xB9, 0xCB, 0x40, 0xC9, 0xE, 0x10>>
  """
  def encode_time_with_tz(%TimeWithTZOffset{time: time, timezone_offset: offset}, bolt_version), do: EncoderHelper.call_encode(:time_with_tz, %TimeWithTZOffset{time: time, timezone_offset: offset}, bolt_version )

  @doc """
  Encode a DATE (represented by Date) into Bolt binary format.
  Encoded in a structure.

  Signature: `0x44`

  Encoding:
  `Marker` `Size` `Signature` ` Content`

  where `Content` is:
  `Nb_days_since_epoch`

  ## Example

      iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_date(~D[2019-04-23], 2))
      <<0xB1, 0x44, 0xC9, 0x46, 0x59>>

  """
  @spec encode_date(Date.t(), integer()) :: Bolt.Sips.Internals.PackStream.value()
  def encode_date(date, bolt_version), do: EncoderHelper.call_encode(:date, date, bolt_version)

  @doc """
  Encode a LOCAL DATETIME (Represented by NaiveDateTime) into Bolt binary format.
  Encoded in a structure.

  WARNING: Nanoseconds are left off as NaiveDateTime doesn't handle them.
  A new Calendar should be implemented to manage them.

  Signature: `0x64`

  Encoding:
  `Marker` `Size` `Signature` ` Content`

  where `Content` is:
  `Nb_seconds_since_epoch` `Remainder_in_nanoseconds`

  ## Example

      iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_local_datetime(~N[2019-04-23 13:45:52.678], 2))
      <<0xB2, 0x64, 0xCA, 0x5C, 0xBF, 0x17, 0x10, 0xCA, 0x28, 0x69, 0x75, 0x80>>

  """
  @spec encode_local_datetime(Calendar.naive_datetime(), integer()) ::
          Bolt.Sips.Internals.PackStream.value()
  def encode_local_datetime(local_datetime, bolt_version), do: EncoderHelper.call_encode(:local_datetime, local_datetime, bolt_version)

  @doc """
  Encode DATETIME WITH TIMEZONE ID (represented by Calendar.DateTime) into Bolt binary format.
  Encoded in a structure.

  WARNING: Nanoseconds are left off as NaiveDateTime doesn't handle them.
  A new Calendar should be implemented to manage them.

  Signature: `0x66`

  Encoding:
  `Marker` `Size` `Signature` ` Content`

  where `Content` is:
  `Nb_seconds_since_epoch` `Remainder_in_nanoseconds` `Zone_id`

  ## Example

      iex> d = Bolt.Sips.TypesHelper.datetime_with_micro(~N[2013-11-12 07:32:02.003],
      ...> "Europe/Berlin")
      #DateTime<2013-11-12 07:32:02.003+01:00 CET Europe/Berlin>
      iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_datetime_with_tz_id(d, 2))
      <<0xB3, 0x66, 0xCA, 0x52, 0x81, 0xD9, 0x72, 0xCA, 0x0, 0x2D, 0xC6, 0xC0, 0x8D, 0x45, 0x75,
      0x72, 0x6F, 0x70, 0x65, 0x2F, 0x42, 0x65, 0x72, 0x6C, 0x69, 0x6E>>

  """
  @spec encode_datetime_with_tz_id(Calendar.datetime(), integer()) ::
          Bolt.Sips.Internals.PackStream.value()
  def encode_datetime_with_tz_id(datetime, bolt_version), do: EncoderHelper.call_encode(:datetime_with_tz_id, datetime, bolt_version)

  @doc """
  Encode DATETIME WITH TIMEZONE OFFSET (represented by DateTimeWithTZOffset) into Bolt binary format.
  Encoded in a structure.

  WARNING: Nanoseconds are left off as NaiveDateTime doesn't handle them.
  A new Calendar should be implemented to manage them.

  Signature: `0x46`

  Encoding:
  `Marker` `Size` `Signature` ` Content`

  where `Content` is:
  `Nb_seconds_since_epoch` `Remainder_in_nanoseconds` `Zone_offset`

  ## Example

      iex> d = Bolt.Sips.Types.DateTimeWithTZOffset.create(~N[2013-11-12 07:32:02.003], 7200)
      %Bolt.Sips.Types.DateTimeWithTZOffset{
              naive_datetime: ~N[2013-11-12 07:32:02.003],
              timezone_offset: 7200
            }
      iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_datetime_with_tz_offset(d, 2))
      <<0xB3, 0x46, 0xCA, 0x52, 0x81, 0xD9, 0x72, 0xCA, 0x0, 0x2D, 0xC6, 0xC0, 0xC9, 0x1C, 0x20>>

  """
  @spec encode_datetime_with_tz_offset(DateTimeWithTZOffset.t(), integer()) ::
          Bolt.Sips.Internals.PackStream.value()
  def encode_datetime_with_tz_offset(
        %DateTimeWithTZOffset{naive_datetime: ndt, timezone_offset: tz_offset},
        bolt_version
  ), do: EncoderHelper.call_encode(:datetime_with_tz_offset,

        %DateTimeWithTZOffset{naive_datetime: ndt, timezone_offset: tz_offset},
        bolt_version
  )


  @doc """
  Encode DURATION (represented by Duration) into Bolt binary format.
  Encoded in a structure.

  Signature: `0x45`

  Encoding:
  `Marker` `Size` `Signature` ` Content`

  where `Content` is:
  `Months` `Days` `Seconds` `Nanoseconds`

  ## Example

      iex(60)> d = %Bolt.Sips.Types.Duration{
      ...(60)>   years: 3,
      ...(60)>   months: 1,
      ...(60)>   weeks: 7,
      ...(60)>   days: 4,
      ...(60)>   hours: 13,
      ...(60)>   minutes: 2,
      ...(60)>   seconds: 21,
      ...(60)>   nanoseconds: 554
      ...(60)> }
      %Bolt.Sips.Types.Duration{
        days: 4,
        hours: 13,
        minutes: 2,
        months: 1,
        nanoseconds: 554,
        seconds: 21,
        weeks: 7,
        years: 3
      }
      iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_duration(d, 2))
      <<0xB4, 0x45, 0x25, 0x35, 0xCA, 0x0, 0x0, 0xB7, 0x5D, 0xC9, 0x2, 0x2A>>
  """
  @spec encode_duration(Duration.t(), integer()) :: Bolt.Sips.Internals.PackStream.value()
  def encode_duration(%Duration{} = duration, bolt_version), do: EncoderHelper.call_encode(:duration, duration, bolt_version)


  @doc """
  Encode POINT 2D & 3D (represented by Point) into Bolt binary format.
  Encoded in a structure.


  ## Point 2D
  Signature: `0x58`

  Encoding:
  `Marker` `Size` `Signature` ` Content`

  where `Content` is:
  `SRID` `x_or_longitude` `y_or_latitude`

  ## Example

      iex> p = Bolt.Sips.Types.Point.create(:wgs_84, 65.43, 12.54)
      %Bolt.Sips.Types.Point{
              crs: "wgs-84",
              height: nil,
              latitude: 12.54,
              longitude: 65.43,
              srid: 4326,
              x: 65.43,
              y: 12.54,
              z: nil
            }
      iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_point(p, 2))
      <<0xB3, 0x58, 0xC9, 0x10, 0xE6, 0xC1, 0x40, 0x50, 0x5B, 0x85, 0x1E, 0xB8, 0x51, 0xEC, 0xC1,
      0x40, 0x29, 0x14, 0x7A, 0xE1, 0x47, 0xAE, 0x14>>

  ## Point 3D
  Signature: `0x58`

  Encoding:
  `Marker` `Size` `Signature` ` Content`

  where `Content` is:
  `SRID` `x_or_longitude` `y_or_latitude` `z_or_height`

  ## Example

      iex> p = Bolt.Sips.Types.Point.create(:wgs_84, 45.0003, 40.3245, 23.1)
      %Bolt.Sips.Types.Point{
              crs: "wgs-84-3d",
              height: 23.1,
              latitude: 40.3245,
              longitude: 45.0003,
              srid: 4979,
              x: 45.0003,
              y: 40.3245,
              z: 23.1
            }
      iex> :erlang.iolist_to_binary(Bolt.Sips.Internals.PackStream.EncoderV2.encode_point(p, 2))
      <<0xB4, 0x59, 0xC9, 0x13, 0x73, 0xC1, 0x40, 0x46, 0x80, 0x9, 0xD4, 0x95, 0x18, 0x2B, 0xC1,
      0x40, 0x44, 0x29, 0x89, 0x37, 0x4B, 0xC6, 0xA8, 0xC1, 0x40, 0x37, 0x19, 0x99, 0x99, 0x99,
      0x99, 0x9A>>

  """
  @spec encode_point(Point.t(), integer()) :: Bolt.Sips.Internals.PackStream.value()
  def encode_point(%Point{z: nil} = point, bolt_version), do: EncoderHelper.call_encode(:point, point, bolt_version) 

  def encode_point(%Point{} = point, bolt_version), do: EncoderHelper.call_encode(:point, point, bolt_version)
end
