package fj;

import static fj.Function.compose;

import fj.data.*;
import fj.data.vector.V2;
import fj.data.vector.V3;
import fj.data.vector.V4;
import fj.data.vector.V5;
import fj.data.vector.V6;
import fj.data.vector.V7;
import fj.data.vector.V8;
import fj.parser.Result;

import java.math.BigDecimal;
import java.math.BigInteger;

/**
 * Produces a hash code for an object which should attempt uniqueness.
 */
public final class Hash<A> {
  private final F<A, Integer> f;

  private Hash(final F<A, Integer> f) {
    this.f = f;
  }

  /**
   * Compute the hash of the given value.
   *
   * @param a The value to compute the hash value for.
   * @return The hash value.
   */
  public int hash(final A a) {
    return f.f(a);
  }

  /**
   * Maps the given function across this hash as a contra-variant functor.
   *
   * @param g The function to map.
   * @return A new hash.
   */
  public <B> Hash<B> contramap(final F<B, A> g) {
    return hash(compose(f, g));
  }

  /**
   * Construct a hash with the given hash function.
   *
   * @param f The function to construct the hash with.
   * @return A hash that uses the given function.
   */
  public static <A> Hash<A> hash(final F<A, Integer> f) {
    return new Hash<>(f);
  }

  /**
   * A hash that uses {@link Object#hashCode()}.
   *
   * @return A hash that uses {@link Object#hashCode()}.
   */
  public static <A> Hash<A> anyHash() {
    return hash(Object::hashCode);
  }

  /**
   * A hash instance for the <code>boolean</code> type.
   */
  public static final Hash<Boolean> booleanHash = anyHash();

  /**
   * A hash instance for the <code>byte</code> type.
   */
  public static final Hash<Byte> byteHash = anyHash();

  /**
   * A hash instance for the <code>char</code> type.
   */
  public static final Hash<Character> charHash = anyHash();

  /**
   * A hash instance for the <code>double</code> type.
   */
  public static final Hash<Double> doubleHash = anyHash();

  /**
   * A hash instance for the <code>float</code> type.
   */
  public static final Hash<Float> floatHash = anyHash();

  /**
   * A hash instance for the <code>int</code> type.
   */
  public static final Hash<Integer> intHash = anyHash();

  /**
   * A hash instance for the <code>long</code> type.
   */
  public static final Hash<Long> longHash = anyHash();

  /**
   * A hash instance for the <code>short</code> type.
   */
  public static final Hash<Short> shortHash = anyHash();

  /**
   * A hash instance for the <code>BigInteger</code> type.
   */
  public static final Hash<BigInteger> bigintHash = anyHash();

  /**
   * A hash instance for the <code>BigDecimal</code> type.
   */
  public static final Hash<BigDecimal> bigdecimalHash = anyHash();

  /**
   * A hash instance for the {@link Natural} type.
   */
  public static final Hash<Natural> naturalHash = bigintHash.contramap(Natural::bigIntegerValue);

  /**
   * A hash instance for the <code>String</code> type.
   */
  public static final Hash<String> stringHash = anyHash();

  /**
   * A hash instance for the {@link StringBuffer} type.
   */
  public static final Hash<StringBuffer> stringBufferHash = hash(sb -> {
      final int p = 419;
      int r = 239;

      for (int i = 0; i < sb.length(); i++)
          r = p * r + sb.charAt(i);

      return r;
  });

  /**
   * A hash instance for the {@link StringBuilder} type.
   */
  public static final Hash<StringBuilder> stringBuilderHash = hash(sb -> {
      final int p = 419;
      int r = 239;

      for (int i = 0; i < sb.length(); i++)
          r = p * r + sb.charAt(i);

      return r;
  });

  /**
   * A hash instance for the {@link Either} type.
   *
   * @param ha Hash the left side of <code>Either</code>.
   * @param hb Hash the right side of <code>Either</code>.
   * @return A hash instance for the {@link Either} type.
   */
  public static <A, B> Hash<Either<A, B>> eitherHash(final Hash<A> ha, final Hash<B> hb) {
    return hash(e -> e.isLeft() ? ha.hash(e.left().value()) : hb.hash(e.right().value()));
  }

    public static <A, B, C> Hash<Either3<A, B, C>> either3Hash(final Hash<A> ha, final Hash<B> hb, final Hash<C> hc) {
        return hash(e -> e.either(a -> ha.hash(a), b -> hb.hash(b), c -> hc.hash(c)));
    }


