defmodule Lens do
  alias Focus.Types

  @moduledoc """
  Lenses combine getters and setters for keys in data structures.

  Lenses should match/operate over a single value in a data structure,
  e.g. a key in a map/struct.
  """

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

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

  @doc """
  Define a lens to Focus on a part of a data structure.

  ## Examples

      iex> person = %{name: "Homer"}
      iex> name_lens = Lens.make_lens(:name)
      iex> name_lens |> Focus.view(person)
      "Homer"
      iex> name_lens |> Focus.set(person, "Bart")
      %{name: "Bart"}
  """
  @spec make_lens(any) :: Lens.t()
  def make_lens(path) do
    %Lens{
      get: fn s -> Lensable.getter(s, path) end,
      put: fn s ->
        fn f ->
          Lensable.setter(s, path, f)
        end
      end
    }
  end

  @doc """
  Automatically generate the valid lenses for the supplied map-like data structure.

  ## Examples

      iex> lisa = %{name: "Lisa", pets: %{cat: "Snowball"}}
      iex> lisa_lenses = Lens.make_lenses(lisa)
      iex> lisa_lenses.name
      ...> |> Focus.view(lisa)
      "Lisa"
      iex> pet_lenses = Lens.make_lenses(lisa.pets)
      iex> lisa_lenses.pets
      ...> ~> pet_lenses.cat
      ...> |> Focus.set(lisa, "Snowball II")
      %{name: "Lisa", pets: %{cat: "Snowball II"}}
  """
  @spec make_lenses(Types.traversable()) :: %{
          optional(atom) => Lens.t(),
          optional(String.t()) => Lens.t()
        }
  def make_lenses(%{} = structure) when is_map(structure) do
    for key <- Map.keys(structure), into: %{} do
      {key, Lens.make_lens(key)}
    end
  end

  @doc """
  Define a struct and derive lenses for the struct's keys as functions
  in the module.

  Examples assume the following module:

    ```elixir
    defmodule PersonExample do
      import Lens
      deflenses name: nil, age: nil
    end
    ```

  ## Example

      iex> function_exported?(PersonExample, :age_lens, 0)
      true
      iex> function_exported?(PersonExample, :name_lens, 0)
      true
      iex> bart = %PersonExample{name: "Bart", age: 10}
      iex> PersonExample.name_lens |> Focus.view(bart)
      "Bart"
  end
  """
  defmacro deflenses(fields) do
    quote do
      Module.register_attribute(__MODULE__, :struct_fields, accumulate: true)

      for field <- unquote(fields) do
        Module.put_attribute(__MODULE__, :struct_fields, field)
      end

      Module.eval_quoted(
        __ENV__,
        [
          Lens.__defstruct__(@struct_fields),
          Lens.__deflenses__(@struct_fields)
        ]
      )
    end
  end

  @doc false
  def __deflenses__(fields) do
    if Keyword.keyword?(fields) do
      for {field, _val} <- fields do
        quote do
          def unquote(:"#{field}_lens")() do
            Lens.make_lens(unquote(field))
          end
        end
      end
    else
      for field <- fields do
        quote do
          def unquote(:"#{field}_lens")() do
            Lens.make_lens(unquote(field))
          end
        end
      end
    end
  end

  @doc false
  def __defstruct__(fields) do
    quote do
      defstruct unquote(Macro.escape(fields))

      defimpl Lensable, for: __MODULE__ do
        def getter(s, x), do: Map.get(s, x)
        def setter(s, x, f), do: Map.put(s, x, f)
      end
    end
  end

  @doc """
  A lens that focuses on an index in a list.

  ## Examples

      iex> first_elem = Lens.idx(0)
      iex> first_elem |> Focus.view([1,2,3,4,5])
      1

      iex> bad_index = Lens.idx(10)
      iex> bad_index |> Focus.view([1,2,3])
      nil
  """
  @spec idx(number) :: Lens.t()
  def idx(num) when is_number(num), do: make_lens(num)

  defimpl Focusable do
    @doc """
    View the data that an optic Focuses on.

    ## Examples

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

    @doc """
    Modify the part of a data structure that a lens Focuses on.

    ## Examples

        iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}}
        iex> name_lens = Lens.make_lens(:name)
        iex> Focus.over(name_lens, marge, &String.upcase/1)
        %{name: "MARGE", address: %{street: "123 Fake St.", city: "Springfield"}}
    """
    @spec over(Lens.t(), Types.traversable(), (any -> any)) :: Types.traversable()
    def over(%Lens{put: setter} = lens, structure, f) do
      with {:ok, d} <- Lens.safe_view(lens, structure) do
        setter.(structure).(f.(d))
      end
    end

    @doc """
    Update the part of a data structure the lens Focuses on.

    ## Examples

        iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}}
        iex> name_lens = Lens.make_lens(:name)
        iex> Focus.set(name_lens, marge, "Homer")
        %{name: "Homer", address: %{street: "123 Fake St.", city: "Springfield"}}

        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.set(composed, marge, "42 Wallaby Way")
        %{name: "Marge", address: %{street: "42 Wallaby Way", city: "Springfield"}}
    """
    @spec set(Lens.t(), Types.traversable(), any) :: Types.traversable()
    def set(lens, structure, val) do
      over(lens, structure, fn _ -> val end)
    end
  end

  @doc """
  Get a piece of a data structure that a lens Focuses on;
  returns {:ok, data} | {:error, :bad_lens_path} | {:error, :bad_data_structure}

  ## Examples

      iex> marge = %{name: "Marge", address: %{street: "123 Fake St.", city: "Springfield"}}
      iex> name_lens = Lens.make_lens(:name)
      iex> Lens.safe_view(name_lens, marge)
      {:ok, "Marge"}
  """
  @spec safe_view(Lens.t(), Types.traversable()) ::
          {:error, {:lens, :bad_path}} | {:error, {:lens, :bad_data_structure}} | {:ok, any}
  def safe_view(%Lens{} = lens, structure) do
    res = Focus.view(lens, structure)

    case res do
      {:error, err} -> {:error, err}
      _ -> {:ok, res}
    end
  end
end
