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

import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;

import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.MimeIOException;
import org.apache.james.mime4j.codec.DecodeMonitor;
import org.apache.james.mime4j.dom.Body;
import org.apache.james.mime4j.dom.Disposable;
import org.apache.james.mime4j.dom.Entity;
import org.apache.james.mime4j.dom.FieldParser;
import org.apache.james.mime4j.dom.Header;
import org.apache.james.mime4j.dom.Message;
import org.apache.james.mime4j.dom.MessageBuilder;
import org.apache.james.mime4j.dom.Multipart;
import org.apache.james.mime4j.dom.SingleBody;
import org.apache.james.mime4j.dom.field.ParsedField;
import org.apache.james.mime4j.field.DefaultFieldParser;
import org.apache.james.mime4j.field.LenientFieldParser;
import org.apache.james.mime4j.internal.ParserStreamContentHandler;
import org.apache.james.mime4j.parser.AbstractContentHandler;
import org.apache.james.mime4j.parser.MimeStreamParser;
import org.apache.james.mime4j.stream.BodyDescriptorBuilder;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
import org.apache.james.mime4j.stream.NameValuePair;

/**
 * Default implementation of {@link MessageBuilder}.
 */
public class DefaultMessageBuilder implements MessageBuilder {

    private FieldParser<? extends ParsedField> fieldParser = null;
    private MessageImplFactory messageImplFactory = null;
    private BodyFactory bodyFactory = null;
    private MimeConfig config = null;
    private BodyDescriptorBuilder bodyDescBuilder = null;
    private boolean contentDecoding = true;
    private boolean flatMode = false;
    private DecodeMonitor monitor = null;

    public DefaultMessageBuilder() {
        super();
    }

    public void setFieldParser(final FieldParser<? extends ParsedField> fieldParser) {
        this.fieldParser = fieldParser;
    }

    public void setMessageImplFactory(final MessageImplFactory messageImplFactory) {
        this.messageImplFactory = messageImplFactory;
    }

    public void setBodyFactory(final BodyFactory bodyFactory) {
        this.bodyFactory = bodyFactory;
    }

    public void setMimeEntityConfig(final MimeConfig config) {
        this.config = config;
    }

    public void setBodyDescriptorBuilder(final BodyDescriptorBuilder bodyDescBuilder) {
        this.bodyDescBuilder  = bodyDescBuilder;
    }

    public void setDecodeMonitor(final DecodeMonitor monitor) {
        this.monitor = monitor;
    }

    public void setContentDecoding(boolean contentDecoding) {
        this.contentDecoding = contentDecoding;
    }

    public void setFlatMode(boolean flatMode) {
        this.flatMode = flatMode;
    }

    /**
     * Creates a new <code>Header</code> from the specified
     * <code>Header</code>. The <code>Header</code> instance is initialized
     * with a copy of the list of {@link Field}s of the specified
     * <code>Header</code>. The <code>Field</code> objects are not copied
     * because they are immutable and can safely be shared between headers.
     *
     * @param other
     *            header to copy.
     */
    public Header copy(Header other) {
        HeaderImpl copy = new HeaderImpl();
        for (Field otherField : other.getFields()) {
            copy.addField(otherField);
        }
        return copy;
    }

    /**
     * Creates a new <code>BodyPart</code> from the specified
     * <code>Entity</code>. The <code>BodyPart</code> instance is initialized
     * with copies of header and body of the specified <code>Entity</code>.
     * The parent entity of the new body part is <code>null</code>.
     *
     * @param other
     *            body part to copy.
     * @throws UnsupportedOperationException
     *             if <code>other</code> contains a {@link SingleBody} that
     *             does not support the {@link SingleBody#copy() copy()}
     *             operation.
     * @throws IllegalArgumentException
     *             if <code>other</code> contains a <code>Body</code> that
     *             is neither a {@link Message}, {@link Multipart} or
     *             {@link SingleBody}.
     */
    public BodyPart copy(Entity other) {
        BodyPart copy = new BodyPart();
        if (other.getHeader() != null) {
            copy.setHeader(copy(other.getHeader()));
        }
        if (other.getBody() != null) {
            copy.setBody(copy(other.getBody()));
        }
        return copy;
    }

    /**
     * Creates a new <code>Multipart</code> from the specified
     * <code>Multipart</code>. The <code>Multipart</code> instance is
     * initialized with copies of preamble, epilogue, sub type and the list of
     * body parts of the specified <code>Multipart</code>. The parent entity
     * of the new multipart is <code>null</code>.
     *
     * @param other
     *            multipart to copy.
     * @throws UnsupportedOperationException
     *             if <code>other</code> contains a {@link SingleBody} that
     *             does not support the {@link SingleBody#copy() copy()}
     *             operation.
     * @throws IllegalArgumentException
     *             if <code>other</code> contains a <code>Body</code> that
     *             is neither a {@link Message}, {@link Multipart} or
     *             {@link SingleBody}.
     */
    public Multipart copy(Multipart other) {
        MultipartImpl copy = new MultipartImpl(other.getSubType(), other.getContentTypeParameters());
        for (Entity otherBodyPart : other.getBodyParts()) {
            copy.addBodyPart(copy(otherBodyPart));
        }
        copy.setPreamble(other.getPreamble());
        copy.setEpilogue(other.getEpilogue());
        return copy;
    }


