import TypeClass

defclass Witchcraft.Foldable do
  @moduledoc """
  Data that can be folded over to change its structure by altering or combining elements.

  Unlike `Witchcraft.Functors`s, the end result will not respect the original structure
  unless you build it back up manually.

  ## Examples

      iex> right_fold([1, 2, 3], 0, &+/2) # sum
      6

  ## Properties

  People are working on Foldable properties. This is one of the exceptions to
  there needing to conform to properties. In the meantime, we are testing that
  naturality is preserved, which is be a free theorm.

  If that fails, something is very wrong with the instance.

  ## Type Class

  An instance of `Witchcraft.Foldable` define `Witchcraft.Foldable.right_fold/3`.

      Foldable   [right_fold/3]
  """

  alias __MODULE__
  alias Witchcraft.{Apply, Ord, Monad, Monoid, Semigroup, Unit}

  import Kernel, except: [length: 1, max: 2, min: 2, then: 2]
  import Exceptional.Safe, only: [safe: 1]

  require Foldable.EmptyError

  use Witchcraft.Internal, overrides: [min: 2, max: 2, length: 1], deps: [Semigroup, Ord]

  use Witchcraft.Applicative
  use Quark

  @type t :: any()

  where do
    @doc ~S"""
    Right-associative fold over a structure to alter the structure and/or reduce
    it to a single summary value. The right-association makes it possible to
    cease computation on infinite streams of data.

    The folder must be a binary function, with the second argument being the
    accumulated value thus far.

    ## Examples

        iex> sum = fn xs -> right_fold(xs, 0, &+/2) end
        iex> sum.([1, 2, 3])
        6
        iex> sum.([4, 5, 6])
        15

    """
    @spec right_fold(Foldable.t(), any(), (any(), any() -> any())) :: any()
    def right_fold(foldable, seed, folder)
  end

  properties do
    # Free theorm
    def naturality(data) do
      foldable = generate(data)
      seed = "seed"

      f = &Quark.constant/2
      g = &Quark.id/1

      left =
        foldable
        |> Witchcraft.Foldable.right_fold(seed, f)
        |> g.()

      right =
        foldable
        |> g.()
        |> Witchcraft.Foldable.right_fold(seed, fn x, acc -> f.(g.(x), acc) end)

      equal?(left, right)
    end
  end

  @doc ~S"""
  The same as `right_fold/3`, but uses the first element as the seed

  ## Examples

      iex> right_fold([1, 2, 3], &+/2)
      6

      iex> right_fold([100, 2, 5], &//2)
      40.0 # (2 / (5 / 100))

      iex> right_fold([[], 1, 2, 3], fn(x, acc) -> [x | acc] end)
      [1, 2, 3]

  """
  @spec right_fold(Foldable.t(), fun()) :: any()
  def right_fold(foldable, folder) do
    case to_list(foldable) do
      [] -> []
      [a | as] -> right_fold(as, a, folder)
    end
  end

  @doc ~S"""
  Left-associative fold over a structure to alter the structure and/or reduce
  it to a single summary value.

  The folder must be a binary function, with the second argument being the
  accumulated value thus far.

  ## Examples

      iex> sum = fn xs -> right_fold(xs, 0, &+/2) end
      iex> sum.([1, 2, 3])
      6
      iex> sum.([4, 5, 6])
      15

      iex> left_fold([1, 2, 3], [], fn(acc, x) -> [x | acc] end)
      [3, 2, 1]

      iex> left_fold({1, 2, 3}, [], fn(acc, x) -> [x | acc] end)
      [3]

      iex> left_fold([1, 2, 3], [4, 5, 6], fn(acc, x) -> [x | acc] end)
      [3, 2, 1, 4, 5, 6]

  Note the reducer argument order versus `right_fold/3`

      iex> right_fold([1, 2, 3], [], fn(acc, x) -> [acc | x] end)
      [1, 2, 3]

      iex> left_fold([1, 2, 3], [], fn(acc, x) -> [acc | x] end)
      [[[[] | 1] | 2] | 3]

  """
  @spec left_fold(Foldable.t(), any(), (any(), any() -> any())) :: any()
  def left_fold(foldable, seed, folder) do
    right_fold(foldable, &Quark.id/1, fn b, g ->
      fn x ->
        x
        |> folder.(b)
        |> g.()
      end
    end).(seed)
  end

  @doc ~S"""
  The same as `left_fold/3`, but uses the first element as the seed

  ## Examples

      iex> left_fold([1, 2, 3], &+/2)
      6

      iex> left_fold([100, 2, 5], &//2)
      10.0 # ((100 / 2) / 5)

      iex> left_fold([1, 2, 3], [], fn(acc, x) -> [x | acc] end)
      [3, 2, 1]

  Note the reducer argument order versus `right_fold/2`

      iex> right_fold([100, 20, 10], &//2)
      200.0

      iex> left_fold([100, 20, 10], &//2)
      0.5

  """
  @spec left_fold(Foldable.t(), (any(), any() -> any())) :: any()
  def left_fold(foldable, folder) do
    [x | xs] = to_list(foldable)
    left_fold(xs, x, folder)
  end

  @doc """
  Combine all elements using monoidal append

  ## Examples

      iex> fold([1, 2, 3])
      6

      iex> fold([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
      [1, 2, 3, 4, 5, 6, 7, 8, 9]

  """
  @spec fold(Foldable.t()) :: any()
  def fold(foldable), do: left_fold(foldable, &Semigroup.append/2)

  @doc """
  Map a functional over all elements and `fold` them together

  ## Examples

      iex> fold_map([1, 2, 3], fn x -> [x, x * 10] end)
      [1, 10, 2, 20, 3, 30]

      iex> fold_map([[1, 2, 3], [4, 5, 6], [7, 8, 9]], fn x -> [x, x] end)
      [
        [1, 2, 3], [1, 2, 3],
        [4, 5, 6], [4, 5, 6],
        [7, 8, 9], [7, 8, 9]
      ]

  """
  @spec fold_map(Foldable.t(), fun()) :: any()
  def fold_map(foldable, fun) do
    right_fold(foldable, Monoid.empty(foldable), fn element, acc ->
      element
      |> fun.()
      |> Semigroup.append(acc)
    end)
  end

  @doc """
  Turn any `Foldable` into a `List`

  ## Example

      iex> to_list({1, 2, 3})
      [1, 2, 3]

      iex> to_list(%{a: 1, b: 2, c: 3})
      [1, 2, 3]

  """
  @spec to_list(Foldable.t()) :: [any()]
  def to_list(list) when is_list(list), do: list
  def to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
  def to_list(string) when is_bitstring(string), do: String.to_charlist(string)
  def to_list(foldable), do: right_fold(foldable, [], fn x, acc -> [x | acc] end)

  @doc """
  Check if a foldable data structure is empty

  ## Examples

      iex> empty?("")
      true

      iex> empty?("hi")
      false

      iex> empty?(%{})
      true

  """
  @spec empty?(Foldable.t()) :: boolean
  def empty?(foldable), do: right_fold(foldable, true, fn _focus, _acc -> false end)

  @doc """
  Count the number of elements in a foldable structure

  ## Examples

      iex> use Witchcraft.Foldable
      iex> length(%{})
      0
      iex> length(%{a: 1, b: 2})
      2
      iex> length("ࠀabc")
      4

  """
  @spec length(Foldable.t()) :: non_neg_integer()
  def length(list) when is_list(list), do: Kernel.length(list)
  def length(foldable), do: right_fold(foldable, 0, fn _, acc -> 1 + acc end)

  defalias count(foldable), as: :length
  defalias size(foldable), as: :length

  @doc """
  Check if a foldable structure contains a particular element

  ## Examples

      iex> member?([1, 2, 3], 2)
      true

      iex> member?([1, 2, 3], 99)
      false

      iex> member?(%{a: 1, b: 2}, 2)
      true

      iex> member?(%{a: 1, b: 2}, 99)
      false

  """
  @spec member?(Foldable.t(), any()) :: boolean()
  def member?(list, target) when is_list(list), do: Enum.member?(list, target)
  def member?(map, target) when is_map(map), do: map |> Map.values() |> Enum.member?(target)

  def member?(tuple, target) when is_tuple(tuple) do
    tuple
    |> Tuple.to_list()
    |> Enum.member?(target)
  end

  def member?(string, target) when is_bitstring(string) do
    string
    |> String.to_charlist()
    |> Enum.member?(target)
  end

  def member?(foldable, target) do
    right_fold(foldable, false, fn focus, acc -> acc or focus == target end)
  end

  @doc """
  Find the maximum element in a foldable structure using a custom comparitor

  Elements must implement `Witchcraft.Ord`.

  Comes in both a safe and unsafe(`!`) version

  ## Examples

      iex> use Witchcraft.Foldable
      ...> [1, 2, 7]
      ...> |> max(by: fn(x, y) ->
      ...>   x
      ...>   |> Integer.mod(3)
      ...>   |> Witchcraft.Ord.compare(Integer.mod(y, 3))
      ...> end)
      2

  """
  @spec max(Foldable.t(), by: (any, any -> Ord.ordering())) :: Ord.t()
  def max(foldable, by: comparator) do
    Witchcraft.Foldable.right_fold(foldable, fn focus, acc ->
      case comparator.(focus, acc) do
        :greater -> focus
        _ -> acc
      end
    end)
  end

  @doc """
  Find the maximum element in a foldable structure using the default ordering
  from `Witchcraft.Ord`.

  Elements must implement `Witchcraft.Ord`.

  ## Examples

      iex> use Witchcraft.Foldable
      iex> max([2, 3, 1])
      3
      iex> max([[4], [1, 2, 3, 4]])
      [4]

      %BinaryTree{
        node: 1,
        left: %BinaryTree{
          node: 3
          left: 4
        },
        right: 2
      }
      |> max()
      #=> 4

  """
  @spec max(Foldable.t()) :: Ord.t()
  def max(foldable_comparable), do: max(foldable_comparable, by: &Ord.compare/2)

  @doc """
  Find the maximum element in a foldable structure using a custom comparitor

  Elements must implement `Witchcraft.Ord`.

  Comes in both a safe and unsafe(`!`) version

  ## Examples

      iex> use Witchcraft.Foldable
      ...> [8, 2, 1]
      ...> |> min(by: fn(x, y) ->
      ...>   x
      ...>   |> Integer.mod(4)
      ...>   |> Witchcraft.Ord.compare(Integer.mod(y, 4))
      ...> end)
      8

  """
  @spec min(Foldable.t(), by: (any(), any() -> Ord.t())) :: any()
  def min(foldable, by: comparitor) do
    right_fold(foldable, fn focus, acc ->
      case comparitor.(focus, acc) do
        :lesser -> focus
        _ -> acc
      end
    end)
  end

  @doc """
  Find the minimum element in a foldable structure using the default ordering
  from `Witchcraft.Ord`.

  Elements must implement `Witchcraft.Ord`.

  ## Examples

      iex> use Witchcraft.Foldable
      iex> min([2, 3, 1])
      1
      iex> min([[4], [1, 2, 3, 4]])
      [1, 2, 3, 4]

      %BinaryTree{
        node: 4,
        left: %BinaryTree{
          node: 3
          left: 1
        },
        right: 2
      }
      |> min()
      #=> 1

  """
  def min(foldable), do: min(foldable, by: &Ord.compare/2)

  @doc """
  Get a random element from a foldable structure.

  ## Examples

      random([1, 2, 3])
      #=> 1

      random([1, 2, 3])
      #=> 3

      random(%BinaryTree{left: %Empty{}, node: 2, right: %BinaryTree{node: 1}})
      1

  """
  @spec random(Foldable.t()) :: any() | Foldable.EmptyError.t()
  def random(foldable) do
    foldable
    |> to_list
    |> safe(&Enum.random/1).()
    |> case do
      %Enum.EmptyError{} -> Foldable.EmptyError.new(foldable)
      value -> value
    end
  end

  @doc ~S"""
  Sum all numbers in a foldable

  ## Examples

      iex> sum([1, 2, 3])
      6

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

      %BinaryTree{
        left:  4,
        right: %BinaryTree{
          left: 2,
          right: 10
        }
      } |> sum()
      #=> 16

  """
  @spec sum(Foldable.t()) :: number()
  def sum(foldable), do: right_fold(foldable, 0, &+/2)

  @doc ~S"""
  Product of all numbers in a foldable

  ## Examples

      iex> product([1, 2, 3])
      6

      iex> product({1, 2, 3})
      6

      %BinaryTree{
        left:  4,
        right: %BinaryTree{
          left: 2,
          right: 10
        }
      }
      |> product()
      #=> 80

  """
  @spec product(Foldable.t()) :: number()
  def product(foldable), do: right_fold(foldable, &*/2)

  @doc ~S"""
  Concatenate all lists in a foldable structure

  ## Examples

      iex> flatten([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
      [1, 2, 3, 4, 5, 6, 7, 8, 9]

      iex> flatten({[1, 2, 3], [4, 5, 6], [7, 8, 9]})
      [7, 8, 9]

      %BinaryTree{
        left:  [1, 2, 3],
        right: %BinaryTree{
          left:  [4, 5],
          right: [6]
        }
      }
      |> flatten()
      #=> [1, 2, 3, 4, 5, 6]

  """
  @spec flatten(Foldable.t()) :: [any()]
  def flatten(contained_lists) do
    right_fold(contained_lists, [], &Semigroup.append/2)
  end

  @doc ~S"""
  Lift a function over a foldable structure generating lists of results,
  and then concatenate the resulting lists

  ## Examples

      iex> flat_map([1, 2, 3, 4, 5, 6], fn x -> [x, x] end)
      [1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6]

      iex> flat_map({1, 2, 3, 4, 5, 6}, fn x -> [x, x] end)
      [6, 6]

      %BinaryTree{
        left:  1,
        right: %BinaryTree{
          left:  2,
          right: 3
        }
      }
      |> flat_map(fn x -> [x, x] end)
      #=> [1, 1, 2, 2, 3, 3]

  """
  @spec flat_map(Foldable.t(), (any() -> [any()])) :: [any()]
  def flat_map(foldable, mapper) do
    foldable
    |> right_fold([], fn inner_focus, acc ->
      [mapper.(inner_focus) | acc]
    end)
    |> flatten()
  end

  @doc """
  Test whether the structure is empty. The default implementation is
  optimized for structures that are similar to lists, because there
  is no general way to do better.

  ## Examples

      iex> null?([])
      true

      iex> null?([1, 2, 3])
      false

  """
  @spec null?(Foldable.t()) :: boolean()
  def null?(foldable), do: right_fold(foldable, true, fn _, _ -> false end)

  @doc ~S"""
  Check if a foldable is full of only `true`s

  ## Examples

      iex> all?([true, true, false])
      false

      iex> all?({true, true, false})
      false

      %BinaryTree{
        left:  true,
        right: %BinaryTree{
          left:  true,
          right: false
        }
      } |> all?()
      #=> false

  """
  @spec all?(Foldable.t()) :: boolean()
  def all?(foldable_bools), do: right_fold(foldable_bools, true, &and/2)

  @doc ~S"""
  The same as `all?/1`, but with a custom predicate matcher

  ## Examples

      iex> import Integer
      iex> all?([1, 2, 3], &is_odd/1)
      false

      %BinaryTree{
        left:  1,
        right: %BinaryTree{
          left:  2,
          right: 3
        }
      }
      |> all?(&Integer.is_odd?/1)
      #=> false

  """
  @spec all?(Foldable.t(), (any() -> boolean())) :: boolean()
  def all?(foldable, predicate) do
    right_fold(foldable, true, fn focus, acc -> predicate.(focus) and acc end)
  end

  @doc ~S"""
  Check if a foldable contains any `true`s

  ## Examples

      iex> any? [true, true, false]
      true

      %BinaryTree{
        left:  true,
        right: %BinaryTree{
          left:  true,
          right: false
        }
      } |> any?()
      #=> true

  Not that the `Tuple` instance behaves somewhat conterintuitively

      iex> any? {true, true, false}
      false

      iex> any? {true, false, true}
      true

  """
  @spec any?(Foldable.t()) :: boolean()
  def any?(foldable_bools), do: right_fold(foldable_bools, false, &or/2)

  @doc ~S"""
  The same as `all?/1`, but with a custom predicate matcher

  ## Examples

      iex> require Integer
      iex> any?([1, 2, 3], &Integer.is_odd/1)
      true

      %BinaryTree{
        left:  1,
        right: %BinaryTree{
          left:  2,
          right: 3
        }
      }
      |> any(&Integer.is_odd?/1)
      #=> true

  """
  @spec any?(Foldable.t(), (any() -> boolean())) :: boolean()
  def any?(foldable, predicate) do
    right_fold(foldable, false, fn focus, acc -> predicate.(focus) or acc end)
  end

  @doc """
  Run each action from left to right, discarding all values.

  Always returns `%Witchcraft.Unit{}` in the same foldbale structure that you started with.

  ## Examples

      iex> then_sequence([[1, 2, 3], [4, 5, 6]])
      [
        %Witchcraft.Unit{},
        %Witchcraft.Unit{},
        %Witchcraft.Unit{},
        %Witchcraft.Unit{},
        %Witchcraft.Unit{},
        %Witchcraft.Unit{},
        %Witchcraft.Unit{},
        %Witchcraft.Unit{},
        %Witchcraft.Unit{}
      ]

      iex> then_sequence({{1, 2, 3}, {4, 5, 6}})
      {4, 5, %Witchcraft.Unit{}}

      iex> then_sequence({[1, 2, 3], [4, 5, 6]})
      [
        %Witchcraft.Unit{},
        %Witchcraft.Unit{},
        %Witchcraft.Unit{}
      ]

  """
  @spec then_sequence(Foldable.t()) :: Monad.t()
  def then_sequence(foldable_monad) do
    seed =
      foldable_monad
      |> to_list()
      |> hd()
      |> of(%Unit{})

    right_fold(foldable_monad, seed, &Witchcraft.Apply.then/2)
  end

  @doc """
  `traverse` actions over data, but ignore the results.

  Not a typo: this is in the correct module, since it doens't depend directly
  on `Witchcraft.Traversable`, but behaves in a similar manner.

  ## Examples

      iex> [1, 2, 3]
      ...> |> then_traverse(fn x -> [x, x * 5, x * 10] end)
      [
          #
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          #
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          #
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{}
      ]

  """
  @spec then_traverse(Foldable.t(), Apply.fun()) :: Apply.t()
  def then_traverse(foldable, fun) do
    right_fold(foldable, of(foldable, %Unit{}), fn step, acc ->
      step
      |> fun.()
      |> Witchcraft.Apply.then(acc)
    end)
  end

  @doc """
  The same as `then_traverse`, but with the arguments flipped.

  ## Examples

      iex> fn x -> [x, x * 5, x * 10] end
      ...> |> then_through([1, 2, 3])
      [
          #
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          #
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          #
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{},
          %Witchcraft.Unit{}, %Witchcraft.Unit{}, %Witchcraft.Unit{}
      ]

  """
  @spec then_through(Apply.fun(), Foldable.t()) :: Apply.t()
  def then_through(fun, traversable), do: then_traverse(traversable, fun)
end
