defmodule Prism do
  alias Focus.Types

  @moduledoc """
  Prisms are like lenses, but used when the view focused on may not exist.

  This includes sum types; although not backed by an explicit Maybe type,
  the [{:ok, any} | {:error}] convention is explicitly supported as a value that
  a prism can focus on.
  """

  @enforce_keys [:get, :put]
  defstruct [:get, :put]

  @type t :: %Prism{
          get: (any -> any),
          put: ((any -> any) -> any)
        }

  @doc """
  A prism that matches an {:ok, _} tuple.

  ## Examples

      iex> ok = Prism.ok
      iex> ok |> Focus.view({:ok, 5})
      {:ok, 5}
      iex> ok |> Focus.set({:ok, 5}, "Banana")
      {:ok, "Banana"}
      iex> ok |> Focus.view({:error, :oops})
      {:error, {:prism, :bad_path}}
  """
  @spec ok() :: Prism.t()
  def ok() do
    %Prism{
      get: fn s -> get_ok(s) end,
      put: fn s ->
        fn f ->
          set_ok(s, f)
        end
      end
    }
  end

  defp get_ok({:ok, x}), do: x
  defp get_ok({:error, _}), do: nil
  defp set_ok({:ok, _x}, f), do: {:ok, f}

  @doc """
  A prism that matches an {:error, _} tuple.
  Note that on a successful match, view/set/over will
  return {:ok, _}

  ## Examples

      iex> error = Prism.error
      iex> error |> Focus.view({:error, 5})
      {:ok, 5}
      iex> error |> Focus.set({:error, 5}, "Banana")
      {:ok, "Banana"}
      iex> error |> Focus.view({:ok, :oops})
      {:error, {:prism, :bad_path}}
  """
  @spec error() :: Prism.t()
  def error() do
    %Prism{
      get: fn s -> get_error(s) end,
      put: fn s ->
        fn f ->
          set_error(s, f)
        end
      end
    }
  end

  defp get_error({:error, x}), do: x
  defp get_error({:ok, _}), do: nil
  defp set_error({:error, _x}, f), do: {:ok, f}

  defimpl Focusable do
    @doc """
    Get a piece of a data structure that a prism focuses on;
    returns {:ok, data} | {:error, :bad_prism_path}

    ## Examples

        iex> ok = Prism.ok
        iex> ok |> Focus.view({:ok, 5})
        {:ok, 5}
    """
    @spec view(Prism.t(), Types.traversable()) :: {:error, {:prism, :bad_path}} | {:ok, any}
    def view(%Prism{get: get}, structure) do
      res = get.(structure)

      case res do
        nil -> {:error, {:prism, :bad_path}}
        _ -> {:ok, res}
      end
    end

    @doc """
    Modify the part of a data structure that a prism focuses on.

    ## Examples

        iex> ok = Prism.ok
        iex> ok |> Focus.over({:ok, "banana"}, &String.upcase/1)
        {:ok, "BANANA"}
    """
    @spec over(Prism.t(), Types.traversable(), (any -> any)) :: Types.traversable()
    def over(%Prism{put: put} = prism, structure, f) do
      with {:ok, data_view} <- view(prism, structure) do
        put.(structure).(f.(data_view))
      end
    end

    @doc """
    Update the part of a data structure the prism focuses on.

    ## Examples

        iex> ok = Prism.ok
        iex> ok |> Focus.set({:ok, "banana"}, "pineapple")
        {:ok, "pineapple"}
    """
    @spec set(Prism.t(), Types.traversable(), any) :: Types.traversable()
    def set(%Prism{put: put} = prism, structure, val) do
      with {:ok, _data_view} <- view(prism, structure) do
        put.(structure).(val)
      end
    end
  end
end
