/****************************************************************
 * 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.parser;

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

import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.codec.Base64InputStream;
import org.apache.james.mime4j.codec.QuotedPrintableInputStream;
import org.apache.james.mime4j.descriptor.BodyDescriptor;
import org.apache.james.mime4j.io.BufferedLineReaderInputStream;
import org.apache.james.mime4j.io.LimitedInputStream;
import org.apache.james.mime4j.io.LineNumberSource;
import org.apache.james.mime4j.io.LineReaderInputStream;
import org.apache.james.mime4j.io.LineReaderInputStreamAdaptor;
import org.apache.james.mime4j.io.MimeBoundaryInputStream;
import org.apache.james.mime4j.util.ByteSequence;
import org.apache.james.mime4j.util.ContentUtil;
import org.apache.james.mime4j.util.MimeUtil;

public class MimeEntity extends AbstractEntity {

    /**
     * Internal state, not exposed.
     */
    private static final int T_IN_BODYPART = -2;
    /**
     * Internal state, not exposed.
     */
    private static final int T_IN_MESSAGE = -3;

    private final LineNumberSource lineSource;
    private final BufferedLineReaderInputStream inbuffer;
    
    private int recursionMode;
    private MimeBoundaryInputStream mimeStream;
    private LineReaderInputStreamAdaptor dataStream;
    private boolean skipHeader;
    
    private byte[] tmpbuf;
    
    public MimeEntity(
            LineNumberSource lineSource,
            BufferedLineReaderInputStream inbuffer,
            BodyDescriptor parent, 
            int startState, 
            int endState,
            MimeEntityConfig config) {
        super(parent, startState, endState, config);
        this.lineSource = lineSource;
        this.inbuffer = inbuffer;
        this.dataStream = new LineReaderInputStreamAdaptor(
                inbuffer,
                config.getMaxLineLen());
        this.skipHeader = false;
    }

    public MimeEntity(
            LineNumberSource lineSource,
            BufferedLineReaderInputStream inbuffer,
            BodyDescriptor parent, 
            int startState, 
            int endState) {
        this(lineSource, inbuffer, parent, startState, endState, 
                new MimeEntityConfig());
    }

    public int getRecursionMode() {
        return recursionMode;
    }

    public void setRecursionMode(int recursionMode) {
        this.recursionMode = recursionMode;
    }

    public void skipHeader(String contentType) {
        if (state != EntityStates.T_START_MESSAGE) {
            throw new IllegalStateException("Invalid state: " + stateToString(state));
        }
        skipHeader = true;
        ByteSequence raw = ContentUtil.encode("Content-Type: " + contentType);
        body.addField(new RawField(raw, 12));
    }

    @Override
    protected int getLineNumber() {
        if (lineSource == null)
            return -1;
        else
            return lineSource.getLineNumber();
    }
    
    @Override
    protected LineReaderInputStream getDataStream() {
        return dataStream;
    }
    
    public EntityStateMachine advance() throws IOException, MimeException {
        switch (state) {
        case EntityStates.T_START_MESSAGE:
            if (skipHeader) {
                state = EntityStates.T_END_HEADER;
            } else {
                state = EntityStates.T_START_HEADER;
            }
            break;
        case EntityStates.T_START_BODYPART:
            state = EntityStates.T_START_HEADER;
            break;
        case EntityStates.T_START_HEADER:
        case EntityStates.T_FIELD:
            state = parseField() ? EntityStates.T_FIELD : EntityStates.T_END_HEADER;
            break;
        case EntityStates.T_END_HEADER:
            String mimeType = body.getMimeType();
            if (recursionMode == RecursionMode.M_FLAT) {
                state = EntityStates.T_BODY;
            } else if (MimeUtil.isMultipart(mimeType)) {
                state = EntityStates.T_START_MULTIPART;
                clearMimeStream();
            } else if (recursionMode != RecursionMode.M_NO_RECURSE 
                    && MimeUtil.isMessage(mimeType)) {
                state = T_IN_MESSAGE;
                return nextMessage();
            } else {
                state = EntityStates.T_BODY;
            }
            break;
        case EntityStates.T_START_MULTIPART:
            if (dataStream.isUsed()) {
                advanceToBoundary();            
                state = EntityStates.T_END_MULTIPART;
            } else {
                createMimeStream();
                state = EntityStates.T_PREAMBLE;
            }
            break;
        case EntityStates.T_PREAMBLE:
            advanceToBoundary();            
            if (mimeStream.isLastPart()) {
                clearMimeStream();
                state = EntityStates.T_END_MULTIPART;
            } else {
                clearMimeStream();
                createMimeStream();
                state = T_IN_BODYPART;
                return nextMimeEntity();
            }
            break;
        case T_IN_BODYPART:
            advanceToBoundary();
            if (mimeStream.eof() && !mimeStream.isLastPart()) {
                monitor(Event.MIME_BODY_PREMATURE_END);
            } else {
                if (!mimeStream.isLastPart()) {
                    clearMimeStream();
                    createMimeStream();
                    state = T_IN_BODYPART;
                    return nextMimeEntity();
                }
            }
            clearMimeStream();
            state = EntityStates.T_EPILOGUE;
            break;
        case EntityStates.T_EPILOGUE:
            state = EntityStates.T_END_MULTIPART;
            break;
        case EntityStates.T_BODY:
        case EntityStates.T_END_MULTIPART:
        case T_IN_MESSAGE:
            state = endState;
            break;
        default:
            if (state == endState) {
                state = EntityStates.T_END_OF_STREAM;
                break;
            }
            throw new IllegalStateException("Invalid state: " + stateToString(state));
        }
        return null;
    }

    private void createMimeStream() throws MimeException, IOException {
        String boundary = body.getBoundary();
        int bufferSize = 2 * boundary.length();
        if (bufferSize < 4096) {
            bufferSize = 4096;
        }
        try {
            if (mimeStream != null) {
                mimeStream = new MimeBoundaryInputStream(
                        new BufferedLineReaderInputStream(
                                mimeStream, 
                                bufferSize, 
                                config.getMaxLineLen()), 
                        boundary);
            } else {
                inbuffer.ensureCapacity(bufferSize);
                mimeStream = new MimeBoundaryInputStream(inbuffer, boundary);
            }
        } catch (IllegalArgumentException e) {
            // thrown when boundary is too long
            throw new MimeException(e.getMessage(), e);
        }
        dataStream = new LineReaderInputStreamAdaptor(
                mimeStream,
                config.getMaxLineLen()); 
    }
    
    private void clearMimeStream() {
        mimeStream = null;
        dataStream = new LineReaderInputStreamAdaptor(
                inbuffer,
                config.getMaxLineLen()); 
    }
    
    private void advanceToBoundary() throws IOException {
        if (!dataStream.eof()) {
            if (tmpbuf == null) {
                tmpbuf = new byte[2048];
            }
            InputStream instream = getLimitedContentStream();
            while (instream.read(tmpbuf)!= -1) {
            }
        }
    }
    
    private EntityStateMachine nextMessage() {
        String transferEncoding = body.getTransferEncoding();
        InputStream instream;
        if (MimeUtil.isBase64Encoding(transferEncoding)) {
            log.debug("base64 encoded message/rfc822 detected");
            instream = new Base64InputStream(dataStream);                    
        } else if (MimeUtil.isQuotedPrintableEncoded(transferEncoding)) {
            log.debug("quoted-printable encoded message/rfc822 detected");
            instream = new QuotedPrintableInputStream(dataStream);                    
        } else {
            instream = dataStream;
        }
        
        if (recursionMode == RecursionMode.M_RAW) {
            RawEntity message = new RawEntity(instream);
            return message;
        } else {
            MimeEntity message = new MimeEntity(
                    lineSource, 
                    new BufferedLineReaderInputStream(
                            instream, 
                            4 * 1024,
                            config.getMaxLineLen()),
                    body, 
                    EntityStates.T_START_MESSAGE, 
                    EntityStates.T_END_MESSAGE,
                    config);
            message.setRecursionMode(recursionMode);
            return message;
        }
    }
    
    private EntityStateMachine nextMimeEntity() {
        if (recursionMode == RecursionMode.M_RAW) {
            RawEntity message = new RawEntity(mimeStream);
            return message;
        } else {
            BufferedLineReaderInputStream stream = new BufferedLineReaderInputStream(
                    mimeStream, 
                    4 * 1024,
                    config.getMaxLineLen());
            MimeEntity mimeentity = new MimeEntity(
                    lineSource, 
                    stream,
                    body, 
                    EntityStates.T_START_BODYPART, 
                    EntityStates.T_END_BODYPART,
                    config);
            mimeentity.setRecursionMode(recursionMode);
            return mimeentity;
        }
    }
    
    private InputStream getLimitedContentStream() {
        long maxContentLimit = config.getMaxContentLen();
        if (maxContentLimit >= 0) {
            return new LimitedInputStream(dataStream, maxContentLimit);
        } else {
            return dataStream;
        }
    }
    
    public InputStream getContentStream() {
        switch (state) {
        case EntityStates.T_START_MULTIPART:
        case EntityStates.T_PREAMBLE:
        case EntityStates.T_EPILOGUE:
        case EntityStates.T_BODY:
            return getLimitedContentStream();
        default:
            throw new IllegalStateException("Invalid state: " + stateToString(state));
        }
    }

}
