#            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
#                    Version 2, December 2004
#
#            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
#   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
#
#  0. You just DO WHAT THE FUCK YOU WANT TO.

defmodule Data.Seq do
  alias Data.Protocol, as: P
  alias Data.Protocol.Sequence, as: S
  alias Data.Error, as: E

  @type t :: P.Sequence.t | P.ToSequence.t | P.ToList.t

  def first(sequence) do
    Data.seq(sequence) |> S.first
  end

  def next(sequence) do
    Data.seq(sequence) |> S.next
  end

  defmodule WithIndex do
    defstruct index: 0, seq: nil

    def new(seq) do
      case Data.seq(seq) do
        nil ->
          nil

        new ->
          %__MODULE__{index: 0, seq: new}
      end
    end

    def first(%__MODULE__{index: index, seq: seq}) do
      { S.first(seq), index }
    end

    def next(%__MODULE__{index: index, seq: seq}) do
      case S.next(seq) do
        nil ->
          nil

        next ->
          %__MODULE__{index: index + 1, seq: next}
      end
    end

    defimpl P.Sequence do
      defdelegate first(self), to: WithIndex
      defdelegate next(self),  to: WithIndex
    end
  end

  def with_index(sequence) do
    WithIndex.new(sequence)
  end

  @spec all?(t, (any -> boolean)) :: boolean
  def all?(sequence, fun \\ fn(x) -> x end) do
    do_all?(Data.seq(sequence), fun)
  end

  defp do_all?(nil, _) do
    true
  end

  defp do_all?(sequence, fun) do
    if fun.(S.first(sequence)) do
      do_all?(S.next(sequence), fun)
    else
      false
    end
  end

  @spec any?(t, (any -> as_boolean(any))) :: boolean
  def any?(sequence, fun \\ fn(x) -> x end) do
    do_any?(Data.seq(sequence), fun)
  end

  defp do_any?(nil, _) do
    false
  end

  defp do_any?(sequence, fun) do
    if fun.(S.first(sequence)) do
      true
    else
      do_any?(S.next(sequence), fun)
    end
  end

  @spec at(t, non_neg_integer, any) :: any
  def at(sequence, index, default \\ nil) do
    do_at(0, Data.seq(sequence), index, default)
  end

  defp do_at(_, nil, _, default) do
    default
  end

  defp do_at(current, sequence, index, _) when current == index do
    S.first(sequence)
  end

  defp do_at(current, sequence, index, default) do
    do_at(current + 1, S.next(sequence), index, default)
  end

  @spec find(t, (any -> as_boolean(any)))      :: any
  @spec find(t, any, (any -> as_boolean(any))) :: any
  def find(sequence, if_none \\ nil, fun) do
    do_find(Data.seq(sequence), if_none, fun)
  end

  defp do_find(nil, if_none, _) do
    if_none
  end

  defp do_find(sequence, if_none, fun) do
    value = S.first(sequence)

    if fun.(value) do
      value
    else
      do_find(S.next(sequence), if_none, fun)
    end
  end

  @spec find_value(t, (any -> any))      :: any
  @spec find_value(t, any, (any -> any)) :: any
  def find_value(sequence, if_none \\ nil, fun) do
    do_find_value(Data.seq(sequence), if_none, fun)
  end

  defp do_find_value(nil, if_none, _) do
    if_none
  end

  defp do_find_value(sequence, if_none, fun) do
    value = fun.(S.first(sequence))

    if value do
      value
    else
      do_find_value(S.next(sequence), if_none, fun)
    end
  end

  @spec find_index(t, (any -> as_boolean(any))) :: any
  def find_index(sequence, fun) do
    do_find_index(0, Data.seq(sequence), fun)
  end

  defp do_find_index(_, nil, _) do
    nil
  end

  defp do_find_index(index, sequence, fun) do
    if fun.(S.first(sequence)) do
      index
    else
      do_find_index(index + 1, S.next(sequence), fun)
    end
  end

  @spec contains?(t, any) :: boolean
  @spec contains?(t, any, (any -> any)) :: boolean
  def contains?(sequence, value, fun \\ fn(x) -> x end) do
    do_contains?(Data.seq(sequence), value, fun)
  end

  defp do_contains?(nil, _, _) do
    false
  end

  defp do_contains?(sequence, value, fun) do
    if fun.(S.first(sequence)) == value do
      true
    else
      do_contains?(S.next(sequence), value, fun)
    end
  end

  @spec drop(t, non_neg_integer) :: t
  def drop(sequence, count) do
    do_drop(Data.seq(sequence), count)
  end

  defp do_drop(nil, _) do
    nil
  end

  defp do_drop(sequence, 0) do
    sequence
  end

  defp do_drop(sequence, count) do
    do_drop(S.next(sequence), count - 1)
  end

  @spec drop_while(t, (any -> as_boolean(term))) :: t
  def drop_while(sequence, fun) do
    do_drop_while(Data.seq(sequence), fun)
  end

  defp do_drop_while(nil, _) do
    nil
  end

  defp do_drop_while(sequence, fun) do
    if fun.(S.first(sequence)) do
      do_drop_while(S.next(sequence), fun)
    else
      sequence
    end
  end

  @spec take(t, non_neg_integer) :: t
  def take(sequence, count) do
    Data.seq(do_take([], Data.seq(sequence), count))
  end

  defp do_take(acc, nil, _) do
    acc |> :lists.reverse
  end

  defp do_take(acc, _, 0) do
    acc |> :lists.reverse
  end

  defp do_take(acc, sequence, count) do
    [S.first(sequence) | acc] |> do_take(S.next(sequence), count - 1)
  end

  @spec take_while(t, (any -> as_boolean(any))) :: t
  def take_while(sequence, fun) do
    Data.seq(do_take_while([], Data.seq(sequence), fun))
  end

  defp do_take_while(acc, nil, _) do
    acc |> :lists.reverse
  end

  defp do_take_while(acc, sequence, fun) do
    value = S.first(sequence)

    if fun.(value) do
      [value | acc] |> do_take_while(S.next(sequence), fun)
    else
      acc |> :lists.reverse
    end
  end

  @spec each(t, (any -> none)) :: none
  def each(sequence, fun) do
    do_each(Data.seq(sequence), fun)
  end

  defp do_each(nil, _) do
    nil
  end

  defp do_each(sequence, fun) do
    fun.(S.first(sequence))

    do_each(S.next(sequence), fun)
  end

  @spec select(t, (any -> as_boolean(term))) :: t
  def select(sequence, fun) do
    do_select([], Data.seq(sequence), fun)
  end

  defp do_select(acc, nil, _) do
    acc |> :lists.reverse
  end

  defp do_select(acc, sequence, fun) do
    value = S.first(sequence)

    if fun.(value) do
      do_select([value | acc], S.next(sequence), fun)
    else
      do_select(acc, S.next(sequence), fun)
    end
  end

  @spec reject(t, (any -> as_boolean(term))) :: t
  def reject(sequence, fun) do
    do_reject([], Data.seq(sequence), fun)
  end

  defp do_reject(acc, nil, _) do
    acc |> :lists.reverse
  end

  defp do_reject(acc, sequence, fun) do
    value = S.first(sequence)

    if fun.(value) do
      do_reject(acc, S.next(sequence), fun)
    else
      do_reject([value | acc], S.next(sequence), fun)
    end
  end

  @spec map(t, (any -> any)) :: t
  def map(sequence, fun) do
    do_map([], Data.seq(sequence), fun)
  end

  defp do_map(acc, nil, _) do
    acc |> :lists.reverse
  end

  defp do_map(acc, sequence, fun) do
    [fun.(S.first(sequence)) | acc] |> do_map(S.next(sequence), fun)
  end

  @spec flat_map(t, (any -> any)) :: t
  def flat_map(sequence, fun) do
    do_flat_map([], Data.seq(sequence), fun)
  end

  defp do_flat_map(acc, nil, _) do
    acc |> :lists.reverse
  end

  defp do_flat_map(acc, sequence, fun) do
    do_flat_map(Data.list(fun.(S.first(sequence))) ++ acc, S.next(sequence), fun)
  end

  @spec reverse(t) :: t
  def reverse(sequence) when sequence |> is_list do
    :lists.reverse(sequence)
  end

  def reverse(sequence) do
    do_reverse([], Data.seq(sequence))
  end

  defp do_reverse(acc, nil) do
    acc
  end

  defp do_reverse(acc, sequence) do
    [S.first(sequence) | acc] |> do_reverse(S.next(sequence))
  end

  @spec reduce(t, (any, any -> any)) :: any
  def reduce(sequence, fun) do
    reduce(next(sequence), first(sequence), fun)
  end

  @spec reduce(t, any, (any, any -> any)) :: any
  def reduce(sequence, acc, fun) when sequence |> is_list do
    :lists.foldl(fun, acc, sequence)
  end

  def reduce(sequence, acc, fun) do
    do_reduce(acc, Data.seq(sequence), fun)
  end

  defp do_reduce(acc, nil, _) do
    acc
  end

  defp do_reduce(acc, sequence, fun) do
    fun.(S.first(sequence), acc) |> do_reduce(S.next(sequence), fun)
  end

  @spec sort(t) :: t
  def sort(sequence) when sequence |> is_list do
    :lists.sort(sequence)
  end

  def sort(sequence) do
    to_list(sequence) |> sort
  end

  @spec sort(t, (any, any -> boolean)) :: t
  def sort(sequence, fun)

  def sort(sequence, fun) when sequence |> is_list do
    :lists.sort(fun, sequence)
  end

  def sort(sequence, fun) do
    to_list(sequence) |> sort(fun)
  end

  @spec empty?(t) :: boolean
  def empty?(sequence) do
    Data.seq(sequence) == nil
  end

  @spec count(t) :: non_neg_integer
  def count(sequence) when sequence |> is_list do
    sequence |> length
  end

  def count(sequence) do
    do_count(0, Data.seq(sequence))
  end

  defp do_count(acc, nil) do
    acc
  end

  defp do_count(acc, seq) do
    do_count(acc + 1, S.next(seq))
  end

  @spec zip(t, t) :: t
  def zip(sequence1, sequence2) do
    do_zip([], Data.seq(sequence1), Data.seq(sequence2))
  end

  defp do_zip(acc, nil, _) do
    acc |> :lists.reverse
  end

  defp do_zip(acc, sequence, nil) do
    [{ S.first(sequence), nil } | acc] |> do_zip(S.next(sequence), nil)
  end

  defp do_zip(acc, sequence1, sequence2) do
    [{ S.first(sequence1), S.first(sequence2) } | acc] |> do_zip(S.next(sequence1), S.next(sequence2))
  end

  @spec max(t) :: any
  def max(sequence) when is_list(sequence) do
    :lists.max(sequence)
  end

  def max(sequence) do
    if Data.empty?(sequence) do
      raise E.Empty
    else
      reduce Data.seq(sequence), S.first(sequence), fn current, max ->
        if current > max, do: current, else: max
      end
    end
  end

  @spec max(t, (any -> any)) :: any
  def max(sequence, fun) do
    { max, _ } = reduce Data.seq(sequence), fun.(S.first(sequence)), fn current, { _, max } = old ->
      value = fun.(current)

      if value > max, do: { current, value }, else: old
    end

    max
  end

  @spec min(t) :: any
  def min(sequence) when is_list(sequence) do
    :lists.min(sequence)
  end

  def min(sequence) do
    if Data.empty?(sequence) do
      raise E.Empty
    else
      reduce Data.seq(sequence), S.first(sequence), fn current, min ->
        if current < min, do: current, else: min
      end
    end
  end

  @spec min(t, (any -> any)) :: any
  def min(sequence, fun) do
    { min, _ } = reduce Data.seq(sequence), fun.(S.first(sequence)), fn current, { _, min } = old ->
      value = fun.(current)

      if value < min, do: { current, value }, else: old
    end

    min
  end

  @spec uniq(t)               :: t
  @spec uniq(t, (any -> any)) :: t
  def uniq(sequence, fun \\ fn x -> x end) do
    { list, _ } = reduce Data.seq(sequence), { [], [] }, fn(current, { acc, fun_acc }) ->
      value = fun.(current)

      if :lists.member(value, fun_acc) do
        { acc, fun_acc }
      else
        { [current | acc], [value | fun_acc] }
      end
    end

    :lists.reverse list
  end

  @spec count(t, (any -> boolean)) :: non_neg_integer
  def count(sequence, predicate) do
    do_count(0, Data.seq(sequence), predicate)
  end

  defp do_count(acc, nil, _) do
    acc
  end

  defp do_count(acc, seq, pred) do
    if pred.(S.first(seq)) do
      do_count(acc + 1, S.next(seq), pred)
    else
      do_count(acc, S.next(seq), pred)
    end
  end

  @spec to_list(t) :: list
  def to_list(sequence) do
    do_to_list([], Data.seq(sequence))
  end

  defp do_to_list(acc, nil) do
    acc |> :lists.reverse
  end

  defp do_to_list(acc, seq) do
    [S.first(seq) | acc] |> do_to_list(S.next(seq))
  end

  @spec last(t) :: term
  def last(sequence) when sequence |> is_list do
    :lists.last(sequence)
  end

  def last(sequence) do
    do_last(first(sequence), Data.seq(sequence))
  end

  defp do_last(last, nil) do
    last
  end

  defp do_last(_, seq) do
    first(seq) |> do_last(next(seq))
  end

  @spec join(t, String.t) :: String.t
  def join(seq, string) do
    [first | rest] = map seq, &[string, to_string(&1)]

    [tl(first), rest] |> IO.iodata_to_binary
  end

  @spec group_by(t, (term -> term))              :: P.Dictionary.t
  @spec group_by(t, P.Dictionary.t, (term -> term)) :: P.Dictionary.t
  def group_by(seq, into \\ [], fun) do
    do_group_by(into, Data.seq(seq), fun)
  end

  defp do_group_by(into, nil, _) do
    into
  end

  defp do_group_by(into, seq, fun) do
    value = first(seq)

    Data.Dict.update(into, fun.(value), [], &(&1 ++ [value]))
      |> do_group_by(next(seq), fun)
  end

  @spec split(t, integer) :: { [term], [term] }
  def split(seq, count) when count >= 0 do
    { _, list1, list2 } = reduce Data.seq(seq), { count, [], [] }, fn(entry, { counter, acc1, acc2 }) ->
      if counter > 0 do
        { counter - 1, [entry | acc1], acc2 }
      else
        { counter, acc1, [entry | acc2] }
      end
    end

    { reverse(list1), reverse(list2) }
  end

  def split(_seq, count) when count < 0 do
    { [], [] }
  end

  @spec into(t, P.Into.t) :: P.Into.t
  def into(seq, out) do
    reduce Data.seq(seq), out, &P.Into.into(&2, &1)
  end
end
