/**
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.cxf.attachment;


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

public class MimeBodyPartInputStream extends InputStream {

    PushbackInputStream inStream;

    boolean boundaryFound;
    int pbAmount;
    byte[] boundary;
    byte[] boundaryBuffer;

    private boolean closed;

    public MimeBodyPartInputStream(PushbackInputStream inStreamParam,
                                   byte[] boundaryParam,
                                   int pbsize) {
        super();
        this.inStream = inStreamParam;
        this.boundary = boundaryParam;
        this.pbAmount = pbsize;
    }

    @Override
    public int read(byte[] buf, int origOff, int origLen) throws IOException {
        byte[] b = buf;
        int off = origOff;
        int len = origLen;
        if (boundaryFound || closed) {
            return -1;
        }
        if ((off < 0) || (off > b.length) || (len < 0)
            || ((off + len) > b.length) || ((off + len) < 0)) {

            throw new IndexOutOfBoundsException();
        }
        if (len == 0) {
            return 0;
        }
        boolean bufferCreated = false;
        if (len < Math.addExact(boundary.length, boundary.length)) {
            //buffer is too short to detect boundaries with it.  We'll need to create a larger buffer
            bufferCreated = true;
            if (boundaryBuffer == null) {
                boundaryBuffer = new byte[Math.addExact(boundary.length, boundary.length)];
            }
            b = boundaryBuffer;
            off = 0;
            len = boundaryBuffer.length;
        }
        if (len > pbAmount) {
            len = pbAmount;  //can only pushback that much so make sure we can
        }
        int read = 0;
        int idx = 0;
        while (read >= 0 && idx < len && idx < Math.addExact(boundary.length, boundary.length)) {
            //make sure we read enough to detect the boundary
            read = inStream.read(b, off + idx, len - idx);
            if (read != -1) {
                idx += read;
            }
        }
        if (read == -1 && idx == 0) {
            return -1;
        }
        len = idx;

        int i = processBuffer(b, off, len);
        if (bufferCreated && i > 0) {
            // read more than we need, push it back
            if (origLen >= i) {
                System.arraycopy(b, 0, buf, origOff, i);
            } else {
                System.arraycopy(b, 0, buf, origOff, origLen);
                inStream.unread(b, origLen, i - origLen);
                i = origLen;
            }
        } else if (i == 0 && boundaryFound) {
            return -1;
        }

        return i;
    }

    //Has Data after encountering CRLF
    private boolean hasData(byte[] b, int initialPointer, int pointer, int off, int len)
        throws IOException {
        if (pointer < (off + len)) {
            return true;
        } else if (pointer >= 1000000000) {
            inStream.unread(b, initialPointer, (off + len) - initialPointer);
            return false;
        } else {
            int x = inStream.read();
            if (x != -1) {
                inStream.unread(x);
                inStream.unread(b, initialPointer, (off + len) - initialPointer);
                return false;
            }
            return true;
        }
    }

    protected int processBuffer(byte[] buffer, int off, int len) throws IOException {
        for (int i = off; i < (off + len); i++) {
            boolean needUnread0d0a = false;
            int value = buffer[i];
            int initialI = i;
            if (value == 13) {
                if (!hasData(buffer, initialI, initialI + 1, off, len)) {
                    return initialI - off;
                }
                value = buffer[initialI + 1];
                if (value != 10) {
                    continue;
                }
                if (!hasData(buffer, initialI, initialI + 2, off, len)) {
                    return initialI - off;
                }
                value = buffer[initialI + 2];
                if ((byte) value != boundary[0]) {
                    i++;
                    continue;
                }
                needUnread0d0a = true;
                i += 2; //i after this points to boundary[0] element
            } else if (value != boundary[0]) {
                continue;
            }

            int boundaryIndex = 0;
            while ((boundaryIndex < boundary.length) && (value == boundary[boundaryIndex])) {
                if (!hasData(buffer, initialI, i + 1, off, len)) {
                    return initialI - off;
                }
                value = buffer[++i];
                boundaryIndex++;
            }
            if (boundaryIndex == boundary.length) {
                // read the end of line character
                if (initialI != off) {
                    i = 1000000000;
                }
                if (initialI - off != 0
                    && !hasData(buffer, initialI, i + 1, off, len)) {
                    return initialI - off;
                }
                boundaryFound = true;
                int j = i + 1;
                if (j < len && buffer[j] == 45 && value == 45) {
                    // Last mime boundary should have a succeeding "--"
                    // as we are on it, read the terminating CRLF
                    i += 2;
                    //last mime boundary
                }

                //boundary matched (may or may not be last mime boundary)
                int processed = initialI - off;
                if ((len - ((i - off) + 2)) > 0) {
                    inStream.unread(buffer, i + 2, len - (i + 2) + off);
                }
                return processed;
            }

            // Boundary not found. Restoring bytes skipped.
            // write first skipped byte, push back the rest
            if (value != -1) { //pushing back first byte of boundary
                // Stream might have ended
                i--;
            }
            if (needUnread0d0a) { //Pushing all,  returning 13
                i = i - boundaryIndex;
                i--; //for 10
//                value = 13;
            } else {
                i = i - boundaryIndex;
                i++;
//                value = boundary[0];
            }
        }
        return len;
    }

    public int read() throws IOException {
        boolean needUnread0d0a = false;
        if (boundaryFound) {
            return -1;
        }

        // read the next value from stream
        int value = inStream.read();
        // A problem occurred because all the mime parts tends to have a /r/n
        // at the end. Making it hard to transform them to correct
        // DataSources.
        // This logic introduced to handle it
        if (value == 13) {
            value = inStream.read();
            if (value != 10) {
                inStream.unread(value);
                return 13;
            }
            value = inStream.read();
            if ((byte) value != boundary[0]) {
                inStream.unread(value);
                inStream.unread(10);
                return 13;
            }
            needUnread0d0a = true;
        } else if ((byte) value != boundary[0]) {
            return value;
        }
        // read value is the first byte of the boundary. Start matching the
        // next characters to find a boundary
        int boundaryIndex = 0;
        while ((boundaryIndex < boundary.length) && ((byte) value == boundary[boundaryIndex])) {
            value = inStream.read();
            boundaryIndex++;
        }
        if (boundaryIndex == boundary.length) {
            // boundary found
            boundaryFound = true;
            int dashNext = inStream.read();
            // read the end of line character
            if (dashNext == 45 && value == 45) {
                // Last mime boundary should have a succeeding "--"
                // as we are on it, read the terminating CRLF
                inStream.read();
                inStream.read();
            }
            return -1;
        }
        // Boundary not found. Restoring bytes skipped.
        // write first skipped byte, push back the rest
        if (value != -1) {
            // Stream might have ended
            inStream.unread(value);
        }
        if (needUnread0d0a) {
            inStream.unread(boundary, 0, boundaryIndex);
            inStream.unread(10);
            value = 13;
        } else {
            inStream.unread(boundary, 1, boundaryIndex - 1);
            value = boundary[0];
        }
        return value;
    }

    @Override
    public void close() throws IOException {
        this.closed = true;
    }
}
