import TypeClass

defclass Witchcraft.Chain do
  @moduledoc """
  Chain function applications on contained data that may have some additional effect

  As a diagram:

      %Container<data> --- (data -> %Container<updated_data>) ---> %Container<updated_data>

  ## Examples

      iex> chain([1, 2, 3], fn x -> [x, x] end)
      [1, 1, 2, 2, 3, 3]

      alias Algae.Maybe.{Nothing, Just}

      %Just{just: 42} >>> fn x -> %Just{just: x + 1} end
      #=> %Just{just: 43}

      %Just{just: 42}
      >>> fn x -> if x > 50, do: %Just{just: x + 1}, else: %Nothing{} end
      >>> fn y -> y * 100 end
      #=> %Nothing{}

  ## Type Class

  An instance of `Witchcraft.Chain` must also implement `Witchcraft.Apply`,
  and define `Witchcraft.Chain.chain/2`.

      Functor  [map/2]
         ↓
       Apply   [convey/2]
         ↓
       Chain   [chain/2]
  """

  alias __MODULE__

  extend Witchcraft.Apply

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

  use Witchcraft.Apply

  @type t :: any()
  @type link :: (any() -> Chain.t())

  where do
    @doc """
    Sequentially compose actions, piping values through successive function chains.

    The applied linking function must be unary and return data in the same
    type of container as the input. The chain function essentially "unwraps"
    a contained value, applies a linking function that returns
    the initial (wrapped) type, and collects them into a flat(ter) structure.

    `chain/2` is sometimes called "flat map", since it can also
    be expressed as `data |> map(link_fun) |> flatten()`.

    As a diagram:

        %Container<data> --- (data -> %Container<updated_data>) ---> %Container<updated_data>

    ## Examples

        iex> chain([1, 2, 3], fn x -> [x, x] end)
        [1, 1, 2, 2, 3, 3]

        iex> [1, 2, 3]
        ...> |> chain(fn x -> [x, x] end)
        ...> |> chain(fn y -> [y, 2 * y, 3 * y] end)
        [1, 2, 3, 1, 2, 3, 2, 4, 6, 2, 4, 6, 3, 6, 9, 3, 6, 9]

        iex> chain([1, 2, 3], fn x ->
        ...>   chain([x + 1], fn y ->
        ...>     chain([y + 2, y + 10], fn z ->
        ...>       [x, y, z]
        ...>     end)
        ...>   end)
        ...> end)
        [1, 2, 4, 1, 2, 12, 2, 3, 5, 2, 3, 13, 3, 4, 6, 3, 4, 14]

    """
    @spec chain(Chain.t(), Chain.link()) :: Chain.t()
    def chain(chainable, link_fun)
  end

  @doc """
  `chain/2` but with the arguments flipped.

  ## Examples

      iex> draw(fn x -> [x, x] end, [1, 2, 3])
      [1, 1, 2, 2, 3, 3]

      iex> (fn y -> [y * 5, y * 10] end)
      ...> |> draw((fn x -> [x, x] end)
      ...> |> draw([1, 2, 3])) # note the "extra" closing paren
      [5, 10, 5, 10, 10, 20, 10, 20, 15, 30, 15, 30]

  """
  @spec draw(Chain.link(), Chain.t()) :: Chain.t()
  def draw(chain_fun, chainable), do: chain(chainable, chain_fun)

  @doc """
  An alias for `chain/2`.

  Provided as a convenience for those coming from other languages.
  """
  @spec bind(Chain.t(), Chain.link()) :: Chain.t()
  defalias bind(chainable, binder), as: :chain

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

  Extends the `~>` / `~>>` heirarchy with one more level of power / abstraction

  ## Examples

      iex> to_monad = fn x -> (fn _ -> x end) end
      ...> bound = to_monad.(&(&1 * 10)) >>> to_monad.(&(&1 + 10))
      ...> bound.(10)
      20

  In Haskell, this is the famous `>>=` operator, but Elixir doesn't allow that
  infix operator.

  """
  @spec Chain.t() >>> Chain.link() :: Chain.t()
  defalias chainable >>> chain_fun, as: :chain

  @doc """
  Operator alias for `draw/2`

  Extends the `<~` / `<<~` heirarchy with one more level of power / abstraction

  ## Examples

      iex> to_monad = fn x -> (fn _ -> x end) end
      ...> bound = to_monad.(&(&1 + 10)) <<< to_monad.(&(&1 * 10))
      ...> bound.(10)
      20

  In Haskell, this is the famous `=<<` operator, but Elixir doesn't allow that
  infix operator.

  """
  @spec Chain.t() <<< Chain.link() :: Chain.t()
  defalias chain_fun <<< chainable, as: :draw

  @doc """
  Join together one nested level of a data structure that contains itself

  ## Examples

      iex> join([[1, 2, 3]])
      [1, 2, 3]

      iex> join([[1, 2, 3], [4, 5, 6]])
      [1, 2, 3, 4, 5, 6]

      iex> join([[[1, 2, 3], [4, 5, 6]]])
      [[1, 2, 3], [4, 5, 6]]

      alias Algae.Maybe.{Nothing, Just}
      %Just{
        just: %Just{
          just: 42
        }
      } |> join()
      #=> %Just{just: 42}

      join %Just{just: %Nothing{}}
      #=> %Nothing{}

      join %Just{just: %Just{just: %Nothing{}}}
      #=> %Just{just: %Nothing{}}

      %Nothing{} |> join() |> join() |> join() # ...and so on, forever
      #=> %Nothing{}

  Joining tuples is a bit counterintuitive, as it requires a very specific format:

      iex> join {      # Outer 2-tuple
      ...>   {1, 2},   # Inner 2-tuple
      ...>   {
      ...>     {3, 4}, # Doubly inner 2-tuple
      ...>     {5, 6, 7}
      ...>   }
      ...> }
      {{4, 6}, {5, 6, 7}}

      iex> join {
      ...>   {"a", "b"},
      ...>   {
      ...>     {"!", "?"},
      ...>     {:ok, 123}
      ...>   }
      ...> }
      {{"a!", "b?"}, {:ok, 123}}

  """
  @spec join(Chain.t()) :: Chain.t()
  def join(nested), do: nested >>> (&Quark.id/1)

  @spec flatten(Chain.t()) :: Chain.t()
  defalias flatten(nested), as: :join

  @doc """
  Compose link functions to create a new link function.

  Note that this runs the same direction as `<|>` ("the math way").

  This is `pipe_compose_link/2` with arguments flipped.

  ## Examples

      iex> links =
      ...>   fn x -> [x, x] end
      ...>   |> compose_link(fn y -> [y * 10] end)
      ...>   |> compose_link(fn z -> [z + 42] end)
      ...>
      ...> [1, 2, 3] >>> links
      [430, 430, 440, 440, 450, 450]

  """
  @spec compose_link(Chain.link(), Chain.link()) :: Chain.link()
  def compose_link(action_g, action_f), do: pipe_compose_link(action_f, action_g)

  @doc """
  Compose link functions to create a new link function.

  This is `compose_link/2` with arguments flipped.

  ## Examples

      iex> links =
      ...>   fn x -> [x, x] end
      ...>   |> pipe_compose_link(fn y -> [y * 10] end)
      ...>   |> pipe_compose_link(fn z -> [z + 42] end)
      ...>
      ...> [1, 2, 3] >>> links
      [52, 52, 62, 62, 72, 72]

  """
  @spec pipe_compose_link(Chain.link(), Chain.link()) :: Chain.link()
  def pipe_compose_link(action_f, action_g) do
    fn data -> action_f.(data) >>> action_g end
  end

  @doc """
  `do` notation sugar

  Sequences chainable actions. Note that each line must be of the same type.

  For a version with `return`, please see `Witchcraft.Monad.monad/2`

  ## Examples

      iex> chain do
      ...>   [1]
      ...> end
      [1]

      iex> chain do
      ...>   [1, 2, 3]
      ...>   [4, 5, 6]
      ...>   [7, 8, 9]
      ...> end
      [
        7, 8, 9,
        7, 8, 9,
        7, 8, 9,
        7, 8, 9,
        7, 8, 9,
        7, 8, 9,
        7, 8, 9,
        7, 8, 9,
        7, 8, 9
      ]

      iex> chain do
      ...>   a <- [1, 2, 3]
      ...>   b <- [4, 5, 6]
      ...>   [a * b]
      ...> end
      [
        4,  5,  6,
        8,  10, 12,
        12, 15, 18
      ]

  Normal functions are fine within the `do` as well, as long as each line
  ends up being the same chainable type

      iex> import Witchcraft.{Functor, Applicative}
      ...> chain do
      ...>   map([1, 2, 3], fn x -> x + 1 end)
      ...>   of([], 42)
      ...>   [7, 8, 9] ~> fn x -> x * 10 end
      ...> end
      [
        70, 80, 90,
        70, 80, 90,
        70, 80, 90
      ]

  Or with a custom type

      alias Algae.Maybe.{Nothing, Just}

      chain do
        %Just{just: 4}
        %Just{just: 5}
        %Just{just: 6}
      end
      #=> %Just{just: 6}

      chain do
        %Just{just: 4}
        %Nothing{}
        %Just{just: 6}
      end
      #=> %Nothing{}

  ## `let` bindings

  `let`s allow you to hold static or intermediate values inside a
  do-block, much like normal assignment

      iex> chain do
      ...>   let a = 4
      ...>   [a]
      ...> end
      [4]

      iex> chain do
      ...>   a <- [1, 2]
      ...>   b <- [3, 4]
      ...>   let [h | _] = [a * b]
      ...>   [h, h, h]
      ...> end
      [3, 3, 3, 4, 4, 4, 6, 6, 6, 8, 8, 8]

  ## Desugaring

  ### Sequencing

  The most basic form

      chain do
        [1, 2, 3]
        [4, 5, 6]
        [7, 8, 9]
      end

  is equivalent to

      [1, 2, 3]
      |> then([4, 5, 6])
      |> then([7, 8, 9])

  ### `<-` ("drawn from")

  Drawing values from within a chainable structure is similar feels similar
  to assignmet, but it is pulling each value separately in a chain link function.

  For instance

      iex> chain do
      ...>   a <- [1, 2, 3]
      ...>   b <- [4, 5, 6]
      ...>   [a * b]
      ...> end
      [4, 5, 6, 8, 10, 12, 12, 15, 18]

  desugars to this

      iex> [1, 2, 3] >>> fn a ->
      ...>   [4, 5, 6] >>> fn b ->
      ...>     [a * b]
      ...>   end
      ...> end
      [4, 5, 6, 8, 10, 12, 12, 15, 18]

  but is often much cleaner to read in do-notation, as it cleans up all of the
  nested functions (especially when the chain is very long).

  You can also use values recursively:

      # iex> chain do
      # ...>   a <- [1, 2, 3]
      # ...>   b <- [a, a * 10, a * 100]
      # ...>   [a + 1, b + 1]
      # ...> end
      # [
      #   2, 2, 2, 11, 2, 101,
      #   3, 3, 3, 21, 3, 201,
      #   4, 4, 4, 31, 4, 301
      # ]

  """
  defmacro chain(do: input) do
    Witchcraft.Chain.do_notation(input, &Witchcraft.Chain.chain/2)
  end

  @doc false
  # credo:disable-for-lines:31 Credo.Check.Refactor.Nesting
  def do_notation(input, _chainer) do
    input
    |> normalize()
    |> Enum.reverse()
    |> Witchcraft.Foldable.left_fold(fn
      continue, {:let, _, [{:=, _, [assign, value]}]} ->
        quote do: unquote(value) |> (fn unquote(assign) -> unquote(continue) end).()

      continue, {:<-, _, [assign, value]} ->
        quote do
          import Witchcraft.Chain, only: [>>>: 2]

          unquote(value) >>> fn unquote(assign) -> unquote(continue) end
        end

      continue, value ->
        quote do
          import Witchcraft.Chain, only: [>>>: 2]
          unquote(value) >>> fn _ -> unquote(continue) end
        end
    end)
  end

  @doc false
  def normalize({:__block__, _, inner}), do: inner
  def normalize(single) when is_list(single), do: [single]
  def normalize(plain), do: List.wrap(plain)

  properties do
    def associativity(data) do
      a = generate(data)
      f = fn x -> Witchcraft.Applicative.of(a, inspect(x)) end
      g = fn y -> Witchcraft.Applicative.of(a, y <> y) end

      left = a |> Chain.chain(f) |> Chain.chain(g)
      right = a |> Chain.chain(fn x -> x |> f.() |> Chain.chain(g) end)

      equal?(left, right)
    end
  end
end

definst Witchcraft.Chain, for: Function do
  alias Witchcraft.Chain
  use Quark

  @spec chain(Chain.t(), (any() -> any())) :: Chain.t()
  def chain(fun, chain_fun), do: fn r -> curry(chain_fun).(fun.(r)).(r) end
end

definst Witchcraft.Chain, for: List do
  use Quark
  def chain(list, chain_fun), do: Enum.flat_map(list, curry(chain_fun))
end

definst Witchcraft.Chain, for: Tuple do
  use Witchcraft.Semigroup

  custom_generator(_) do
    import TypeClass.Property.Generator, only: [generate: 1]
    seed = fn -> Enum.random([0, 1.1, "", []]) end
    {generate(seed.()), generate(seed.())}
  end

  def chain({a, b}, chain_fun) do
    {c, d} = chain_fun.(b)
    {a <> c, d}
  end
end
