import TypeClass

defclass Witchcraft.Arrow do
  @moduledoc """
  Arrows abstract the idea of computations, potentially with a context.

  Arrows are in fact an abstraction above monads, and can be used both to
  express all other type classes in Witchcraft. They also enable some nice
  flow-based reasoning about computation.

  For a nice illustrated explination,
  see [Haskell/Understanding arrows](https://en.wikibooks.org/wiki/Haskell/Understanding_arrows)

  Arrows let you think diagrammatically, and is a powerful way of thinking
  about flow programming, concurrency, and more.

                   ┌---> f --------------------------┐
                   |                                 v
      input ---> split                            unsplit ---> result
                   |                                 ^
                   |              ┌--- h ---┐        |
                   |              |         v        |
                   └---> g ---> split     unsplit ---┘
                                  |         ^
                                  └--- i ---┘

  ## Type Class

  An instance of `Witchcraft.Arrow` must also implement `Witchcraft.Category`,
  and define `Witchcraft.Arrow.arrowize/2`.

      Semigroupoid  [compose/2, apply/2]
          ↓
       Category     [identity/1]
          ↓
        Arrow       [arrowize/2]

  """

  alias __MODULE__
  extend Witchcraft.Category

  use Witchcraft.Internal, deps: [Witchcraft.Category]

  use Witchcraft.Category

  @type t :: fun()

  where do
    @doc """
    Lift a function into an arrow, much like how `of/2` does with data.

    Essentially a label for composing functions end-to-end, where instances
    may have their own special idea of what composition means. The simplest example
    is a regular function. Others are possible, such as Kleisli arrows.

    ## Examples

        iex> use Witchcraft.Arrow
        ...> times_ten = arrowize(fn -> nil end, &(&1 * 10))
        ...> 5 |> pipe(times_ten)
        50

    """
    @spec arrowize(Arrow.t(), fun()) :: Arrow.t()
    def arrowize(sample, fun)
  end

  properties do
    def arrow_identity(sample) do
      a = generate(nil)

      left = Arrow.arrowize(sample, &Quark.id/1)
      right = &Quark.id/1

      equal?(a |> pipe(left), a |> pipe(right))
    end

    def arrow_composition(sample) do
      use Witchcraft.Category

      a = generate(nil)

      f = fn x -> "#{x}-#{x}" end
      g = &inspect/1

      left = Arrow.arrowize(sample, f) <|> Arrow.arrowize(sample, g)
      right = Arrow.arrowize(sample, f <|> g)

      equal?(pipe(a, left), pipe(a, right))
    end

    def first_commutativity(sample) do
      a = {generate(nil), generate(nil)}
      f = &inspect/1

      left = Witchcraft.Arrow.first(Arrow.arrowize(sample, f))
      right = Arrow.arrowize(sample, Witchcraft.Arrow.first(f))

      equal?(pipe(a, left), pipe(a, right))
    end

    def first_composition(sample) do
      a = {generate(nil), generate(nil)}

      f = Arrow.arrowize(sample, fn x -> "#{x}-#{x}" end)
      g = Arrow.arrowize(sample, &inspect/1)

      left = Witchcraft.Arrow.first(f <|> g)
      right = Witchcraft.Arrow.first(f) <|> Witchcraft.Arrow.first(g)

      equal?(pipe(a, left), pipe(a, right))
    end

    def second_arrow_commutativity(sample) do
      a = {generate(nil), generate(nil)}
      f = &inspect/1

      left = Witchcraft.Arrow.second(Arrow.arrowize(sample, f))
      right = Arrow.arrowize(sample, Witchcraft.Arrow.second(f))

      equal?(pipe(a, left), pipe(a, right))
    end

    def second_composition(sample) do
      a = {generate(nil), generate(nil)}

      f = Arrow.arrowize(sample, fn x -> "#{x}-#{x}" end)
      g = Arrow.arrowize(sample, &inspect/1)

      left = Witchcraft.Arrow.second(f <|> g)
      right = Witchcraft.Arrow.second(f) <|> Witchcraft.Arrow.second(g)

      equal?(pipe(a, left), pipe(a, right))
    end

    def product_composition(sample) do
      a = {generate(nil), generate(nil)}

      f = &inspect/1
      g = fn x -> "#{inspect(x)}-#{inspect(x)}" end

      left =
        Witchcraft.Arrow.product(
          Arrow.arrowize(sample, f),
          Arrow.arrowize(sample, g)
        )

      right = Arrow.arrowize(sample, Witchcraft.Arrow.product(f, g))

      equal?(pipe(a, left), pipe(a, right))
    end

    def fanout_composition(sample) do
      a = generate(nil)

      f = &inspect/1
      g = fn x -> "#{inspect(x)}-#{inspect(x)}" end

      left =
        Witchcraft.Arrow.fanout(
          Arrow.arrowize(sample, f),
          Arrow.arrowize(sample, g)
        )

      right = Arrow.arrowize(sample, Witchcraft.Arrow.fanout(f, g))

      equal?(pipe(a, left), pipe(a, right))
    end

    def first_reassociaton(sample) do
      a = {{generate(nil), generate(nil)}, {generate(nil), generate(nil)}}
      f = fn x -> "#{inspect(x)}-#{inspect(x)}" end

      x = Witchcraft.Arrow.first(Arrow.arrowize(sample, f))
      y = Arrow.arrowize(sample, &Witchcraft.Arrow.reassociate/1)

      left = Witchcraft.Arrow.first(x) <~> y
      right = y <~> x

      equal?(a |> pipe(left), a |> pipe(right))
    end

    def first_identity(sample) do
      a = {generate(nil), generate(nil)}
      f = fn x -> "#{inspect(x)}-#{inspect(x)}" end

      left = Witchcraft.Arrow.first(f) <~> Arrow.arrowize(sample, fn {x, _} -> x end)
      right = Arrow.arrowize(sample, fn {x, _} -> x end) <~> f

      equal?(pipe(a, left), pipe(a, right))
    end

    def first_product_commutativity(sample) do
      a = {generate(nil), generate(nil)}

      f = &inspect/1
      g = fn x -> "#{inspect(x)}-#{inspect(x)}" end

      x = Arrow.arrowize(sample, Witchcraft.Arrow.product(&Quark.id/1, g))
      y = Witchcraft.Arrow.first(f)

      left = x <|> y
      right = y <|> x

      equal?(pipe(a, left), pipe(a, right))
    end
  end

  @doc """
  Take two arguments (as a 2-tuple), and run one function on the left side (first element),
  and run a different function on the right side (second element).

        ┌------> f.(a) = x -------┐
        |                         v
      {a, b}                    {x, y}
        |                         ^
        └------> g.(b) = y -------┘

  ## Examples

      iex> product(&(&1 - 10), &(&1 <> "!")).({42, "Hi"})
      {32, "Hi!"}

  """
  @spec product(Arrow.t(), Arrow.t()) :: Arrow.t()
  def product(arrow_f, arrow_g), do: first(arrow_f) <~> second(arrow_g)

  @doc """
  Alias for `product/2`, meant to invoke a spacial metaphor.

  ## Examples

      iex> beside(&(&1 - 10), &(&1 <> "!")).({42, "Hi"})
      {32, "Hi!"}

  """
  @spec beside(Arrow.t(), Arrow.t()) :: Arrow.t()
  defalias beside(a, b), as: :product

  @doc """
  Operator alias for `product/2`.

  ## Examples

      iex> arr = fn x -> x - 10 end ^^^ fn y -> y <> "!" end
      ...> arr.({42, "Hi"})
      {32, "Hi!"}

      iex> {42, "Hi"} |> (fn x -> x - 10 end ^^^ fn y -> y <> "!" end).()
      {32, "Hi!"}

  """
  @spec Arrow.t() ^^^ Arrow.t() :: Arrow.t()
  defalias a ^^^ b, as: :product

  @doc """
  Swap positions of elements in a tuple.

  ## Examples

      iex> swap({1, 2})
      {2, 1}

  """
  @spec swap({any(), any()}) :: {any(), any()}
  def swap({x, y}), do: {y, x}

  @doc """
  Target the first element of a tuple.

  ## Examples

      iex> first(fn x -> x * 50 end).({1, 1})
      {50, 1}

  """
  @spec first(Arrow.t()) :: Arrow.t()
  def first(arrow) do
    arrowize(arrow, fn {x, y} ->
      {
        x |> pipe(arrow),
        y |> pipe(id_arrow(arrow))
      }
    end)
  end

  @doc """
  Target the second element of a tuple.

  ## Examples

      iex> second(fn x -> x * 50 end).({1, 1})
      {1, 50}

  """
  @spec second(Arrow.t()) :: Arrow.t()
  def second(arrow) do
    arrowize(arrow, fn {x, y} ->
      {
        x |> pipe(id_arrow(arrow)),
        y |> pipe(arrow)
      }
    end)
  end

  @doc """
  The identity function lifted into an arrow of the correct type.

  ## Examples

      iex> id_arrow(fn -> nil end).(99)
      99

  """
  @spec id_arrow(Arrow.t()) :: (any() -> Arrow.t())
  def id_arrow(sample), do: arrowize(sample, &Quark.id/1)

  @doc """
  Duplicate incoming data into both halves of a 2-tuple, and run one function
  on the left copy, and a different function on the right copy.

               ┌------> f.(a) = x ------┐
               |                        v
      a ---> split = {a, a}           {x, y}
               |                        ^
               └------> g.(a) = y ------┘

  ## Examples

      iex> Witchcraft.Semigroupoid.pipe(42, fanout(&(&1 - 10), &(inspect(&1) <> "!")))
      {32, "42!"}

  """
  @spec fanout(Arrow.t(), Arrow.t()) :: Arrow.t()
  def fanout(arrow_f, arrow_g) do
    arrow_f |> arrowize(&split/1) <~> (arrow_f ^^^ arrow_g)
  end

  @doc """
  Operator alias for `fanout/2`.

  ## Examples

      iex> fanned = fn x -> x - 10 end &&& fn y -> inspect(y) <> "!" end
      ...> fanned.(42)
      {32, "42!"}

      iex> fanned =
      ...>   fn x -> x - 10 end
      ...>   &&& fn y -> inspect(y) <> "!" end
      ...>   &&& fn z -> inspect(z) <> "?" end
      ...>   &&& fn d -> inspect(d) <> inspect(d) end
      ...>   &&& fn e -> e / 2 end
      ...>
      ...> fanned.(42)
      {{{{32, "42!"}, "42?"}, "4242"}, 21.0}

  """
  @spec Arrow.t() &&& Arrow.t() :: Arrow.t()
  defalias a &&& b, as: :fanout

  @doc """
  Copy a single value into both positions of a 2-tuple.

  This is useful is you want to run functions on the input separately.

  ## Examples

      iex> split(42)
      {42, 42}

      iex> import Witchcraft.Semigroupoid, only: [<~>: 2]
      ...> 5
      ...> |> split()
      ...> |> (second(fn x -> x - 2 end)
      ...> <~> first(fn y -> y * 10 end)
      ...> <~> second(&inspect/1)).()
      {50, "3"}

      iex> use Witchcraft.Arrow
      ...> 5
      ...> |> split()
      ...> |> pipe(second(fn x -> x - 2 end))
      ...> |> pipe(first(fn y -> y * 10 end))
      ...> |> pipe(second(&inspect/1))
      {50, "3"}

  """
  @spec split(any()) :: {any(), any()}
  def split(x), do: {x, x}

  @doc """
  Merge two tuple values with a combining function.

  ## Examples

      iex> unsplit({1, 2}, &+/2)
      3

  """
  @spec unsplit({any(), any()}, (any(), any() -> any())) :: any()
  def unsplit({x, y}, combine), do: combine.(x, y)

  @doc """
  Switch the associativity of a nested tuple. Helpful since many arrows act
  on a subset of a tuple, and you may want to move portions in and out of that stream.

  ## Examples

      iex> reassociate({1, {2, 3}})
      {{1, 2}, 3}

      iex> reassociate({{1, 2}, 3})
      {1, {2, 3}}

  """
  @spec reassociate({any(), {any(), any()}} | {{any(), any()}, any()}) ::
          {{any(), any()}, any()} | {any(), {any(), any()}}
  def reassociate({{a, b}, c}), do: {a, {b, c}}
  def reassociate({a, {b, c}}), do: {{a, b}, c}

  @doc """
  Compose a function (left) with an arrow (right) to produce a new arrow.

  ## Examples

      iex> f = precompose(
      ...>   fn x -> x + 1 end,
      ...>   arrowize(fn _ -> nil end, fn y -> y * 10 end)
      ...> )
      ...> f.(42)
      430

  """
  @spec precompose(fun(), Arrow.t()) :: Arrow.t()
  def precompose(fun, arrow), do: arrowize(arrow, fun) <~> arrow

  @doc """
  Compose an arrow (left) with a function (right) to produce a new arrow.

  ## Examples

      iex> f = postcompose(
      ...>   arrowize(fn _ -> nil end, fn x -> x + 1 end),
      ...>   fn y -> y * 10 end
      ...> )
      ...> f.(42)
      430

  """
  @spec postcompose(Arrow.t(), fun()) :: Arrow.t()
  def postcompose(arrow, fun), do: arrow <~> arrowize(arrow, fun)
end

definst Witchcraft.Arrow, for: Function do
  use Quark

  def arrowize(_, fun), do: curry(fun)
  def first(arrow), do: fn {target, unchanged} -> {arrow.(target), unchanged} end
end
