/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.richfaces.request;

import java.io.IOException;
import java.io.InputStream;

public class ByteSequenceMatcher {
    public interface BytesHandler {
        void handle(byte[] bytes, int length) throws IOException;
    }

    private static final int ZERO_READS_NUMBER = 20;
    private byte[] buffer;
    private int readLength = 0;
    private int zeroReadCounter = ZERO_READS_NUMBER;
    private boolean bufferEOF = false;
    private boolean isEOF = false;
    private boolean isMatched = false;
    private InputStream inputStream;
    private BytesHandler bytesHandler;

    public ByteSequenceMatcher(InputStream inputStream, int bufferSize) {
        this.inputStream = inputStream;
        this.buffer = new byte[bufferSize];
    }

    public BytesHandler getBytesHandler() {
        return bytesHandler;
    }

    public void setBytesHandler(BytesHandler bytesHandler) {
        this.bytesHandler = bytesHandler;
    }

    protected void prefillBuffer(int usedLength) throws IOException {
        if (usedLength > readLength) {
            throw new IllegalArgumentException();
        }

        if (usedLength != readLength && usedLength != 0) {
            System.arraycopy(buffer, usedLength, buffer, 0, readLength - usedLength);
        }

        readLength -= usedLength;

        while (!bufferEOF) {
            int remaining = buffer.length - readLength;
            if (remaining <= 0) {
                break;
            }
            int read = inputStream.read(buffer, readLength, remaining);

            if (read > 0) {
                readLength += read;
            } else if (read == 0) {
                --zeroReadCounter;

                if (zeroReadCounter == 0) {
                    throw new IllegalStateException("Maximum number of zero reads reached");
                }
            } else if (read < 0) {
                bufferEOF = true;
            }
        }
    }

    private boolean match(int startOffset, byte[]... sequences) {
        int index = startOffset;

        for (byte[] bs : sequences) {
            for (byte b : bs) {

                if (index >= readLength) {
                    return false;
                }

                if (buffer[index] != b) {
                    return false;
                }

                index++;
            }
        }

        return true;
    }

    public void findSequence(int limit, byte[]... sequences) throws IOException {
        isMatched = false;

        int userLimit = limit;
        if (userLimit <= 0) {
            userLimit = Integer.MAX_VALUE;
        }

        prefillBuffer(0);

        int sequencesLength = 0;
        for (byte[] bs : sequences) {
            sequencesLength += bs.length;
        }

        int i = 0;

        while (!isMatched && i <= readLength - sequencesLength) {
            if (match(i, sequences)) {
                isMatched = true;
                bytesHandler.handle(buffer, i);
                prefillBuffer(i + sequencesLength);
            } else {
                int sequenceLimit = readLength - sequencesLength + 1;
                int realLimit;

                if (sequenceLimit < userLimit) {
                    realLimit = sequenceLimit;
                } else {
                    realLimit = userLimit;
                }

                if (realLimit > 0 && i == realLimit - 1) {
                    // report limit
                    bytesHandler.handle(buffer, realLimit);
                    prefillBuffer(realLimit);

                    i = 0;
                } else {
                    i++;
                }
            }
        }

        flushUnmatchedBytes();

        if (this.readLength == 0) {
            this.isEOF = true;
        }
    }

    private void flushUnmatchedBytes() throws IOException {
        if (!isMatched) {
            if (readLength > 0) {
                bytesHandler.handle(buffer, readLength);
                prefillBuffer(readLength);
            }
        }
    }

    public boolean isEOF() {
        return isEOF;
    }

    public boolean isMatched() {
        return isMatched;
    }

    public boolean isMatchedAndNotEOF() {
        return isMatched() && !isEOF();
    }
}
