defmodule Eflatbuffers do

  def parse_schema(schema_str) do
    Eflatbuffers.Schema.parse(schema_str)
  end

  def parse_schema!(schema_str) do
    case parse_schema(schema_str) do
      {:ok, schema}   -> schema
      error           -> throw error
    end
  end

  def write!(map, schema_str) when is_binary(schema_str) do
    write!(map, parse_schema!(schema_str))
  end

  def write!(map, {_, %{root_type: root_type} = options} = schema) do
    root_table = [<< vtable_offset :: little-size(16) >> | _] = Eflatbuffers.Writer.write({:table, %{ name: root_type }}, map, [], schema)

    file_identifier =
      case Map.get(options, :file_identifier) do
        << bin :: size(32) >> -> << bin :: size(32) >>
        _                     -> << 0, 0, 0, 0 >>
      end

    [<< (vtable_offset + 4 + byte_size(file_identifier)) :: little-size(32) >>, file_identifier, root_table]
    |> :erlang.iolist_to_binary
  end

  def write(map, schema_str) when is_binary(schema_str) do
    case parse_schema(schema_str) do
      {:ok, schema} -> write(map, schema)
      error         -> error
    end
  end

  def write(map, schema) do
    try do
      {:ok, write!(map, schema)}
    catch
      error -> error
    rescue
      error -> {:error, error}
    end
  end

  def read!(data, schema_str) when is_binary(schema_str) do
    read!(data, parse_schema!(schema_str))
  end

  def read!(data, {_, schema_options = %{root_type: root_type}} = schema) do
    match_identifiers(data, schema_options)
    Eflatbuffers.Reader.read({:table, %{ name: root_type }}, 0, data, schema)
  end

  def match_identifiers( << _::size(4)-binary, identifier_data::size(4)-binary, _::binary >>, schema_options) do
    case Map.get(schema_options, :file_identifier) do
      # nothing in schema
      nil                -> :ok
      # schema matches data
      ^identifier_data   -> :ok
      # defined in schema but data says something else
      identifier_schema  -> throw({:error, {:identifier_mismatch, %{data: identifier_data, schema: identifier_schema}}})
    end
  end

  def read(data, schema_str) when is_binary(schema_str) do
    case parse_schema(schema_str) do
      {:ok, schema} -> read(data, schema)
      error         -> error
    end
  end

  def read(data, schema) do
    try do
      {:ok, read!(data, schema)}
    catch
      error -> error
    rescue
      error -> {:error, error}
    end
  end

  def get(data, path, schema) do
    try do
      {:ok, get!(data, path, schema)}
    catch
      error -> error
    rescue
      error -> {:error, error}
    end
  end

  def get!(data, path, schema) when is_binary(schema) do
    get!(data, path, parse_schema!(schema))
  end
  def get!(data, path, {_tables, %{root_type: root_type}} = schema) do
    Eflatbuffers.RandomAccess.get(path, {:table, %{ name: root_type }}, 0, data, schema)
  end


end
