defmodule Drab.Config do
  @moduledoc """
  Drab configuration.

  Drab works over the Phoenix Endpoints. You must provide configuration for each endpoint. The
  minimum is to set up the endpoint module name, and the application name, so for the application
  configured like:

      config :my_app_web, MyAppWeb.Endpoint,
        ...

  provide corresponding Drab configuration:

      config :drab, MyAppWeb.Endpoint,
        otp_app: :my_app_web,
        ...

  There are also global Drab options, which can't be configured by the endpoint, like
  `:enable_live_scripts`. See the whole list below.

  ## Configuration options:

  ### Endpoint related options

  This options are set within the endpoint:

      config :drab, MyAppWeb.Endpoint, option: value, option: value

  #### :access_session *(default: `[]`)*
    Keys of the session map, which will be included to the Drab Session globally, usually
    `:user_id`, etc. See `Drab.Commander.access_session/1` for more.

  #### :browser_response_timeout *(default: `5000`)*
    Timeout, after which all functions querying/updating browser UI will give up; integer in
    milliseconds, or `:infinity`.

  #### :disable_controls_while_processing *(default: `true`)*
    After sending request to the server, sender object will be disabled until it gets the answer.
    Warning: this behaviour is not broadcasted, so only the control in the current browser is going
    to be disabled.

  #### :disable_controls_when_disconnected *(default: `true`)*
    Shall controls be disabled when there is no connectivity between the browser and the server?

  #### :drab_store_storage *(default: `:session_storage`)*
    Where to keep the Drab Store - `:memory`, `:local_storage` or `:session_storage`. Data in
    the memory is kept to the next page load, session storage persist until browser (or a tab)
    is closed, local storage is kept forever.

  #### :events_shorthands *(default: `["click", "change", "keyup", "keydown"]`)*
    The list of the shorthand attributes to be used in drab-controlled DOM object, ie:
    `<drab-click="handler">`. Please keep the list small, as it affects the client JS performance.

  #### :events_to_disable_while_processing *(default: `["click"]`)*
    Controls with those Drab events will be disabled when waiting for server response.

  #### :js_socket_constructor, *(default: `"require(\"phoenix\").Socket"`)*
    Javascript constructor for the Socket; more info in Drab.Client.

  #### :live_conn_pass_through, *(default: `%{private: %{phoenix_endpoint: true,
                                  phoenix_controller: true, phoenix_action: true}}`)*
    A deep map marking fields which should be preserved in the fake `@conn` assign. See `Drab.Live`
    for more detailed explanation on conn case.

  #### :socket *(default: `"/socket"`)*
    Path to the socket on which Drab operates.

  #### :templates_path *(default: "priv/templates/drab")*
    Path to the user-defined Drab templates (not to be confused with Phoenix application templates,
    these are to be used internally, see `Drab.Modal` for the example usage). Must start with
    "priv/".

  #### :token_max_age *(default: `86_400`)*
    Socket token max age in seconds.

  ### Global configuration options
  Those options are set globally and works in every endpoint.

      config :drab, option: value, option: value

  #### :default_encoder *(default: `Drab.Coder.Cipher`)*
    Sets the default encoder/decoder for the various functions, like `Drab.Browser.set_cookie/3`.

  #### :default_modules *(default: `[Drab.Live, Drab.Element, Drab.Modal]`)*
    Sets the default Drab Modules. May be overwritten individually in the commander with
    `use Drab.Commander, modules: [...]`.

  #### :enable_live_scripts *(default: `false`)*
    Re-evaluation of JavaScripts containing living assigns is disabled by default.

  #### :modal_css *(default: `:bootstrap3`)*
    A CSS framework used to show `Drab.Modal`. Available: `:bootstrap3`, `:bootstrap4`.

  #### :phoenix_channel_options *(default: `[]`)*
    An options passed to `use Phoenix.Channel`, for example: `[log_handle_in: false]`.

  #### :presence *(default: `false`)*
    Runs the `Drab.Presence` server. Defaults to false to avoid unnecessary load. See
    `Drab.Presence` for more information.

  #### :secret_key_base *(default taken from endpoint)*
    Random key for ciphering. May be generated by `mix phx.gen.secret`. In single-endpoint
    configuration it is taken from the endpoint.
  """

  @doc """
  Returns the name of the client Phoenix Application for given endpoint.

      iex> Drab.Config.app_name(DrabTestApp.Endpoint)
      :drab
  """
  @spec app_name(atom) :: atom | no_return
  def app_name(endpoint) do
    case :drab |> Application.get_env(endpoint, []) |> Keyword.fetch(:otp_app) do
      {:ok, app} -> app
      :error -> raise_app_not_found()
    end
  end

  @doc false
  @spec get_all_env(atom) :: Keyword.t() | no_return
  defp get_all_env(endpoint) do
    Application.get_env(:drab, endpoint) || raise_app_not_found()
  end

  @spec get_env(atom, atom, any) :: any
  defp get_env(endpoint, key, default) do
    Keyword.get(get_all_env(endpoint), key, default)
  end

  @doc """
  Returns the name of all configured Drab applications.

      iex> Drab.Config.app_names()
      [:drab]
  """
  @spec app_names() :: [atom]
  def app_names() do
    Enum.map(app_endpoints(), &app_name/1)
  end

  @doc """
  Returns the name of all configured Drab endpoints.

      iex> Drab.Config.app_endpoints()
      [DrabTestApp.Endpoint]
  """
  @spec app_endpoints() :: [atom]
  def app_endpoints() do
    :drab
    |> Application.get_all_env()
    |> Enum.filter(fn {x, _} -> is_module?(x) end)
    |> Keyword.keys()
  end

  @spec is_module?(any) :: boolean
  defp is_module?(atom) when is_atom(atom), do: is_module?(Atom.to_string(atom))
  defp is_module?("Elixir." <> _), do: true
  defp is_module?(_), do: false

  @spec raise_app_not_found :: no_return
  defp raise_app_not_found() do
    raise """
        Drab can't find the web application or endpoint name.

        Please add your app name and the endpoint to the config.exs:

            config :drab, main_phoenix_app: :my_app_web, endpoint: MyAppWeb.Endpoint
    """
  end

  @doc """
  Returns the PubSub module of the given endpoint.

      iex> Drab.Config.pubsub(DrabTestApp.Endpoint)
      DrabTestApp.PubSub
  """
  @spec pubsub(atom) :: atom | no_return
  def pubsub(endpoint) do
    with app <- app_name(endpoint),
         config <- Application.get_env(app, endpoint),
         {:ok, pubsub_conf} <- Keyword.fetch(config, :pubsub),
         {:ok, name} <- Keyword.fetch(pubsub_conf, :name) do
      name
    else
      _ -> raise_app_not_found()
    end
  end

  @doc false
  @spec default_pubsub() :: atom | no_return
  def default_pubsub() do
    case app_endpoints() do
      [endpoint] ->
        with app <- app_name(endpoint),
             config <- Application.get_env(app, endpoint),
             {:ok, pubsub_conf} <- Keyword.fetch(config, :pubsub),
             {:ok, name} <- Keyword.fetch(pubsub_conf, :name) do
          name
        else
          _ -> raise_app_not_found()
        end

      _ ->
        raise """
        Can't find the default PubSub module. Please ensure that it is set in config.exs.

        In multiple endpoint environments, broadcasting with a topic requires endpoint to
        be specified:

            broadcast_js same_topic(MyAppWeb.Endpoint, "product_10"), "console.log(2+2);"
        """
    end
  end

  @doc false
  @spec default_endpoint() :: atom | no_return
  def default_endpoint() do
    case app_endpoints() do
      [endpoint] ->
        endpoint

      _ ->
        raise """
        Can't find the default Endpoint module. Please ensure that it is set in config.exs.

        In multiple endpoint environments, you must specify which enpoint to use with presence:

            config :drab, :presence, endpoint: MyAppWeb.Endpoint
        """
    end
  end

  @doc false
  @spec secret_key_base() :: String.t() | no_return
  def secret_key_base() do
    get(:secret_key_base) ||
      case app_endpoints() do
        [endpoint] ->
          with app <- app_name(endpoint),
               config <- Application.get_env(app, endpoint),
               {:ok, secret_key_base} <- Keyword.fetch(config, :secret_key_base) do
            secret_key_base
          else
            _ -> raise_app_not_found()
          end

        _ ->
          raise """
          Can't find the default secret key base. Please ensure that it is set in config.exs.

          In multiple endpoint environments, you must specify it globally for Drab:

              config :drab, secret_key_base: "remember to put it in prod_secret.exs"
          """
      end
  end

  @doc false
  @spec secret_key_base(atom) :: String.t() | no_return
  def secret_key_base(endpoint) do
    with app <- app_name(endpoint),
         config <- Application.get_env(app, endpoint),
         {:ok, secret_key_base} <- Keyword.fetch(config, :secret_key_base) do
      secret_key_base
    else
      _ -> raise_app_not_found()
    end
  end

  @doc """
  Returns configured Drab.Live.Engine Extension. String with dot at the begin.

  Example, for config:

      config :phoenix, :template_engines,
        drab: Drab.Live.Engine

  it will return ".drab"

      iex> Drab.Config.drab_extension()
      ".drab"
  """
  @spec drab_extension :: String.t()
  def drab_extension() do
    {drab_ext, Drab.Live.Engine} =
      Phoenix.Template.engines()
      |> Enum.find(fn {_, v} -> v == Drab.Live.Engine end)

    "." <> to_string(drab_ext)
  end

  @doc false
  @spec default_controller_for(atom | nil) :: atom | nil
  def default_controller_for(commander) do
    replace_last(commander, "Commander", "Controller")
  end

  @doc false
  @spec default_view_for(atom | nil) :: atom | nil
  def default_view_for(commander) do
    replace_last(default_controller_for(commander), "Controller", "View")
  end

  @doc false
  @spec default_commander_for(atom | nil) :: atom | nil
  def default_commander_for(controller) do
    replace_last(controller, "Controller", "Commander")
  end

  @spec replace_last(atom, String.t(), String.t()) :: atom
  defp replace_last(atom, from, to) do
    path = Module.split(atom)
    new_last = path |> List.last() |> String.replace(from, to)
    new_path = List.replace_at(path, -1, new_last)
    Module.concat(new_path)
  end

  @doc false
  @spec drab_internal_commanders() :: list
  def drab_internal_commanders() do
    [Drab.Logger]
  end

  @doc """
  Returns Drab configuration for the given atom.

      iex> Drab.Config.get(:default_encoder)
      Drab.Coder.Cipher
  """
  @spec get(atom) :: term
  def get(key)

  def get(:enable_live_scripts), do: Application.get_env(:drab, :enable_live_scripts, false)

  def get(:phoenix_channel_options), do: Application.get_env(:drab, :phoenix_channel_options, [])

  def get(:default_encoder), do: Application.get_env(:drab, :default_encoder, Drab.Coder.Cipher)

  def get(:secret_key_base), do: Application.get_env(:drab, :secret_key_base, nil)

  def get(:presence), do: Application.get_env(:drab, :presence, false)

  def get(:default_modules),
    do: Application.get_env(:drab, :default_modules, [Drab.Live, Drab.Element, Drab.Modal])

  def get(:modal_css), do: Application.get_env(:drab, :modal_css, :bootstrap3)

  @doc """
  Returns Drab configuration for the given endpoint and atom.

      iex> Drab.Config.get(DrabTestApp.Endpoint, :templates_path)
      "priv/custom_templates"
  """
  @spec get(atom, atom) :: term
  def get(endpoint, key)

  def get(:presence, :id) do
    case get(:presence) do
      options when is_list(options) -> Keyword.get(options, :id, session: :user_id)
      _ -> nil
    end
  end

  def get(:presence, :endpoint) do
    case get(:presence) do
      options when is_list(options) -> Keyword.get(options, :endpoint, nil)
      _ -> nil
    end
  end

  def get(:presence, :module) do
    case get(:presence) do
      options when is_list(options) -> Keyword.get(options, :module, Drab.Presence)
      _ -> Drab.Presence
    end
  end

  def get(endpoint, :disable_controls_while_processing),
    do: get_env(endpoint, :disable_controls_while_processing, true)

  def get(endpoint, :events_to_disable_while_processing),
    do: get_env(endpoint, :events_to_disable_while_processing, ["click"])

  def get(endpoint, :events_shorthands),
    do: get_env(endpoint, :events_shorthands, ["click", "change", "keyup", "keydown"])

  def get(endpoint, :disable_controls_when_disconnected),
    do: get_env(endpoint, :disable_controls_when_disconnected, true)

  def get(endpoint, :drab_store_storage),
    do: get_env(endpoint, :drab_store_storage, :session_storage)

  def get(endpoint, :templates_path),
    do: get_env(endpoint, :templates_path, "priv/templates/drab")

  def get(endpoint, :token_max_age), do: get_env(endpoint, :token_max_age, 86_400)

  def get(endpoint, :socket), do: get_env(endpoint, :socket, "/socket")

  def get(endpoint, :browser_response_timeout),
    do: get_env(endpoint, :browser_response_timeout, 5000)

  def get(endpoint, :js_socket_constructor),
    do: get_env(endpoint, :js_socket_constructor, "require(\"phoenix\").Socket")

  def get(endpoint, :access_session) do
    if get(:presence) do
      [presence_session_id() | get_env(endpoint, :access_session, [])]
    else
      Application.get_env(endpoint, :access_session, [])
    end
  end

  def get(endpoint, :live_conn_pass_through) do
    get_env(endpoint, :live_conn_pass_through, %{
      private: %{
        phoenix_endpoint: true,
        phoenix_controller: true,
        phoenix_action: true
      }
    })
  end

  defp presence_session_id() do
    Keyword.get(get(:presence, :id), :session, nil)
  end
end
