defmodule Eflatbuffers.Generator do

  @defaults %{
    max_string_len: 100,
    max_vector_len: 15,
    skip_key_probability: 0.1,
    default_probability: 0.1
  }

  def map_from_schema(schema_str, opts \\ %{}) when is_binary(schema_str) do
    {:ok, {schema, %{root_type: root_type}}} =
      schema_str
      |> Eflatbuffers.Schema.lexer
      |> :schema_parser.parse
    gen_type(schema, root_type, Map.merge(@defaults, opts))
  end

  def gen_type(_schema, :byte, opts), do: random_num_signed(1) |> maybe_default(0, opts)
  def gen_type(_schema, :ubyte, opts), do: random_num_unsigned(1) |> maybe_default(0, opts)

  def gen_type(_schema, :short, opts), do: random_num_signed(2) |> maybe_default(0, opts)
  def gen_type(_schema, :ushort, opts), do: random_num_unsigned(2) |> maybe_default(0, opts)

  def gen_type(_schema, :int, opts), do: random_num_signed(4) |> maybe_default(0, opts)
  def gen_type(_schema, :uint, opts), do: random_num_unsigned(4) |> maybe_default(0, opts)

  def gen_type(_schema, :long, opts), do: random_num_signed(8) |> maybe_default(0, opts)
  def gen_type(_schema, :ulong, opts), do: random_num_signed(8) |> maybe_default(0, opts)

  def gen_type(_schema, :float, opts), do: random_float(4) |> maybe_default(0.0, opts)
  def gen_type(_schema, :double, opts), do: random_float(8) |> maybe_default(0.0, opts)

  def gen_type(_schema, :bool, opts), do: random_bool() |> maybe_default(false, opts)

  def gen_type(_schema, :string, opts), do: random_string(:rand.uniform(opts.max_string_len))

  def gen_type(schema, {:vector, type}, opts) do
    1..opts.max_vector_len
    |> Enum.map(fn(_) -> gen_type(schema, type, opts) end)
  end

  def gen_type(_schema, {{:enum, _type}, enum_options}, _opts) do
    [elem] = Enum.take_random(enum_options, 1)
    Atom.to_string(elem)
  end

  def gen_type(schema, {:union, types}, opts) do
    [type] = Enum.take_random(types, 1)
    [Atom.to_string(type), gen_type(schema, type, opts)]
  end

  def gen_type(schema, {:table, types}, opts) do
    types
    |> Enum.filter(fn(_) -> :rand.uniform() > opts.skip_key_probability end)
    |> Enum.map(fn ({name, type}) ->
        case gen_type(schema, type, opts) do
          [union_type, union_data] ->
            [{String.to_atom(Atom.to_string(name) <> "_type"), union_type}, {name, union_data}]
          data ->
            {name, data}
        end
      end)
    |> List.flatten
    |> Enum.into(%{})
  end

  def gen_type(schema, {type, _default}, opts) do
    gen_type(schema, type, opts)
  end

  def gen_type(schema, type, opts) do
    case Map.get(schema, type) do
      nil      -> throw ("type not found: #{inspect(type)}")
      type_def -> gen_type(schema, type_def, opts)
    end
  end

  ### random generators ###

  def random_num_signed(size) do
    bit_size = size * 8
    << num :: signed-size(bit_size) >> = random_bytes(size)
    num
  end

  def random_num_unsigned(size) do
    bit_size = size * 8
    << num :: unsigned-size(bit_size) >> = random_bytes(size)
    num
  end

  def random_float(_) do
    :rand.uniform()
  end

  def random_bool() do
    case :rand.uniform(2) do
      1 -> false
      2 -> true
    end
  end

  def random_string(size) do
    alphabet =
      "abcdefghijklmnopqrstuvwxyz"
      <> "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      <> "0123456789"
    alphabet_length = alphabet |> String.length()
    Enum.map_join(1..size, fn(_) -> alphabet |> String.at(:rand.uniform( alphabet_length ) - 1) end)
  end

  def random_bytes(size) do
    :crypto.strong_rand_bytes(size)
  end

  def maybe_default(value, default, opts) do
    prob = opts.default_probability
    case :rand.uniform do
      x when x < prob -> default
      _ -> value
    end
  end

end
