/*
 *
 *    OPEN-XCHANGE legal information
 *
 *    All intellectual property rights in the Software are protected by
 *    international copyright laws.
 *
 *
 *    In some countries OX, OX Open-Xchange, open xchange and OXtender
 *    as well as the corresponding Logos OX Open-Xchange and OX are registered
 *    trademarks of the Open-Xchange, Inc. group of companies.
 *    The use of the Logos is not covered by the GNU General Public License.
 *    Instead, you are allowed to use these Logos according to the terms and
 *    conditions of the Creative Commons License, Version 2.5, Attribution,
 *    Non-commercial, ShareAlike, and the interpretation of the term
 *    Non-commercial applicable to the aforementioned license is published
 *    on the web site http://www.open-xchange.com/EN/legal/index.html.
 *
 *    Please make sure that third-party modules and libraries are used
 *    according to their respective licenses.
 *
 *    Any modifications to this package must retain all copyright notices
 *    of the original copyright holder(s) for the original code used.
 *
 *    After any such modifications, the original and derivative code shall remain
 *    under the copyright of the copyright holder(s) and/or original author(s)per
 *    the Attribution and Assignment Agreement that can be located at
 *    http://www.open-xchange.com/EN/developer/. The contributing author shall be
 *    given Attribution for the derivative code and a license granting use.
 *
 *     Copyright (C) 2004-2011 Open-Xchange, Inc.
 *     Mail: info@open-xchange.com
 *
 *
 *     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, write to the Free Software Foundation, Inc., 59
 *     Temple Place, Suite 330, Boston, MA 02111-1307 USA
 *
 */

package com.openexchange.imap;

import static com.openexchange.mail.dataobjects.MailFolder.DEFAULT_FOLDER_ID;
import java.util.Set;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.MessagingException;
import com.openexchange.groupware.contexts.Context;
import com.openexchange.groupware.contexts.impl.ContextException;
import com.openexchange.groupware.contexts.impl.ContextStorage;
import com.openexchange.imap.acl.ACLExtension;
import com.openexchange.imap.cache.RightsCache;
import com.openexchange.imap.config.IMAPConfig;
import com.openexchange.mail.MailException;
import com.openexchange.mail.api.MailMessageStorage;
import com.openexchange.mail.api.enhanced.MailMessageStorageLong;
import com.openexchange.mail.mime.MIMEMailException;
import com.openexchange.mail.mime.MIMESessionPropertyNames;
import com.openexchange.mail.usersetting.UserSettingMail;
import com.openexchange.mail.usersetting.UserSettingMailStorage;
import com.openexchange.session.Session;
import com.sun.mail.imap.DefaultFolder;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPStore;
import com.sun.mail.imap.Rights.Right;

/**
 * {@link IMAPFolderWorker} - An abstract class that extends {@link MailMessageStorage} by convenience methods for working on a certain IMAP
 * folder.
 * 
 * @author <a href="mailto:thorben.betten@open-xchange.com">Thorben Betten</a>
 */
public abstract class IMAPFolderWorker extends MailMessageStorageLong {

    private static final org.apache.commons.logging.Log LOG = org.apache.commons.logging.LogFactory.getLog(IMAPFolderWorker.class);

    protected static final String STR_INBOX = "INBOX";

    protected static final String STR_FALSE = "false";

    protected static final Flags FLAGS_SEEN = new Flags(Flags.Flag.SEEN);

    /*
     * Fields
     */
    protected final IMAPStore imapStore;

    protected final Session session;

    protected final int accountId;

    protected final Context ctx;

    protected final IMAPAccess imapAccess;

    protected final UserSettingMail usm;

    protected final IMAPConfig imapConfig;

    protected final ACLExtension aclExtension;

    protected IMAPFolder imapFolder;

    protected int holdsMessages = -1;

    /**
     * Initializes a new {@link IMAPFolderWorker}.
     * 
     * @param imapStore The IMAP store
     * @param imapAccess The IMAP access
     * @param session The session providing needed user data
     * @throws IMAPException If context lading fails
     */
    public IMAPFolderWorker(final IMAPStore imapStore, final IMAPAccess imapAccess, final Session session) throws IMAPException {
        super();
        this.imapStore = imapStore;
        this.imapAccess = imapAccess;
        accountId = imapAccess.getAccountId();
        this.session = session;
        try {
            ctx = ContextStorage.getStorageContext(session.getContextId());
        } catch (final ContextException e) {
            throw new IMAPException(e);
        }
        usm = UserSettingMailStorage.getInstance().getUserSettingMail(session.getUserId(), ctx);
        imapConfig = imapAccess.getIMAPConfig();
        aclExtension = imapConfig.getACLExtension();
    }

    @Override
    public void releaseResources() throws MailException {
        if (null != imapFolder) {
            closeIMAPFolder();
        }
    }

