package info.knacki.pass.git;

import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayDeque;
import java.util.logging.Logger;
import java.util.zip.DataFormatException;
import java.util.zip.Inflater;

import info.knacki.pass.git.entities.GitPackable;
import info.knacki.pass.git.entities.GitPackableFactory;
import info.knacki.pass.git.entities.GitPackableUtil;
import info.knacki.pass.io.CharsetHelper;

public class PacmanBuilder {
    private final static Logger log = Logger.getLogger(PacmanBuilder.class.getName());

    private byte[] fBuffer;
    private int fPos;

    public static class InvalidPackException extends Throwable {
        InvalidPackException(String msg) {
            super(msg);
        }

        InvalidPackException(String msg, Throwable cause) {
            super(msg, cause);
        }
    }

    public Pacman build(byte[] data) throws InvalidPackException {
        if (data == null)
            throw new InvalidPackException("No data provided");
        final String header = CharsetHelper.ByteArrayToString(data, 0, 4);
        if (!"PACK".equals(header))
            throw new InvalidPackException("Invalid header " +header);

        try {
            MessageDigest checksum = MessageDigest.getInstance("SHA1");
            checksum.update(data, 0, data.length - 20);
            if (!GitSha1.CompareChecksum(checksum.digest(), data, data.length - 20))
                throw new InvalidPackException("Checksum mismatch");
        }
        catch (NoSuchAlgorithmException e) {
            throw new InvalidPackException("Cannot check pack integrity: " +e.getMessage(), e);
        }

        final int packVersion = ByteBuffer.wrap(data, 4, 4).getInt();
        fBuffer = new byte[data.length -8 - 20];
        fPos = 0;
        System.arraycopy(data, 8, fBuffer, 0, fBuffer.length);

        switch (packVersion) {
            case 2:
                return ReadPackV2();
            default:
                throw new InvalidPackException("Unsupported pack version " +packVersion);
        }
    }

    private GitPackable.PackV2Header ReadPackV2Header() throws InvalidPackException {
        int currentByte;
        int nbBits = 4;

        GitPackable.PackV2Header header = new GitPackable.PackV2Header();
        currentByte = fBuffer[fPos++];
        final int type = (currentByte & 0b01110000) >> 4;
        header.type = GitPackableUtil.getObjType(type);
        if (header.type == null)
            throw new InvalidPackException("Unknown object type " +type +" in pack");
        header.length = currentByte & 0b00001111;
        while ((currentByte & 0b10000000) != 0) {
            currentByte = fBuffer[fPos++];
            header.length += (currentByte & 0b01111111) << nbBits;
            nbBits += 7;
        }
        return header;
    }

    private byte[] InflateObject(GitPackable.PackV2Header header) throws InvalidPackException {
        byte[] data = new byte[header.length];
        try {
            Inflater inf = new Inflater(false);
            int totalRead = 0;
            do {
                if (inf.needsInput())
                    inf.setInput(fBuffer, (int) (fPos +inf.getBytesRead()), fBuffer.length -fPos -(int)inf.getBytesRead());
                totalRead += inf.inflate(data, totalRead, 1);
            } while (!inf.finished() && totalRead < header.length);
            fPos += inf.getBytesRead();
        } catch (DataFormatException e) {
            throw new InvalidPackException("Zip error: " + e.getMessage(), e);
        }
        return data;
    }

    private void InflateDiscard(int inflatedSize) throws InvalidPackException {
        byte[] data = new byte[1];
        try {
            Inflater inf = new Inflater(false);
            int totalRead = 0;
            do {
                if (inf.needsInput())
                    inf.setInput(fBuffer, (int) (fPos +inf.getBytesRead()), fBuffer.length -fPos -(int)inf.getBytesRead());
                totalRead += inf.inflate(data, 0, 1);
            } while (!inf.finished() && totalRead < inflatedSize);
            fPos += inf.getBytesRead();
        } catch (DataFormatException e) {
            throw new InvalidPackException("Zip error: " + e.getMessage(), e);
        }
    }

    private Pacman ReadPackV2() throws InvalidPackException {
        final int packCount = ByteBuffer.wrap(fBuffer, fPos, 4).getInt();
        fPos += 4;
        log.info("Found " +packCount +" object in pack file");
        ArrayDeque<GitPackable> packed = new ArrayDeque<>();

        for (int i =0; i < packCount; ++i) {
            GitPackable.PackV2Header header = ReadPackV2Header();

            switch (header.type) {
                case eType_Blob:
                case eType_Commit:
                case eType_Tree:
                case eType_Tag:
                    byte[] inflated = InflateObject(header);
                    String hash = GitSha1.GetSha1OfData(header.type, inflated);
                    GitPackable pack = GitPackableFactory.build(header.type, hash, inflated);

                    if (pack == null) {
                        log.warning("Cannot parse object type " + header.type);
                        continue;
                    }
                    packed.add(pack);
                    log.info("Read " +GitSha1.getSha1OfPackable(pack) +" " + pack + " from pack");
                    break;

                case eType_RefDelta:
                    log.info("Found ref_delta from base " +GitSha1.BytesToString(fBuffer, fPos));
                    fPos += 20;
                    InflateDiscard(header.length);
                    break;

                case eType_OfsDelta:
                    //noinspection StatementWithEmptyBody
                    while ((fBuffer[fPos++] & 0b10000000) != 0) {}
                    InflateDiscard(header.length);
                    break;
            }
        }
        log.info("End reading pack, " +packed.size() +" object created");
        return new Pacman(packed);
    }
}
