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))

      contents =
        case opts[:format_elixir] do
          true -> [Code.format_string!(contents), ?\n]
          _ -> contents
        end

      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
    * `:format_elixir` (since v1.18.0) - if `true`, apply formatter to the generated file

  ## 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
