/****************************************************************
 * 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.james.mime4j.io;

import org.apache.james.mime4j.util.ByteArrayBuffer;

import java.io.IOException;

/**
 * Stream that constrains itself to a single MIME body part.
 * After the stream ends (i.e. read() returns -1) {@link #isLastPart()}
 * can be used to determine if a final boundary has been seen or not.
 */
public class MimeBoundaryInputStream extends LineReaderInputStream {

    private final byte[] boundary;
    
    private boolean eof;
    private int limit;
    private boolean atBoundary;
    private int boundaryLen;
    private boolean lastPart;
    private boolean completed;

    private BufferedLineReaderInputStream buffer;

    /**
     * Creates a new MimeBoundaryInputStream.
     * 
     * @param inbuffer The underlying stream.
     * @param boundary Boundary string (not including leading hyphens).
     * @throws IllegalArgumentException when boundary is too long
     */
    public MimeBoundaryInputStream(BufferedLineReaderInputStream inbuffer, String boundary) 
            throws IOException {
        super(inbuffer);
        if (inbuffer.capacity() <= boundary.length()) {
            throw new IllegalArgumentException("Boundary is too long");
        }
        this.buffer = inbuffer;
        this.eof = false;
        this.limit = -1;
        this.atBoundary = false;
        this.boundaryLen = 0;
        this.lastPart = false;
        this.completed = false;
        
        this.boundary = new byte[boundary.length() + 2];
        this.boundary[0] = (byte) '-';
        this.boundary[1] = (byte) '-';
        for (int i = 0; i < boundary.length(); i++) {
            byte ch = (byte) boundary.charAt(i);
            if (ch == '\r' || ch == '\n') {
                throw new IllegalArgumentException("Boundary may not contain CR or LF");
            }
            this.boundary[i + 2] = ch;
        }
        fillBuffer();
    }

    /**
     * Closes the underlying stream.
     * 
     * @throws IOException on I/O errors.
     */
    @Override
    public void close() throws IOException {
    }

    /**
     * @see java.io.InputStream#markSupported()
     */
    @Override
    public boolean markSupported() {
        return false;
    }

    /**
     * @see java.io.InputStream#read()
     */
    @Override
    public int read() throws IOException {
        if (completed) {
            return -1;
        }
        if (endOfStream() && !hasData()) {
            skipBoundary();            
            return -1;
        }
        for (;;) {
            if (hasData()) {
                return buffer.read();
            } else if (endOfStream()) {
                skipBoundary();            
                return -1;
            }
            fillBuffer();
        }
    }
    
    @Override
    public int read(byte[] b, int off, int len) throws IOException {
        if (completed) {
            return -1;
        }
        if (endOfStream() && !hasData()) {
            skipBoundary();            
            return -1;
        }
        fillBuffer();
        if (!hasData()) {
            return read(b, off, len);
        }
        int chunk = Math.min(len, limit - buffer.pos());
        return buffer.read(b, off, chunk);
    }

    @Override
    public int readLine(final ByteArrayBuffer dst) throws IOException {
        if (dst == null) {
            throw new IllegalArgumentException("Destination buffer may not be null");
        }
        if (completed) {
            return -1;
        }
        if (endOfStream() && !hasData()) {
            skipBoundary();            
            return -1;
        }

        int total = 0;
        boolean found = false;
        int bytesRead = 0;
        while (!found) {
            if (!hasData()) {
                bytesRead = fillBuffer();
                if (!hasData() && endOfStream()) {
                    skipBoundary();
                    bytesRead = -1;
                    break;
                }
            }
            int len = this.limit - this.buffer.pos();
            int i = this.buffer.indexOf((byte)'\n', this.buffer.pos(), len);
            int chunk;
            if (i != -1) {
                found = true;
                chunk = i + 1 - this.buffer.pos();
            } else {
                chunk = len;
            }
            if (chunk > 0) {
                dst.append(this.buffer.buf(), this.buffer.pos(), chunk);
                this.buffer.skip(chunk);
                total += chunk;
            }
        }
        if (total == 0 && bytesRead == -1) {
            return -1;
        } else {
            return total;
        }
    }
    
    private boolean endOfStream() {
        return eof || atBoundary;
    }
    
    private boolean hasData() {
        return limit > buffer.pos() && limit <= buffer.limit();
    }
    
    private int fillBuffer() throws IOException {
        if (eof) {
            return -1;
        }
        int bytesRead;
        if (!hasData()) {
            bytesRead = buffer.fillBuffer();
        } else {
            bytesRead = 0;
        }
        eof = bytesRead == -1;
        
        
        int i = buffer.indexOf(boundary);
        // NOTE this currently check only for LF. It doesn't check for canonical CRLF
        // and neither for isolated CR. This will require updates according to MIME4J-60
        while (i > 0 && buffer.charAt(i-1) != '\n') {
            // skip the "fake" boundary (it does not contain LF or CR so we cannot have
            // another boundary starting before this is complete.
            i = i + boundary.length;
            i = buffer.indexOf(boundary, i, buffer.limit() - i);
        }
        if (i != -1) {
            limit = i;
            atBoundary = true;
            calculateBoundaryLen();
        } else {
            if (eof) {
                limit = buffer.limit();
            } else {
                limit = buffer.limit() - (boundary.length + 1); 
                                          // \r\n + (boundary - one char)
            }
        }
        return bytesRead;
    }
    
    private void calculateBoundaryLen() throws IOException {
        boundaryLen = boundary.length;
        int len = limit - buffer.pos();
        if (len > 0) {
            if (buffer.charAt(limit - 1) == '\n') {
                boundaryLen++;
                limit--;
            }
        }
        if (len > 1) {
            if (buffer.charAt(limit - 1) == '\r') {
                boundaryLen++;
                limit--;
            }
        }
    }
    
    private void skipBoundary() throws IOException {
        if (!completed) {
            completed = true;
            buffer.skip(boundaryLen);
            boolean checkForLastPart = true;
            for (;;) {
                if (buffer.length() > 1) {
                    int ch1 = buffer.charAt(buffer.pos());
                    int ch2 = buffer.charAt(buffer.pos() + 1);
                    
                    if (checkForLastPart) if (ch1 == '-' && ch2 == '-') {
                        this.lastPart = true;
                        buffer.skip(2);
                        checkForLastPart = false;
                        continue;
                    }
                    
                    if (ch1 == '\r' && ch2 == '\n') {
                        buffer.skip(2);
                        break;
                    } else if (ch1 == '\n') {
                        buffer.skip(1);
                        break;
                    } else {
                        // ignoring everything in a line starting with a boundary.
                        buffer.skip(1);
                    }
                    
                } else {
                    if (eof) {
                        break;
                    }
                    fillBuffer();
                }
            }
        }
    }
    
    public boolean isLastPart() {
        return lastPart;        
    }
    
    public boolean eof() {
        return eof && !buffer.hasBufferedData();
    }

    @Override
    public String toString() {
        final StringBuilder buffer = new StringBuilder("MimeBoundaryInputStream, boundary ");
        for (byte b : boundary) {
            buffer.append((char) b);
        }
        return buffer.toString();
    }
}
