defmodule Eflatbuffers.Reader do
  alias Eflatbuffers.Utils

  def read({ :bool, _options }, vtable_pointer, data, _) do
    case read_from_data_buffer(vtable_pointer, data, 8) do
      << 0 >> -> false
      << 1 >> -> true
    end
  end

  def read({ :byte, _options }, vtable_pointer, data, _) do
    << value :: signed-size(8) >> = read_from_data_buffer(vtable_pointer, data, 8)
    value
  end

  def read({ :ubyte, _options }, vtable_pointer, data, _) do
    << value :: unsigned-size(8) >> = read_from_data_buffer(vtable_pointer, data, 8)
    value
  end

  def read({ :short, _options}, vtable_pointer, data, _) do
    << value :: signed-little-size(16) >> = read_from_data_buffer(vtable_pointer, data, 16)
    value
  end

  def read({ :ushort, _options},  vtable_pointer, data, _) do
    << value :: unsigned-little-size(16) >> = read_from_data_buffer(vtable_pointer, data, 16)
    value
  end

  def read({ :int, _options},  vtable_pointer, data, _) do
    << value :: signed-little-size(32) >> = read_from_data_buffer(vtable_pointer, data, 32)
    value
  end

  def read({ :uint, _options},  vtable_pointer, data, _) do
    << value :: unsigned-little-size(32) >> = read_from_data_buffer(vtable_pointer, data, 32)
    value
  end

  def read({ :float, _options},  vtable_pointer, data, _) do
    << value :: float-little-size(32) >> = read_from_data_buffer(vtable_pointer, data, 32)
    value
  end

  def read({ :long, _options},  vtable_pointer, data, _) do
    << value :: signed-little-size(64) >> = read_from_data_buffer(vtable_pointer, data, 64)
    value
  end

  def read({ :ulong, _options},  vtable_pointer, data, _) do
    << value :: unsigned-little-size(64) >> = read_from_data_buffer(vtable_pointer, data, 64)
    value
  end

  def read({ :double, _options},  vtable_pointer, data, _) do
    << value :: float-little-size(64) >> = read_from_data_buffer(vtable_pointer, data, 64)
    value
  end

  # complex types

  def read({ :string, _options},  vtable_pointer, data, _) do
    << string_offset :: unsigned-little-size(32) >> = read_from_data_buffer(vtable_pointer, data, 32)
    string_pointer = vtable_pointer + string_offset
    << _ :: binary-size(string_pointer), string_length :: unsigned-little-size(32), string :: binary-size(string_length), _ :: binary >> = data
    string
  end

  def read({:vector, options}, vtable_pointer, data, schema) do
    type = options.type
    << vector_offset :: unsigned-little-size(32) >> = read_from_data_buffer(vtable_pointer, data, 32)
    vector_pointer = vtable_pointer + vector_offset
    << _ :: binary-size(vector_pointer), vector_count :: unsigned-little-size(32), _ :: binary >> = data
    is_scalar = Utils.scalar?(type)
    read_vector_elements(type, is_scalar, vector_pointer + 4, vector_count, data, schema)
  end

  def read({:enum, %{ name: enum_name }}, vtable_pointer, data, {tables, _options} = schema) do
    {:enum, options} =  Map.get(tables, enum_name)
    members = options.members
    type    = options.type
    index = read(type, vtable_pointer, data, schema)
    case Map.get(members, index) do
      nil ->
        throw({:error, {:not_in_enum, index, members}})
      value_atom ->
        Atom.to_string(value_atom)
    end
  end

  # read a complete table, given a pointer to the springboard
  def read({:table, %{ name: table_name }}, table_pointer_pointer, data, {tables, _options} = schema) when is_atom(table_name) do
    << _ :: binary-size(table_pointer_pointer), table_offset :: little-size(32), _ :: binary >> = data
    table_pointer     = table_pointer_pointer + table_offset
    {:table, options} = Map.get(tables, table_name)
    fields            = options.fields
    << _ :: binary-size(table_pointer), vtable_offset :: little-signed-size(32), _ :: binary >> = data
    vtable_pointer = table_pointer - vtable_offset
    << _ :: binary-size(vtable_pointer), vtable_length :: little-size(16), _data_buffer_length :: little-size(16), _ :: binary >> = data
    vtable_fields_pointer = vtable_pointer + 4
    vtable_fields_length  = vtable_length  - 4
    << _ :: binary-size(vtable_fields_pointer), vtable :: binary-size(vtable_fields_length), _ :: binary >> = data
    data_buffer_pointer = table_pointer
    read_table_fields(fields, vtable, data_buffer_pointer, data, schema)
  end

  # fail if nothing matches
  def read({type, _}, _, _, _) do
    throw({:error, {:unknown_type, type}})
  end

  def read_vector_elements(_, _, _, 0, _, _) do
    []
  end

  def read_vector_elements(type, true, vector_pointer, vector_count, data, schema) do
    value  = read(type, vector_pointer, data, schema)
    offset = Utils.scalar_size(Utils.extract_scalar_type(type, schema))
    [value | read_vector_elements(type, true, vector_pointer + offset, vector_count - 1, data, schema)]
  end

  def read_vector_elements(type, false, vector_pointer, vector_count, data, schema) do
    value  = read(type, vector_pointer, data, schema)
    offset = 4
    [value | read_vector_elements(type, false, vector_pointer + offset, vector_count - 1, data, schema)]
  end

  # this is a utility that just reads data_size bytes from data after data_pointer
  def read_from_data_buffer(data_pointer, data, data_size) do
    << _ :: binary-size(data_pointer), value :: bitstring-size(data_size), _ :: binary >> = data
    value
  end

  def read_table_fields(fields, vtable, data_buffer_pointer, data, schema) do
    read_table_fields(fields, vtable, data_buffer_pointer, data, schema, %{})
  end

  # we might still have more fields but we ran out of vtable slots
  # this happens if the schema has more fields than the data (schema evolution)
  def read_table_fields(_, <<>>, _, _, _, map) do
    map
  end

  # we might have more data but no more fields
  # that means the data is ahead and has more data than the schema
  def read_table_fields([], _, _, _, _, map) do
    map
  end

  def read_table_fields([{name, {:union, %{ name: union_name }}} | fields], << data_offset :: little-size(16), vtable :: binary >>, data_buffer_pointer, data, {tables, _options} = schema, map) do
    # for a union byte field named $fieldname$_type is prefixed
    union_index = read({ :byte, %{ default: 0 }}, data_buffer_pointer + data_offset, data, schema)
    case union_index do
      0 ->
        # index is null, so field is not set
        # carry on
        read_table_fields(fields, vtable, data_buffer_pointer, data, schema, map)
      _ ->
        # we have a table set so we get the type and
        # expect it as the next record in the vtable
        {:union, options} = Map.get(tables, union_name)
        members           = options.members
        union_type        = Map.get(members, union_index - 1)
        union_type_key    = String.to_atom(Atom.to_string(name) <> "_type")
        map_new           = Map.put(map, union_type_key, Atom.to_string(union_type))
        read_table_fields([{name, {:table, %{ name: union_type }}} | fields], vtable, data_buffer_pointer, data, schema, map_new)
    end
  end
  # we find a null pointer
  # so we set the dafault
  def read_table_fields([{name, {:enum, options }} | fields], << 0, 0, vtable :: binary >>, data_buffer_pointer, data, {tables, _} = schema, map) do
    {_, enum_options} = Map.get(tables, options.name)
    {_, %{ default: default }} = enum_options.type

    map_new = Map.put(map, name, Atom.to_string(Map.get(enum_options.members, default)))
    read_table_fields(fields, vtable, data_buffer_pointer, data, schema, map_new)
  end
  def read_table_fields([{name, { _type, options }} | fields],     << 0, 0, vtable :: binary >>, data_buffer_pointer, data, schema, map) do
    map_new =
    case Map.get(options, :default) do
      nil     -> map
      default -> Map.put(map, name, default)
    end
    read_table_fields(fields, vtable, data_buffer_pointer, data, schema, map_new)
  end
  def read_table_fields([{name, type} | fields], << data_offset :: little-size(16), vtable :: binary >>, data_buffer_pointer, data, schema, map) do
    value   = read(type, data_buffer_pointer + data_offset, data, schema)
    map_new = Map.put(map, name, value)
    read_table_fields(fields, vtable, data_buffer_pointer, data, schema, map_new)
  end

end
