/**
 * Copyright (c) 2025 SoftCorporation LLC. All rights reserved.
 *
 * The Software License, Version 1.0
 *
 * SoftCorporation LLC. grants you ("Licensee") a non-exclusive, royalty free,
 * license to use, modify and redistribute this software in source and binary
 * code form, provided that the following conditions are met:
 *
 * 1. The end-user documentation included with the redistribution,
 *    if any, must include the following acknowledgment:
 *       "This product includes software developed by the
 *        SoftCorporation LLC. (http://www.softcorporation.com)."
 *    Alternately, this acknowledgment may appear in the software itself,
 *    if and wherever such third-party acknowledgments normally appear.
 *
 * 2. This software and SoftCorporation name must not be used to
 *    promote products derived from the software without prior
 *    written permission. For written permission, please contact
 *    info@softcorporation.com.
 *
 * This software is provided "AS IS," without a warranty of any kind.
 * ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED.
 * IN NO EVENT SHALL THE SOFTCORPORATION BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION).
 *
 */
package com.softcorporation.sandi.base.util;

import java.nio.charset.StandardCharsets;

/**
 * HashService provides SimHash computation for short phrases.
 *
 * Features used per word: the whole word, character bigrams, and character trigrams.
 * This makes the hash sensitive to both word-level and character-level differences,
 * enabling near-duplicate detection with tolerance for typos and minor variations.
 *
 * Output is a 32-bit hash stored as int. Similar phrases produce hashes with
 * small Hamming distance; dissimilar phrases produce large Hamming distance.
 */
public final class HashService {

    private static final int HASH_BITS = 32;

    private static final int MURMURHASH_C1 = 0xcc9e2d51;
    private static final int MURMURHASH_C2 = 0x1b873593;
    private static final int MURMURHASH_SEED = 0;

    /**
     * Constructor
     */
    private HashService() {
    }

    /**
     * Compute 32-bit SimHash for a short phrase.
     *
     * For each word in the phrase, the following tokens are hashed and
     * contributed to the SimHash bit vector:
     *   - the whole word
     *   - all character bigrams (n=2) of the word
     *   - all character trigrams (n=3) of the word
     *
     * @param phrase input phrase, may contain multiple words
     * @return 32-bit SimHash as int
     */
    public static int simHash(final String phrase) {
        if (phrase == null || phrase.isEmpty()) {
            return 0;
        }

        int[] v = new int[HASH_BITS];
        String normalized = phrase.toLowerCase(java.util.Locale.ROOT).trim();
        String[] words = normalized.split("\\s+");

        for (String word : words) {
            if (word.isEmpty()) {
                continue;
            }
            contribute(v, word);
            contributeNgrams(v, word, 1);
            contributeNgrams(v, word, 2);
            contributeNgrams(v, word, 3);
        }

        int result = 0;
        for (int i = 0; i < HASH_BITS; i++) {
            if (v[i] > 0) {
                result |= (1 << i);
            }
        }
        return result;
    }

    /**
     * Compute 64-bit SimHash for a short phrase.
     *
     * For each word in the phrase, the following tokens are hashed and
     * contributed to the SimHash bit vector:
     *   - the whole word
     *   - all character bigrams (n=2) of the word
     *   - all character trigrams (n=3) of the word
     *
     * @param phrase input phrase, may contain multiple words
     * @return 64-bit SimHash as long
     */
    public static long simHash2(final String phrase, final boolean isQuery) {
        if (phrase == null || phrase.isEmpty()) {
            return 0;
        }

        int[] v = new int[HASH_BITS];
        String normalized = phrase.toLowerCase(java.util.Locale.ROOT).trim();
        String[] words = normalized.split("\\s+");

        for (String word : words) {
            if (word.isEmpty()) {
                continue;
            }
            contribute(v, word);
            contributeNgrams(v, word, 2);
        }
        contributeNgrams(v, phrase, 1);
        contributeNgrams(v, phrase, 2);
        contributeNgrams(v, phrase, 3);
        
        System.out.println("phrase: " + phrase);
        for (int i = 0; i < HASH_BITS; i++) {
            System.out.print(i + ":" + v[i] + " ");
        }
        System.out.println();
        
        // sort bit positions by v[i] descending
        Integer[] positions = new Integer[HASH_BITS];
        for (int i = 0; i < HASH_BITS; i++) {
            positions[i] = i;
        }
        java.util.Arrays.sort(positions, (a, b) -> Integer.compare(v[b], v[a]));

        int limit;
        if (isQuery) {
        	limit = 8;
        } else {
        	limit = 31;
        }
        
        // top 16 by v[i] (most positive votes) → result1
        long result1 = 0;
        for (int rank = 0; rank < limit; rank++) {
            result1 |= (1L << (HASH_BITS - 1 - positions[rank]));
        }
//        System.out.printf("1 bits=%s", String.format("%64s\n", Long.toBinaryString(result1)).replace(' ', '0'));

        // bottom 16 by v[i] (most negative votes) → result2
        long result2 = 0;
        for (int rank = HASH_BITS - limit; rank < HASH_BITS; rank++) {
            result2 |= (1L << (HASH_BITS - 1 - positions[rank]));
        }
//        System.out.printf("2 bits=%s", String.format("%64s\n", Long.toBinaryString(result2)).replace(' ', '0'));

        long result = (result1 << 32) | result2;
        System.out.printf("3 bits=%s", String.format("%64s\n", Long.toBinaryString(result)).replace(' ', '0'));
        return result;
    }

