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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.descriptor.BodyDescriptor;
import org.apache.james.mime4j.descriptor.DefaultBodyDescriptor;
import org.apache.james.mime4j.descriptor.MaximalBodyDescriptor;
import org.apache.james.mime4j.descriptor.MutableBodyDescriptor;
import org.apache.james.mime4j.io.LineReaderInputStream;
import org.apache.james.mime4j.io.MaxHeaderLimitException;
import org.apache.james.mime4j.io.MaxLineLimitException;
import org.apache.james.mime4j.util.ByteArrayBuffer;
import org.apache.james.mime4j.util.CharsetUtil;

/**
 * Abstract MIME entity.
 */
public abstract class AbstractEntity implements EntityStateMachine {

    protected final Log log;
    
    protected final BodyDescriptor parent;
    protected final int startState;
    protected final int endState;
    protected final MimeEntityConfig config;
    protected final MutableBodyDescriptor body;
    
    protected int state;

    private final ByteArrayBuffer linebuf;

    private int lineCount;
    private Field field;
    private boolean endOfHeader;
    private int headerCount;

    private static final BitSet fieldChars = new BitSet();

    static {
        for (int i = 0x21; i <= 0x39; i++) {
            fieldChars.set(i);
        }
        for (int i = 0x3b; i <= 0x7e; i++) {
            fieldChars.set(i);
        }
    }

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

    AbstractEntity(
            BodyDescriptor parent,
            int startState, 
            int endState,
            MimeEntityConfig config) {
        this.log = LogFactory.getLog(getClass());        
        this.parent = parent;
        this.state = startState;
        this.startState = startState;
        this.endState = endState;
        this.config = config;
        this.body = newBodyDescriptor(parent);
        this.linebuf = new ByteArrayBuffer(64);
        this.lineCount = 0;
        this.endOfHeader = false;
        this.headerCount = 0;
    }

    public int getState() {
        return state;
    }
    
    /**
     * Creates a new instance of {@link BodyDescriptor}. Subclasses may override
     * this in order to create body descriptors, that provide more specific
     * information.
     */
    protected MutableBodyDescriptor newBodyDescriptor(BodyDescriptor pParent) {
        final MutableBodyDescriptor result;
        if (config.isMaximalBodyDescriptor()) {
            result = new MaximalBodyDescriptor(pParent);
        } else {
            result = new DefaultBodyDescriptor(pParent);
        }
        return result;
    }

    /**
     * Returns the current line number or <code>-1</code> if line number
     * information is not available.
     */
    protected abstract int getLineNumber();
    
    protected abstract LineReaderInputStream getDataStream();
    
    private ByteArrayBuffer fillFieldBuffer() throws IOException, MimeException {
        if (endOfHeader) 
            throw new IllegalStateException();

        int maxLineLen = config.getMaxLineLen();
        LineReaderInputStream instream = getDataStream();
        ByteArrayBuffer fieldbuf = new ByteArrayBuffer(64);

        for (;;) {
            // If there's still data stuck in the line buffer
            // copy it to the field buffer
            int len = linebuf.length();
            if (maxLineLen > 0 && fieldbuf.length() + len >= maxLineLen) {
                throw new MaxLineLimitException("Maximum line length limit exceeded");
            }
            if (len > 0) {
                fieldbuf.append(linebuf.buffer(), 0, len);
            }
            linebuf.clear();
            if (instream.readLine(linebuf) == -1) {
                monitor(Event.HEADERS_PREMATURE_END);
                endOfHeader = true;
                break;
            }
            len = linebuf.length();
            if (len > 0 && linebuf.byteAt(len - 1) == '\n') {
                len--;
            }
            if (len > 0 && linebuf.byteAt(len - 1) == '\r') {
                len--;
            }
            if (len == 0) {
                // empty line detected 
                endOfHeader = true;
                break;
            }
            lineCount++;
            if (lineCount > 1) {
                int ch = linebuf.byteAt(0);
                if (ch != CharsetUtil.SP && ch != CharsetUtil.HT) {
                    // new header detected
                    break;
                }
            }
        }

        return fieldbuf;
    }

    protected boolean parseField() throws MimeException, IOException {
        int maxHeaderLimit = config.getMaxHeaderCount();
        for (;;) {
            if (endOfHeader) {
                return false;
            }
            if (headerCount >= maxHeaderLimit) {
                throw new MaxHeaderLimitException("Maximum header limit exceeded");
            }

            ByteArrayBuffer fieldbuf = fillFieldBuffer();
            headerCount++;

            // Strip away line delimiter
            int len = fieldbuf.length();
            if (len > 0 && fieldbuf.byteAt(len - 1) == '\n') {
                len--;
            }
            if (len > 0 && fieldbuf.byteAt(len - 1) == '\r') {
                len--;
            }
            fieldbuf.setLength(len);
            
            boolean valid = true;
            
            int pos = fieldbuf.indexOf((byte) ':');
            if (pos <= 0) {
                monitor(Event.INALID_HEADER);
                valid = false;
            } else {
                for (int i = 0; i < pos; i++) {
                    if (!fieldChars.get(fieldbuf.byteAt(i) & 0xff)) {
                        monitor(Event.INALID_HEADER);
                        valid = false;
                        break;
                    }
                }
            }
            if (valid) {
                field = new RawField(fieldbuf, pos);
                body.addField(field);            
                return true;
            }
        }
    }

