/**
 * GmailAssistant 1.1 (2008-03-16)
 * Copyright 2008 Zach Scrivena
 * zachscrivena@gmail.com
 * http://gmailassistant.sourceforge.net/
 *
 * Notifier for multiple Gmail accounts.
 *
 * TERMS AND CONDITIONS:
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package gmailassistant;

import java.util.ArrayDeque;
import java.util.Date;
import java.util.Deque;
import java.util.regex.Pattern;
import javax.mail.Address;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Multipart;
import javax.mail.Part;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.NewsAddress;


/**
 * Represent an email message retrieved from a mail server.
 */
class Mail
        implements Comparable<Mail>
{
    /** maximum length of the email text snippet */
    private static final int SNIPPET_MAX_LENGTH = 500;

    /** mail sequence number */
    final int sequenceNumber;

    /** "from" addresses (senders) */
    final String from;

    /** "to" addresses (recipients) */
    final String to;

    /** email sent date */
    final Date date;

    /** email subject */
    final String subject;

    /** email text snippet */
    final String snippet;

    /** "keep" flag */
    boolean keep;


    /**
    * Constructor.
    *
    * @param msg
    *      Message object corresponding to this mail
    * @param sequenceNumber
    *      sequence number of this mail
    * @throws javax.mail.MessagingException
    *      if thrown by the specified Message object when accessing it
    */
    Mail(
            final Message msg,
            final int sequenceNumber)
            throws MessagingException
    {
        /* mail sequence number */
        this.sequenceNumber = sequenceNumber;

        /* "from" addresses (senders) */
        final Address[] fromAddresses = msg.getFrom();

        if ((fromAddresses == null) || (fromAddresses.length == 0))
        {
            this.from = "";
        }
        else
        {
            final StringBuilder sb = new StringBuilder();

            for (Address a : fromAddresses)
            {
                sb.append(addressToString(a));
                sb.append("; ");
            }

            this.from = sb.substring(0, sb.length() - 2);
        }

        /* "to" addresses (recipients) */
        final Address[] toAddresses = msg.getAllRecipients();

        if ((toAddresses == null) || (toAddresses.length == 0))
        {
            this.to = "";
        }
        else
        {
            final StringBuilder sb = new StringBuilder();

            for (Address a : fromAddresses)
            {
                sb.append(addressToString(a));
                sb.append("; ");
            }

            this.to = sb.substring(0, sb.length() - 2);
        }

        /* email sent date */
        final Date msgDate = msg.getSentDate();
        this.date = (msgDate == null) ? new Date() : msgDate;

        /* email subject */
        final String msgSubject = msg.getSubject();
        this.subject = (msgSubject == null) ? "(no subject)" : msgSubject;

        /* email text snippet */
        if (msg instanceof MimeMessage)
        {
            this.snippet = getEmailTextSnippet((MimeMessage) msg);
        }
        else
        {
            this.snippet = "";
        }
    }


    /**
    * Compare this mail to the specified mail.
    * Only the sequenceNumber field is used for comparison.
    */
    public int compareTo(
            Mail o)
    {
        if (this.sequenceNumber > o.sequenceNumber) return 1;
        if (this.sequenceNumber < o.sequenceNumber) return -1;
        return 0;
    }


    /**
    * Check for equality between this mail and the specified mail.
    * Only the sequenceNumber field is used for comparison.
    */
    @Override
    public boolean equals(
            Object o)
    {
        if (o instanceof Mail)
        {
            final Mail other = (Mail) o;
            return (this.sequenceNumber == other.sequenceNumber);
        }
        else
        {
            return false;
        }
    }


    /**
    * Generate a hash code for this mail.
    * Only the sequenceNumber field is used in generating the hash code.
    */
    @Override
    public int hashCode()
    {
        return this.sequenceNumber;
    }


    /**
    * Return a string representation of this mail item.
    */
    @Override
    public String toString()
    {
        final StringBuilder sb = new StringBuilder();

        sb.append("[FROM] ");
        sb.append(this.from);

        sb.append(" [TO] ");
        sb.append(this.to);

        sb.append(" [SUBJECT] ");
        sb.append(this.subject);

        sb.append(" [DATE] ");
        sb.append(this.date);

        sb.append(" [SNIPPET] ");
        sb.append(this.snippet);

        return sb.toString();
    }


    /**
    * Return an email text snippet for a MIME message.
    *
    * @param mm
    *      MIME message
    * @return
    *      text snippet
    */
    private static String getEmailTextSnippet(
            final MimeMessage mm)
    {
        /* text snippet */
        final StringBuilder sb = new StringBuilder();

        /* stack of message parts to be processed */
        final Deque<Part> stack = new ArrayDeque<Part>();

        stack.push(mm);

        while (!stack.isEmpty())
        {
            /* process a message part */
            final Part p = stack.pop();

            try
            {
                if (p.isMimeType("text/plain") ||
                        p.isMimeType("text/html"))
                {
                    final Object o = p.getContent();

                    if (o instanceof String)
                    {
                        sb.append(' ');
                        sb.append((String) o);
                    }
                }
                else if (p.isMimeType("multipart/*"))
                {
                    final Object o = p.getContent();

                    if (o instanceof Multipart)
                    {
                        final Multipart mp = (Multipart) o;
                        final int n = mp.getCount();

                        for (int i = n - 1; i >= 0; i--)
                        {
                            stack.push(mp.getBodyPart(i));
                        }
                    }
                }
                else if (p.isMimeType("message/rfc822"))
                {
                    final Object o = p.getContent();

                    if (o instanceof Part)
                    {
                        stack.push((Part) o);
                    }
                }
            }
            catch (Exception e)
            {
                /* ignore */
            }

            if (sb.length() >= Mail.SNIPPET_MAX_LENGTH)
            {
                break;
            }
        }

        return sb.toString()
                .replaceAll("(?s)<head.*</head>", "")  /* ignore HTML header         */
                .replaceAll("(?s)<[^>]+>", "")         /* ignore HTML tags           */
                .replaceAll("[" + Pattern.quote("~`!@#$%^&*()_-+={[}]|\\:;\"'<,>.?/") + "]{2,}", " ") /* ignore "lines" */
                .replaceAll("([\\s]|(&nbsp;))+", " ")  /* replace consecutive spaces */
                .trim();
    }


    /**
    * Return a suitable string representation of an address.
    *
    * @param a
    *      address
    * @return
    *      string representation
    */
    private static String addressToString(
            final Address a)
    {
        String s;

        if (a instanceof InternetAddress)
        {
            final InternetAddress ia = (InternetAddress) a;
            s = ia.getPersonal();

            if ((s == null) || s.isEmpty())
            {
                s = ia.getAddress();

                if ((s == null) || s.isEmpty())
                {
                    s = a.toString();
                }
            }
        }
        else if (a instanceof NewsAddress)
        {
            final NewsAddress na = (NewsAddress) a;
            s = na.getNewsgroup();

            if ((s == null) || s.isEmpty())
            {
                s = na.getHost();

                if ((s == null) || s.isEmpty())
                {
                    s = a.toString();
                }
            }
        }
        else
        {
            s = a.toString();
        }

        return s;
    }
}