defmodule PhoenixSwagger do
  use Application
  alias PhoenixSwagger.Path
  alias PhoenixSwagger.Path.PathObject

  @moduledoc """
  The PhoenixSwagger module provides macros for defining swagger operations and schemas.

  Example:

      use PhoenixSwagger

      swagger_path :create do
        post "/api/v1/{team}/users"
        summary "Create a new user"
        consumes "application/json"
        produces "application/json"
        parameters do
          user :body, Schema.ref(:User), "user attributes"
          team :path, :string, "Users team ID"
        end
        response 200, "OK", Schema.ref(:User)
      end

      def swagger_definitions do
        %{
          User: swagger_schema do
            title "User"
            description "A user of the application"
            properties do
              name :string, "Users name", required: true
              id :string, "Unique identifier", required: true
              address :string, "Home adress"
            end
          end
        }
      end
  """

  @table :validator_table

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      # Define workers and child supervisors to be supervised
      # worker(Test.Worker, [arg1, arg2, arg3]),
    ]

    :ets.new(@table, [:public, :named_table])

    # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: Test.Supervisor]
    Supervisor.start_link(children, opts)
  end

  defmacro __using__(_) do
    quote do
      import PhoenixSwagger
      require PhoenixSwagger.Schema, as: Schema
      require PhoenixSwagger.JsonApi, as: JsonApi
    end
  end

  @doc """
  Builds a swagger schema map using a DSL from the functions defined in `PhoenixSwagger.Schema`.

  ## Example

      iex> use PhoenixSwagger
      ...> swagger_schema do
      ...>   title "Pet"
      ...>   description "A pet in the pet store"
      ...>   properties do
      ...>     id :integer, "Unique identifier", required: true, format: :int64
      ...>     name :string, "Pets name", required: true
      ...>     tags array(:string), "Tag categories for this pet"
      ...>   end
      ...>   additional_properties false
      ...> end
      %{
        "title" => "Pet",
        "type" => "object",
        "description" => "A pet in the pet store",
        "properties" => %{
          "id" => %{
            "description" => "Unique identifier",
            "format" => "int64",
            "type" => "integer"
          },
         "name" => %{
            "description" => "Pets name",
            "type" => "string"
          },
          "tags" => %{
            "description" => "Tag categories for this pet",
            "items" => %{
              "type" => "string"
            },
            "type" => "array"
          }
        },
        "required" => ["name", "id"],
        "additionalProperties" => false
      }

      iex> use PhoenixSwagger
      ...> swagger_schema do
      ...>   title "Phone"
      ...>   description "An 8 digit phone number with optional 2 digit area code"
      ...>   type :string
      ...>   max_length 11
      ...>   pattern ~S"^(\([0-9]{2}\))?[0-9]{4}-[0-9]{4}$"
      ...> end
      %{
        "description" => "An 8 digit phone number with optional 2 digit area code",
        "maxLength" => 11,
        "pattern" => "^(\\([0-9]{2}\\))?[0-9]{4}-[0-9]{4}$",
        "title" => "Phone",
        "type" => "string"
      }
  """
  defmacro swagger_schema(block) do
    exprs =
      case block do
        [do: {:__block__, _, exprs}] -> exprs
        [do: expr] -> [expr]
      end

    acc =
      quote do
        %Schema{type: :object}
      end

    body =
      Enum.reduce(exprs, acc, fn expr, acc ->
        quote do
          unquote(acc) |> unquote(expr)
        end
      end)

    # Immediately invoked anonymous function for locally scoped import
    quote do
      (fn ->
         import PhoenixSwagger.Schema
         alias PhoenixSwagger.Schema

         unquote(body)
         |> PhoenixSwagger.to_json()
       end).()
    end
  end

  @doc """
  Swagger operations (aka "paths") are defined inside a `swagger_path` block.

  Within the do-end block, the DSL provided by the `PhoenixSwagger.Path` module can be used.
  The DSL is any chain of functions with first argument being a `PhoenixSwagger.Path.PathObject` struct.

  The verb and path can be set explicitly using the `get`, `put`, `post`, `patch`, `delete` functions, or
  inferred from the phoenix router automatically.

  Swagger `tags` will default to match the module name with trailing `Controller` removed.
  Eg operations defined in module MyApp.UserController will have `tags: ["User"]`.

  Swagger `operationId` will default to the fully qualified action function name.
  Eg `index` action in `MyApp.UserController` will have `operationId: "MyApp.UserController.index"`.

  ## Example

      defmodule ExampleController do
        use ExampleApp.Web, :controller
        use PhoenixSwagger

        swagger_path :index do
          get "/users"
          summary "Get users"
          description "Get users, filtering by account ID"
          parameter :query, :id, :integer, "account id", required: true
          response 200, "Description", :Users
          tag "users"
        end

        def index(conn, _params) do
          posts = Repo.all(Post)
          render(conn, "index.json", posts: posts)
        end
      end
  """
  defmacro swagger_path(action, do: {:__block__, _, [first_expr | exprs]}) do
    fun_name = "swagger_path_#{action}" |> String.to_atom()

    body =
      Enum.reduce(exprs, first_expr, fn expr, acc ->
        quote do
          unquote(acc) |> unquote(expr)
        end
      end)

    quote do
      def unquote(fun_name)(route) do
        import PhoenixSwagger.Path

        %PhoenixSwagger.Path.PathObject{}
        |> unquote(body)
        |> PhoenixSwagger.ensure_operation_id(__MODULE__, unquote(action))
        |> PhoenixSwagger.ensure_tag(__MODULE__)
        |> PhoenixSwagger.ensure_verb_and_path(route)
        |> PhoenixSwagger.Path.nest()
        |> PhoenixSwagger.to_json()
      end
    end
  end

  @doc false
  # Add a default operationId based on model name and action if required
  def ensure_operation_id(path = %PathObject{operation: %{operationId: ""}}, module, action) do
    Path.operation_id(path, String.replace_prefix("#{module}.#{action}", "Elixir.", ""))
  end

  def ensure_operation_id(path, _module, _action), do: path

  @doc false
  # Add a default tag based on controller module name if none present
  def ensure_tag(path = %PathObject{operation: %{tags: []}}, module) do
    tags =
      module
      |> Module.split()
      |> Enum.reverse()
      |> hd()
      |> String.split("Controller")
      |> Enum.filter(&(String.length(&1) > 0))

    put_in(path.operation.tags, tags)
  end

  def ensure_tag(path, _module), do: path

  @doc false
  # Allow the verb and path to be inferred from the phoenix route
  def ensure_verb_and_path(path = %PathObject{verb: nil, path: nil}, route) do
    %{path | verb: route.verb, path: route.path}
  end

  def ensure_verb_and_path(path, _route), do: path

  @doc """
  Use JSON library from phoenix configuration
  """
  def json_library do
    Application.get_env(:phoenix_swagger, :json_library, Poison)
  end

  @doc false
  # Converts a Schema struct to regular map, removing nils
  def to_json(value = %{__struct__: _}) do
    value
    |> Map.from_struct()
    |> to_json()
  end

  def to_json(value) when is_map(value) do
    value
    |> Enum.map(fn {k, v} -> {to_string(k), to_json(v)} end)
    |> Enum.filter(fn
      {_, :null} -> false
      _ -> true
    end)
    |> Enum.into(%{})
  end

  def to_json(value) when is_list(value) do
    Enum.map(value, &to_json/1)
  end

  def to_json(nil) do
    :null
  end

  def to_json(:null) do
    :null
  end

  def to_json(true) do
    true
  end

  def to_json(false) do
    false
  end

  def to_json(value) when is_atom(value) do
    to_string(value)
  end

  def to_json(value) do
    value
  end
end
