
# Helper for structured reading of values from a RustBuffer.
class RustBufferStream

  def initialize(rbuf)
    @rbuf = rbuf
    @offset = 0
  end

  def remaining
    @rbuf.len - @offset
  end

  def read(size)
    raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len

    data = @rbuf.data.get_bytes @offset, size

    @offset += size

    data
  end

  {% for typ in ci.iter_types() -%}
  {%- let canonical_type_name = canonical_name(typ).borrow()|class_name_rb -%}
  {%- match typ -%}

  {% when Type::Int8 -%}

  def readI8
    unpack_from 1, 'c'
  end

  {% when Type::UInt8 -%}

  def readU8
    unpack_from 1, 'c'
  end

  {% when Type::Int16 -%}

  def readI16
    unpack_from 2, 's>'
  end

  {% when Type::UInt16 -%}

  def readU16
    unpack_from 2, 'S>'
  end

  {% when Type::Int32 -%}

  def readI32
    unpack_from 4, 'l>'
  end

  {% when Type::UInt32 -%}

  def readU32
    unpack_from 4, 'L>'
  end

  {% when Type::Int64 -%}

  def readI64
    unpack_from 8, 'q>'
  end

  {% when Type::UInt64 -%}

  def readU64
    unpack_from 8, 'Q>'
  end

  {% when Type::Float32 -%}

  def readF32
    unpack_from 4, 'g'
  end

  {% when Type::Float64 -%}

  def readF64
    unpack_from 8, 'G'
  end

  {% when Type::Boolean -%}

  def readBool
    v = unpack_from 1, 'c'

    return false if v == 0
    return true if v == 1

    raise InternalError, 'Unexpected byte for Boolean type'
  end

  {% when Type::String -%}

  def readString
    size = unpack_from 4, 'l>'

    raise InternalError, 'Unexpected negative string length' if size.negative?

    read(size).force_encoding(Encoding::UTF_8)
  end

  {% when Type::Bytes -%}

  def readBytes
    size = unpack_from 4, 'l>'

    raise InternalError, 'Unexpected negative byte string length' if size.negative?

    read(size).force_encoding(Encoding::BINARY)
  end

  {% when Type::Timestamp -%}
  # The Timestamp type.
  ONE_SECOND_IN_NANOSECONDS = 10**9

  def read{{ canonical_type_name }}
    seconds = unpack_from 8, 'q>'
    nanoseconds = unpack_from 4, 'L>'

    # UniFFi conventions assume that nanoseconds part has to represent nanoseconds portion of
    # duration between epoch and the timestamp moment. Ruby `Time#tv_nsec` returns the number of
    # nanoseconds for the subsecond part, which is sort of opposite to "duration" meaning.
    # Hence we need to convert value returned by `Time#tv_nsec` back and forth with the following
    # logic:
    if seconds < 0 && nanoseconds != 0
      # In order to get duration nsec we shift by 1 second:
      nanoseconds = ONE_SECOND_IN_NANOSECONDS - nanoseconds

      # Then we compensate 1 second shift:
      seconds -= 1
    end

    Time.at(seconds, nanoseconds, :nanosecond, in: '+00:00').utc
  end

  {% when Type::Duration -%}
  # The Duration type.

  def read{{ canonical_type_name }}
    seconds = unpack_from 8, 'q>'
    nanoseconds = unpack_from 4, 'L>'

    Time.at(seconds, nanoseconds, :nanosecond, in: '+00:00').utc
  end

  {% when Type::Object with { name: object_name, module_path, imp } -%}
  # The Object type {{ object_name }}.

  def read{{ canonical_type_name }}
    pointer = FFI::Pointer.new unpack_from 8, 'Q>'
    return {{ object_name|class_name_rb }}.uniffi_allocate(pointer)
  end

  {% when Type::Enum { name, module_path } -%}
  {%- let e = ci|get_enum_definition(name) -%}
  {% if !ci.is_name_used_as_error(name) %}
  {% let enum_name = name %}
  # The Enum type {{ enum_name }}.

  def read{{ canonical_type_name }}
    variant = unpack_from 4, 'l>'
    {% if e.is_flat() -%}
    {%- for variant in e.variants() %}
    if variant == {{ loop.index }}
      return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }}
    end
    {%- endfor %}

    raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}'
    {%- else -%}
    {%- for variant in e.variants() %}
    if variant == {{ loop.index }}
        {%- if variant.has_fields() %}
        return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }}.new(
            {%- for field in variant.fields() %}
            self.read{{ canonical_name(field.as_type().borrow()).borrow()|class_name_rb }}(){% if loop.last %}{% else %},{% endif %}
            {%- endfor %}
        )
        {%- else %}
        return {{ enum_name|class_name_rb }}::{{ variant.name()|enum_name_rb }}.new
        {% endif %}
    end
    {%- endfor %}
    raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}'
    {%- endif %}
  end

  {% else %}

  {% let error_name = name %}

  # The Error type {{ error_name }}

  def read{{ canonical_type_name }}
    variant = unpack_from 4, 'l>'
    {% if e.is_flat() -%}
    {%- for variant in e.variants() %}
    if variant == {{ loop.index }}
      return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new(
        readString()
      )
    end
    {%- endfor %}

    raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}'
    {%- else -%}
    {%- for variant in e.variants() %}
    if variant == {{ loop.index }}
        {%- if variant.has_fields() %}
        return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new(
            {%- for field in variant.fields() %}
            read{{ canonical_name(field.as_type().borrow()).borrow()|class_name_rb }}(){% if loop.last %}{% else %},{% endif %}
            {%- endfor %}
        )
        {%- else %}
        return {{ error_name|class_name_rb }}::{{ variant.name()|class_name_rb }}.new
        {%- endif %}
    end
    {%- endfor %}

    raise InternalError, 'Unexpected variant tag for {{ canonical_type_name }}'
    {%- endif %}
  end
  {% endif %}

  {% when Type::Record { name: record_name, module_path } -%}
  {%- let rec = ci|get_record_definition(record_name) -%}
  # The Record type {{ record_name }}.

  def read{{ canonical_type_name }}
    {{ rec.name()|class_name_rb }}.new(
      {%- for field in rec.fields() %}
      {{ field.name()|var_name_rb }}: read{{ canonical_name(field.as_type().borrow()).borrow()|class_name_rb }}{% if loop.last %}{% else %},{% endif %}
      {%- endfor %}
    )
  end

  {% when Type::Optional { inner_type } -%}
  # The Optional<T> type for {{ canonical_name(inner_type) }}.

  def read{{ canonical_type_name }}
    flag = unpack_from 1, 'c'

    if flag == 0
      return nil
    elsif flag == 1
      return read{{ canonical_name(inner_type).borrow()|class_name_rb }}
    else
      raise InternalError, 'Unexpected flag byte for {{ canonical_type_name }}'
    end
  end

  {% when Type::Sequence { inner_type } -%}
  # The Sequence<T> type for {{ canonical_name(inner_type) }}.

  def read{{ canonical_type_name }}
    count = unpack_from 4, 'l>'

    raise InternalError, 'Unexpected negative sequence length' if count.negative?

    items = []

    count.times do
      items.append read{{ canonical_name(inner_type).borrow()|class_name_rb }}
    end

    items
  end

  {% when Type::Map { key_type: k, value_type: inner_type } -%}
  # The Map<T> type for {{ canonical_name(inner_type) }}.

  def read{{ canonical_type_name }}
    count = unpack_from 4, 'l>'
    raise InternalError, 'Unexpected negative map size' if count.negative?

    items = {}
    count.times do
      key = readString
      items[key] = read{{ canonical_name(inner_type).borrow()|class_name_rb }}
    end

    items
  end
  {%- else -%}
  # This type is not yet supported in the Ruby backend.
  def read{{ canonical_type_name }}
    raise InternalError, 'RustBufferStream.read not implemented yet for {{ canonical_type_name }}'
  end

  {%- endmatch -%}
  {%- endfor %}

  def unpack_from(size, format)
    raise InternalError, 'read past end of rust buffer' if @offset + size > @rbuf.len

    value = @rbuf.data.get_bytes(@offset, size).unpack format

    @offset += size

    # TODO: verify this
    raise 'more than one element!!!' if value.size > 1

    value[0]
  end
end

private_constant :RustBufferStream
