package fj.data;

import fj.*;
import fj.function.Integers;

import java.util.Iterator;

import static fj.Function.compose;
import static fj.Function.curry;
import static fj.Function.flip;
import static fj.Function.join;
import static fj.Function.uncurryF2;
import static fj.data.Option.none;
import static fj.data.Option.some;
import static fj.data.Stream.nil;
import static fj.data.Stream.repeat;

/**
 * Provides a pointed stream, which is a non-empty zipper-like stream structure that tracks an index (focus)
 * position in a stream. Focus can be moved forward and backwards through the stream, elements can be inserted
 * before or after the focused position, and the focused item can be deleted.
 * <p/>
 * Based on the pointedlist library by Jeff Wheeler.
 */
public final class Zipper<A> implements Iterable<Zipper<A>> {
  private final Stream<A> left;
  private final A focus;
  private final Stream<A> right;

  private Zipper(final Stream<A> left, final A focus, final Stream<A> right) {
    this.left = left;
    this.focus = focus;
    this.right = right;
  }


  /**
   * Creates a new Zipper with the given streams before and after the focus, and the given focused item.
   *
   * @param left  The stream of elements before the focus.
   * @param focus The element under focus.
   * @param right The stream of elements after the focus.
   * @return a new Zipper with the given streams before and after the focus, and the given focused item.
   */
  public static <A> Zipper<A> zipper(final Stream<A> left, final A focus, final Stream<A> right) {
    return new Zipper<>(left, focus, right);
  }

  /**
   * Creates a new Zipper from the given triple.
   *
   * @param p A triple of the elements before the focus, the focus element, and the elements after the focus,
   *          respectively.
   * @return a new Zipper created from the given triple.
   */
  public static <A> Zipper<A> zipper(final P3<Stream<A>, A, Stream<A>> p) {
    return new Zipper<>(p._1(), p._2(), p._3());
  }

  /**
   * First-class constructor of zippers.
   *
   * @return A function that yields a new zipper given streams on the left and right and a focus element.
   */
  public static <A> F3<Stream<A>, A, Stream<A>, Zipper<A>> zipper() {
    return Zipper::zipper;
  }

  /**
   * Returns the product-3 representation of this Zipper.
   *
   * @return the product-3 representation of this Zipper.
   */
  public P3<Stream<A>, A, Stream<A>> p() {
    return P.p(left, focus, right);
  }

  /**
   * A first-class function that yields the product-3 representation of a given Zipper.
   *
   * @return A first-class function that yields the product-3 representation of a given Zipper.
   */
  public static <A> F<Zipper<A>, P3<Stream<A>, A, Stream<A>>> p_() {
    return Zipper::p;
  }

  /**
   * An Ord instance for Zippers.
   *
   * @param o An Ord instance for the element type.
   * @return An Ord instance for Zippers.
   */
  public static <A> Ord<Zipper<A>> ord(final Ord<A> o) {
    final Ord<Stream<A>> so = Ord.streamOrd(o);
    return Ord.p3Ord(so, o, so).contramap(Zipper.p_());
  }

  @Override
  public final boolean equals(Object other) {
    return Equal.equals0(Zipper.class, this, other, () -> Equal.zipperEqual(Equal.anyEqual()));
  }

  @Override
  public final int hashCode() {
    return Hash.zipperHash(Hash.<A>anyHash()).hash(this);
  }

  /**
   * An Equal instance for Zippers.
   *
   * @param e An Equal instance for the element type.
   * @return An Equal instance for Zippers.
   */
  public static <A> Equal<Zipper<A>> eq(final Equal<A> e) {
    final Equal<Stream<A>> se = Equal.streamEqual(e);
    return Equal.p3Equal(se, e, se).contramap(Zipper.p_());
  }

  /**
   * A Show instance for Zippers.
   *
   * @param s A Show instance for the element type.
   * @return A Show instance for Zippers.
   */
  public static <A> Show<Zipper<A>> show(final Show<A> s) {
    final Show<Stream<A>> ss = Show.streamShow(s);
    return Show.p3Show(ss, s, ss).contramap(Zipper.p_());
  }

  /**
   * Maps the given function across the elements of this zipper (covariant functor pattern).
   *
   * @param f A function to map across this zipper.
   * @return A new zipper with the given function applied to all elements.
   */
  public <B> Zipper<B> map(final F<A, B> f) {
    return zipper(left.map(f), f.f(focus), right.map(f));
  }

  /**
   * Performs a right-fold reduction across this zipper.
   *
   * @param f The function to apply on each element of this zipper.
   * @param z The beginning value to start the application from.
   * @return the final result after the right-fold reduction.
   */
  public <B> B foldRight(final F<A, F<B, B>> f, final B z) {
    return left.foldLeft(flip(f),
                         right.cons(focus).foldRight(compose(
                             Function.<P1<B>, B, B>andThen().f(P1.__1()), f), z));
  }

