package top.r3944realms.docchecktoolrefactored.core;

import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.DigestInputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;

import static org.apache.commons.codec.digest.MessageDigestAlgorithms.MD5;

/**
 * MD5哈希计算实现
 */
public class MD5HashCalculator implements FileHashCalculator {
    private static final int BUFFER_SIZE = 8192;
    private static final HexFormat HEX_FORMAT = HexFormat.of();

    @Override
    public String calculateHash(Path file) throws IOException {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            try (var is = Files.newInputStream(file)) {
                byte[] buffer = new byte[BUFFER_SIZE];
                int bytesRead;
                while ((bytesRead = is.read(buffer)) != -1) {
                    md.update(buffer, 0, bytesRead);
                }
            }
            return bytesToHex(md.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5算法不可用", e);
        }
    }
    @Override
    public String calculateHashStreaming(Path file) throws IOException {
        try (InputStream is = Files.newInputStream(file);
             DigestInputStream dis = new DigestInputStream(is, MessageDigest.getInstance(MD5))) {

            byte[] buffer = new byte[getStreamingBufferSize()];
            // 读取整个文件以更新摘要
            while (dis.read(buffer) != -1) {
                // 只需读取即可，DigestInputStream会自动更新摘要
                //noinspection UnnecessaryContinue
                continue;
            }

            byte[] digest = dis.getMessageDigest().digest();
            return HEX_FORMAT.formatHex(digest);
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("MD5 algorithm not available", e);
        }
    }

    /**
     * <h1>读取文件头部：</h1>
     * <ul>
     *      <li>读取文件的前partialChunkSize字节（如果文件大小小于这个值，则读取整个文件）</li>
     *      <li>这些字节会被添加到MD5计算中</li>
     * </ul>
     * <h1>读取文件中间部分：</h1>
     * <ul>
     *      <li>只有当文件大小大于partialChunkSize * 2时才会执行</li>
     *      <li>从文件中间位置前后各取partialChunkSize/2字节（总共partialChunkSize字节）</li>
     *      <li>这些字节会被追加到MD5计算中 </li>
     * </ul>
     * <h1>读取文件尾部：</h1>
     * <ul>
     *      <li>只有当文件大小大于partialChunkSize时才会执行</li>
     *      <li>读取文件最后的partialChunkSize字节</li>
     *      <li>这些字节会被追加到MD5计算中</li>
     * </ul>
     */
    @Override
    public String calculatePartialHash(Path file, int partialChunkSize) throws IOException {
        try {
            MessageDigest digest = MessageDigest.getInstance("MD5");

            try (var channel = Files.newByteChannel(file)) {
                long size = channel.size();

                // Read head
                if (size > 0) {
                    int chunkSize = (int) Math.min(partialChunkSize, size);
                    ByteBuffer buffer = ByteBuffer.allocate(chunkSize);
                    channel.read(buffer);
                    buffer.flip();
                    digest.update(buffer);
                }

                // Read middle
                if (size > partialChunkSize * 2L) {
                    long midPos = size / 2 - partialChunkSize / 2;
                    channel.position(midPos);
                    ByteBuffer buffer = ByteBuffer.allocate(partialChunkSize);
                    channel.read(buffer);
                    buffer.flip();
                    digest.update(buffer);
                }

                // Read tail
                if (size > partialChunkSize) {
                    long endPos = size - partialChunkSize;
                    channel.position(endPos);
                    ByteBuffer buffer = ByteBuffer.allocate(partialChunkSize);
                    channel.read(buffer);
                    buffer.flip();
                    digest.update(buffer);
                }
            }

            return bytesToHex(digest.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5算法不可用", e);
        }
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02x", b));
        }
        return sb.toString();
    }
}