    /**
   * A hash instance for the {@link Result} type.
   *
   * @param ha Hash the <code>Result</code> value.
   * @param hi Hash the <code>Result</code> remainder.
   * @return A hash instance for the {@link Result} type.
   */
  public static <I, A> Hash<Result<I, A>> resultHash(Hash<A> ha, Hash<I> hi) {
    return hash(res -> {
      final int p = 419;
      int r = 239;

      r = p * r + ha.hash(res.value());
      r = p * r + hi.hash(res.rest());

      return r;
    });
  }

  /**
   * A hash instance for the {@link Validation} type.
   *
   * @param ha Hash the failing side of <code>Validation</code>.
   * @param hb Hash the succeeding side of <code>Validation</code>.
   * @return A hash instance for the {@link Validation} type.
   */
  public static <A, B> Hash<Validation<A, B>> validationHash(final Hash<A> ha, final Hash<B> hb) {
    return eitherHash(ha, hb).contramap(Validation.either());
  }

  /**
   * A hash instance for the {@link List} type.
   *
   * @param ha A hash for the elements of the list.
   * @return A hash instance for the {@link List} type.
   */
  public static <A> Hash<List<A>> listHash(final Hash<A> ha) {
    return hash(as -> {
        final int p = 419;
        int r = 239;
        List<A> aas = as;

        while (!aas.isEmpty()) {
            r = p * r + ha.hash(aas.head());
            aas = aas.tail();
        }

        return r;
    });
  }

  /**
   * A hash instance for the {@link NonEmptyList} type.
   *
   * @param ha A hash for the elements of the non-empty list.
   * @return A hash instance for the {@link NonEmptyList} type.
   */
  public static <A> Hash<NonEmptyList<A>> nonEmptyListHash(final Hash<A> ha) {
    return listHash(ha).contramap(NonEmptyList.toList_());
  }

  /**
   * A hash instance for the {@link Option} type.
   *
   * @param ha A hash for the element of the optional value.
   * @return A hash instance for the {@link Option} type.
   */
  public static <A> Hash<Option<A>> optionHash(final Hash<A> ha) {
    return hash(o -> o.isNone() ? 0 : ha.hash(o.some()));
  }

    public static <A> Hash<Seq<A>> seqHash(final Hash<A> h) {
        return hash(s -> streamHash(h).hash(s.toStream()));
    }

    public static <A> Hash<Set<A>> setHash(final Hash<A> h) {
        return hash(s -> streamHash(h).hash(s.toStream()));
    }

  /**
   * A hash instance for the {@link Stream} type.
   *
   * @param ha A hash for the elements of the stream.
   * @return A hash instance for the {@link Stream} type.
   */
  public static <A> Hash<Stream<A>> streamHash(final Hash<A> ha) {
    return hash(as -> {
        final int p = 419;
        int r = 239;
        Stream<A> aas = as;

        while (!aas.isEmpty()) {
            r = p * r + ha.hash(aas.head());
            aas = aas.tail()._1();
        }

        return r;
    });
  }

  /**
   * A hash instance for the {@link Array} type.
   *
   * @param ha A hash for the elements of the array.
   * @return A hash instance for the {@link Array} type.
   */
  public static <A> Hash<Array<A>> arrayHash(final Hash<A> ha) {
    return hash(as -> {
        final int p = 419;
        int r = 239;

        for (int i = 0; i < as.length(); i++) {
            r = p * r + ha.hash(as.get(i));
        }

        return r;
    });
  }

  /**
   * A hash instance for the {@link Zipper} type.
   *
   * @param ha A hash for the elements of the zipper.
   * @return A hash instance for the {@link Zipper} type.
   */
  public static <A> Hash<Zipper<A>> zipperHash(final Hash<A> ha) {
    Hash<Stream<A>> sh = streamHash(ha);
    return hash(as -> {
        final int p = 419;
        int r = 239;

        r = p * r + sh.hash(as.lefts());
        r = p * r + ha.hash(as.focus());
        r = p * r + sh.hash(as.rights());

        return r;
    });
  }

    /**
     * A hash instance for the {@link TreeZipper} type.
     *
     * @param ha A hash for the elements of the tree zipper.
     * @return A hash instance for the {@link TreeZipper} type.
     */
    public static <A> Hash<TreeZipper<A>> treeZipperHash(final Hash<A> ha) {
        Hash<Tree<A>> th = treeHash(ha);
        Hash<Stream<Tree<A>>> sth = streamHash(treeHash(ha));
        Hash<Stream<P3<Stream<Tree<A>>, A, Stream<Tree<A>>>>> tsp =
                streamHash(p3Hash(streamHash(treeHash(ha)), ha, streamHash(treeHash(ha))));
        return hash(as -> {
            final int p = 419;
            int r = 239;

            r = p * r + th.hash(as.focus());
            r = p * r + sth.hash(as.lefts());
            r = p * r + sth.hash(as.rights());
            r = p * r + tsp.hash(as.parents());

            return r;
        });
    }

