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

import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.axiom.blob.MemoryBlob;
import org.apache.axiom.blob.WritableBlobFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.stream.EntityState;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
import org.apache.james.mime4j.stream.MimeTokenStream;
import org.apache.james.mime4j.stream.RecursionMode;

/**
 * A MIME multipart message read from a stream. This class exposes an API that represents the
 * message as a sequence of {@link Part} instances. It implements the {@link Iterable} interface to
 * enable access to all parts. In addition, it supports lookup by content ID. Data is read from the
 * stream on demand. This means that the stream must be kept open until all parts have been
 * processed or {@link #detach()} has been called. It also means that any invocation of a method on
 * an instance of this class or an individual {@link Part} instance may trigger a
 * {@link MIMEException} if there is an I/O error on the stream or a MIME parsing error.
 * <p>
 * Instances of this class are created using a fluent builder; see {@link #builder()}.
 */
public final class MultipartBody implements Iterable<Part> {
    public interface PartCreationListener {
        void partCreated(Part part);
    }

    public final static class Builder {
        private InputStream inputStream;
        private ContentType contentType;
        private WritableBlobFactory<?> attachmentBlobFactory;
        private PartBlobFactory partBlobFactory;
        private PartCreationListener partCreationListener;
        
        Builder() {}

        public Builder setInputStream(InputStream inputStream) {
            this.inputStream = inputStream;
            return this;
        }

        public Builder setContentType(ContentType contentType) {
            this.contentType = contentType;
            return this;
        }

        public Builder setContentType(String contentType) {
            try {
                this.contentType = new ContentType(contentType);
            } catch (ParseException ex) {
                throw new MIMEException(ex);
            }
            return this;
        }

        public Builder setAttachmentBlobFactory(WritableBlobFactory<?> attachmentBlobFactory) {
            this.attachmentBlobFactory = attachmentBlobFactory;
            return this;
        }

        public Builder setPartBlobFactory(PartBlobFactory partBlobFactory) {
            this.partBlobFactory = partBlobFactory;
            return this;
        }

        public Builder setPartCreationListener(PartCreationListener partCreationListener) {
            this.partCreationListener = partCreationListener;
            return this;
        }

        public MultipartBody build() {
            if (inputStream == null) {
                throw new IllegalArgumentException("inputStream is mandatory");
            }
            if (contentType == null) {
                throw new IllegalArgumentException("contentType is mandatory");
            }
            return new MultipartBody(
                    inputStream,
                    contentType,
                    attachmentBlobFactory == null ? MemoryBlob.FACTORY : attachmentBlobFactory,
                    partBlobFactory == null ? PartBlobFactory.DEFAULT : partBlobFactory,
                    partCreationListener);
        }
    }

    private static final Log log = LogFactory.getLog(MultipartBody.class);
    
    private static final MimeConfig config = MimeConfig.custom().setStrictParsing(true).build();
    
    /** <code>ContentType</code> of the MIME message */
    private final ContentType contentType;
    private final String rootPartContentID;
    private final MimeTokenStream parser;
    
    /**
     * Stores the already parsed MIME parts by Content IDs.
     */
    private final Map<String,PartImpl> partMap = new HashMap<String,PartImpl>();

    /**
     * The MIME part currently being processed.
     */
    private PartImpl currentPart;
    
    private PartImpl firstPart;
    private PartImpl rootPart;

    private int partCount;

    private final WritableBlobFactory<?> attachmentBlobFactory;
    private final PartBlobFactory partBlobFactory;
    private final PartCreationListener partCreationListener;
    
    MultipartBody(InputStream inStream, ContentType contentType,
            WritableBlobFactory<?> attachmentBlobFactory,
            PartBlobFactory partBlobFactory,
            PartCreationListener partCreationListener) {
        this.attachmentBlobFactory = attachmentBlobFactory;
        this.partBlobFactory = partBlobFactory;
        this.partCreationListener = partCreationListener;
        this.contentType = contentType;

        String start = contentType.getParameter("start");
        rootPartContentID = start == null ? null : normalizeContentID(start);

        parser = new MimeTokenStream(config);
        parser.setRecursionMode(RecursionMode.M_NO_RECURSE);
        parser.parseHeadless(inStream, contentType.toString());
        
        // Move the parser to the beginning of the first part
        while (parser.getState() != EntityState.T_START_BODYPART) {
            try {
                parser.next();
            } catch (IOException ex) {
                throw new MIMEException(ex);
            } catch (MimeException ex) {
                throw new MIMEException(ex);
            }
        }
    }

