package fj.data.fingertrees;

import fj.P;
import fj.P2;
import fj.P3;
import fj.Show;
import fj.data.Option;
import fj.data.Stream;
import fj.data.vector.V4;
import fj.F;

import static fj.data.Option.none;
import static fj.data.Option.some;
import static fj.data.fingertrees.FingerTree.mkTree;

/**
 * A four-element prefix or suffix of a finger tree.
 */
public final class Four<V, A> extends Digit<V, A> {
  private final V4<A> as;

  Four(final Measured<V, A> m, final V4<A> as) {
    super(m);
    this.as = as;
  }

  public <B> B foldRight(final F<A, F<B, B>> aff, final B z) {
    return aff.f(as._1()).f(aff.f(as._2()).f(aff.f(as._3()).f(aff.f(as._4()).f(z))));
  }

  public <B> B foldLeft(final F<B, F<A, B>> bff, final B z) {
    return as.toStream().foldLeft(bff, z);
  }

  @Override public <B> B match(
      final F<One<V, A>, B> one, final F<Two<V, A>, B> two, final F<Three<V, A>, B> three,
      final F<Four<V, A>, B> four) {
    return four.f(this);
  }

  /**
   * Returns the elements of this digit as a vector.
   *
   * @return the elements of this digit as a vector.
   */
  public V4<A> values() {
    return as;
  }

  @Override P3<Option<Digit<V, A>>, A, Option<Digit<V, A>>> split1(final F<V, Boolean> predicate, final V acc) {
    final Measured<V, A> m = measured();
    final MakeTree<V, A> mk = mkTree(m);
    final F<A, V> measure = m.measure();
    final V acc1 = m.sum(acc, measure.f(as._1()));
    if (predicate.f(acc1)) {
      return P.p(none(), as._1(), some(mk.three(as._2(), as._3(), as._4())));
    } else {
      final V acc2 = m.sum(acc1, measure.f(as._2()));
      if (predicate.f(acc2)) {
        return P.p(some(mk.one(as._1())), as._2(), some(mk.two(as._3(), as._4())));
      } else if (predicate.f(m.sum(acc2, measure.f(as._3())))) {
        return P.p(some(mk.two(as._1(), as._2())), as._3(), some(mk.one(as._4())));
      } else {
        return P.p(some(mk.three(as._1(), as._2(), as._3())), as._4(), none());
      }
    }
  }

  @Override public P2<Integer, A> lookup(final F<V, Integer> o, final int i) {
    final F<A, V> m = measured().measure();
    final int s1 = o.f(m.f(as._1()));
    if (i < s1) {
      return P.p(i, as._1());
    } else {
      final int s2 = s1 + o.f(m.f(as._2()));
      if (i < s2) {
        return P.p(i - s1, as._2());
      } else {
        final int s3 = s2 + o.f(m.f(as._3()));
        if (i < s3) {
          return P.p(i - s2, as._3());
        } else {
          return P.p(i - s3, as._4());
        }
      }
    }
  }

    @Override
    public int length() {
        return 4;
    }

  public String toString() {
    return Show.digitShow(Show.<V>anyShow(), Show.<A>anyShow()).showS(this);
  }

  public Stream<A> toStream() {
    return values().toStream();
  }

}