    /**
   * A hash instance for the {@link Tree} type.
   *
   * @param ha A hash for the elements of the tree.
   * @return A hash instance for the {@link Tree} type.
   */
  public static <A> Hash<Tree<A>> treeHash(final Hash<A> ha) {
    return streamHash(ha).contramap(Tree.flatten_());
  }

    public static <K, V> Hash<TreeMap<K, V>> treeMapHash(final Hash<K> h, final Hash<V> v) {
        return hash(t -> streamHash(p2Hash(h, v)).hash(t.toStream()));
    }

  /**
   * A hash instance for a product-1.
   *
   * @param ha A hash for the first element of the product.
   * @return A hash instance for a product-1.
   */
  public static <A> Hash<P1<A>> p1Hash(final Hash<A> ha) {
    return ha.contramap(P1.__1());
  }

  /**
   * A hash instance for a product-2.
   *
   * @param ha A hash for the first element of the product.
   * @param hb A hash for the second element of the product.
   * @return A hash instance for a product-2.
   */
  public static <A, B> Hash<P2<A, B>> p2Hash(final Hash<A> ha, final Hash<B> hb) {
    return hash(p2 -> {
        final int p = 419;
        int r = 239;

        r = p * r + ha.hash(p2._1());
        r = p * r + hb.hash(p2._2());

        return r;
    });
  }

  /**
   * A hash instance for a product-3.
   *
   * @param ha A hash for the first element of the product.
   * @param hb A hash for the second element of the product.
   * @param hc A hash for the third element of the product.
   * @return A hash instance for a product-3.
   */
  public static <A, B, C> Hash<P3<A, B, C>> p3Hash(final Hash<A> ha, final Hash<B> hb, final Hash<C> hc) {
    return hash(p3 -> {
        final int p = 419;
        int r = 239;

        r = p * r + ha.hash(p3._1());
        r = p * r + hb.hash(p3._2());
        r = p * r + hc.hash(p3._3());

        return r;
    });
  }

  /**
   * A hash instance for a product-4.
   *
   * @param ha A hash for the first element of the product.
   * @param hb A hash for the second element of the product.
   * @param hc A hash for the third element of the product.
   * @param hd A hash for the fourth element of the product.
   * @return A hash instance for a product-4.
   */
  public static <A, B, C, D> Hash<P4<A, B, C, D>> p4Hash(final Hash<A> ha, final Hash<B> hb, final Hash<C> hc,
                                                         final Hash<D> hd) {
    return hash(p4 -> {
      final int p = 419;
      int r = 239;

      r = p * r + ha.hash(p4._1());
      r = p * r + hb.hash(p4._2());
      r = p * r + hc.hash(p4._3());
      r = p * r + hd.hash(p4._4());

      return r;
    });
  }

  /**
   * A hash instance for a product-5.
   *
   * @param ha A hash for the first element of the product.
   * @param hb A hash for the second element of the product.
   * @param hc A hash for the third element of the product.
   * @param hd A hash for the fourth element of the product.
   * @param he A hash for the fifth element of the product.
   * @return A hash instance for a product-5.
   */
  public static <A, B, C, D, E> Hash<P5<A, B, C, D, E>> p5Hash(final Hash<A> ha, final Hash<B> hb, final Hash<C> hc,
                                                               final Hash<D> hd, final Hash<E> he) {
    return hash(p5 -> {
      final int p = 419;
      int r = 239;

      r = p * r + ha.hash(p5._1());
      r = p * r + hb.hash(p5._2());
      r = p * r + hc.hash(p5._3());
      r = p * r + hd.hash(p5._4());
      r = p * r + he.hash(p5._5());

      return r;
    });
  }

  /**
   * A hash instance for a product-6.
   *
   * @param ha A hash for the first element of the product.
   * @param hb A hash for the second element of the product.
   * @param hc A hash for the third element of the product.
   * @param hd A hash for the fourth element of the product.
   * @param he A hash for the fifth element of the product.
   * @param hf A hash for the sixth element of the product.
   * @return A hash instance for a product-6.
   */
  public static <A, B, C, D, E, F$> Hash<P6<A, B, C, D, E, F$>> p6Hash(final Hash<A> ha, final Hash<B> hb,
                                                                       final Hash<C> hc, final Hash<D> hd,
                                                                       final Hash<E> he, final Hash<F$> hf) {
    return hash(p6 -> {
      final int p = 419;
      int r = 239;

      r = p * r + ha.hash(p6._1());
      r = p * r + hb.hash(p6._2());
      r = p * r + hc.hash(p6._3());
      r = p * r + hd.hash(p6._4());
      r = p * r + he.hash(p6._5());
      r = p * r + hf.hash(p6._6());

      return r;
    });
  }