    public static Builder builder() {
        return new Builder();
    }

    private static String normalizeContentID(String contentID) {
        contentID = contentID.trim();
        if (contentID.length() >= 2 && contentID.charAt(0) == '<'
                && contentID.charAt(contentID.length()-1) == '>') {
            contentID = contentID.substring(1, contentID.length()-1);
        }
        // There is some evidence that some broken MIME implementations add
        // a "cid:" prefix to the Content-ID; remove it if necessary.
        if (contentID.length() > 4 && contentID.startsWith("cid:")) {
            contentID = contentID.substring(4);
        }
        return contentID;
    }

    PartBlobFactory getPartBlobFactory() {
        return partBlobFactory;
    }

    public ContentType getContentType() {
        return contentType;
    }

    /**
     * Get the MIME part with the given content ID.
     * 
     * @param contentID
     *            the content ID of the part to retrieve
     * @return the MIME part, or {@code null} if the message doesn't have a part with the given
     *         content ID
     */
    public Part getPart(String contentID) {
        do {
            PartImpl part = partMap.get(contentID);
            if (part != null) {
                return part;
            }
        } while (getNextPart() != null);
        return null;
    }

    /**
     * Get the number of parts in this multipart.
     * 
     * @return the number of parts
     */
    public int getPartCount() {
        detach();
        return partCount;
    }

    PartImpl getFirstPart() {
        if (firstPart == null) {
            getNextPart();
        }
        return firstPart;
    }

    public Part getRootPart() {
        do {
            if (rootPart != null) {
                return rootPart;
            }
        } while (getNextPart() != null);
        throw new MIMEException(
                "Mandatory root MIME part is missing");
    }

    PartImpl getNextPart() {
        if (currentPart != null) {
            currentPart.fetch();
        }
        if (parser.getState() == EntityState.T_END_MULTIPART) {
            currentPart = null;
        } else {
            String partContentID = null;
            boolean isRootPart;

            try {
                checkParserState(parser.next(), EntityState.T_START_HEADER);
                
                List<Header> headers = new ArrayList<Header>();
                while (parser.next() == EntityState.T_FIELD) {
                    Field field = parser.getField();
                    String name = field.getName();
                    String value = field.getBody();
                    
                    if (log.isDebugEnabled()){
                        log.debug("addHeader: (" + name + ") value=(" + value +")");
                    }
                    headers.add(new Header(name, value));
                    if (partContentID == null && name.equalsIgnoreCase("Content-ID")) {
                        partContentID = normalizeContentID(value);
                    }
                }
                
                checkParserState(parser.next(), EntityState.T_BODY);
                
                if (rootPartContentID == null) {
                    isRootPart = firstPart == null;
                } else {
                    isRootPart = rootPartContentID.equals(partContentID);
                }
                
                PartImpl part = new PartImpl(this, isRootPart ? MemoryBlob.FACTORY : attachmentBlobFactory, partContentID, headers, parser);
                if (currentPart == null) {
                    firstPart = part;
                } else {
                    currentPart.setNextPart(part);
                }
                currentPart = part;
            } catch (IOException ex) {
                throw new MIMEException(ex);
            } catch (MimeException ex) {
                throw new MIMEException(ex);
            }

            partCount++;
            if (partContentID != null) {
                if (partMap.containsKey(partContentID)) {
                    throw new MIMEException(
                            "Two MIME parts with the same Content-ID not allowed.");
                }
                partMap.put(partContentID, currentPart);
            }
            if (isRootPart) {
                rootPart = currentPart;
            }
            if (partCreationListener != null) {
                partCreationListener.partCreated(currentPart);
            }
        }
        return currentPart;
    }

    private static void checkParserState(EntityState state, EntityState expected) throws IllegalStateException {
        if (expected != state) {
            throw new IllegalStateException("Internal error: expected parser to be in state "
                    + expected + ", but got " + state);
        }
    }

    @Override
    public Iterator<Part> iterator() {
        return new PartIterator(this);
    }

    public void detach() {
        while (getNextPart() != null) {
            // Just loop
        }
    }
}