    /**
     * Compute Hamming distance between two SimHash values.
     * Lower distance means more similar phrases.
     *
     * @param a first hash
     * @param b second hash
     * @return number of differing bits (0 to 32)
     */
    public static int hammingDistance(final int a, final int b) {
        return Integer.bitCount(a ^ b);
    }

    /**
     * Compute Hamming distance between two SimHash values.
     * Lower distance means more similar phrases.
     *
     * @param a first hash
     * @param b second hash
     * @return number of differing bits (0 to 64)
     */
    public static int hammingDistance2(final long a, final long b) {
        return Long.bitCount(a ^ b);
    }

    /**
     * Compute match between two SimHash values.
     *
     * @param a first hash
     * @param b second hash
     * @return number of differing bits (0 to 64)
     */
    public static boolean hammingMatch(final long query, final long doc) {
        return (query & doc) == query;
    }

    /**
     * Split a 32-bit SimHash into LSH bands for Solr indexing.
     * numBands must evenly divide 32.
     *
     * @param hash     the SimHash value
     * @param numBands number of bands (e.g. 2 or 4)
     * @return array of band values
     */
    public static int[] bands(final int hash, final int numBands) {
        int bitsPerBand = HASH_BITS / numBands;
        int mask = (1 << bitsPerBand) - 1;
        int[] result = new int[numBands];
        for (int i = 0; i < numBands; i++) {
            result[i] = (hash >>> (i * bitsPerBand)) & mask;
        }
        return result;
    }

    /**
     * Hash a single token and add its contribution to the bit vector.
     */
    private static void contribute(final int[] v, final String token) {
        int h = murmurHash3(token.getBytes(StandardCharsets.UTF_8), MURMURHASH_SEED);
        for (int i = 0; i < HASH_BITS; i++) {
            if (((h >>> i) & 1) == 1) {
                v[i]++;
            } else {
                v[i]--;
            }
        }
    }

    /**
     * Extract all character n-grams from a word and contribute each to the bit vector.
     */
    private static void contributeNgrams(final int[] v, final String word, final int n) {
        for (int i = 0; i <= word.length() - n; i++) {
            contribute(v, word.substring(i, i + n));
        }
    }

    /**
     * MurmurHash3 32-bit (x86 variant).
     *
     * @param data input bytes
     * @param seed hash seed
     * @return 32-bit hash
     */
    static int murmurHash3(final byte[] data, final int seed) {
        int h = seed;
        int len = data.length;
        int blocks = len >> 2;

        for (int i = 0; i < blocks; i++) {
            int offset = i << 2;
            int k = (data[offset] & 0xFF)
                    | ((data[offset + 1] & 0xFF) << 8)
                    | ((data[offset + 2] & 0xFF) << 16)
                    | ((data[offset + 3] & 0xFF) << 24);

            k *= MURMURHASH_C1;
            k = Integer.rotateLeft(k, 15);
            k *= MURMURHASH_C2;

            h ^= k;
            h = Integer.rotateLeft(h, 13);
            h = h * 5 + 0xe6546b64;
        }

        int tail = blocks << 2;
        int k = 0;
        int remaining = len & 3;
        if (remaining == 3) {
            k ^= (data[tail + 2] & 0xFF) << 16;
        }
        if (remaining >= 2) {
            k ^= (data[tail + 1] & 0xFF) << 8;
        }
        if (remaining >= 1) {
            k ^= (data[tail] & 0xFF);
            k *= MURMURHASH_C1;
            k = Integer.rotateLeft(k, 15);
            k *= MURMURHASH_C2;
            h ^= k;
        }

        h ^= len;
        h ^= (h >>> 16);
        h *= 0x85ebca6b;
        h ^= (h >>> 13);
        h *= 0xc2b2ae35;
        h ^= (h >>> 16);

        return h;
    }

}
