defmodule Corsica.Router do
  @moduledoc """
  A router to handle and respond to CORS requests.

  This module provides facilities for creating
  [`Plug.Router`](https://hexdocs.pm/plug/Plug.Router.html)-based routers that
  handle CORS requests. A generated router will handle a CORS request by:

    * responding to it if it's a preflight request (refer to
      `Corsica.send_preflight_resp/4` for more information) or
    * adding the right CORS headers to the `Plug.Conn` connection if it's a
      valid CORS request.

  When a module calls `use Corsica.Router`, it can pass the same options that
  can be passed to the `Corsica` plug to the `use` call. Look at the
  documentation for the `Corsica` module for more information about these
  options.

  CORS rules in a `Corsica.Router` can be defined through the `resource/2`
  macro.

  > #### `use Corsica.Router` {: .info}
  >
  > When you `use Corsica.Router`, your module will become a `Plug.Router`.

  ## Examples

      defmodule MyApp.CORS do
        use Corsica.Router,
          origins: ["http://foo.com", "http://bar.com"],
          allow_credentials: true,
          max_age: 600

        resource "/*"

        # We can override single settings as well.
        resource "/public/*", allow_credentials: false
      end

  Now in your application's endpoint:

      defmodule MyApp.Endpoint do
        plug Plug.Head
        plug MyApp.CORS
        plug Plug.Static
        plug MyApp.Router
      end

  Note that a `Corsica.Router` router will always define a match-all route after
  the `resource` routes; this match-all route will simply return the connection
  unchanged, effectively continuing with the plug pipeline.
  """

  # A Corsica.Router is a wrapper around Plug.Router: the resource/2 macro will
  # just register resources (as a {route, options} tuple) in the @corsica_routes
  # module attribute, then Corsica.Router.__before_compile__/1 will take care of
  # converting the routes in @corsica_routes to calls to Plug.Router.options/2
  # and Plug.Router.match/2. __before_compile__/1 will also take care of
  # defining the final match-all clause for this router.
  @doc false
  defmacro __using__(opts) do
    quote do
      import unquote(__MODULE__)

      @corsica_router_opts unquote(opts)

      Module.register_attribute(__MODULE__, :corsica_routes, accumulate: true)
      @before_compile Corsica.Router

      use Plug.Router
      plug :match
      plug :dispatch
    end
  end

  @doc """
  Defines a CORS-enabled resource.

  This macro takes advantage of the macros defined by
  [`Plug.Router`](https://hexdocs.pm/plug/Plug.Router.html) (like `options/3`
  and `match/3`) in order to define regular `Plug.Router`-like routes that
  efficiently match on the request url; the bodies of the autogenerated routes
  just perform a couple of checks before calling either
  `Corsica.put_cors_simple_resp_headers/2` or `Corsica.send_preflight_resp/4`.

  Note that if the request is a CORS preflight request (whether it's a valid one
  or not), a response is immediately sent to the client (whether the request is
  a valid one or not). This behaviour, combined with the definition of an
  additional `OPTIONS` route to `route`, makes `Corsica.Router` ideal to just
  put before any router in a plug pipeline, letting it handle preflight requests
  by itself.

  The options given to `resource/2` are merged with the default options like it
  happens with the rest of the functions in the `Corsica` module. The `resource/2`
  macro also accepts the following options (similar to `Plug.Router.match/3`):

    * `:host` - the host which the route should match. Defaults to `nil`, meaning
    no host match, but can be a string like `"example.com"` or a string ending with `.`,
    like `"subdomain."`, for a subdomain match.

  ## Examples

      resource "/foo", origins: "*"
      resource "/wildcards/are/ok/*", max_age: 600
      resource "/only/on/subdomain", host: "mysubdomain."

  """
  defmacro resource(route, opts \\ []) do
    quote do
      @corsica_routes {unquote(route), unquote(Macro.escape(opts))}
    end
  end

  defmacro __before_compile__(env) do
    global_opts = Module.get_attribute(env.module, :corsica_router_opts)
    routes = Module.get_attribute(env.module, :corsica_routes) |> Enum.reverse()

    quote bind_quoted: [global_opts: Macro.escape(global_opts), routes: routes] do
      for {route, opts} <- routes do
        {match_opts, opts} = Keyword.split(opts, [:host])

        corsica_opts =
          global_opts
          |> Keyword.merge(opts)
          |> Corsica.sanitize_opts()
          |> Macro.escape()

        # Plug.Router wants this.
        route = route <> if(String.ends_with?(route, "*"), do: "_", else: "")

        options route, match_opts do
          conn = var!(conn)

          if Corsica.preflight_req?(conn) do
            Corsica.send_preflight_resp(conn, unquote(corsica_opts))
          else
            conn
          end
        end

        match route, match_opts do
          conn = var!(conn)

          if Corsica.cors_req?(conn) do
            Corsica.put_cors_simple_resp_headers(conn, unquote(corsica_opts))
          else
            conn
          end
        end
      end

      # If there is a match-all route like "/*", we don't do anything,
      # otherwise we add a match-all clause that just returns the
      # connection unchanged.
      unless Enum.any?(routes, &match?({"/*", _opts}, &1)) do
        match _ do
          var!(conn)
        end
      end
    end
  end
end
