package org.troy.capstone.data_structures.item_table;

import java.math.BigInteger;

/**
 * Class representing a hash key for item IDs, using universal hashing to minimize collisions in the item table. The hash code is computed by collapsing the string ID into an integer using a {@code Rabin-Karp style polynomial rolling hash method}, and then applying a universal hash function with random coefficients {@code I} and {@code J} and a prime modulus {@code P}. This approach helps ensure a good distribution of hash values even for similar string IDs, which is important for maintaining efficient lookups in the item table. Implements {@code Cloneable} to allow for deep copy of {@code IdHashKey} instances.
 */
public class IdHashKey implements Cloneable {
    /** The original string value of the item ID, stored for equality checks and potential debugging purposes. */
    private final String value;

    /** A prime number larger than the maximum possible hash value from collapsing the strings, to ensure good distribution in universal hashing. */
    private static final BigInteger P = BigInteger.valueOf(SieveOfEratosthenes.staticPrimeUnder100mil().orElseThrow());
    /**
     * A random coefficient for the universal hash function, chosen uniformly from 1 to P-1.
     * Used as the multiplier in the universal hash formula. Not final to allow changing in testing.
     */
    @SuppressWarnings("FieldMayBeFinal")
    private static BigInteger I = BigInteger.valueOf((long) (Math.random() * P.longValue()) + 1);

    /**
     * A random coefficient for the universal hash function, chosen uniformly from 0 to P-1.
     * Used as the additive constant in the universal hash formula. Not final to allow changing in testing.
     */
    @SuppressWarnings("FieldMayBeFinal")
    private static BigInteger J = BigInteger.valueOf((long) (Math.random() * (P.longValue() - 1L)));

    /** 961 entries, 0.75 load factor -> 2048 table size
    2048 is the lowest power of 2 above 961/0.75.
    The table size is chosen to be a power of 2 for efficient modulo operations. */
    private static final int TABLE_SIZE = 2048;

    /** Constructor for the IdHashKey class.
     * @param value The original string value of the item ID to be hashed.
    */
    public IdHashKey(String value) {
        this.value = value;
    }

    /** Getter for the original string value of the item ID.
     * @return The original string value of the item ID.
    */
    public String getValue() {
        return value;
    }
    
    /** Getter for the random coefficient I used in the universal hash function.
     * @return The random coefficient I.
    */
    static BigInteger getI() {
        return I;
    }
    
    /** Getter for the random coefficient J used in the universal hash function.
     * @return The random coefficient J.
    */
    static BigInteger getJ() {
        return J;
    }
    
    /** Getter for the prime modulus P used in the universal hash function.
     * @return The prime modulus P.
    */
    static BigInteger getP() {
        return P;
    }
    
    /** Using universal hashing code from textbook, with small alterations.
     * @return The hash code for the item ID.
     */
    @Override
    public int hashCode() {
        BigInteger collapsed = collapseStringToInt(value);
        return I.multiply(collapsed)
            .add(J)
            .mod(BigInteger.valueOf(TABLE_SIZE))
            .intValue();
    }

    /**
     * Rabin-Karp style string to int collapse, using polynomial rolling hash method.
     * Algorithm source is [3].
     * @pre str is not null and only contains ASCII characters from '!' to '~' (printable characters excluding space and delete).
     * 
     * @param str The string to collapse into an integer hash value.
     * @return A hash value representing the input string collapsed via Rabin-Karp rolling hash.
     */
    private BigInteger collapseStringToInt(String str) {
        //Number of possible characters (ASCII chars from '!' to '~', printables excluding space and delete)
        BigInteger b = BigInteger.valueOf('~' - '!' + 1);
        int L = str.length();
        BigInteger hash = BigInteger.ZERO;
        for (int i = 0; i < L; i++){
            //Map '!' to 0, '"' to 1, ..., '~' to 93
            int rankingOfChar = str.charAt(i) - '!';
            hash = hash.add(BigInteger.valueOf(rankingOfChar).multiply(b.pow(L - i - 1)));
        }
        return hash.mod(P);
    }

    /**
     * Checks if this {@code IdHashKey} is equal to another object.
     * @param obj The object to compare with.
     * @return true if the objects are equal, false otherwise.
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null || getClass() != obj.getClass())
            return false;
        IdHashKey other = (IdHashKey) obj;
        return value.equals(other.value);
    }

    /** Returns a string representation of the {@code IdHashKey}, including the original string value and the computed hash code.
     * @return A string representation of the {@code IdHashKey}.
    */
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

}