    /**
     * Reports a modification of the IMAP folder denoted by specified fullname. If stored IMAP folder's fullname equals specified fullname,
     * it is closed quietly.
     * 
     * @param modifiedFullname The fullname of the folder which has been modified
     */
    public void notifyIMAPFolderModification(final String modifiedFullname) {
        if ((null == imapFolder) || !modifiedFullname.equals(imapFolder.getFullName())) {
            /*
             * Modified folder did not affect remembered IMAP folder
             */
            return;
        }
        try {
            closeIMAPFolder();
        } catch (final Exception e) {
            LOG.error(e.getMessage(), e);
        }

    }

    /**
     * Reports a modification of the IMAP folders denoted by specified set of fullnames. If stored IMAP folder's fullname is contained in
     * set of fullnames, it is closed quietly.
     * 
     * @param modifiedFullnames The fullnames of the folders which have been modified
     */
    public void notifyIMAPFolderModification(final Set<String> modifiedFullnames) {
        if ((null == imapFolder) || !modifiedFullnames.contains(imapFolder.getFullName())) {
            /*
             * Modified folders did not affect remembered IMAP folder
             */
            return;
        }
        try {
            closeIMAPFolder();
        } catch (final Exception e) {
            LOG.error(e.getMessage(), e);
        }
    }

    private void closeIMAPFolder() throws MailException {
        try {
            imapFolder.close(false);
        } catch (final IllegalStateException e) {
            LOG.warn("Invoked close() on a closed folder", e);
        } catch (final MessagingException e) {
            throw MIMEMailException.handleMessagingException(e, imapConfig, session);
        } finally {
            resetIMAPFolder();
        }
    }

    /**
     * Resets the IMAP folder by setting field {@link #imapFolder} to <code>null</code> and field {@link #holdsMessages} to <code>-1</code>.
     */
    protected void resetIMAPFolder() {
        holdsMessages = -1;
        imapFolder = null;
    }

    /**
     * Determine if field {@link #imapFolder} indicates to hold messages.<br>
     * <b>NOTE</b>: This method assumes that field {@link #imapFolder} is <b>not</b> <code>null</code>.
     * 
     * <pre>
     * return ((imapFolder.getType() &amp; IMAPFolder.HOLDS_MESSAGES) == 1)
     * </pre>
     * 
     * @return <code>true</code> if field {@link #imapFolder} indicates to hold messages
     * @throws MessagingException If a messaging error occurs
     */
    protected boolean holdsMessages() throws MessagingException {
        if (holdsMessages == -1) {
            holdsMessages = ((imapFolder.getType() & Folder.HOLDS_MESSAGES) == 0) ? 0 : 1;
        }
        return holdsMessages > 0;
    }

    /**
     * Sets and opens (only if exists) the folder in a safe manner, checks if selectable and for right {@link Right#READ}
     * 
     * @param fullname The folder fullname
     * @param desiredMode The desired opening mode (either {@link Folder#READ_ONLY} or {@link Folder#READ_WRITE})
     * @return The properly opened IMAP folder
     * @throws MessagingException If a messaging error occurs
     * @throws MailException If user does not hold sufficient rights to open the IMAP folder in desired mode
     */
    protected final IMAPFolder setAndOpenFolder(final String fullname, final int desiredMode) throws MessagingException, MailException {
        return setAndOpenFolder(null, fullname, desiredMode);
    }

