defmodule Mix.Tasks.Coh.Gen.Controllers do
  @moduledoc """
  Generate Coherence controllers.

  Use this task to generate the Coherence controllers when
  you what to customize the controllers' behavior.

  ## Examples

      # Install all the controllers for the coherence opts
      # defined in your `config/config.exs` file
      mix coh.gen.controllers

  Once the controllers have been generated, you must update your
  `router.ex` file to properly scope the generated controller(s).

  For example:

      # lib/my_app_web
      def MyAppWeb.Router do
        # ...

        scope "/", MyAppWeb do
          pipe_through :browser
          coherence_routes()
        end
        scope "/", MyAppWeb do
          pipe_through :protected
          coherence_routes :protected
        end
        # ...
      end

  ## Options

  * `--web-path` override the web path
  * `--no-confirm` silently overwrite files
  """
  use Mix.Task

  @shortdoc "Generate Coherence Controllers"

  @controller_files [
    confirmable: "confirmation_controller.ex",
    invitable: "invitation_controller.ex",
    recoverable: "password_controller.ex",
    registerable: "registration_controller.ex",
    authenticatable: "session_controller.ex",
    unlockable_with_token: "unlock_controller.ex"
  ]
  @default_booleans ~w(confirm)

  @switches [web_path: :string] ++ Enum.map(@default_booleans, &{String.to_atom(&1), :boolean})

  def run(args) do
    {opts, parsed, _unknown} = OptionParser.parse(args, switches: @switches)

    opts
    |> do_config(parsed)
    |> do_run
    |> router_instructions
  end

  defp do_run(config) do
    gen_controllers(config)
  end

  defp gen_controllers(%{binding: binding} = config) do
    files =
      @controller_files
      |> Enum.filter(&installed_option?(config, elem0(&1)))
      |> Enum.map(&{:eex, elem1(&1), elem1(&1)})

    copy_from(
      paths(),
      "priv/templates/coh.gen.controllers/controllers/coherence",
      web_path("controllers/coherence"),
      binding,
      files,
      config
    )
  end

  defp router_instructions(config) do
    web_base = ", " <> config.binding[:web_base]
    router = Application.get_env(:coherence, :router) |> inspect

    Mix.shell().info("""
    Modify your router.ex file and change the name spaces as indicated below:

    defmodule #{router} do
      # ...

      # Change this block
      scope "/"#{web_base} do
        pipe_through(:browser)
        coherence_routes()
      end

      # Change this block
      scope "/"#{web_base} do
        pipe_through(:protected)
        coherence_routes(:protected)
      end

      # ...
    end
    """)

    config
  end

  defp do_config(opts, _parsed) do
    binding =
      Mix.Project.config()
      |> Keyword.fetch!(:app)
      |> Atom.to_string()
      |> Mix.Phoenix.inflect()

    base = opts[:module] || binding[:base]
    web_base = opts[:web_module] || base <> "Web"

    binding =
      Keyword.merge(
        [
          base: base,
          web_base: web_base,
          opts: Keyword.put(opts, :base, base),
          web_path: opts[:web_path] || web_path(),
          web_module: web_base <> ".Coherence"
        ],
        binding
      )

    %{
      binding: binding,
      config_opts: get_config_opts()
    }
    |> do_default_opts(opts)
  end

  def do_default_opts(config, opts) do
    @default_booleans
    |> Enum.map(&String.to_atom/1)
    |> Enum.reduce(config, fn opt, config ->
      value = if opts[opt] == false, do: false, else: true
      Map.put(config, opt, value)
    end)
  end

  defp paths do
    [".", :coherence]
  end

  def copy_from(apps, source_dir, target_dir, binding, mapping, config) when is_list(mapping) do
    roots = Enum.map(apps, &to_app_source(&1, source_dir))

    # create_opts = if config[:confirm], do: [], else: [force: true]
    create_opts = []

    for {format, source_file_path, target_file_path} <- mapping do
      source =
        Enum.find_value(roots, fn root ->
          source = Path.join(root, source_file_path)
          if File.exists?(source), do: source
        end) || raise("could not find #{source_file_path} in any of the sources")

      target = Path.join(target_dir, target_file_path)

      contents =
        case format do
          :text -> File.read!(source)
          :eex -> EEx.eval_file(source, binding)
        end

      Mix.Generator.create_file(target, contents, create_opts)
    end

    config
  end

  defp to_app_source(path, source_dir) when is_binary(path),
    do: Path.join(path, source_dir)

  defp to_app_source(app, source_dir) when is_atom(app),
    do: Application.app_dir(app, source_dir)

  # defp lib_path(path \\ "") do
  #   Path.join ["lib", to_string(Mix.Phoenix.otp_app()), path]
  # end

  defp web_path(path \\ "") do
    otp_app = to_string(Mix.Phoenix.otp_app())
    Path.join(["lib", otp_app <> "_web", path])
  end

  defp get_config_opts do
    :coherence
    |> Application.get_env(:opts, [])
    |> Enum.map(fn
      {opt, _} -> opt
      opt -> opt
    end)
  end

  defp installed_option?(config, option) do
    option in config.config_opts
  end

  defp elem0(tuple), do: elem(tuple, 0)
  defp elem1(tuple), do: elem(tuple, 1)
end