    /**
     * <p>Gets a descriptor for the current entity.
     * This method is valid if {@link #getState()} returns:</p>
     * <ul>
     * <li>{@link EntityStates#T_BODY}</li>
     * <li>{@link EntityStates#T_START_MULTIPART}</li>
     * <li>{@link EntityStates#T_EPILOGUE}</li>
     * <li>{@link EntityStates#T_PREAMBLE}</li>
     * </ul>
     * @return <code>BodyDescriptor</code>, not nulls
     */
    public BodyDescriptor getBodyDescriptor() {
        switch (getState()) {
        case EntityStates.T_BODY:
        case EntityStates.T_START_MULTIPART:
        case EntityStates.T_PREAMBLE:
        case EntityStates.T_EPILOGUE:
        case EntityStates.T_END_OF_STREAM:
            return body;
        default:
            throw new IllegalStateException("Invalid state :" + stateToString(state));
        }
    }

    /**
     * This method is valid, if {@link #getState()} returns {@link EntityStates#T_FIELD}.
     * @return String with the fields raw contents.
     * @throws IllegalStateException {@link #getState()} returns another
     *   value than {@link EntityStates#T_FIELD}.
     */
    public Field getField() {
        switch (getState()) {
        case EntityStates.T_FIELD:
            return field;
        default:
            throw new IllegalStateException("Invalid state :" + stateToString(state));
        }
    }

    /**
     * Monitors the given event.
     * Subclasses may override to perform actions upon events.
     * Base implementation logs at warn.
     * @param event <code>Event</code>, not null
     * @throws MimeException subclasses may elect to throw this exception upon
     * invalid content
     * @throws IOException subclasses may elect to throw this exception
     */
    protected void monitor(Event event) throws MimeException, IOException {
        if (config.isStrictParsing()) {
            throw new MimeParseEventException(event);
        } else {
            warn(event);
        }
    }
    
    /**
     * Creates an indicative message suitable for display
     * based on the given event and the current state of the system.
     * @param event <code>Event</code>, not null
     * @return message suitable for use as a message in an exception
     * or for logging
     */
    protected String message(Event event) {
        final String message;
        if (event == null) {
            message = "Event is unexpectedly null.";
        } else {
            message = event.toString();
        }

        int lineNumber = getLineNumber();
        if (lineNumber <= 0)
            return message;
        else
            return "Line " + lineNumber + ": " + message;
    }
    
    /**
     * Logs (at warn) an indicative message based on the given event 
     * and the current state of the system.
     * @param event <code>Event</code>, not null
     */
    protected void warn(Event event) {
        if (log.isWarnEnabled()) {
            log.warn(message(event));
        }
    }
    
    /**
     * Logs (at debug) an indicative message based on the given event
     * and the current state of the system.
     * @param event <code>Event</code>, not null
     */
    protected void debug(Event event) {
        if (log.isDebugEnabled()) {
            log.debug(message(event));
        }
    }

    @Override
    public String toString() {
        return getClass().getName() + " [" + stateToString(state)
        + "][" + body.getMimeType() + "][" + body.getBoundary() + "]";
    }

    /**
     * Renders a state as a string suitable for logging.
     * @param state 
     * @return rendered as string, not null
     */
    public static final String stateToString(int state) {
        final String result;
        switch (state) {
            case EntityStates.T_END_OF_STREAM:
                result = "End of stream";
                break;
            case EntityStates.T_START_MESSAGE:
                result = "Start message";
                break;
            case EntityStates.T_END_MESSAGE:
                result = "End message";
                break;
            case EntityStates.T_RAW_ENTITY:
                result = "Raw entity";
                break;
            case EntityStates.T_START_HEADER:
                result = "Start header";
                break;
            case EntityStates.T_FIELD:
                result = "Field";
                break;
            case EntityStates.T_END_HEADER:
                result = "End header";
                break;
            case EntityStates.T_START_MULTIPART:
                result = "Start multipart";
                break;
            case EntityStates.T_END_MULTIPART:
                result = "End multipart";
                break;
            case EntityStates.T_PREAMBLE:
                result = "Preamble";
                break;
            case EntityStates.T_EPILOGUE:
                result = "Epilogue";
                break;
            case EntityStates.T_START_BODYPART:
                result = "Start bodypart";
                break;
            case EntityStates.T_END_BODYPART:
                result = "End bodypart";
                break;
            case EntityStates.T_BODY:
                result = "Body";
                break;
            case T_IN_BODYPART:
                result = "Bodypart";
                break;
            case T_IN_MESSAGE:
                result = "In message";
                break;
            default:
                result = "Unknown";
                break;
        }
        return result;
    }
    
}