    /**
     * Sets and opens (only if exists) the folder in a safe manner, checks if selectable and for right {@link Right#READ}
     * 
     * @param imapFolder The IMAP folder to check against
     * @param fullname The folder fullname
     * @param desiredMode The desired opening mode (either {@link Folder#READ_ONLY} or {@link Folder#READ_WRITE})
     * @return The properly opened IMAP folder
     * @throws MessagingException If a messaging error occurs
     * @throws MailException If user does not hold sufficient rights to open the IMAP folder in desired mode
     */
    protected final IMAPFolder setAndOpenFolder(final IMAPFolder imapFolder, final String fullname, final int desiredMode) throws MessagingException, MailException {
        if (null == fullname) {
            throw new MailException(MailException.Code.MISSING_FULLNAME);
        }
        final boolean isDefaultFolder = DEFAULT_FOLDER_ID.equals(fullname);
        final boolean isIdenticalFolder;
        if (isDefaultFolder) {
            isIdenticalFolder = (imapFolder == null ? false : imapFolder instanceof DefaultFolder);
        } else {
            isIdenticalFolder = (imapFolder == null ? false : imapFolder.getFullName().equals(fullname));
        }
        if (imapFolder != null) {
            /*
             * Obtain folder lock once to avoid multiple acquire/releases when invoking folder's getXXX() methods
             */
            synchronized (imapFolder) {
                IMAPCommandsCollection.forceNoopCommand(imapFolder);
                try {
                    /*
                     * This call also checks if folder is opened
                     */
                    final int mode = imapFolder.getMode();
                    if (isIdenticalFolder && (mode >= desiredMode)) {
                        /*
                         * Identical folder is already opened in an appropriate mode.
                         */
                        // IMAPCommandsCollection.updateIMAPFolder(imapFolder,
                        // mode);
                        return imapFolder;
                    }
                    /*
                     * Folder is open, so close folder
                     */
                    try {
                        imapFolder.close(false);
                    } finally {
                        if (imapFolder == this.imapFolder) {
                            resetIMAPFolder();
                        }
                    }
                } catch (final IllegalStateException e) {
                    /*
                     * Folder not open
                     */
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("IMAP folder's mode could not be checked, because folder is closed. Going to open folder.", e);
                    }
                }
                /*
                 * Folder is closed here
                 */
                if (isIdenticalFolder) {
                    final String imapFolderFullname = imapFolder.getFullName();
                    try {
                        if ((imapFolder.getType() & Folder.HOLDS_MESSAGES) == 0) { // NoSelect
                            throw IMAPException.create(
                                IMAPException.Code.FOLDER_DOES_NOT_HOLD_MESSAGES,
                                imapConfig,
                                session,
                                imapFolderFullname);
                        } else if (imapConfig.isSupportsACLs() && !aclExtension.canRead(RightsCache.getCachedRights(
                            imapFolder,
                            true,
                            session,
                            accountId))) {
                            throw IMAPException.create(IMAPException.Code.NO_FOLDER_OPEN, imapFolderFullname);
                        }
                    } catch (final MessagingException e) { // No access
                        throw IMAPException.create(IMAPException.Code.NO_ACCESS, imapConfig, session, e, imapFolderFullname);
                    }
                    if ((desiredMode == Folder.READ_WRITE) && ((imapFolder.getType() & Folder.HOLDS_MESSAGES) == 0) && STR_FALSE.equalsIgnoreCase(imapAccess.getMailProperties().getProperty(
                        MIMESessionPropertyNames.PROP_ALLOWREADONLYSELECT,
                        STR_FALSE)) && IMAPCommandsCollection.isReadOnly(imapFolder)) {
                        throw IMAPException.create(IMAPException.Code.READ_ONLY_FOLDER, imapConfig, session, imapFolderFullname);
                    }
                    /*
                     * Open identical folder in right mode
                     */
                    imapFolder.open(desiredMode);
                    return imapFolder;
                }
            }
        }
        final IMAPFolder retval =
            (isDefaultFolder ? (IMAPFolder) imapStore.getDefaultFolder() : (IMAPFolder) imapStore.getFolder(fullname));
        /*
         * Obtain folder lock once to avoid multiple acquire/releases when invoking folder's getXXX() methods
         */
        synchronized (retval) {
            if (!isDefaultFolder && !retval.exists()) {
                throw IMAPException.create(IMAPException.Code.FOLDER_NOT_FOUND, imapConfig, session, retval.getFullName());
            }
            if ((desiredMode != Folder.READ_ONLY) && (desiredMode != Folder.READ_WRITE)) {
                throw IMAPException.create(IMAPException.Code.UNKNOWN_FOLDER_MODE, imapConfig, session, Integer.valueOf(desiredMode));
            }
            final boolean selectable = isSelectable(retval);
            if (!selectable) { // NoSelect
                throw IMAPException.create(IMAPException.Code.FOLDER_DOES_NOT_HOLD_MESSAGES, imapConfig, session, retval.getFullName());
            }
            try {
                if (imapConfig.isSupportsACLs() && !aclExtension.canRead(RightsCache.getCachedRights(retval, true, session, accountId))) {
                    throw IMAPException.create(IMAPException.Code.NO_FOLDER_OPEN, imapConfig, session, retval.getFullName());
                }
            } catch (final MessagingException e) {
                throw IMAPException.create(IMAPException.Code.NO_ACCESS, imapConfig, session, e, retval.getFullName());
            }
            if ((Folder.READ_WRITE == desiredMode) && (!selectable) && STR_FALSE.equalsIgnoreCase(imapAccess.getMailProperties().getProperty(
                MIMESessionPropertyNames.PROP_ALLOWREADONLYSELECT,
                STR_FALSE)) && IMAPCommandsCollection.isReadOnly(retval)) {
                throw IMAPException.create(IMAPException.Code.READ_ONLY_FOLDER, imapConfig, session, retval.getFullName());
            }
            retval.open(desiredMode);
        }
        return retval;
    }

    /**
     * Checks if specified IMAP folder is allowed for being selected through SELECT command.
     * 
     * @param imapFolder The IMAP folder to check
     * @return <code>true</code> if specified IMAP folder is allowed for being selected through SELECT command; otherwise <code>false</code>
     * @throws MessagingException If IMAP folder's type cannot be determined
     */
    private static final boolean isSelectable(final IMAPFolder imapFolder) throws MessagingException {
        return (imapFolder.getType() & Folder.HOLDS_MESSAGES) == Folder.HOLDS_MESSAGES;
    }
}
