defmodule Waffle.Actions.Store do
  @moduledoc ~S"""
  Store files to a defined adapter.

  The definition module responds to `Avatar.store/1` which
  accepts either:

    * A path to a local file

    * A path to a remote `http` or `https` file

    * A map with a filename and path keys (eg, a `%Plug.Upload{}`)

    * A map with a filename and binary keys (eg, `%{filename: "image.png", binary: <<255,255,255,...>>}`)
    * A map with a filename and stream keys (eg, `%{filename: "image.png", stream: %Stream{...}}`)

    * A two-element tuple consisting of one of the above file formats as well as a scope map

  Example usage as general file store:

      # Store any locally accessible file
      Avatar.store("/path/to/my/file.png") #=> {:ok, "file.png"}

      # Store any remotely accessible file
      Avatar.store("http://example.com/file.png") #=> {:ok, "file.png"}

      # Store a file directly from a `%Plug.Upload{}`
      Avatar.store(%Plug.Upload{filename: "file.png", path: "/a/b/c"}) #=> {:ok, "file.png"}

      # Store a file from a connection body
      {:ok, data, _conn} = Plug.Conn.read_body(conn)
      Avatar.store(%{filename: "file.png", binary: data})

  Example usage as a file attached to a `scope`:

      scope = Repo.get(User, 1)
      Avatar.store({%Plug.Upload{}, scope}) #=> {:ok, "file.png"}

  This scope will be available throughout the definition module to be
  used as an input to the storage parameters (eg, store files in
  `/uploads/#{scope.id}`).

  """

  alias Waffle.Actions.Store
  alias Waffle.Definition.Versioning

  defmacro __using__(_) do
    quote do
      def store(args), do: Store.store(__MODULE__, args)
    end
  end

  def store(definition, {file, scope})
      when is_binary(file) or is_map(file) do
    put(definition, {Waffle.File.new(file, definition), scope})
  end

  def store(definition, filepath)
      when is_binary(filepath) or is_map(filepath) do
    store(definition, {filepath, nil})
  end

  #
  # Private
  #

  defp put(_definition, {error = {:error, _msg}, _scope}), do: error

  defp put(definition, {%Waffle.File{} = file, scope}) do
    case definition.validate({file, scope}) do
      result when result == true or result == :ok ->
        put_versions(definition, {file, scope})
        |> cleanup!(file)

      {:error, message} ->
        {:error, message}

      _ ->
        {:error, :invalid_file}
    end
  end

  defp put_versions(definition, {file, scope}) do
    if definition.async() do
      definition.__versions()
      |> Enum.map(fn(r)    -> async_process_version(definition, r, {file, scope}) end)
      |> Enum.map(fn(task) -> Task.await(task, version_timeout()) end)
      |> ensure_all_success
      |> Enum.map(fn({v, r})    -> async_put_version(definition, v, {r, scope}) end)
      |> Enum.map(fn(task) -> Task.await(task, version_timeout()) end)
      |> handle_responses(file.file_name)
    else
      definition.__versions()
      |> Enum.map(fn(version) -> process_version(definition, version, {file, scope}) end)
      |> ensure_all_success
      |> Enum.map(fn({version, result}) -> put_version(definition, version, {result, scope}) end)
      |> handle_responses(file.file_name)
    end
  end

  defp ensure_all_success(responses) do
    errors = Enum.filter(responses, fn({_version, resp}) -> elem(resp, 0) == :error end)
    if Enum.empty?(errors), do: responses, else: errors
  end

  defp handle_responses(responses, filename) do
    errors = Enum.filter(responses, fn(resp) -> elem(resp, 0) == :error end) |> Enum.map(fn(err) -> elem(err, 1) end)
    if Enum.empty?(errors), do: {:ok, filename}, else: {:error, errors}
  end

  defp version_timeout do
    Application.get_env(:waffle, :version_timeout) || 15_000
  end

  defp async_process_version(definition, version, {file, scope}) do
    Task.async(fn ->
      process_version(definition, version, {file, scope})
    end)
  end

  defp async_put_version(definition, version, {result, scope}) do
    Task.async(fn ->
      put_version(definition, version, {result, scope})
    end)
  end

  defp process_version(definition, version, {file, scope}) do
    {version, Waffle.Processor.process(definition, version, {file, scope})}
  end

  defp put_version(definition, version, {result, scope}) do
    case result do
      {:error, error} -> {:error, error}
      {:ok, nil} -> {:ok, nil}
      {:ok, file} ->
        file_name = Versioning.resolve_file_name(definition, version, {file, scope})
        file      = %Waffle.File{file | file_name: file_name}
        result    = definition.__storage().put(definition, version, {file, scope})

        case definition.transform(version, {file, scope}) do
          :noaction ->
            # We don't have to cleanup after `:noaction` transformations
            # because final `cleanup!` will remove the original temporary file.
            result
          _ ->
            cleanup!(result, file)
        end
    end
  end

  defp cleanup!(result, file) do
    # If we were working with binary data or a remote file, a tempfile was
    # created that we need to clean up.
    if file.is_tempfile? do
      File.rm!(file.path)
    end

    result
  end

end
