defmodule Mix.Generator do
  @moduledoc """
  Conveniences for working with paths and generating content.
  """

  @doc ~S"""
  Creates a file with the given contents.

  If the file already exists and the contents are not the same,
  it asks for user confirmation.

  ## Options

    * `:force` - forces creation without a shell prompt
    * `:quiet` - does not log command output

  ## Examples

      iex> Mix.Generator.create_file(".gitignore", "_build\ndeps\n")
      * creating .gitignore
      true

  """
  @spec create_file(Path.t(), iodata, keyword) :: boolean()
  def create_file(path, contents, opts \\ []) when is_binary(path) do
    log(:green, :creating, Path.relative_to_cwd(path), opts)

    if opts[:force] || overwrite?(path, contents) do
      File.mkdir_p!(Path.dirname(path))
      File.write!(path, contents)
      true
    else
      false
    end
  end

  @doc """
  Creates a directory if one does not exist yet.

  This function does nothing if the given directory already exists; in this
  case, it still logs the directory creation.

  ## Options

    * `:quiet` - does not log command output

  ## Examples

      iex> Mix.Generator.create_directory("path/to/dir")
      * creating path/to/dir
      true

  """
  @spec create_directory(Path.t(), keyword) :: true
  def create_directory(path, options \\ []) when is_binary(path) do
    log(:green, "creating", Path.relative_to_cwd(path), options)
    File.mkdir_p!(path)
    true
  end

  @doc ~S"""
  Copies `source` to `target`.

  If `target` already exists and the contents are not the same,
  it asks for user confirmation.

  ## Options

    * `:force` - forces copying without a shell prompt
    * `:quiet` - does not log command output

  ## Examples

      iex> Mix.Generator.copy_file("source/gitignore", ".gitignore")
      * creating .gitignore
      true

  """
  @doc since: "1.9.0"
  @spec copy_file(Path.t(), Path.t(), keyword) :: boolean()
  def copy_file(source, target, options \\ []) do
    create_file(target, File.read!(source), options)
  end

  @doc ~S"""
  Evaluates and copy templates at `source` to `target`.

  The template in `source` is evaluated with the given `assigns`.

  If `target` already exists and the contents are not the same,
  it asks for user confirmation.

  ## Options

    * `:force` - forces copying without a shell prompt
    * `:quiet` - does not log command output

  ## Examples

      iex> assigns = [project_path: "/Users/joe/newproject"]
      iex> Mix.Generator.copy_template("source/gitignore", ".gitignore", assigns)
      * creating .gitignore
      true

  """
  @doc since: "1.9.0"
  @spec copy_template(Path.t(), Path.t(), keyword, keyword) :: boolean()
  def copy_template(source, target, assigns, options \\ []) do
    create_file(target, EEx.eval_file(source, assigns: assigns), options)
  end

  @doc """
  Prompts the user to overwrite the file if it exists.

  Returns false if the file exists and the user forbade
  to override it. Returns true otherwise.
  """
  @doc since: "1.9.0"
  @spec overwrite?(Path.t()) :: boolean
  def overwrite?(path) do
    if File.exists?(path) do
      full = Path.expand(path)
      Mix.shell().yes?(Path.relative_to_cwd(full) <> " already exists, overwrite?")
    else
      true
    end
  end

  @doc """
  Prompts the user to overwrite the file if it exists.

  The contents are compared to avoid asking the user to
  override if the contents did not change. Returns false
  if the file exists and the content is the same or the
  user forbade to override it. Returns true otherwise.
  """
  @doc since: "1.9.0"
  @spec overwrite?(Path.t(), iodata) :: boolean
  def overwrite?(path, contents) do
    case File.read(path) do
      {:ok, binary} ->
        if binary == IO.iodata_to_binary(contents) do
          false
        else
          full = Path.expand(path)
          Mix.shell().yes?(Path.relative_to_cwd(full) <> " already exists, overwrite?")
        end

      _ ->
        true
    end
  end

  defp log(color, command, message, opts) do
    unless opts[:quiet] do
      Mix.shell().info([color, "* #{command} ", :reset, message])
    end
  end

  @doc """
  Embeds a template given by `contents` into the current module.

  It will define a private function with the `name` followed by
  `_template` that expects assigns as arguments.

  This function must be invoked passing a keyword list.
  Each key in the keyword list can be accessed in the
  template using the `@` macro.

  For more information, check `EEx.SmartEngine`.

  ## Examples

      defmodule Mix.Tasks.MyTask do
        require Mix.Generator
        Mix.Generator.embed_template(:log, "Log: <%= @log %>")
      end

  """
  defmacro embed_template(name, contents) do
    quote bind_quoted: binding() do
      contents =
        case contents do
          [from_file: file] ->
            @file file
            File.read!(file)

          c when is_binary(c) ->
            @file {__ENV__.file, __ENV__.line + 1}
            c

          _ ->
            raise ArgumentError, "expected string or from_file: file"
        end

      require EEx

      source = "<% _ = assigns %>" <> contents
      EEx.function_from_string(:defp, :"#{name}_template", source, [:assigns])
    end
  end

  @doc """
  Embeds a text given by `contents` into the current module.

  It will define a private function with the `name` followed by
  `_text` that expects no arguments.

  ## Examples

      defmodule Mix.Tasks.MyTask do
        require Mix.Generator
        Mix.Generator.embed_text(:error, "There was an error!")
      end

  """
  defmacro embed_text(name, contents) do
    quote bind_quoted: binding() do
      contents =
        case contents do
          [from_file: f] -> File.read!(f)
          c when is_binary(c) -> c
          _ -> raise ArgumentError, "expected string or from_file: file"
        end

      defp unquote(:"#{name}_text")(), do: unquote(contents)
    end
  end
end