  /**
   * Creates a new zipper with a single element.
   *
   * @param a The focus element of the new zipper.
   * @return a new zipper with a single element which is in focus.
   */
  public static <A> Zipper<A> single(final A a) {
    return zipper(Stream.nil(), a, Stream.nil());
  }

  /**
   * Possibly create a zipper if the provided stream has at least one element, otherwise None.
   * The provided stream's head will be the focus of the zipper, and the rest of the stream will follow
   * on the right side.
   *
   * @param a The stream from which to create a zipper.
   * @return a new zipper if the provided stream has at least one element, otherwise None.
   */
  @SuppressWarnings("IfMayBeConditional")
  public static <A> Option<Zipper<A>> fromStream(final Stream<A> a) {
    if (a.isEmpty())
      return none();
    else
      return some(zipper(Stream.nil(), a.head(), a.tail()._1()));
  }

  /**
   * Possibly create a zipper if the provided stream has at least one element, otherwise None.
   * The provided stream's last element will be the focus of the zipper, following the rest of the stream in order,
   * to the left.
   *
   * @param a The stream from which to create a zipper.
   * @return a new zipper if the provided stream has at least one element, otherwise None.
   */
  public static <A> Option<Zipper<A>> fromStreamEnd(final Stream<A> a) {
    if (a.isEmpty())
      return none();
    else {
      final Stream<A> xs = a.reverse();
      return some(zipper(xs.tail()._1(), xs.head(), Stream.nil()));
    }
  }

  /**
   * Returns the focus element of this zipper.
   *
   * @return the focus element of this zipper.
   */
  public A focus() {
    return focus;
  }

  /**
   * Possibly moves the focus to the next element in the list.
   *
   * @return An optional zipper with the focus moved one element to the right, if there are elements to the right of
   *         focus, otherwise None.
   */
  public Option<Zipper<A>> next() {
    return right.isEmpty() ? Option.none() : some(tryNext());
  }

  /**
   * Attempts to move the focus to the next element, or throws an error if there are no more elements.
   *
   * @return A zipper with the focus moved one element to the right, if there are elements to the right of
   *         focus, otherwise throws an error.
   */
  public Zipper<A> tryNext() {
    if (right.isEmpty())
      throw new Error("Tried next at the end of a zipper.");
    else
      return zipper(left.cons(focus), right.head(), right.tail()._1());
  }

  /**
   * Possibly moves the focus to the previous element in the list.
   *
   * @return An optional zipper with the focus moved one element to the left, if there are elements to the left of
   *         focus, otherwise None.
   */
  public Option<Zipper<A>> previous() {
    return left.isEmpty() ? Option.none() : some(tryPrevious());
  }

  /**
   * Attempts to move the focus to the previous element, or throws an error if there are no more elements.
   *
   * @return A zipper with the focus moved one element to the left, if there are elements to the left of
   *         focus, otherwise throws an error.
   */
  public Zipper<A> tryPrevious() {
    if (left.isEmpty())
      throw new Error("Tried previous at the beginning of a zipper.");
    else
      return zipper(left.tail()._1(), left.head(), right.cons(focus));
  }

  /**
   * First-class version of the next() function.
   *
   * @return A function that moves the given zipper's focus to the next element.
   */
  public static <A> F<Zipper<A>, Option<Zipper<A>>> next_() {
    return Zipper::next;
  }

  /**
   * First-class version of the previous() function.
   *
   * @return A function that moves the given zipper's focus to the previous element.
   */
  public static <A> F<Zipper<A>, Option<Zipper<A>>> previous_() {
    return Zipper::previous;
  }

  /**
   * Inserts an element to the left of the focus, then moves the focus to the new element.
   *
   * @param a A new element to insert into this zipper.
   * @return A new zipper with the given element in focus, and the current focus element on its right.
   */
  public Zipper<A> insertLeft(final A a) {
    return zipper(left, a, right.cons(focus));
  }

  /**
   * Inserts an element to the right of the focus, then moves the focus to the new element.
   *
   * @param a A new element to insert into this zipper.
   * @return A new zipper with the given element in focus, and the current focus element on its left.
   */
  public Zipper<A> insertRight(final A a) {
    return zipper(left.cons(focus), a, right);
  }

