/**
 * SHA-224 hash function implementation in Java
 * Based on the Secure Hash Algorithm 2 as defined in FIPS 180-4
 *
 * @author SHA224.com
 * @license MIT
 */
package com.sha224;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class SHA224 {
    // SHA-224 initial hash values (in hex)
    // These values are the second 32 bits of the fractional parts of the
    // square roots of the 9th through 16th prime numbers
    private static final int[] INITIAL_HASH = {
            0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,
            0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4
    };
    
    // SHA-256 round constants
    // First 32 bits of the fractional parts of the cube roots of the 
    // first 64 prime numbers
    private static final int[] K = {
            0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
            0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
            0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
            0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
            0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
            0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
            0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
            0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
            0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
            0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
            0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
            0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
            0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
            0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
            0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
            0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
    };
    
    // Hash state
    private final int[] h = new int[8];
    
    // Working buffer for unprocessed input
    private byte[] buffer = new byte[64];
    private int bufferLength = 0;
    
    // Total length of the input in bytes
    private long totalLength = 0;
    
    /**
     * Creates a new SHA-224 hash object with default initialization.
     */
    public SHA224() {
        reset();
    }
    
    /**
     * Resets the hash object to its initial state.
     */
    public void reset() {
        System.arraycopy(INITIAL_HASH, 0, h, 0, INITIAL_HASH.length);
        bufferLength = 0;
        totalLength = 0;
        Arrays.fill(buffer, (byte) 0);
    }
    
    /**
     * Updates the hash with new data.
     * 
     * @param data the input data to hash
     * @return this SHA224 instance for method chaining
     */
    public SHA224 update(byte[] data) {
        return update(data, 0, data.length);
    }
    
    /**
     * Updates the hash with new data.
     * 
     * @param data the input data to hash
     * @param offset the start offset in the data
     * @param length the number of bytes to process
     * @return this SHA224 instance for method chaining
     */
    public SHA224 update(byte[] data, int offset, int length) {
        if (data == null || length == 0) {
            return this;
        }
        
        // Update total length
        totalLength += length;
        
        // Fill buffer with data and process blocks
        int dataIndex = offset;
        int remainingLength = length;
        
        // If there's data in the buffer, fill it first
        if (bufferLength > 0) {
            int toFill = Math.min(64 - bufferLength, remainingLength);
            System.arraycopy(data, dataIndex, buffer, bufferLength, toFill);
            bufferLength += toFill;
            dataIndex += toFill;
            remainingLength -= toFill;
            
            // If buffer is full, process it
            if (bufferLength == 64) {
                processBlock(buffer, 0);
                bufferLength = 0;
            }
        }
        
        // Process as many full blocks as possible directly from input
        while (remainingLength >= 64) {
            processBlock(data, dataIndex);
            dataIndex += 64;
            remainingLength -= 64;
        }
        
        // Store remaining bytes in buffer
        if (remainingLength > 0) {
            System.arraycopy(data, dataIndex, buffer, bufferLength, remainingLength);
            bufferLength += remainingLength;
        }
        
        return this;
    }
    
    /**
     * Updates the hash with a string (UTF-8 encoded).
     * 
     * @param data the input string to hash
     * @return this SHA224 instance for method chaining
     */
    public SHA224 update(String data) {
        return update(data.getBytes(StandardCharsets.UTF_8));
    }
    
    /**
     * Finalizes the hash computation and returns the digest.
     * 
     * @return the 28-byte (224-bit) hash digest
     */
    public byte[] digest() {
        // Create a copy of the current state
        int[] hCopy = Arrays.copyOf(h, h.length);
        byte[] bufferCopy = Arrays.copyOf(buffer, buffer.length);
        int bufferLengthCopy = bufferLength;
        long totalLengthCopy = totalLength;
        
        // Pad the message
        
        // Add the "1" bit
        bufferCopy[bufferLengthCopy] = (byte) 0x80;
        bufferLengthCopy++;
        
        // If there's not enough space for the length (8 bytes), process this block
        // and pad a new one
        if (bufferLengthCopy > 56) {
            // Fill the rest of the block with zeros
            Arrays.fill(bufferCopy, bufferLengthCopy, 64, (byte) 0);
            processBlock(bufferCopy, 0, hCopy);
            bufferLengthCopy = 0;
        }
        
        // Fill with zeros up to the point where the length goes
        Arrays.fill(bufferCopy, bufferLengthCopy, 56, (byte) 0);
        
        // Append length as 64-bit big-endian integer (in bits)
        long bitLength = totalLengthCopy * 8;
        ByteBuffer.wrap(bufferCopy, 56, 8).putLong(bitLength);
        
        // Process the final block
        processBlock(bufferCopy, 0, hCopy);
        
        // Convert hash state to byte array (224 bits = 28 bytes)
        byte[] digest = new byte[28];
        for (int i = 0; i < 7; i++) {
            digest[i * 4] = (byte) (hCopy[i] >>> 24);
            digest[i * 4 + 1] = (byte) (hCopy[i] >>> 16);
            digest[i * 4 + 2] = (byte) (hCopy[i] >>> 8);
            digest[i * 4 + 3] = (byte) hCopy[i];
        }
        
        // Reset for potential reuse
        reset();
        
        return digest;
    }
    
    /**
     * Finalizes the hash computation and returns the digest as a hex string.
     * 
     * @return the 56-character hex string representation of the hash
     */
    public String digestHex() {
        byte[] digest = digest();
        StringBuilder hexString = new StringBuilder(56);
        
        for (byte b : digest) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        
        return hexString.toString();
    }
    
    /**
     * Process a single 64-byte block.
     * 
     * @param data the data containing the block
     * @param offset the offset in the data where the block starts
     */
    private void processBlock(byte[] data, int offset) {
        processBlock(data, offset, h);
    }
    
    /**
     * Process a single 64-byte block and update the given hash state.
     * 
     * @param data the data containing the block
     * @param offset the offset in the data where the block starts
     * @param state the hash state to update
     */
    private void processBlock(byte[] data, int offset, int[] state) {
        // Initialize message schedule
        int[] w = new int[64];
        
        // Fill the first 16 words with the block data
        for (int i = 0; i < 16; i++) {
            w[i] = ((data[offset + i * 4] & 0xff) << 24) |
                   ((data[offset + i * 4 + 1] & 0xff) << 16) |
                   ((data[offset + i * 4 + 2] & 0xff) << 8) |
                   (data[offset + i * 4 + 3] & 0xff);
        }
        
        // Extend the first 16 words into the remaining 48
        for (int i = 16; i < 64; i++) {
            int s0 = rightRotate(w[i - 15], 7) ^ rightRotate(w[i - 15], 18) ^ (w[i - 15] >>> 3);
            int s1 = rightRotate(w[i - 2], 17) ^ rightRotate(w[i - 2], 19) ^ (w[i - 2] >>> 10);
            w[i] = w[i - 16] + s0 + w[i - 7] + s1;
        }
        
        // Initialize working variables
        int a = state[0];
        int b = state[1];
        int c = state[2];
        int d = state[3];
        int e = state[4];
        int f = state[5];
        int g = state[6];
        int h = state[7];
        
        // Main loop
        for (int i = 0; i < 64; i++) {
            int S1 = rightRotate(e, 6) ^ rightRotate(e, 11) ^ rightRotate(e, 25);
            int ch = (e & f) ^ ((~e) & g);
            int temp1 = h + S1 + ch + K[i] + w[i];
            
            int S0 = rightRotate(a, 2) ^ rightRotate(a, 13) ^ rightRotate(a, 22);
            int maj = (a & b) ^ (a & c) ^ (b & c);
            int temp2 = S0 + maj;
            
            h = g;
            g = f;
            f = e;
            e = d + temp1;
            d = c;
            c = b;
            b = a;
            a = temp1 + temp2;
        }
        
        // Update state
        state[0] += a;
        state[1] += b;
        state[2] += c;
        state[3] += d;
        state[4] += e;
        state[5] += f;
        state[6] += g;
        state[7] += h;
    }
    
    /**
     * Right rotate a 32-bit integer by the specified number of bits.
     * 
     * @param x the integer to rotate
     * @param n the number of bits to rotate by
     * @return the rotated integer
     */
    private int rightRotate(int x, int n) {
        return (x >>> n) | (x << (32 - n));
    }
    
    /**
     * Converts a byte array to a hex string.
     * 
     * @param bytes the byte array to convert
     * @return the hex string representation
     */
    private static String bytesToHex(byte[] bytes) {
        StringBuilder hexString = new StringBuilder(2 * bytes.length);
        
        for (byte b : bytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        
        return hexString.toString();
    }
    
    /**
     * Static method to compute a SHA-224 hash in one step.
     * 
     * @param data the data to hash
     * @return the SHA-224 hash as a hex string
     */
    public static String hash(byte[] data) {
        return new SHA224().update(data).digestHex();
    }
    
    /**
     * Static method to compute a SHA-224 hash of a string in one step.
     * 
     * @param data the string to hash (UTF-8 encoded)
     * @return the SHA-224 hash as a hex string
     */
    public static String hash(String data) {
        return new SHA224().update(data).digestHex();
    }
    
    /**
     * Verify if a message matches a given SHA-224 hash.
     * 
     * @param message the message to verify
     * @param hash the expected hash (hex string)
     * @return true if the hash matches, false otherwise
     */
    public static boolean verify(byte[] message, String hash) {
        String calculated = hash(message);
        
        // Normalize hash format (lowercase)
        String expected = hash.toLowerCase();
        String actual = calculated.toLowerCase();
        
        // Constant-time comparison to prevent timing attacks
        if (expected.length() != actual.length()) {
            return false;
        }
        
        int result = 0;
        for (int i = 0; i < actual.length(); i++) {
            result |= actual.charAt(i) ^ expected.charAt(i);
        }
        
        return result == 0;
    }
    
    /**
     * Verify if a string matches a given SHA-224 hash.
     * 
     * @param message the string to verify (UTF-8 encoded)
     * @param hash the expected hash (hex string)
     * @return true if the hash matches, false otherwise
     */
    public static boolean verify(String message, String hash) {
        return verify(message.getBytes(StandardCharsets.UTF_8), hash);
    }
    
    /**
     * Main method for testing.
     * 
     * @param args command line arguments (not used)
     */
    public static void main(String[] args) {
        // Example usage
        String message = "Hello, World!";
        String hash = SHA224.hash(message);
        System.out.println("Message: " + message);
        System.out.println("SHA-224 Hash: " + hash);
        System.out.println("Verification: " + SHA224.verify(message, hash));
        
        // Test vectors
        testVector("", "d14a028c2a3a2bc9476102bb288234c415a2b01f828ea62ac5b3e42f");
        testVector("abc", "23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7");
        testVector("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
                  "75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525");
    }
    
    private static void testVector(String input, String expectedHash) {
        String actualHash = hash(input);
        boolean matches = actualHash.equals(expectedHash);
        System.out.println("Test vector: \"" + input + "\"");
        System.out.println("  Expected: " + expectedHash);
        System.out.println("  Actual:   " + actualHash);
        System.out.println("  Result:   " + (matches ? "PASS" : "FAIL"));
    }
}