defmodule Phoenix.Router.Route do
  # This module defines the Route struct that is used
  # throughout Phoenix's router. This struct is private
  # as it contains internal routing information.
  @moduledoc false

  alias Phoenix.Router.Route

  @doc """
  The `Phoenix.Router.Route` struct. It stores:

    * `:verb` - the HTTP verb as an atom
    * `:line` - the line the route was defined
    * `:kind` - the kind of route, one of `:match`, `:forward`
    * `:path` - the normalized path as string
    * `:hosts` - the list of request hosts or host prefixes
    * `:plug` - the plug module
    * `:plug_opts` - the plug options
    * `:helper` - the name of the helper as a string (may be nil)
    * `:private` - the private route info
    * `:assigns` - the route info
    * `:pipe_through` - the pipeline names as a list of atoms
    * `:metadata` - general metadata used on telemetry events and route info
    * `:trailing_slash?` - whether or not the helper functions append a trailing slash
    * `:warn_on_verify?` - whether or not to warn on route verification
  """

  defstruct [:verb, :line, :kind, :path, :hosts, :plug, :plug_opts,
             :helper, :private, :pipe_through, :assigns, :metadata,
             :trailing_slash?, :warn_on_verify?]

  @type t :: %Route{}

  @doc "Used as a plug on forwarding"
  def init(opts), do: opts

  @doc "Used as a plug on forwarding"
  def call(%{path_info: path, script_name: script} = conn, {fwd_segments, plug, opts}) do
    new_path = path -- fwd_segments
    {base, ^new_path} = Enum.split(path, length(path) - length(new_path))
    conn = %{conn | path_info: new_path, script_name: script ++ base}
    conn = plug.call(conn, plug.init(opts))
    %{conn | path_info: path, script_name: script}
  end

  @doc """
  Receives the verb, path, plug, options and helper
  and returns a `Phoenix.Router.Route` struct.
  """
  @spec build(non_neg_integer, :match | :forward, atom, String.t, String.t | nil, atom, atom, atom | nil, list(atom), map, map, map, boolean, boolean) :: t
  def build(line, kind, verb, path, hosts, plug, plug_opts, helper, pipe_through, private, assigns, metadata, trailing_slash?, warn_on_verify?)
      when is_atom(verb) and is_list(hosts) and
           is_atom(plug) and (is_binary(helper) or is_nil(helper)) and
           is_list(pipe_through) and is_map(private) and is_map(assigns) and
           is_map(metadata) and kind in [:match, :forward] and
           is_boolean(trailing_slash?) do
    %Route{kind: kind, verb: verb, path: path, hosts: hosts, private: private,
           plug: plug, plug_opts: plug_opts, helper: helper,
           pipe_through: pipe_through, assigns: assigns, line: line, metadata: metadata,
           trailing_slash?: trailing_slash?, warn_on_verify?: warn_on_verify?}
  end

  @doc """
  Builds the compiled expressions used by the route.
  """
  def exprs(route, forwards) do
    {path, binding} = build_path_and_binding(route)

    %{
      path: path,
      binding: binding,
      dispatch: build_dispatch(route, forwards),
      hosts: build_host_match(route.hosts),
      path_params: build_path_params(binding),
      prepare: build_prepare(route),
      verb_match: verb_match(route.verb)
    }
  end

  def build_host_match([]), do: [Plug.Router.Utils.build_host_match(nil)]
  def build_host_match([_ | _] = hosts) do
    for host <- hosts, do: Plug.Router.Utils.build_host_match(host)
  end

  defp verb_match(:*), do: Macro.var(:_verb, nil)
  defp verb_match(verb), do: verb |> to_string() |> String.upcase()

  defp build_path_params(binding), do: {:%{}, [], binding}

  defp build_path_and_binding(%Route{path: path} = route) do
    {_params, segments} =
      case route.kind do
        :forward -> Plug.Router.Utils.build_path_match(path <> "/*_forward_path_info")
        :match   -> Plug.Router.Utils.build_path_match(path)
      end

    rewrite_segments(segments)
  end

  # We rewrite segments to use consistent variable naming as we want to group routes later on.
  defp rewrite_segments(segments) do
    {segments, {binding, _counter}} =
      Macro.prewalk(segments, {[], 0}, fn
        {name, _meta, nil}, {binding, counter} when is_atom(name) and name != :_forward_path_info ->
          var = Macro.var(:"arg#{counter}", __MODULE__)
          {var, {[{Atom.to_string(name), var} | binding], counter + 1}}

        other, acc ->
          {other, acc}
      end)

    {segments, Enum.reverse(binding)}
  end

  defp build_prepare(route) do
    {match_params, merge_params} = build_params()
    {match_private, merge_private} = build_prepare_expr(:private, route.private)
    {match_assigns, merge_assigns} = build_prepare_expr(:assigns, route.assigns)

    match_all = match_params ++ match_private ++ match_assigns
    merge_all = merge_params ++ merge_private ++ merge_assigns

    if merge_all != [] do
      quote do
        %{unquote_splicing(match_all)} = var!(conn, :conn)
        %{var!(conn, :conn) | unquote_splicing(merge_all)}
      end
    else
      quote do
        var!(conn, :conn)
      end
    end
  end

  defp build_prepare_expr(_key, data) when data == %{}, do: {[], []}

  defp build_prepare_expr(key, data) do
    var = Macro.var(key, :conn)
    merge = quote(do: Map.merge(unquote(var), unquote(Macro.escape(data))))
    {[{key, var}], [{key, merge}]}
  end

  defp build_dispatch(%Route{kind: :match, plug: plug, plug_opts: plug_opts}, _forwards) do
    quote do
      {unquote(plug), unquote(Macro.escape(plug_opts))}
    end
  end

  defp build_dispatch(%Route{kind: :forward, plug: plug, plug_opts: plug_opts}, forwards) do
    segments = Map.fetch!(forwards, plug)

    quote do
      {
        Phoenix.Router.Route,
        {unquote(segments), unquote(plug), unquote(Macro.escape(plug_opts))}
      }
    end
  end

  defp build_params() do
    params = Macro.var(:params, :conn)
    path_params = Macro.var(:path_params, :conn)
    merge_params = quote(do: Phoenix.Router.Route.merge_params(unquote(params), unquote(path_params)))

    {
      [{:params, params}],
      [{:params, merge_params}, {:path_params, path_params}]
    }
  end

  @doc """
  Merges params from router.
  """
  def merge_params(%Plug.Conn.Unfetched{}, path_params), do: path_params
  def merge_params(params, path_params), do: Map.merge(params, path_params)
end