  /**
   * Possibly deletes the element at the focus, then moves the element on the left into focus.
   * If no element is on the left, focus on the element to the right.
   * Returns None if the focus element is the only element in this zipper.
   *
   * @return A new zipper with this zipper's focus element removed, or None if deleting the focus element
   *         would cause the zipper to be empty.
   */
  public Option<Zipper<A>> deleteLeft() {
    return left.isEmpty() && right.isEmpty()
           ? Option.none()
           : some(zipper(left.isEmpty() ? left : left.tail()._1(),
                         left.isEmpty() ? right.head() : left.head(),
                         left.isEmpty() ? right.tail()._1() : right));
  }

  /**
   * Possibly deletes the element at the focus, then moves the element on the right into focus.
   * If no element is on the right, focus on the element to the left.
   * Returns None if the focus element is the only element in this zipper.
   *
   * @return A new zipper with this zipper's focus element removed, or None if deleting the focus element
   *         would cause the zipper to be empty.
   */
  public Option<Zipper<A>> deleteRight() {
    return left.isEmpty() && right.isEmpty()
           ? Option.none()
           : some(zipper(right.isEmpty() ? left.tail()._1() : left,
                         right.isEmpty() ? left.head() : right.head(),
                         right.isEmpty() ? right : right.tail()._1()));
  }

  /**
   * Deletes all elements in the zipper except the focus.
   *
   * @return A new zipper with the focus element as the only element.
   */
  public Zipper<A> deleteOthers() {
    final Stream<A> nil = nil();
    return zipper(nil, focus, nil);
  }

  /**
   * Returns the length of this zipper.
   *
   * @return the length of this zipper.
   */
  public int length() {
    return foldRight(Function.constant(Integers.add.f(1)), 0);
  }

  /**
   * Returns whether the focus is on the first element.
   *
   * @return true if the focus is on the first element, otherwise false.
   */
  public boolean atStart() {
    return left.isEmpty();
  }

  /**
   * Returns whether the focus is on the last element.
   *
   * @return true if the focus is on the last element, otherwise false.
   */
  public boolean atEnd() {
    return right.isEmpty();
  }

  /**
   * Creates a zipper of variations of this zipper, in which each element is focused,
   * with this zipper as the focus of the zipper of zippers (comonad pattern).
   *
   * @return a zipper of variations of the provided zipper, in which each element is focused,
   *         with this zipper as the focus of the zipper of zippers.
   */
  public Zipper<Zipper<A>> positions() {
    final Stream<Zipper<A>> left = Stream.unfold(
            p -> p.previous().map(join(P.p2())), this);
    final Stream<Zipper<A>> right = Stream.unfold(
            p -> p.next().map(join(P.p2())), this);

    return zipper(left, this, right);
  }

  /**
   * Maps over variations of this zipper, such that the given function is applied to each variation (comonad pattern).
   *
   * @param f The comonadic function to apply for each variation of this zipper.
   * @return A new zipper, with the given function applied for each variation of this zipper.
   */
  public <B> Zipper<B> cobind(final F<Zipper<A>, B> f) {
    return positions().map(f);
  }

  /**
   * Zips the elements of this zipper with a boolean that indicates whether that element has focus.
   * All of the booleans will be false, except the focused element.
   *
   * @return A new zipper of pairs, with each element of this zipper paired with a boolean that is true if that
   *         element has focus, and false otherwise.
   */
  public Zipper<P2<A, Boolean>> zipWithFocus() {
    return zipper(left.zip(repeat(false)), P.p(focus, true), right.zip(repeat(false)));
  }

  /**
   * Move the focus to the specified index.
   *
   * @param n The index to which to move the focus.
   * @return A new zipper with the focus moved to the specified index, or none if there is no such index.
   */
  public Option<Zipper<A>> move(final int n) {
    final int ll = left.length();
    final int rl = right.length();
    Option<Zipper<A>> p = some(this);
    if (n < 0 || n >= length())
      return none();
    else if (ll >= n)
      for (int i = ll - n; i > 0; i--)
        p = p.bind(Zipper.previous_());
    else if (rl >= n)
      for (int i = rl - n; i > 0; i--)
        p = p.bind(Zipper.next_());
    return p;
  }

  /**
   * A first-class version of the move function.
   *
   * @return A function that moves the focus of the given zipper to the given index.
   */
  public static <A> F<Integer, F<Zipper<A>, Option<Zipper<A>>>> move() {
    return curry((i, a) -> a.move(i));
  }

  /**
   * Moves the focus to the element matching the given predicate, if present.
   *
   * @param p A predicate to match.
   * @return A new zipper with the nearest matching element focused if it is present in this zipper.
   */
  public Option<Zipper<A>> find(final F<A, Boolean> p) {
    if (p.f(focus()))
      return some(this);
    else {
      final Zipper<Zipper<A>> ps = positions();
      return ps.lefts().interleave(ps.rights()).find(zipper -> p.f(zipper.focus()));
    }
  }