    /**
     * Returns a copy of the given {@link Body} that can be used (and modified)
     * independently of the original. The copy should be
     * {@link Disposable#dispose() disposed of} when it is no longer needed.
     * <p>
     * The {@link Body#getParent() parent} of the returned copy is
     * <code>null</code>, that is, the copy is detached from the parent
     * entity of the original.
     *
     * @param body
     *            body to copy.
     * @return a copy of the given body.
     * @throws UnsupportedOperationException
     *             if <code>body</code> is an instance of {@link SingleBody}
     *             that does not support the {@link SingleBody#copy() copy()}
     *             operation (or contains such a <code>SingleBody</code>).
     * @throws IllegalArgumentException
     *             if <code>body</code> is <code>null</code> or
     *             <code>body</code> is a <code>Body</code> that is neither
     *             a {@link MessageImpl}, {@link Multipart} or {@link SingleBody}
     *             (or contains such a <code>Body</code>).
     */
    public Body copy(Body body) {
        if (body == null)
            throw new IllegalArgumentException("Body is null");

        if (body instanceof Message)
            return copy((Message) body);

        if (body instanceof Multipart)
            return copy((Multipart) body);

        if (body instanceof SingleBody)
            return ((SingleBody) body).copy();

        throw new IllegalArgumentException("Unsupported body class");
    }

    /**
     * Creates a new <code>Message</code> from the specified
     * <code>Message</code>. The <code>Message</code> instance is
     * initialized with copies of header and body of the specified
     * <code>Message</code>. The parent entity of the new message is
     * <code>null</code>.
     *
     * @param other
     *            message to copy.
     * @throws UnsupportedOperationException
     *             if <code>other</code> contains a {@link SingleBody} that
     *             does not support the {@link SingleBody#copy() copy()}
     *             operation.
     * @throws IllegalArgumentException
     *             if <code>other</code> contains a <code>Body</code> that
     *             is neither a {@link MessageImpl}, {@link Multipart} or
     *             {@link SingleBody}.
     */
    public Message copy(Message other) {
        MessageImpl copy = newMessageImpl();
        if (other.getHeader() != null) {
            copy.setHeader(copy(other.getHeader()));
        }
        if (other.getBody() != null) {
            copy.setBody(copy(other.getBody()));
        }
        return copy;
    }

    public Header newHeader() {
        return new HeaderImpl();
    }

    public Header newHeader(final Header source) {
        return copy(source);
    }

    public Multipart newMultipart(final String subType) {
        return new MultipartImpl(subType);
    }

    public Multipart newMultipart(final String subType, NameValuePair... contentTypeParameters) {
        return new MultipartImpl(subType, Arrays.asList(contentTypeParameters));
    }

    public Multipart newMultipart(final Multipart source) {
        return copy(source);
    }

    public Header parseHeader(final InputStream is) throws IOException, MimeIOException {
        final MimeConfig cfg = config != null ? config : MimeConfig.DEFAULT;
        boolean strict = cfg.isStrictParsing();
        final DecodeMonitor mon = monitor != null ? monitor :
            strict ? DecodeMonitor.STRICT : DecodeMonitor.SILENT;
        final FieldParser<? extends ParsedField> fp = fieldParser != null ? fieldParser :
            strict ? DefaultFieldParser.getParser() : LenientFieldParser.getParser();
        final HeaderImpl header = new HeaderImpl();
        final MimeStreamParser parser = new MimeStreamParser(cfg, mon, null);
        parser.setContentHandler(new AbstractContentHandler() {
            @Override
            public void endHeader() {
                parser.stop();
            }
            @Override
            public void field(Field field) throws MimeException {
                ParsedField parsedField;
                if (field instanceof ParsedField) {
                    parsedField = (ParsedField) field;
                } else {
                    parsedField = fp.parse(field, mon);
                }
                header.addField(parsedField);
            }
        });
        try {
            parser.parse(is);
        } catch (MimeException ex) {
            throw new MimeIOException(ex);
        }
        return header;
    }

    public Message newMessage() {
        return newMessageImpl();
    }

    public Message newMessage(final Message source) {
        return copy(source);
    }

    public Message parseMessage(final InputStream is) throws IOException, MimeIOException {
        try {
            MessageImpl message = newMessageImpl();
            MimeConfig cfg = config != null ? config : MimeConfig.DEFAULT;
            boolean strict = cfg.isStrictParsing();
            DecodeMonitor mon = monitor != null ? monitor :
                strict ? DecodeMonitor.STRICT : DecodeMonitor.SILENT;
            BodyDescriptorBuilder bdb = bodyDescBuilder != null ? bodyDescBuilder :
                new DefaultBodyDescriptorBuilder(null, fieldParser != null ? fieldParser :
                    strict ? DefaultFieldParser.getParser() : LenientFieldParser.getParser(), mon);
            BodyFactory bf = bodyFactory != null ? bodyFactory : new BasicBodyFactory(!strict);
            MimeStreamParser parser = new MimeStreamParser(cfg, mon, bdb);
            parser.setContentHandler(new ParserStreamContentHandler(message, bf));
            parser.setContentDecoding(contentDecoding);
            if (flatMode) {
                parser.setFlat();
            } else {
                parser.setRecurse();
            }
            parser.parse(is);
            parser.stop();
            return message;
        } catch (MimeException e) {
            throw new MimeIOException(e);
        }
    }

    private MessageImpl newMessageImpl() {
        MessageImplFactory mif = messageImplFactory != null ? messageImplFactory : new DefaultMessageImplFactory();
        return mif.messageImpl();
    }

    public Multipart newMultipart(MultipartBuilder source) {
        return newMultipart(source.build());
    }

    public Message newMessage(Message.Builder source) {
        return newMessage(source.build());
    }
}
