defmodule Mix.Tasks.Admin.Install do
  @moduledoc """
  Install ExAdmin supporting files.

      mix admin.install

  # Defaults:

    * assets - copy js, css, and image files
    * brunch - append instructions to brunch-config.js
    * config - add configuration to config/config.exs
    * dashboard - create a default dashboard
    * route - display instructions to add the admin routes

  ## Options:

    * --no-brunch - Write assets to priv/static instead of web/static/
    * --no-assets - Skip the assets
    * --no-config - Skip the config
    * --no-dashboard - Skip the dashboard
    * --no-route - Skip the route instructions
  """

  # @shortdoc "Install ExAdmin"

  use Mix.Task
  import Mix.ExAdmin.Utils
  import ExAdmin.Gettext

  @boolean_switchs ~w(assets config route dashboard brunch)a
  @switches Enum.map(@boolean_switchs, &{&1, :boolean})

  defmodule Config do
    @moduledoc false
    defstruct route: true,
              assets: true,
              dashboard: true,
              package_path: nil,
              config: true,
              brunch: true
  end

  def run(args) do
    do_run(args)
  end

  def do_run(args) do
    parse_args(args)
    |> do_install()
  end

  def do_install(config) do
    config
    |> check_project
    |> do_assets
    |> do_config
    |> do_dashboard
    |> do_route
    |> do_paging
    |> do_assets_instructions
  end

  def check_project(config) do
    config
    |> check_config
    |> check_assets
  end

  defp check_config(%{config: true} = config) do
    config_path = Path.join(~w(config config.exs))

    unless File.exists?(config_path) do
      Mix.raise("""
      Can't find #{config_path}
      """)
    end

    config
  end

  defp check_config(config), do: config

  defp check_assets(%{assets: true, brunch: true} = config) do
    unless File.exists?("brunch-config.js") do
      Mix.raise("""
      Can't find brunch-config.js
      """)
    end

    config
  end

  defp check_assets(%{assets: true} = config) do
    path = Path.join(~w(priv static))

    unless File.exists?(path) do
      Mix.raise("""
      Can't find #{path}
      """)
    end

    config
  end

  defp check_assets(config), do: config

  def do_assets(%Config{assets: true, brunch: true} = config) do
    base_path = Path.join(~w(priv static))

    File.mkdir_p(Path.join(~w{web static vendor}))
    File.mkdir_p(Path.join(~w{web static assets fonts}))
    File.mkdir_p(Path.join(~w{web static assets images ex_admin datepicker}))

    status_msg("creating", "css files")

    ~w(admin_lte2.css admin_lte2.css.map active_admin.css.css active_admin.css.css.map)
    |> Enum.each(&copy_vendor(base_path, "css", &1))

    status_msg("creating", "js files")

    (~w(jquery.min.js admin_lte2.js jquery.min.js.map admin_lte2.js.map) ++
       ~w(ex_admin_common.js ex_admin_common.js.map))
    |> Enum.each(&copy_vendor(base_path, "js", &1))

    copy_vendor_r(base_path, "fonts")
    copy_vendor_r(base_path, "images")

    case File.read("brunch-config.js") do
      {:ok, file} ->
        File.write!("brunch-config.js", file <> brunch_instructions())

      error ->
        Mix.raise("""
        Could not open brunch-config.js file. #{inspect(error)}
        """)
    end

    config
  end

  def do_assets(%Config{assets: true} = config) do
    base = ~w(priv static)
    base_path = Path.join(base)

    Enum.each(~w(fonts css js), &File.mkdir_p(Path.join(base ++ [&1])))
    File.mkdir_p(Path.join(~w{priv static images ex_admin datepicker}))
    File.mkdir_p(Path.join(~w(web admin)))

    status_msg("creating", "css files")

    ~w(admin_lte2.css admin_lte2.css.map active_admin.css.css active_admin.css.css.map)
    |> Enum.each(&copy_file(base_path, "css", &1))

    status_msg("creating", "js files")

    (~w(jquery.min.js admin_lte2.js jquery.min.js.map admin_lte2.js.map) ++
       ~w(ex_admin_common.js ex_admin_common.js.map))
    |> Enum.each(&copy_file(base_path, "js", &1))

    copy_r(base_path, "fonts")
    copy_r(base_path, "images")

    config
  end

  def do_assets(config) do
    config
  end

  def do_route(%Config{route: true} = config) do
    Mix.shell().info("""

    Add the admin routes to your web/router.ex:

      use ExAdmin.Router
      # your app's routes
      scope "/admin", ExAdmin do
        pipe_through :browser
        admin_routes()
      end
    """)

    config
  end

  def do_route(config), do: config

  def do_config(%Config{config: true} = config) do
    status_msg("updating", "config/config.exs")
    dest_path = Path.join([File.cwd!() | ~w(config)])
    dest_file_path = Path.join(dest_path, "config.exs")
    source = File.read!(dest_file_path)

    ""
    |> config_xain(config, source)
    |> config_write(config, dest_file_path, source)
  end

  def do_config(config), do: config

  defp config_xain(append, _config, source) do
    unless String.contains?(source, ":xain, :after_callback") do
      append <>
        """
        config :xain, :after_callback, {Phoenix.HTML, :raw}

        """
    else
      notice_msg("skipping", "xain config. It already exists.")
      append
    end
  end

  defp config_write("", config, _dest_file_path, _source), do: config

  defp config_write(append, config, dest_file_path, source) do
    File.write!(dest_file_path, source <> "\n" <> append)
    config
  end

  def do_dashboard(%Config{dashboard: true} = config) do
    dest_path = Path.join([File.cwd!() | ~w(web admin)])
    dest_file_path = Path.join(dest_path, "dashboard.ex")

    source =
      Path.join([config.package_path | ~w(priv templates admin.install dashboard.exs)])
      |> EEx.eval_file(
        base: get_module(),
        title_txt: gettext("Dashboard"),
        welcome_txt: gettext("Welcome to ExAdmin. This is the default dashboard page."),
        add_txt: gettext("To add dashboard sections, checkout 'web/admin/dashboards.ex'")
      )

    file = Path.join(~w(web admin dashboard.ex))

    if File.exists?(file) do
      notice_msg("skipping", "#{file}. It already exists.")
    else
      status_msg("creating", file)
      File.mkdir_p(dest_path)
      File.write!(dest_file_path, source)
      dashboard_instructions()
    end

    config
  end

  def do_dashboard(config), do: config

  def dashboard_instructions do
    base = get_module()

    Mix.shell().info("""

    Remember to update your config file:

      config :ex_admin,
        repo: #{base}.Repo,
        module: #{base},
        modules: [
          #{base}.ExAdmin.Dashboard,
        ]
    """)
  end

  def do_paging(config) do
    base = get_module()

    Mix.shell().info("""

    Add Scrivener paging to your Repo:

      defmodule #{base}.Repo do
        use Ecto.Repo, otp_app: :#{String.downcase(base)}
        use Scrivener, page_size: 10  # <--- add this
      end
    """)

    config
  end

  def do_assets_instructions(%{assets: true, brunch: true} = config) do
    Mix.shell().info("""

    Check the bottom of your brunch-config.js file.

      Instructions for adding the ExAdmin assets have been added.
    """)

    config
  end

  def do_assets_instructions(config), do: config

  defp copy_r(base_path, path) do
    File.cp_r(
      Path.join([get_package_path(), base_path, path]),
      Path.join([File.cwd!(), base_path, path])
    )

    base_path
  end

  defp copy_file(base_path, path, file_name) do
    File.cp(
      Path.join([get_package_path(), base_path, path, file_name]),
      Path.join([File.cwd!(), base_path, path, file_name])
    )

    base_path
  end

  defp copy_vendor(from_path, path, filename) do
    File.cp(
      Path.join([get_package_path(), from_path, path, filename]),
      Path.join([File.cwd!(), "web", "static", "vendor", filename])
    )
  end

  defp copy_vendor_r(base_path, path) do
    File.cp_r(
      Path.join([get_package_path(), base_path, path]),
      Path.join([File.cwd!(), "web", "static", "assets", path])
    )
  end

  def brunch_instructions do
    """

    // To add the ExAdmin generated assets to your brunch build, do the following:
    //
    // Replace
    //
    //     javascripts: {
    //       joinTo: "js/app.js"
    //     },
    //
    // With
    //
    //     javascripts: {
    //       joinTo: {
    //         "js/app.js": /^(web\\/static\\/js)|(node_modules)/,
    //         "js/ex_admin_common.js": ["web/static/vendor/ex_admin_common.js"],
    //         "js/admin_lte2.js": ["web/static/vendor/admin_lte2.js"],
    //         "js/jquery.min.js": ["web/static/vendor/jquery.min.js"],
    //       }
    //     },
    //
    // Replace
    //
    //     stylesheets: {
    //       joinTo: "css/app.css",
    //       order: {
    //         after: ["web/static/css/app.css"] // concat app.css last
    //       }
    //     },
    //
    // With
    //
    //     stylesheets: {
    //       joinTo: {
    //         "css/app.css": /^(web\\/static\\/css)/,
    //         "css/admin_lte2.css": ["web/static/vendor/admin_lte2.css"],
    //         "css/active_admin.css.css": ["web/static/vendor/active_admin.css.css"],
    //       },
    //       order: {
    //         after: ["web/static/css/app.css"] // concat app.css last
    //       }
    //     },
    //
    """
  end

  defp parse_args(args) do
    {opts, _values, _} = OptionParser.parse(args, switches: @switches)

    Enum.reduce(opts, %Config{package_path: get_package_path()}, fn
      {key, value}, config ->
        if key in Map.keys(config) do
          struct(config, [{key, value}])
        else
          raise_option(key)
        end

      other, _config ->
        raise_option(inspect(other))
    end)
  end

  defp raise_option(option) do
    Mix.raise("""
    Invalid option --#{option}
    """)
  end
end
