defmodule ExGuard.Guard do
  @moduledoc """
  Users use functions in this module to build config files.

      guard("test")
      |> command("mix text --color")
      |> watch(~r{\\.(ex|exs)$})
      |> notification(:auto)
  """

  alias ExGuard.Notifier
  alias ExGuard.Config
  defstruct title: "", cmd: "", watch: [], notification: :auto, ignore: [], options: []

  @doc """
  Creates ExGuard config.

      guard("test")

  Options:

    * `:run_on_start`, set this option If you want to run the command when `mix guard` has been executed
    * `:umbrella_app`, set this option If you are running guard on the main directory of an umbrella project and using `watch` command to match changed filed with test

      guard("test", run_on_start: true, umbrella_app: true)
  """
  def guard(title, opts \\ []) do
    guard_struct = %ExGuard.Guard{title: title, options: opts}
    Config.put_guard(title, guard_struct)
  end

  @doc """
  Sets command for given guard config.

      guard("test")
      |> command("mix text --color")
  """
  def command(guard_struct, cmd) do
    guard_struct = %ExGuard.Guard{guard_struct | cmd: cmd}
    Config.put_guard(guard_struct.title, guard_struct)
  end

  @doc ~S"""
  Sets watch pattern for given guard config.

  To watch all Elixir and Erlang files set:
      guard("Elixir/Erlang files")
      |> watch(~r{\\.(erl|ex|exs|eex|xrl|yrl)\\z}i)

  To only execute the command for specific files use:
      guard("execute specific tests")
      |> watch({~r{lib/(?<dir>.+)/(?<file>.+).ex$}, fn(m) -> "test/#{m["dir"]}/#{m["file"]}_test.exs" end})
  """
  def watch(guard_struct, pattern) do
    cur_watch = guard_struct.watch ++ [pattern]
    guard_struct = %ExGuard.Guard{guard_struct | watch: cur_watch}
    Config.put_guard(guard_struct.title, guard_struct)
  end

  @doc """
  It can be used to exclude files and directories from the set of files being watched.

      guard("text files")
      |> ignore(~r/\\.txt$/)
  """
  def ignore(guard_struct, ignore_rule) do
    cur_ignore_rule = guard_struct.ignore ++ [ignore_rule]
    guard_struct = %ExGuard.Guard{guard_struct | ignore: cur_ignore_rule}
    Config.put_guard(guard_struct.title, guard_struct)
  end

  @doc """
  Sets notification for given guard config.
  By default notification is on.

      guard("test")
      |> notification(:auto)

  To turn off the notification set it to `:off`
      guard("not notification")
      |> notification(:off)
  """
  def notification(guard_struct, :auto) do
    guard_struct = %ExGuard.Guard{guard_struct | notification: :auto}
    Config.put_guard(guard_struct.title, guard_struct)
  end

  def notification(guard_struct, :off) do
    guard_struct = %ExGuard.Guard{guard_struct | notification: :off}
    Config.put_guard(guard_struct.title, guard_struct)
  end

  @doc """
  Executes the command for a guard config.

  If files is a list, append them to guard.cmd and executes it.
  """
  def execute({guard_config, files}) do
    umbrella_app? = Keyword.get(guard_config.options, :umbrella_app, false)

    files =
      if umbrella_app? do
        Enum.map(files, fn path ->
          String.replace(path, ~r{^apps/[a-zA-z_]+/}, "")
        end)
      else
        files
      end

    arg = Enum.join(files, " ")
    cmd = String.trim("#{guard_config.cmd} #{arg}")
    IO.puts("ex_guard is executing #{cmd}")

    case Mix.Shell.IO.cmd(cmd) do
      0 -> {:ok, 0, "", guard_config}
      status -> {:error, status, "", guard_config}
    end
  end

  def execute(guard_config) do
    execute({guard_config, []})
  end

  @doc """
  notifies the result
  """
  def notify({_, _, _, %ExGuard.Guard{notification: :off}}) do
    :off
  end

  def notify({:ok, _status_code, _message, %ExGuard.Guard{notification: :auto} = guard_config}) do
    Notifier.notify(
      title: guard_config.title,
      message: "successfully executed",
      status: :ok
    )
  end

  def notify({:error, _status_code, _message, %ExGuard.Guard{notification: :auto} = guard_config}) do
    Notifier.notify(
      title: guard_config.title,
      message: "failed to execute",
      status: :error
    )
  end
end
