package net.ranides.assira.io;

import lombok.experimental.UtilityClass;

import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.jar.JarInputStream;
import java.util.zip.ZipInputStream;

/**
 * Simple fix for ZipInputStream and JarInputStream allowing reading files if
 * prefix bytes before signature. It simply skips bytes until signature is found
 * and then processes data in ordinary way.
 */
@UtilityClass
public class ZipReader {

    /**
     * Creates JarInputStream which ignores bytes before ZIP signature.
     *
     * @param istream istream
     * @return JarInputStream
     * @throws IOException on error
     */
    public static JarInputStream openJAR(InputStream istream) throws IOException {
        return new JarInputStream(new SkipInputStream(istream));
    }

    /**
     * Creates ZipInputStream which ignores bytes before ZIP signature.
     *
     * @param istream istream
     * @return ZipInputStream
     */
    public static ZipInputStream openZIP(InputStream istream) {
        return new ZipInputStream(new SkipInputStream(istream));
    }

    private static class SkipInputStream extends FilterInputStream {

        private static final int BUFFER_SIZE = 512;

        private static final byte[] ZIP_LOCAL = { 0x50, 0x4b, 0x03, 0x04 };

        private byte[] buffer;
        private int bufferSize;
        private int bufferPosition;

        private int signatureFound;
        private int signatureReturned;

        public SkipInputStream(InputStream is) {
            super(is);
            this.buffer = new byte[BUFFER_SIZE];
            this.bufferSize = 0;
            this.bufferPosition = 0;
        }


        public int read() throws IOException {
            // if we already returned ZIP_LOCAL and data from skipBuffer, then delegate to ordinary I/O
            if(buffer == null) {
                return super.read();
            }

            // If ZIP_LOCAL is not found, search it by using buffered "read"
            // We are going to skip everything before ZIP_LOCAL, including ZIP_LOCAL itself.
            // Skipping ZIP_LOCAL is necessary because it can be stored across two consecutive buffered reads
            while(signatureFound < ZIP_LOCAL.length) {
                int c = readbuf();
                if(c == -1) {
                    return -1;
                }
                if (c == ZIP_LOCAL[signatureFound]) {
                    signatureFound++;
                } else {
                    signatureFound = 0;
                }
            }

            // "ZIP_LOCAL" is consumed from "buffer"
            // we need to return it literally, by reading ZIP_LOCAL byte by byte
            if(signatureReturned < ZIP_LOCAL.length) {
                return ZIP_LOCAL[signatureReturned++];
            }

            // buffer can contain some bytes after ZIP_LOCAL, let's return them byte by byte
            if(bufferPosition < bufferSize) {
                // "readbuf" won't fill new buffer because it fills it only if bufferPosition == bufferSize
                return readbuf();
            } else {
                // if we returned whole buffer, lets clear it and delegate to ordinary I/O
                // method will detect empty buffer on next calls
                buffer = null;
                return super.read();
            }
        }

        private int readbuf() throws IOException {
            // read new block if current one is exhausted
            if(bufferPosition >= bufferSize) {
                bufferSize = super.read(buffer, 0, buffer.length);
                if(bufferSize == -1) {
                    buffer = null;
                    return -1;
                }
                bufferPosition = 0;
            }
            // return data from buffer, byte by byte
            return buffer[bufferPosition++];
        }

        @Override
        public int read(byte[] output, int off, int len) throws IOException {
            // if we already returned signature, then use ordinary I/O
            if (buffer == null) {
                return super.read(output, off, len);
            }

            // read byte by byte from our buffer
            int rem = Math.min(len, bufferRemaining());
            int count = 0;
            while (count < rem) {
                byte b = (byte) read();
                if(b == -1) {
                    return count;
                }
                output[off + count++] = b;
            }

            // if we read "len" bytes, then just return
            if(count == len) {
                return count;
            }

            // if we read less than "len" bytes, then use ordinary I/O for read remaining bytes
            return count + super.read(output, off + count, len - count);
        }

        private int bufferRemaining() {
            if(signatureReturned != ZIP_LOCAL.length) {
                return buffer.length + ZIP_LOCAL.length;
            }
            return bufferSize - bufferPosition;
        }
    }

}
