/*
 * Copyright 2013 Marc Nuri San Felix
 *
 * Licensed 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 com.marcnuri.mnimapsync.store;

import static com.marcnuri.mnimapsync.imap.IMAPUtils.sourceFolderNameToTarget;

import com.marcnuri.mnimapsync.MNIMAPSync;
import com.marcnuri.mnimapsync.index.Index;
import com.sun.mail.imap.IMAPFolder;
import com.sun.mail.imap.IMAPStore;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.mail.Folder;
import javax.mail.MessagingException;
import javax.mail.ReadOnlyFolderException;

/**
 *
 * @author Marc Nuri <[email protected]>
 */
public final class StoreCopier {

    private final ExecutorService service;
    private final IMAPStore sourceStore;
    private final IMAPStore targetStore;
    private final Index sourceIndex;
    private final Index targetIndex;
    private final AtomicInteger foldersCopiedCount;
    private final AtomicInteger foldersSkippedCount;
    private final AtomicLong messagesCopiedCount;
    private final AtomicLong messagesSkippedCount;
    //If no empty, we shouldn't allow deletion
    private final List<MessagingException> copyExceptions;

    public StoreCopier(IMAPStore sourceStore, Index sourceIndex, IMAPStore targetStore,
            Index targetIndex, int threads) {
        this.sourceStore = sourceStore;
        this.sourceIndex = sourceIndex;
        this.targetStore = targetStore;
        this.targetIndex = targetIndex;
        service = Executors.newFixedThreadPool(threads);
        foldersCopiedCount = new AtomicInteger();
        foldersSkippedCount = new AtomicInteger();
        messagesCopiedCount = new AtomicLong();
        messagesSkippedCount = new AtomicLong();
        this.copyExceptions = Collections.synchronizedList(new ArrayList<>());
    }

    public final void copy() throws InterruptedException {
        try {
            sourceIndex
                .setFolderSeparator(String.valueOf(sourceStore.getDefaultFolder().getSeparator()));
            //Copy Folder Structure
            copySourceFolder(sourceStore.getDefaultFolder());
            //Copy messages
            copySourceMessages((IMAPFolder) sourceStore.getDefaultFolder());
        } catch (MessagingException ex) {
            Logger.getLogger(StoreCopier.class.getName()).log(Level.SEVERE, null, ex);
        }
        service.shutdown();
        service.awaitTermination(1, TimeUnit.DAYS);
    }

    /**
     * Create folders in the target server recursively from the source.
     *
     * It also indexes the source store folders if we want to delete target folders that no longer
     * exist
     */
    private void copySourceFolder(Folder folder) throws MessagingException {
        final String sourceFolderName = folder.getFullName();
        final String targetFolderName = sourceFolderNameToTarget(sourceFolderName, sourceIndex,
            targetIndex);
        //Index for delete after copy (if necessary)
        if (sourceIndex != null) {
            sourceIndex.addFolder(sourceFolderName);
        }
        //Copy folder
        if (!targetIndex.containsFolder(targetFolderName)) {
            if (!targetStore.getFolder(targetFolderName).create(folder.getType())) {
                throw new MessagingException(String.format(
                        "Couldn't create folder: %s in target server.", sourceFolderName));
            }
            incrementFoldersCopiedCount();
        } else {
            incrementFoldersSkippedCount();
        }
        //Folder recursion. Get all children
        if ((folder.getType() & Folder.HOLDS_FOLDERS) == Folder.HOLDS_FOLDERS) {
            for (Folder child : folder.list()) {
                copySourceFolder(child);
            }
        }
    }

    /**
     * Once the folder structure has been created it copies messages recursively from the root
     * folder.
     */
    private void copySourceMessages(IMAPFolder sourceFolder) throws MessagingException {
        if (sourceFolder != null) {
            final String sourceFolderName = sourceFolder.getFullName();
            final String targetFolderName = sourceFolderNameToTarget(sourceFolderName, sourceIndex,
                targetIndex);
            if ((sourceFolder.getType() & Folder.HOLDS_MESSAGES) == Folder.HOLDS_MESSAGES) {
                //Manage Servers with public/read only folders.
                try {
                    sourceFolder.open(Folder.READ_WRITE);
                } catch (ReadOnlyFolderException ex) {
                    sourceFolder.open(Folder.READ_ONLY);
                }
                if (sourceFolder.getMode() != Folder.READ_ONLY) {
                    sourceFolder.expunge();
                }
                ///////////////////////
                final int messageCount = sourceFolder.getMessageCount();
                sourceFolder.close(false);
                int pos = 1;
                while (pos + MNIMAPSync.BATCH_SIZE <= messageCount) {
                    //Copy messages
                    service.execute(new MessageCopier(this, sourceFolderName, targetFolderName, pos,
                            pos + MNIMAPSync.BATCH_SIZE, targetIndex.getFolderMessages(
                                    targetFolderName)));
                    pos = pos + MNIMAPSync.BATCH_SIZE;
                }
                service.execute(new MessageCopier(this, sourceFolderName, targetFolderName, pos,
                        messageCount,
                        targetIndex.getFolderMessages(targetFolderName)));
            }
            //Folder recursion. Get all children
            if ((sourceFolder.getType() & Folder.HOLDS_FOLDERS) == Folder.HOLDS_FOLDERS) {
                for (Folder child : sourceFolder.list()) {
                    copySourceMessages((IMAPFolder) child);
                }
            }
        }
    }

    public final boolean hasCopyException() {
        synchronized (copyExceptions) {
            return !copyExceptions.isEmpty();
        }
    }

    private void incrementFoldersCopiedCount() {
        foldersCopiedCount.getAndAdd(1);
    }

    private void incrementFoldersSkippedCount() {
        foldersSkippedCount.getAndAdd(1);
    }

    protected final void updatedMessagesCopiedCount(long delta) {
        messagesCopiedCount.getAndAdd(delta);
    }

    protected final void updateMessagesSkippedCount(long delta) {
        messagesSkippedCount.getAndAdd(delta);
    }

    public final int getFoldersCopiedCount() {
        return foldersCopiedCount.get();
    }

    public final int getFoldersSkippedCount() {
        return foldersSkippedCount.get();
    }

    public final long getMessagesCopiedCount() {
        return messagesCopiedCount.get();
    }

    public final long getMessagesSkippedCount() {
        return messagesSkippedCount.get();
    }

    final IMAPStore getSourceStore() {
        return sourceStore;
    }

    final Index getSourceIndex() {
        return sourceIndex;
    }

    final IMAPStore getTargetStore() {
        return targetStore;
    }

    public final synchronized List<MessagingException> getCopyExceptions() {
        return copyExceptions;
    }

}