  /**
   * A hash instance for a product-7.
   *
   * @param ha A hash for the first element of the product.
   * @param hb A hash for the second element of the product.
   * @param hc A hash for the third element of the product.
   * @param hd A hash for the fourth element of the product.
   * @param he A hash for the fifth element of the product.
   * @param hf A hash for the sixth element of the product.
   * @param hg A hash for the seventh element of the product.
   * @return A hash instance for a product-7.
   */
  public static <A, B, C, D, E, F$, G> Hash<P7<A, B, C, D, E, F$, G>> p7Hash(final Hash<A> ha, final Hash<B> hb,
                                                                             final Hash<C> hc, final Hash<D> hd,
                                                                             final Hash<E> he, final Hash<F$> hf,
                                                                             final Hash<G> hg) {
    return hash(p7 -> {
      final int p = 419;
      int r = 239;

      r = p * r + ha.hash(p7._1());
      r = p * r + hb.hash(p7._2());
      r = p * r + hc.hash(p7._3());
      r = p * r + hd.hash(p7._4());
      r = p * r + he.hash(p7._5());
      r = p * r + hf.hash(p7._6());
      r = p * r + hg.hash(p7._7());

      return r;
    });
  }

  /**
   * A hash instance for a product-8.
   *
   * @param ha A hash for the first element of the product.
   * @param hb A hash for the second element of the product.
   * @param hc A hash for the third element of the product.
   * @param hd A hash for the fourth element of the product.
   * @param he A hash for the fifth element of the product.
   * @param hf A hash for the sixth element of the product.
   * @param hg A hash for the seventh element of the product.
   * @param hh A hash for the eighth element of the product.
   * @return A hash instance for a product-8.
   */
  public static <A, B, C, D, E, F$, G, H> Hash<P8<A, B, C, D, E, F$, G, H>> p8Hash(final Hash<A> ha, final Hash<B> hb,
                                                                                   final Hash<C> hc, final Hash<D> hd,
                                                                                   final Hash<E> he, final Hash<F$> hf,
                                                                                   final Hash<G> hg, final Hash<H> hh) {
    return hash(p8 -> {
      final int p = 419;
      int r = 239;

      r = p * r + ha.hash(p8._1());
      r = p * r + hb.hash(p8._2());
      r = p * r + hc.hash(p8._3());
      r = p * r + hd.hash(p8._4());
      r = p * r + he.hash(p8._5());
      r = p * r + hf.hash(p8._6());
      r = p * r + hg.hash(p8._7());
      r = p * r + hh.hash(p8._8());

      return r;
    });
  }

  /**
   * A hash instance for a vector-2.
   *
   * @param ea A hash for the elements of the vector.
   * @return A hash instance for a vector-2.
   */
  public static <A> Hash<V2<A>> v2Hash(final Hash<A> ea) {
    return streamHash(ea).contramap(V2.toStream_());
  }

  /**
   * A hash instance for a vector-3.
   *
   * @param ea A hash for the elements of the vector.
   * @return A hash instance for a vector-3.
   */
  public static <A> Hash<V3<A>> v3Hash(final Hash<A> ea) {
    return streamHash(ea).contramap(V3.toStream_());
  }

  /**
   * A hash instance for a vector-4.
   *
   * @param ea A hash for the elements of the vector.
   * @return A hash instance for a vector-4.
   */
  public static <A> Hash<V4<A>> v4Hash(final Hash<A> ea) {
    return streamHash(ea).contramap(V4.toStream_());
  }

  /**
   * A hash instance for a vector-5.
   *
   * @param ea A hash for the elements of the vector.
   * @return A hash instance for a vector-5.
   */
  public static <A> Hash<V5<A>> v5Hash(final Hash<A> ea) {
    return streamHash(ea).contramap(V5.toStream_());
  }

  /**
   * A hash instance for a vector-6.
   *
   * @param ea A hash for the elements of the vector.
   * @return A hash instance for a vector-6.
   */
  public static <A> Hash<V6<A>> v6Hash(final Hash<A> ea) {
    return streamHash(ea).contramap(V6.toStream_());
  }

  /**
   * A hash instance for a vector-7.
   *
   * @param ea A hash for the elements of the vector.
   * @return A hash instance for a vector-7.
   */
  public static <A> Hash<V7<A>> v7Hash(final Hash<A> ea) {
    return streamHash(ea).contramap(V7.toStream_());
  }

  /**
   * A hash instance for a vector-8.
   *
   * @param ea A hash for the elements of the vector.
   * @return A hash instance for a vector-8.
   */
  public static <A> Hash<V8<A>> v8Hash(final Hash<A> ea) {
    return streamHash(ea).contramap(V8.toStream_());
  }

}
