defprotocol Focusable do
  @doc "View the data that an optic focuses on."
  def view(optic, structure)

  @doc "Modify the data that an optic focuses on."
  def over(optic, structure, f)

  @doc "Set the data that an optic focuses on."
  def set(optic, structure, value)
end

defmodule Focus do
  alias Focus.Types

  @moduledoc "Common functions usable by lenses, prisms, and traversals."

  @doc """
  Wrapper around Focusable.view/2

  Arguments can be passed in with either the lens first and data structure second or vice versa.
  Passing the data structure first allows Focus.view/2 to fit neatly in pipeline operations.

  ## Examples

      iex> marge = %{
      ...>   name: "Marge",
      ...>   address: %{
      ...>     street: "123 Fake St.",
      ...>     city: "Springfield"
      ...>   }
      ...> }
      iex> address_lens = Lens.make_lens(:address)
      iex> address_lens
      ...> |> Focus.view(marge)
      %{street: "123 Fake St.", city: "Springfield"}
      iex> marge
      ...> |> Focus.view(address_lens)
      %{street: "123 Fake St.", city: "Springfield"}
  """
  @spec view(Types.optic() | Types.traversable(), Types.traversable() | Types.optic()) ::
          any | nil
  def view(%{get: _, put: _} = optic, structure), do: Focusable.view(optic, structure)
  def view(structure, %{get: _, put: _} = optic), do: Focusable.view(optic, structure)

  @doc """
  Wrapper around Focusable.over/3

  Arguments can be passed in with either the lens first and data structure second or vice versa.
  Passing the data structure first allows Focus.over/3 to fit neatly in pipeline operations.

  ## Examples

      iex> marge = %{
      ...>   name: "Marge",
      ...>   address: %{
      ...>     street: "123 Fake St.",
      ...>     city: "Springfield"
      ...>   }
      ...> }
      iex> name_lens = Lens.make_lens(:name)
      iex> name_lens
      ...> |> Focus.over(marge, &String.upcase/1)
      %{
        name: "MARGE",
        address: %{
          street: "123 Fake St.",
          city: "Springfield"
        }
      }
      iex> marge
      ...> |> Focus.over(name_lens, &String.upcase/1)
      %{
        name: "MARGE",
        address: %{
          street: "123 Fake St.",
          city: "Springfield"
        }
      }
  """
  @spec over(
          Types.optic() | Types.traversable(),
          Types.traversable() | Types.optic(),
          (any -> any)
        ) :: Types.traversable()
  def over(%{get: _, put: _} = optic, structure, f), do: Focusable.over(optic, structure, f)
  def over(structure, %{get: _, put: _} = optic, f), do: Focusable.over(optic, structure, f)

  @doc """
  Wrapper around Focusable.set/3

  Arguments can be passed in with either the lens first and data structure second or vice versa.
  Passing the data structure first allows Focus.set/3 to fit neatly in pipeline operations.

  ## Examples

  iex> marge = %{
  ...>   name: "Marge",
  ...>   address: %{
  ...>     street: "123 Fake St.",
  ...>     city: "Springfield"
  ...>   }
  ...> }
  iex> name_lens = Lens.make_lens(:name)
  iex> name_lens
  ...> |> Focus.set(marge, "Marjorie")
  %{
    name: "Marjorie",
    address: %{
      street: "123 Fake St.",
      city: "Springfield"
    }
  }
  iex> marge
  ...> |> Focus.set(name_lens, "Marjorie")
  %{
    name: "Marjorie",
    address: %{
      street: "123 Fake St.",
      city: "Springfield"
    }
  }
  """
  @spec set(Types.traversable() | Types.optic(), Types.optic() | Types.traversable(), any) ::
          Types.traversable()
  def set(%{get: _, put: _} = optic, structure, v), do: Focusable.set(optic, structure, v)
  def set(structure, %{get: _, put: _} = optic, v), do: Focusable.set(optic, structure, v)

  @doc """
  Compose with most general lens on the left

  ## Examples

      iex> marge = %{
      ...>   name: "Marge",
      ...>   address: %{
      ...>     street: "123 Fake St.",
      ...>     city: "Springfield"
      ...>   }
      ...> }
      iex> address_lens = Lens.make_lens(:address)
      iex> street_lens = Lens.make_lens(:street)
      iex> composed = Focus.compose(address_lens, street_lens)
      iex> Focus.view(composed, marge)
      "123 Fake St."
  """
  @spec compose(Types.optic(), Types.optic()) :: Types.optic()
  def compose(%{get: get_x, put: set_x}, %{get: get_y, put: set_y}) do
    %Lens{
      get: fn s ->
        case get_x.(s) do
          {:error, {:lens, :bad_path}} ->
            {:error, {:lens, :bad_path}}

          x ->
            get_y.(x)
        end
      end,
      put: fn s ->
        fn f ->
          case get_x.(s) do
            {:error, {:lens, :bad_path}} ->
              {:error, {:lens, :bad_path}}

            x ->
              set_x.(s).(set_y.(x).(f))
          end
        end
      end
    }
  end

  @doc """
  Infix lens composition

  ## Examples

      iex> import Focus
      iex> marge = %{name: "Marge", address: %{
      ...>   local: %{number: 123, street: "Fake St."},
      ...>   city: "Springfield"}
      ...> }
      iex> address_lens = Lens.make_lens(:address)
      iex> local_lens = Lens.make_lens(:local)
      iex> street_lens = Lens.make_lens(:street)
      iex> address_lens ~> local_lens ~> street_lens |> Focus.view(marge)
      "Fake St."
  """
  @spec Types.optic() ~> Types.optic() :: Types.optic()
  def x ~> y do
    compose(x, y)
  end

  @doc """
  Compose a pair of lenses to operate at the same level as one another.
  Calling Focus.view/2, Focus.over/3, or Focus.set/3 on an alongside composed
  pair returns a two-element tuple of the result.

  ## Examples

      iex> nums = [1,2,3,4,5,6]
      iex> Focus.alongside(Lens.idx(0), Lens.idx(3))
      ...> |> Focus.view(nums)
      {1, 4}

      iex> bart = %{name: "Bart", parents: {"Homer", "Marge"}, age: 10}
      iex> Focus.alongside(Lens.make_lens(:name), Lens.make_lens(:age))
      ...> |> Focus.view(bart)
      {"Bart", 10}
  """
  @spec alongside(Types.optic(), Types.optic()) :: Types.optic()
  def alongside(%{get: get_x, put: set_x}, %{get: get_y, put: set_y}) do
    %Lens{
      get: fn s ->
        {get_x.(s), get_y.(s)}
      end,
      put: fn s ->
        fn f ->
          {set_x.(s).(f), set_y.(s).(f)}
        end
      end
    }
  end

  @doc """
  Given a list of lenses and a structure, apply Focus.view/2 for each lens
  to the structure.

  ## Examples

      iex> homer = %{
      ...>   name: "Homer",
      ...>   job: "Nuclear Safety Inspector",
      ...>   children: ["Bart", "Lisa", "Maggie"]
      ...> }
      iex> lenses = Lens.make_lenses(homer)
      iex> [lenses.name, lenses.children]
      ...> |> Focus.view_list(homer)
      ["Homer", ["Bart", "Lisa", "Maggie"]]
  """
  @spec view_list(list(Types.optic()), Types.traversable()) :: [any]
  def view_list(lenses, structure) when is_list(lenses) do
    for lens <- lenses do
      Focus.view(lens, structure)
    end
  end

  @doc """
  Check whether an optic's target is present in a data structure.

  ## Examples

      iex> first_elem = Lens.idx(1)
      iex> first_elem |> Focus.has([0])
      false

      iex> name = Lens.make_lens(:name)
      iex> name |> Focus.has(%{name: "Homer"})
      true
  """
  @spec has(Types.optic(), Types.traversable()) :: boolean
  def has(optic, structure) do
    case Focus.view(optic, structure) do
      nil -> false
      {:error, _} -> false
      _ -> true
    end
  end

  @doc """
  Check whether an optic's target is not present in a data structure.

  ## Examples

      iex> first_elem = Lens.idx(1)
      iex> first_elem |> Focus.hasnt([0])
      true

      iex> name = Lens.make_lens(:name)
      iex> name |> Focus.hasnt(%{name: "Homer"})
      false
  """
  @spec hasnt(Types.optic(), Types.traversable()) :: boolean
  def hasnt(optic, structure), do: !has(optic, structure)

  @doc """
  Partially apply a lens to Focus.over/3, fixing the lens argument and
  returning a function that takes a Types.traversable and an update function.

  ## Examples

      iex> upcase_name = Lens.make_lens(:name)
      ...> |> Focus.fix_over(&String.upcase/1)
      iex> %{name: "Bart", parents: {"Homer", "Marge"}}
      ...> |> upcase_name.()
      %{name: "BART", parents: {"Homer", "Marge"}}

      iex> fst = Lens.idx(0)
      iex> states = [:maryland, :texas, :illinois]
      iex> Focus.over(fst, states, &String.upcase(Atom.to_string(&1)))
      ["MARYLAND", :texas, :illinois]
  """
  @spec fix_over(Types.optic(), (any -> any)) :: (Types.traversable() -> Types.traversable())
  def fix_over(%{get: _, put: _} = lens, f \\ fn x -> x end) when is_function(f) do
    fn structure ->
      Focus.over(lens, structure, f)
    end
  end

  @doc """
  Partially apply a lens to Focus.set/3, fixing the optic argument and
  returning a function that takes a Types.traversable and a new value.

  ## Examples

      iex> name_setter = Lens.make_lens(:name)
      ...> |> Focus.fix_set
      iex> %{name: "Bart", parents: {"Homer", "Marge"}}
      ...> |> name_setter.("Lisa")
      %{name: "Lisa", parents: {"Homer", "Marge"}}

      iex> fst = Lens.idx(0)
      iex> states = [:maryland, :texas, :illinois]
      iex> Focus.over(fst, states, &String.upcase(Atom.to_string(&1)))
      ["MARYLAND", :texas, :illinois]
  """
  @spec fix_set(Types.optic()) :: (Types.traversable(), any -> Types.traversable())
  def fix_set(%{get: _, put: _} = lens) do
    fn structure, val ->
      Focus.set(lens, structure, val)
    end
  end

  @doc """
  Fix Focus.view/2 on a given optic. This partially applies Focus.view/2 with the given
  optic and returns a function that takes a Types.traversable structure.

  ## Examples

      iex> view_name = Lens.make_lens(:name)
      ...> |> Focus.fix_view
      iex> homer = %{name: "Homer"}
      iex> view_name.(homer)
      "Homer"
      iex> [homer, %{name: "Marge"}, %{name: "Bart"}]
      ...> |> Enum.map(&view_name.(&1))
      ["Homer", "Marge", "Bart"]
  """
  @spec fix_view(Types.optic()) :: (Types.traversable() -> any)
  def fix_view(%{get: _, put: _} = optic) do
    fn structure ->
      Focus.view(optic, structure)
    end
  end
end