  /**
   * Returns the index of the focus.
   *
   * @return the index of the focus.
   */
  public int index() {
    return left.length();
  }

  /**
   * Move the focus to the next element. If the last element is focused, loop to the first element.
   *
   * @return A new zipper with the next element focused, unless the last element is currently focused, in which case
   *         the first element becomes focused.
   */
  public Zipper<A> cycleNext() {
    if (left.isEmpty() && right.isEmpty())
      return this;
    else if (right.isEmpty()) {
      final Stream<A> xs = left.reverse();
      return zipper(Stream.nil(), xs.head(), xs.tail()._1().snoc(P.p(focus)));
    } else
      return tryNext();
  }

  /**
   * Move the focus to the previous element. If the first element is focused, loop to the last element.
   *
   * @return A new zipper with the previous element focused, unless the first element is currently focused,
   *         in which case the last element becomes focused.
   */
  public Zipper<A> cyclePrevious() {
    if (left.isEmpty() && right.isEmpty())
      return this;
    else if (left.isEmpty()) {
      final Stream<A> xs = right.reverse();
      return zipper(xs.tail()._1().snoc(P.p(focus)), xs.head(), Stream.nil());
    } else
      return tryPrevious();
  }

  /**
   * Possibly deletes the element at the focus, then move the element on the left into focus. If no element is on the
   * left, focus on the last element. If the deletion will cause the list to be empty, return None.
   *
   * @return A new zipper with the focused element removed, and focus on the previous element to the left, or the last
   *         element if there is no element to the left.
   */
  public Option<Zipper<A>> deleteLeftCycle() {
    if (left.isEmpty() && right.isEmpty())
      return none();
    else if (left.isNotEmpty())
      return some(zipper(left.tail()._1(), left.head(), right));
    else {
      final Stream<A> xs = right.reverse();
      return some(zipper(xs.tail()._1(), xs.head(), Stream.nil()));
    }
  }

  /**
   * Possibly deletes the element at the focus, then move the element on the right into focus. If no element is on the
   * right, focus on the first element. If the deletion will cause the list to be empty, return None.
   *
   * @return A new zipper with the focused element removed, and focus on the next element to the right, or the first
   *         element if there is no element to the right.
   */
  public Option<Zipper<A>> deleteRightCycle() {
    if (left.isEmpty() && right.isEmpty())
      return none();
    else if (right.isNotEmpty())
      return some(zipper(left, right.head(), right.tail()._1()));
    else {
      final Stream<A> xs = left.reverse();
      return some(zipper(Stream.nil(), xs.head(), xs.tail()._1()));
    }
  }

  /**
   * Replaces the element in focus with the given element.
   *
   * @param a An element to replace the focused element with.
   * @return A new zipper with the given element in focus.
   */
  public Zipper<A> replace(final A a) {
    return zipper(left, a, right);
  }

  /**
   * Returns the Stream representation of this zipper.
   *
   * @return A stream that contains all the elements of this zipper.
   */
  public Stream<A> toStream() {
    return left.reverse().snoc(P.p(focus)).append(right);
  }

  /**
   * Returns a Stream of the elements to the left of focus.
   *
   * @return a Stream of the elements to the left of focus.
   */
  public Stream<A> lefts() {
    return left;
  }

  /**
   * Returns a Stream of the elements to the right of focus.
   *
   * @return a Stream of the elements to the right of focus.
   */
  public Stream<A> rights() {
    return right;
  }

  /**
   * Zips this Zipper with another, applying the given function lock-step over both zippers in both directions.
   * The structure of the resulting Zipper is the structural intersection of the two Zippers.
   *
   * @param bs A Zipper to zip this one with.
   * @param f  A function with which to zip together the two Zippers.
   * @return The result of applying the given function over this Zipper and the given Zipper, location-wise.
   */
  public <B, C> Zipper<C> zipWith(final Zipper<B> bs, final F2<A, B, C> f) {
    return f.zipZipperM().f(this, bs);
  }


  /**
   * Zips this Zipper with another, applying the given function lock-step over both zippers in both directions.
   * The structure of the resulting Zipper is the structural intersection of the two Zippers.
   *
   * @param bs A Zipper to zip this one with.
   * @param f  A function with which to zip together the two Zippers.
   * @return The result of applying the given function over this Zipper and the given Zipper, location-wise.
   */
  public <B, C> Zipper<C> zipWith(final Zipper<B> bs, final F<A, F<B, C>> f) {
    return zipWith(bs, uncurryF2(f));
  }

  /**
   * Returns an iterator of all the positions of this Zipper, starting from the leftmost position.
   *
   * @return An iterator of all the positions of this Zipper, starting from the leftmost position.
   */
  public Iterator<Zipper<A>> iterator() { return positions().toStream().iterator(); }
}
