defmodule ExFirebase do
  @moduledoc """
  Provides interfaces for Firebase API calls.
  Also, the following modules provides object-based operation interface.

    - ExFirebase.Records : Helper for Elixir's record-based operations
    - ExFirebase.Objects : Helper for getting/posting objects
  """
  alias ExFirebase.HTTP
  require Record

  @doc """
  Setup base url for Firebase (https://xxx.firebaseio.com/).
  """
  def set_url(url) do
    ExFirebase.Setting.set_url(url)
    :ok
  end

  @doc """
  Get json url for the specified path concatinated with the
  prespecified base url.
  """
  def get_url(path, options \\ nil) do
    generate_url(ExFirebase.Setting.get_url(path), get_auth_token, options)
  end

  @doc """
  Setup auth token for accesing the API.
  """
  def set_auth_token(token) do
    ExFirebase.Setting.set_auth_token(token)
  end

  @doc """
  Returns auth token previously specified with 'set_auth_token'
  """
  def get_auth_token do
    ExFirebase.Setting.get_auth_token
  end

  @doc """
  Get objects on the specified path.
  """
  def get do
    ExFirebase.get("")
  end

  def get(path) do
    send_request(path, &HTTP.get/1)
  end

  @doc """
  Put specified object.
  It replaces the data on the specified path.
  """
  def put(path, object) do
    send_request(path, &HTTP.put/2, object)
  end

  @doc """
  Push the object into the first position of the items
  """
  def push(path, object) do
    items = ExFirebase.get(path)
    ExFirebase.put(path, [object|items])
  end

  @doc """
  Append the object into the last position of the items
  """
  def append(path, object) do
    items = ExFirebase.get(path)
    ExFirebase.put(path, items ++ [object])
  end

  @doc """
  Post specified object.
  It appends the data to the specified path.
  """
  def post(path, object) do
    send_request(path, &HTTP.post/2, object)
  end

  @doc """
  Update data on the specified path with the specified object.
  """
  def patch(path, object) do
    send_request(path, &HTTP.patch/2, object)
  end

  @doc """
  Delete objects on the specified path.
  """
  def delete(path) do
    send_request(path, &HTTP.delete/1)
  end

  @doc """
  Get objects on the specified path in raw json format.
  Options can be used to provide additional parameter for requqest.

    - options[pretty: true] : Specify 'print=pretty' for human readable format.
  """
  def get_raw_json(path \\ "", options \\ nil) do
    HTTP.get(get_url(path, options))
  end

  @doc """
  Send the request to server and returns response in tuple/list format
  """
  def send_request(path, method) do
    method.(get_url(path)) |> parse_json
  end

  @doc """
  Send the request to server and returns response in tuple/list format
  """
  def send_request(path, method, data) do
    method.(get_url(path), JSX.encode!(data)) |> parse_json
  end

  defp generate_url(url, token, options) do
    params =
      (parse_option_param(options) ++ parse_token_param(token))
        |> Enum.join("&")

    if params == "" do
      url
    else
      url <> "?" <> params
    end
  end

  defp parse_token_param(nil), do: []
  defp parse_token_param(token) do
    ["auth=#{token}"]
  end

  defp parse_option_param(nil), do: []
  defp parse_option_param(options) do
    if options[:pretty] == true do
      ["print=pretty"]
    else
      []
    end
  end

  defp parse_json("null"), do: []

  defp parse_json(string) do
    JSX.decode!(string) |> parse_object
  end

  defp parse_object(object) when Record.is_record(object, HashDict) do
    Enum.map(object.to_list, &parse_object/1)
  end

  defp parse_object(object) when is_tuple(object) do
    Enum.map(Tuple.to_list(object), &parse_object/1) |> List.to_tuple
  end

  defp parse_object(object) when is_list(object) do
    Enum.map(object, &parse_object/1)
  end

  defp parse_object(object) do
    object
  end
end
