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

import com.google.common.collect.ImmutableList;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.stream.EntityState;
import org.apache.james.mime4j.stream.MimeConfig;
import org.apache.james.mime4j.stream.MimeTokenStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;
import java.nio.CharBuffer;
import java.nio.charset.IllegalCharsetNameException;
import java.nio.charset.UnsupportedCharsetException;
import java.util.List;

/**
 * Searches an email for content.
 */
public class MessageMatcher {

    public static class MessageMatcherBuilder {

        private List<CharSequence> searchContents;
        private List<String> contentTypes;
        private boolean isCaseInsensitive;
        private boolean includeHeaders;
        private boolean ignoringMime;
        private Logger logger;

        public MessageMatcherBuilder() {
            this.searchContents = ImmutableList.of();
            this.contentTypes = ImmutableList.of();
            this.isCaseInsensitive = false;
            this.includeHeaders = false;
            this.ignoringMime = false;
            this.logger = LoggerFactory.getLogger(MessageMatcher.class);
        }

        public MessageMatcherBuilder searchContents(List<CharSequence> searchContents) {
            this.searchContents = searchContents;
            return this;
        }

        public MessageMatcherBuilder contentTypes(List<String> contentTypes) {
            this.contentTypes = contentTypes;
            return this;
        }

        public MessageMatcherBuilder caseInsensitive(boolean isCaseInsensitive) {
            this.isCaseInsensitive = isCaseInsensitive;
            return this;
        }

        public MessageMatcherBuilder includeHeaders(boolean includeHeaders) {
            this.includeHeaders = includeHeaders;
            return this;
        }

        public MessageMatcherBuilder logger(Logger logger) {
            this.logger = logger;
            return this;
        }

        public MessageMatcherBuilder ignoringMime(boolean ignoringMime) {
            this.ignoringMime = ignoringMime;
            return this;
        }

        public MessageMatcher build() {
            return new MessageMatcher(searchContents, isCaseInsensitive, includeHeaders, ignoringMime, contentTypes, logger);
        }

    }

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

    private final Logger logger;
    private final List<CharSequence> searchContents;
    private final List<String> contentTypes;
    private final boolean isCaseInsensitive;
    private final boolean includeHeaders;
    private final boolean ignoringMime;

    private MessageMatcher(List<CharSequence> searchContents, boolean isCaseInsensitive, boolean includeHeaders,
                           boolean ignoringMime, List<String> contentTypes, Logger logger) {
        this.contentTypes = ImmutableList.copyOf(contentTypes);
        this.searchContents = ImmutableList.copyOf(searchContents);
        this.isCaseInsensitive = isCaseInsensitive;
        this.includeHeaders = includeHeaders;
        this.ignoringMime = ignoringMime;
        this.logger = logger;
    }

    /**
     * Is searchContents found in the given input?
     *
     * @param input
     *            <code>InputStream</code> containing an email
     * @return true if the content exists and the stream contains the content,
     *         false otherwise. It takes the mime structure into account.
     * @throws IOException
     * @throws MimeException
     */
    public boolean messageMatches(final InputStream input) throws IOException, MimeException {
        for (CharSequence charSequence : searchContents) {
            if (charSequence != null) {
                final CharBuffer buffer = createBuffer(charSequence);
                if (ignoringMime) {
                    if (! isFoundIn(new InputStreamReader(input), buffer)) {
                        return false;
                    }
                } else {
                    if (!matchBufferInMailBeingMimeAware(input, buffer)) {
                        return false;
                    }
                }
            }
        }
        return true;
    }

    private boolean matchBufferInMailBeingMimeAware(final InputStream input, final CharBuffer buffer) throws IOException, MimeException {
        try {
            MimeConfig config = MimeConfig.custom().setMaxLineLen(-1).setMaxHeaderLen(-1).build();

            MimeTokenStream parser = new MimeTokenStream(config);
            parser.parse(input);
            while (parser.next() != EntityState.T_END_OF_STREAM) {
                final EntityState state = parser.getState();
                switch (state) {
                    case T_PREAMBLE:
                    case T_EPILOGUE:
                    case T_BODY:
                        if (contentTypes.isEmpty() || contentTypes.contains(parser.getBodyDescriptor().getMimeType())) {
                            if (checkBody(buffer, parser)) {
                                return true;
                            }
                        }
                        break;
                    case T_FIELD:
                        if (includeHeaders) {
                            if (checkHeader(buffer, parser)) {
                                return true;
                            }
                        }
                        break;
                case T_END_BODYPART:
                case T_END_HEADER:
                case T_END_MESSAGE:
                case T_END_MULTIPART:
                case T_END_OF_STREAM:
                case T_RAW_ENTITY:
                case T_START_BODYPART:
                case T_START_HEADER:
                case T_START_MESSAGE:
                case T_START_MULTIPART:
                    break;
                }
            }
        } catch (IllegalCharsetNameException e) {
            handle(e);
        } catch (UnsupportedCharsetException e) {
            handle(e);
        } catch (IllegalStateException e) {
            handle(e);
        }
        return false;
    }

    private boolean checkHeader(final CharBuffer buffer, MimeTokenStream parser) throws IOException {
        final String value = parser.getField().getBody();
        final StringReader reader = new StringReader(value);
        return isFoundIn(reader, buffer);
    }

    private boolean checkBody(final CharBuffer buffer, MimeTokenStream parser) throws IOException {
        try (Reader reader = parser.getReader()) {
            return isFoundIn(reader, buffer);
        }
    }

    private CharBuffer createBuffer(final CharSequence searchContent) {
        final CharBuffer buffer;
        if (isCaseInsensitive) {
            final int length = searchContent.length();
            buffer = CharBuffer.allocate(length);
            for (int i = 0; i < length; i++) {
                final char next = searchContent.charAt(i);
                final char upperCase = Character.toUpperCase(next);
                buffer.put(upperCase);
            }
            buffer.flip();
        } else {
            buffer = CharBuffer.wrap(searchContent);
        }
        return buffer;
    }

    protected void handle(Exception e) throws IOException, MimeException {
        logger.warn("Cannot read MIME body.");
        logger.debug("Failed to read body.", e);
    }

    public boolean isFoundIn(final Reader reader, final CharBuffer buffer) throws IOException {
        int read;
        while ((read = reader.read()) != -1) {
            if (matches(buffer, computeNextChar(isCaseInsensitive, (char) read))) {
                return true;
            }
        }
        return false;
    }

    private char computeNextChar(boolean isCaseInsensitive, char read) {
        if (isCaseInsensitive) {
            return Character.toUpperCase(read);
        } else {
            return read;
        }
    }

    private boolean matches(final CharBuffer buffer, final char next) {
        if (buffer.hasRemaining()) {
            final boolean partialMatch = (buffer.position() > 0);
            final char matching = buffer.get();
            if (next != matching) {
                buffer.rewind();
                if (partialMatch) {
                    return matches(buffer, next);
                }
            }
        } else {
            return true;
        }
        return false;
    }

}
