package com.fsck.k9.fragment;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Future;

import android.app.Activity;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.LoaderManager;
import android.app.LoaderManager.LoaderCallbacks;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.Loader;
import android.database.Cursor;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v4.widget.SwipeRefreshLayout;
import android.text.TextUtils;
import timber.log.Timber;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

import com.fsck.k9.Account;
import com.fsck.k9.Account.SortType;
import com.fsck.k9.BuildConfig;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.activity.ActivityListener;
import com.fsck.k9.activity.ChooseFolder;
import com.fsck.k9.activity.FolderInfoHolder;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.activity.misc.ContactPictureLoader;
import com.fsck.k9.cache.EmailProviderCache;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.fragment.MessageListFragmentComparators.ArrivalComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.AttachmentComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.ComparatorChain;
import com.fsck.k9.fragment.MessageListFragmentComparators.DateComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.FlaggedComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.ReverseComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.ReverseIdComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.SenderComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.SubjectComparator;
import com.fsck.k9.fragment.MessageListFragmentComparators.UnreadComparator;
import com.fsck.k9.helper.ContactPicture;
import com.fsck.k9.helper.MergeCursorWithUniqueId;
import com.fsck.k9.helper.MessageHelper;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mailstore.LocalFolder;
import com.fsck.k9.preferences.StorageEditor;
import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.provider.EmailProvider.MessageColumns;
import com.fsck.k9.provider.EmailProvider.SpecialColumns;
import com.fsck.k9.search.ConditionsTreeNode;
import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.SearchField;
import com.fsck.k9.search.SqlQueryBuilder;

import static com.fsck.k9.fragment.MLFProjectionInfo.ACCOUNT_UUID_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.FLAGGED_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.FOLDER_NAME_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.ID_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.PROJECTION;
import static com.fsck.k9.fragment.MLFProjectionInfo.READ_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.SUBJECT_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.THREADED_PROJECTION;
import static com.fsck.k9.fragment.MLFProjectionInfo.THREAD_COUNT_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.THREAD_ROOT_COLUMN;
import static com.fsck.k9.fragment.MLFProjectionInfo.UID_COLUMN;


public class MessageListFragment extends Fragment implements OnItemClickListener,
        ConfirmationDialogFragmentListener, LoaderCallbacks<Cursor> {

    public static MessageListFragment newInstance(
            LocalSearch search, boolean isThreadDisplay, boolean threadedList) {
        MessageListFragment fragment = new MessageListFragment();
        Bundle args = new Bundle();
        args.putParcelable(ARG_SEARCH, search);
        args.putBoolean(ARG_IS_THREAD_DISPLAY, isThreadDisplay);
        args.putBoolean(ARG_THREADED_LIST, threadedList);
        fragment.setArguments(args);
        return fragment;
    }

    private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1;
    private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2;

    private static final String ARG_SEARCH = "searchObject";
    private static final String ARG_THREADED_LIST = "showingThreadedList";
    private static final String ARG_IS_THREAD_DISPLAY = "isThreadedDisplay";

    private static final String STATE_SELECTED_MESSAGES = "selectedMessages";
    private static final String STATE_ACTIVE_MESSAGE = "activeMessage";
    private static final String STATE_REMOTE_SEARCH_PERFORMED = "remoteSearchPerformed";
    private static final String STATE_MESSAGE_LIST = "listState";

    /**
     * Maps a {@link SortType} to a {@link Comparator} implementation.
     */
    private static final Map<SortType, Comparator<Cursor>> SORT_COMPARATORS;

    static {
        // fill the mapping at class time loading

        final Map<SortType, Comparator<Cursor>> map =
                new EnumMap<>(SortType.class);
        map.put(SortType.SORT_ATTACHMENT, new AttachmentComparator());
        map.put(SortType.SORT_DATE, new DateComparator());
        map.put(SortType.SORT_ARRIVAL, new ArrivalComparator());
        map.put(SortType.SORT_FLAGGED, new FlaggedComparator());
        map.put(SortType.SORT_SUBJECT, new SubjectComparator());
        map.put(SortType.SORT_SENDER, new SenderComparator());
        map.put(SortType.SORT_UNREAD, new UnreadComparator());

        // make it immutable to prevent accidental alteration (content is immutable already)
        SORT_COMPARATORS = Collections.unmodifiableMap(map);
    }

    ListView listView;
    private SwipeRefreshLayout swipeRefreshLayout;
    Parcelable savedListState;

    int previewLines = 0;


    private MessageListAdapter adapter;
    private View footerView;
    private FolderInfoHolder currentFolder;
    private LayoutInflater layoutInflater;
    private MessagingController messagingController;

    private Account account;
    private String[] accountUuids;
    private int unreadMessageCount = 0;

    private Cursor[] cursors;
    private boolean[] cursorValid;
    int uniqueIdColumn;

    /**
     * Stores the name of the folder that we want to open as soon as possible after load.
     */
    private String folderName;

    private boolean remoteSearchPerformed = false;
    private Future<?> remoteSearchFuture = null;
    private List<Message> extraSearchResults;

    private String title;
    private LocalSearch search = null;
    private boolean singleAccountMode;
    private boolean singleFolderMode;
    private boolean allAccounts;

    private final MessageListHandler handler = new MessageListHandler(this);

    private SortType sortType = SortType.SORT_DATE;
    private boolean sortAscending = true;
    private boolean sortDateAscending = false;
    boolean senderAboveSubject = false;
    boolean checkboxes = true;
    boolean stars = true;

    private int selectedCount = 0;
    Set<Long> selected = new HashSet<>();
    private ActionMode actionMode;
    private Boolean hasConnectivity;
    /**
     * Relevant messages for the current context when we have to remember the chosen messages
     * between user interactions (e.g. selecting a folder for move operation).
     */
    private List<MessageReference> activeMessages;
    /* package visibility for faster inner class access */
    MessageHelper messageHelper;
    private final ActionModeCallback actionModeCallback = new ActionModeCallback();
    MessageListFragmentListener fragmentListener;
    boolean showingThreadedList;
    private boolean isThreadDisplay;
    private Context context;
    private final ActivityListener activityListener = new MessageListActivityListener();
    private Preferences preferences;
    private boolean loaderJustInitialized;
    MessageReference activeMessage;
    /**
     * {@code true} after {@link #onCreate(Bundle)} was executed. Used in {@link #updateTitle()} to
     * make sure we don't access member variables before initialization is complete.
     */
    private boolean initialized = false;
    ContactPictureLoader contactsPictureLoader;
    private LocalBroadcastManager localBroadcastManager;
    private BroadcastReceiver cacheBroadcastReceiver;
    private IntentFilter cacheIntentFilter;
    /**
     * Stores the unique ID of the message the context menu was opened for.
     *
     * We have to save this because the message list might change between the time the menu was
     * opened and when the user clicks on a menu item. When this happens the 'adapter position' that
     * is accessible via the {@code ContextMenu} object might correspond to another list item and we
     * would end up using/modifying the wrong message.
     *
     * The value of this field is {@code 0} when no context menu is currently open.
     */
    private long contextMenuUniqueId = 0;




    /**
     * @return The comparator to use to display messages in an ordered
     *         fashion. Never {@code null}.
     */
    private Comparator<Cursor> getComparator() {
        final List<Comparator<Cursor>> chain =
                new ArrayList<>(3 /* we add 3 comparators at most */);

        // Add the specified comparator
        final Comparator<Cursor> comparator = SORT_COMPARATORS.get(sortType);
        if (sortAscending) {
            chain.add(comparator);
        } else {
            chain.add(new ReverseComparator<>(comparator));
        }

        // Add the date comparator if not already specified
        if (sortType != SortType.SORT_DATE && sortType != SortType.SORT_ARRIVAL) {
            final Comparator<Cursor> dateComparator = SORT_COMPARATORS.get(SortType.SORT_DATE);
            if (sortDateAscending) {
                chain.add(dateComparator);
            } else {
                chain.add(new ReverseComparator<>(dateComparator));
            }
        }

        // Add the id comparator
        chain.add(new ReverseIdComparator());

        // Build the comparator chain
        return new ComparatorChain<>(chain);
    }

    void folderLoading(String folder, boolean loading) {
        if (currentFolder != null && currentFolder.name.equals(folder)) {
            currentFolder.loading = loading;
        }
        updateMoreMessagesOfCurrentFolder();
        updateFooterView();
    }

    public void updateTitle() {
        if (!initialized) {
            return;
        }

        setWindowTitle();
        if (!search.isManualSearch()) {
            setWindowProgress();
        }
    }

    private void setWindowProgress() {
        int level = Window.PROGRESS_END;

        if (currentFolder != null && currentFolder.loading && activityListener.getFolderTotal() > 0) {
            int divisor = activityListener.getFolderTotal();
            if (divisor != 0) {
                level = (Window.PROGRESS_END / divisor) * (activityListener.getFolderCompleted()) ;
                if (level > Window.PROGRESS_END) {
                    level = Window.PROGRESS_END;
                }
            }
        }

        fragmentListener.setMessageListProgress(level);
    }

    private void setWindowTitle() {
        // regular folder content display
        if (!isManualSearch() && singleFolderMode) {
            Activity activity = getActivity();
            String displayName = FolderInfoHolder.getDisplayName(activity, account,
                    folderName);

            fragmentListener.setMessageListTitle(displayName);

            String operation = activityListener.getOperation(activity);
            if (operation.length() < 1) {
                fragmentListener.setMessageListSubTitle(account.getEmail());
            } else {
                fragmentListener.setMessageListSubTitle(operation);
            }
        } else {
            // query result display.  This may be for a search folder as opposed to a user-initiated search.
            if (title != null) {
                // This was a search folder; the search folder has overridden our title.
                fragmentListener.setMessageListTitle(title);
            } else {
                // This is a search result; set it to the default search result line.
                fragmentListener.setMessageListTitle(getString(R.string.search_results));
            }

            fragmentListener.setMessageListSubTitle(null);
        }

        // set unread count
        if (unreadMessageCount <= 0) {
            fragmentListener.setUnreadCount(0);
        } else {
            if (!singleFolderMode && title == null) {
                // The unread message count is easily confused
                // with total number of messages in the search result, so let's hide it.
                fragmentListener.setUnreadCount(0);
            } else {
                fragmentListener.setUnreadCount(unreadMessageCount);
            }
        }
    }

    void progress(final boolean progress) {
        fragmentListener.enableActionBarProgress(progress);
        if (swipeRefreshLayout != null && !progress) {
            swipeRefreshLayout.setRefreshing(false);
        }
    }

    @Override
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
        if (view == footerView) {
            if (currentFolder != null && !search.isManualSearch() && currentFolder.moreMessages) {

                messagingController.loadMoreMessages(account, folderName, null);

            } else if (currentFolder != null && isRemoteSearch() &&
                    extraSearchResults != null && extraSearchResults.size() > 0) {

                int numResults = extraSearchResults.size();
                int limit = account.getRemoteSearchNumResults();

                List<Message> toProcess = extraSearchResults;

                if (limit > 0 && numResults > limit) {
                    toProcess = toProcess.subList(0, limit);
                    extraSearchResults = extraSearchResults.subList(limit,
                            extraSearchResults.size());
                } else {
                    extraSearchResults = null;
                    updateFooter(null);
                }

                messagingController.loadSearchResults(account, currentFolder.name, toProcess, activityListener);
            }

            return;
        }

        Cursor cursor = (Cursor) parent.getItemAtPosition(position);
        if (cursor == null) {
            return;
        }

        if (selectedCount > 0) {
            toggleMessageSelect(position);
        } else {
            if (showingThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
                Account account = getAccountFromCursor(cursor);
                String folderName = cursor.getString(FOLDER_NAME_COLUMN);

                // If threading is enabled and this item represents a thread, display the thread contents.
                long rootId = cursor.getLong(THREAD_ROOT_COLUMN);
                fragmentListener.showThread(account, folderName, rootId);
            } else {
                // This item represents a message; just display the message.
                openMessageAtPosition(listViewToAdapterPosition(position));
            }
        }
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        context = activity.getApplicationContext();

        try {
            fragmentListener = (MessageListFragmentListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.getClass() +
                    " must implement MessageListFragmentListener");
        }
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Context appContext = getActivity().getApplicationContext();

        preferences = Preferences.getPreferences(appContext);
        messagingController = MessagingController.getInstance(getActivity().getApplication());

        previewLines = K9.messageListPreviewLines();
        checkboxes = K9.messageListCheckboxes();
        stars = K9.messageListStars();

        if (K9.showContactPicture()) {
            contactsPictureLoader = ContactPicture.getContactPictureLoader(getActivity());
        }

        restoreInstanceState(savedInstanceState);
        decodeArguments();

        createCacheBroadcastReceiver(appContext);

        initialized = true;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {

        layoutInflater = inflater;

        View view = inflater.inflate(R.layout.message_list_fragment, container, false);

        initializePullToRefresh(view);

        initializeLayout();
        listView.setVerticalFadingEdgeEnabled(false);

        return view;
    }

    @Override
    public void onDestroyView() {
        savedListState = listView.onSaveInstanceState();
        super.onDestroyView();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        messageHelper = MessageHelper.getInstance(getActivity());

        initializeMessageList();

        // This needs to be done before initializing the cursor loader below
        initializeSortSettings();

        loaderJustInitialized = true;
        LoaderManager loaderManager = getLoaderManager();
        int len = accountUuids.length;
        cursors = new Cursor[len];
        cursorValid = new boolean[len];
        for (int i = 0; i < len; i++) {
            loaderManager.initLoader(i, null, this);
            cursorValid[i] = false;
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        saveSelectedMessages(outState);
        saveListState(outState);

        outState.putBoolean(STATE_REMOTE_SEARCH_PERFORMED, remoteSearchPerformed);
        if (activeMessage != null) {
            outState.putString(STATE_ACTIVE_MESSAGE, activeMessage.toIdentityString());
        }
    }

    /**
     * Restore the state of a previous {@link MessageListFragment} instance.
     *
     * @see #onSaveInstanceState(Bundle)
     */
    private void restoreInstanceState(Bundle savedInstanceState) {
        if (savedInstanceState == null) {
            return;
        }

        restoreSelectedMessages(savedInstanceState);

        remoteSearchPerformed = savedInstanceState.getBoolean(STATE_REMOTE_SEARCH_PERFORMED);
        savedListState = savedInstanceState.getParcelable(STATE_MESSAGE_LIST);
        String messageReferenceString = savedInstanceState.getString(STATE_ACTIVE_MESSAGE);
        activeMessage = MessageReference.parse(messageReferenceString);
    }

    /**
     * Write the unique IDs of selected messages to a {@link Bundle}.
     */
    private void saveSelectedMessages(Bundle outState) {
        long[] selected = new long[this.selected.size()];
        int i = 0;
        for (Long id : this.selected) {
            selected[i++] = id;
        }
        outState.putLongArray(STATE_SELECTED_MESSAGES, selected);
    }

    /**
     * Restore selected messages from a {@link Bundle}.
     */
    private void restoreSelectedMessages(Bundle savedInstanceState) {
        long[] selected = savedInstanceState.getLongArray(STATE_SELECTED_MESSAGES);
        if (selected != null) {
            for (long id : selected) {
                this.selected.add(id);
            }
        }
    }

    private void saveListState(Bundle outState) {
        if (savedListState != null) {
            // The previously saved state was never restored, so just use that.
            outState.putParcelable(STATE_MESSAGE_LIST, savedListState);
        } else if (listView != null) {
            outState.putParcelable(STATE_MESSAGE_LIST, listView.onSaveInstanceState());
        }
    }

    private void initializeSortSettings() {
        if (singleAccountMode) {
            sortType = account.getSortType();
            sortAscending = account.isSortAscending(sortType);
            sortDateAscending = account.isSortAscending(SortType.SORT_DATE);
        } else {
            sortType = K9.getSortType();
            sortAscending = K9.isSortAscending(sortType);
            sortDateAscending = K9.isSortAscending(SortType.SORT_DATE);
        }
    }

    private void decodeArguments() {
        Bundle args = getArguments();

        showingThreadedList = args.getBoolean(ARG_THREADED_LIST, false);
        isThreadDisplay = args.getBoolean(ARG_IS_THREAD_DISPLAY, false);
        search = args.getParcelable(ARG_SEARCH);
        title = search.getName();

        String[] accountUuids = search.getAccountUuids();

        singleAccountMode = false;
        if (accountUuids.length == 1 && !search.searchAllAccounts()) {
            singleAccountMode = true;
            account = preferences.getAccount(accountUuids[0]);
        }

        singleFolderMode = false;
        if (singleAccountMode && (search.getFolderNames().size() == 1)) {
            singleFolderMode = true;
            folderName = search.getFolderNames().get(0);
            currentFolder = getFolderInfoHolder(folderName, account);
        }

        allAccounts = false;
        if (singleAccountMode) {
            this.accountUuids = new String[] { account.getUuid() };
        } else {
            if (accountUuids.length == 1 &&
                    accountUuids[0].equals(SearchSpecification.ALL_ACCOUNTS)) {
                allAccounts = true;

                List<Account> accounts = preferences.getAccounts();

                this.accountUuids = new String[accounts.size()];
                for (int i = 0, len = accounts.size(); i < len; i++) {
                    this.accountUuids[i] = accounts.get(i).getUuid();
                }

                if (this.accountUuids.length == 1) {
                    singleAccountMode = true;
                    account = accounts.get(0);
                }
            } else {
                this.accountUuids = accountUuids;
            }
        }
    }

    private void initializeMessageList() {
        adapter = new MessageListAdapter(this);

        if (folderName != null) {
            currentFolder = getFolderInfoHolder(folderName, account);
        }

        if (singleFolderMode) {
            listView.addFooterView(getFooterView(listView));
            updateFooterView();
        }

        listView.setAdapter(adapter);
    }

    private void createCacheBroadcastReceiver(Context appContext) {
        localBroadcastManager = LocalBroadcastManager.getInstance(appContext);

        cacheBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                adapter.notifyDataSetChanged();
            }
        };

        cacheIntentFilter = new IntentFilter(EmailProviderCache.ACTION_CACHE_UPDATED);
    }

    private FolderInfoHolder getFolderInfoHolder(String folderName, Account account) {
        try {
            LocalFolder localFolder = MlfUtils.getOpenFolder(folderName, account);
            return new FolderInfoHolder(context, localFolder, account);
        } catch (MessagingException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void onPause() {
        super.onPause();

        localBroadcastManager.unregisterReceiver(cacheBroadcastReceiver);
        activityListener.onPause(getActivity());
        messagingController.removeListener(activityListener);
    }

    /**
     * On resume we refresh messages for the folder that is currently open.
     * This guarantees that things like unread message count and read status
     * are updated.
     */
    @Override
    public void onResume() {
        super.onResume();

        senderAboveSubject = K9.messageListSenderAboveSubject();

        if (!loaderJustInitialized) {
            restartLoader();
        } else {
            loaderJustInitialized = false;
        }

        // Check if we have connectivity.  Cache the value.
        if (hasConnectivity == null) {
            hasConnectivity = Utility.hasConnectivity(getActivity().getApplication());
        }

        localBroadcastManager.registerReceiver(cacheBroadcastReceiver, cacheIntentFilter);
        activityListener.onResume(getActivity());
        messagingController.addListener(activityListener);

        //Cancel pending new mail notifications when we open an account
        List<Account> accountsWithNotification;

        Account account = this.account;
        if (account != null) {
            accountsWithNotification = Collections.singletonList(account);
        } else {
            accountsWithNotification = preferences.getAccounts();
        }

        for (Account accountWithNotification : accountsWithNotification) {
            messagingController.cancelNotificationsForAccount(accountWithNotification);
        }

        if (this.account != null && folderName != null && !search.isManualSearch()) {
            messagingController.getFolderUnreadMessageCount(this.account, folderName, activityListener);
        }

        updateTitle();
    }

    private void restartLoader() {
        if (cursorValid == null) {
            return;
        }

        // Refresh the message list
        LoaderManager loaderManager = getLoaderManager();
        for (int i = 0; i < accountUuids.length; i++) {
            loaderManager.restartLoader(i, null, this);
            cursorValid[i] = false;
        }
    }

    private void initializePullToRefresh(View layout) {
        swipeRefreshLayout = (SwipeRefreshLayout) layout.findViewById(R.id.swiperefresh);
        listView = (ListView) layout.findViewById(R.id.message_list);

        if (isRemoteSearchAllowed()) {
            swipeRefreshLayout.setOnRefreshListener(
                    new SwipeRefreshLayout.OnRefreshListener() {
                        @Override
                        public void onRefresh() {
                            onRemoteSearchRequested();
                        }
                    }
            );
        } else if (isCheckMailSupported()) {
            swipeRefreshLayout.setOnRefreshListener(
                    new SwipeRefreshLayout.OnRefreshListener() {
                        @Override
                        public void onRefresh() {
                            checkMail();
                        }
                    }
            );
        }

        // Disable pull-to-refresh until the message list has been loaded
        swipeRefreshLayout.setEnabled(false);
    }

    private void initializeLayout() {
        listView.setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
        listView.setLongClickable(true);
        listView.setFastScrollEnabled(true);
        listView.setScrollingCacheEnabled(false);
        listView.setOnItemClickListener(this);

        registerForContextMenu(listView);
    }

    public void onCompose() {
        if (!singleAccountMode) {
            /*
             * If we have a query string, we don't have an account to let
             * compose start the default action.
             */
            fragmentListener.onCompose(null);
        } else {
            fragmentListener.onCompose(account);
        }
    }

    private void onReply(MessageReference messageReference) {
        fragmentListener.onReply(messageReference);
    }

    private void onReplyAll(MessageReference messageReference) {
        fragmentListener.onReplyAll(messageReference);
    }

    private void onForward(MessageReference messageReference) {
        fragmentListener.onForward(messageReference);
    }

    private void onResendMessage(MessageReference messageReference) {
        fragmentListener.onResendMessage(messageReference);
    }

    public void changeSort(SortType sortType) {
        Boolean sortAscending = (this.sortType == sortType) ? !this.sortAscending : null;
        changeSort(sortType, sortAscending);
    }

    /**
     * User has requested a remote search.  Setup the bundle and start the intent.
     */
    private void onRemoteSearchRequested() {
        String searchAccount;
        String searchFolder;

        searchAccount = account.getUuid();
        searchFolder = currentFolder.name;

        String queryString = search.getRemoteSearchArguments();

        remoteSearchPerformed = true;
        remoteSearchFuture = messagingController.searchRemoteMessages(searchAccount, searchFolder,
                queryString, null, null, activityListener);

        swipeRefreshLayout.setEnabled(false);

        fragmentListener.remoteSearchStarted();
    }

    /**
     * Change the sort type and sort order used for the message list.
     *
     * @param sortType
     *         Specifies which field to use for sorting the message list.
     * @param sortAscending
     *         Specifies the sort order. If this argument is {@code null} the default search order
     *         for the sort type is used.
     */
    // FIXME: Don't save the changes in the UI thread
    private void changeSort(SortType sortType, Boolean sortAscending) {
        this.sortType = sortType;

        Account account = this.account;

        if (account != null) {
            account.setSortType(this.sortType);

            if (sortAscending == null) {
                this.sortAscending = account.isSortAscending(this.sortType);
            } else {
                this.sortAscending = sortAscending;
            }
            account.setSortAscending(this.sortType, this.sortAscending);
            sortDateAscending = account.isSortAscending(SortType.SORT_DATE);

            account.save(preferences);
        } else {
            K9.setSortType(this.sortType);

            if (sortAscending == null) {
                this.sortAscending = K9.isSortAscending(this.sortType);
            } else {
                this.sortAscending = sortAscending;
            }
            K9.setSortAscending(this.sortType, this.sortAscending);
            sortDateAscending = K9.isSortAscending(SortType.SORT_DATE);

            StorageEditor editor = preferences.getStorage().edit();
            K9.save(editor);
            editor.commit();
        }

        reSort();
    }

    private void reSort() {
        int toastString = sortType.getToast(sortAscending);

        Toast toast = Toast.makeText(getActivity(), toastString, Toast.LENGTH_SHORT);
        toast.show();

        LoaderManager loaderManager = getLoaderManager();
        for (int i = 0, len = accountUuids.length; i < len; i++) {
            loaderManager.restartLoader(i, null, this);
        }
    }

    public void onCycleSort() {
        SortType[] sorts = SortType.values();
        int curIndex = 0;

        for (int i = 0; i < sorts.length; i++) {
            if (sorts[i] == sortType) {
                curIndex = i;
                break;
            }
        }

        curIndex++;

        if (curIndex == sorts.length) {
            curIndex = 0;
        }

        changeSort(sorts[curIndex]);
    }

    private void onDelete(MessageReference message) {
        onDelete(Collections.singletonList(message));
    }

    private void onDelete(List<MessageReference> messages) {
        if (K9.confirmDelete()) {
            // remember the message selection for #onCreateDialog(int)
            activeMessages = messages;
            showDialog(R.id.dialog_confirm_delete);
        } else {
            onDeleteConfirmed(messages);
        }
    }

    private void onDeleteConfirmed(List<MessageReference> messages) {
        if (showingThreadedList) {
            messagingController.deleteThreads(messages);
        } else {
            messagingController.deleteMessages(messages, null);
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode != Activity.RESULT_OK) {
            return;
        }

        switch (requestCode) {
            case ACTIVITY_CHOOSE_FOLDER_MOVE:
            case ACTIVITY_CHOOSE_FOLDER_COPY: {
                if (data == null) {
                    return;
                }

                final String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER);
                final List<MessageReference> messages = activeMessages;

                if (destFolderName != null) {

                    activeMessages = null; // don't need it any more

                    if (messages.size() > 0) {
                        MlfUtils.setLastSelectedFolderName(preferences, messages, destFolderName);
                    }

                    switch (requestCode) {
                        case ACTIVITY_CHOOSE_FOLDER_MOVE:
                            move(messages, destFolderName);
                            break;

                        case ACTIVITY_CHOOSE_FOLDER_COPY:
                            copy(messages, destFolderName);
                            break;
                        }
                }
                break;
            }
        }
    }

    public void onExpunge() {
        if (currentFolder != null) {
            onExpunge(account, currentFolder.name);
        }
    }

    private void onExpunge(final Account account, String folderName) {
        messagingController.expunge(account, folderName);
    }

    private void showDialog(int dialogId) {
        DialogFragment fragment;
        switch (dialogId) {
            case R.id.dialog_confirm_spam: {
                String title = getString(R.string.dialog_confirm_spam_title);

                int selectionSize = activeMessages.size();
                String message = getResources().getQuantityString(
                        R.plurals.dialog_confirm_spam_message, selectionSize, selectionSize);

                String confirmText = getString(R.string.dialog_confirm_spam_confirm_button);
                String cancelText = getString(R.string.dialog_confirm_spam_cancel_button);

                fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message,
                        confirmText, cancelText);
                break;
            }
            case R.id.dialog_confirm_delete: {
                String title = getString(R.string.dialog_confirm_delete_title);

                int selectionSize = activeMessages.size();
                String message = getResources().getQuantityString(
                        R.plurals.dialog_confirm_delete_messages, selectionSize,
                        selectionSize);

                String confirmText = getString(R.string.dialog_confirm_delete_confirm_button);
                String cancelText = getString(R.string.dialog_confirm_delete_cancel_button);

                fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message,
                        confirmText, cancelText);
                break;
            }
            case R.id.dialog_confirm_mark_all_as_read: {
                String title = getString(R.string.dialog_confirm_mark_all_as_read_title);
                String message = getString(R.string.dialog_confirm_mark_all_as_read_message);

                String confirmText = getString(R.string.dialog_confirm_mark_all_as_read_confirm_button);
                String cancelText = getString(R.string.dialog_confirm_mark_all_as_read_cancel_button);

                fragment = ConfirmationDialogFragment.newInstance(dialogId, title, message, confirmText, cancelText);
                break;
            }
            default: {
                throw new RuntimeException("Called showDialog(int) with unknown dialog id.");
            }
        }

        fragment.setTargetFragment(this, dialogId);
        fragment.show(getFragmentManager(), getDialogTag(dialogId));
    }

    private String getDialogTag(int dialogId) {
        return "dialog-" + dialogId;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int itemId = item.getItemId();
        switch (itemId) {
        case R.id.set_sort_date: {
            changeSort(SortType.SORT_DATE);
            return true;
        }
        case R.id.set_sort_arrival: {
            changeSort(SortType.SORT_ARRIVAL);
            return true;
        }
        case R.id.set_sort_subject: {
            changeSort(SortType.SORT_SUBJECT);
            return true;
        }
        case R.id.set_sort_sender: {
            changeSort(SortType.SORT_SENDER);
            return true;
        }
        case R.id.set_sort_flag: {
            changeSort(SortType.SORT_FLAGGED);
            return true;
        }
        case R.id.set_sort_unread: {
            changeSort(SortType.SORT_UNREAD);
            return true;
        }
        case R.id.set_sort_attach: {
            changeSort(SortType.SORT_ATTACHMENT);
            return true;
        }
        case R.id.select_all: {
            selectAll();
            return true;
        }
        }

        if (!singleAccountMode) {
            // None of the options after this point are "safe" for search results
            //TODO: This is not true for "unread" and "starred" searches in regular folders
            return false;
        }

        switch (itemId) {
        case R.id.send_messages: {
            onSendPendingMessages();
            return true;
        }
        case R.id.expunge: {
            if (currentFolder != null) {
                onExpunge(account, currentFolder.name);
            }
            return true;
        }
        default: {
            return super.onOptionsItemSelected(item);
        }
        }
    }

    public void onSendPendingMessages() {
        messagingController.sendPendingMessages(account, null);
    }

    @Override
    public boolean onContextItemSelected(android.view.MenuItem item) {
        if (contextMenuUniqueId == 0) {
            return false;
        }

        int adapterPosition = getPositionForUniqueId(contextMenuUniqueId);
        if (adapterPosition == AdapterView.INVALID_POSITION) {
            return false;
        }

        switch (item.getItemId()) {
            case R.id.deselect:
            case R.id.select: {
                toggleMessageSelectWithAdapterPosition(adapterPosition);
                break;
            }
            case R.id.reply: {
                onReply(getMessageAtPosition(adapterPosition));
                break;
            }
            case R.id.reply_all: {
                onReplyAll(getMessageAtPosition(adapterPosition));
                break;
            }
            case R.id.forward: {
                onForward(getMessageAtPosition(adapterPosition));
                break;
            }
            case R.id.send_again: {
                onResendMessage(getMessageAtPosition(adapterPosition));
                selectedCount = 0;
                break;
            }
            case R.id.same_sender: {
                Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
                String senderAddress = MlfUtils.getSenderAddressFromCursor(cursor);
                if (senderAddress != null) {
                    fragmentListener.showMoreFromSameSender(senderAddress);
                }
                break;
            }
            case R.id.delete: {
                MessageReference message = getMessageAtPosition(adapterPosition);
                onDelete(message);
                break;
            }
            case R.id.mark_as_read: {
                setFlag(adapterPosition, Flag.SEEN, true);
                break;
            }
            case R.id.mark_as_unread: {
                setFlag(adapterPosition, Flag.SEEN, false);
                break;
            }
            case R.id.flag: {
                setFlag(adapterPosition, Flag.FLAGGED, true);
                break;
            }
            case R.id.unflag: {
                setFlag(adapterPosition, Flag.FLAGGED, false);
                break;
            }

            // only if the account supports this
            case R.id.archive: {
                onArchive(getMessageAtPosition(adapterPosition));
                break;
            }
            case R.id.spam: {
                onSpam(getMessageAtPosition(adapterPosition));
                break;
            }
            case R.id.move: {
                onMove(getMessageAtPosition(adapterPosition));
                break;
            }
            case R.id.copy: {
                onCopy(getMessageAtPosition(adapterPosition));
                break;
            }

            // debug options
            case R.id.debug_delete_locally: {
                onDebugClearLocally(getMessageAtPosition(adapterPosition));
                break;
            }
        }

        contextMenuUniqueId = 0;
        return true;
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
        super.onCreateContextMenu(menu, v, menuInfo);

        AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
        Cursor cursor = (Cursor) listView.getItemAtPosition(info.position);

        if (cursor == null) {
            return;
        }

        getActivity().getMenuInflater().inflate(R.menu.message_list_item_context, menu);
        menu.findItem(R.id.debug_delete_locally).setVisible(BuildConfig.DEBUG);

        contextMenuUniqueId = cursor.getLong(uniqueIdColumn);
        Account account = getAccountFromCursor(cursor);

        String subject = cursor.getString(SUBJECT_COLUMN);
        boolean read = (cursor.getInt(READ_COLUMN) == 1);
        boolean flagged = (cursor.getInt(FLAGGED_COLUMN) == 1);

        menu.setHeaderTitle(subject);

        if (selected.contains(contextMenuUniqueId)) {
            menu.findItem(R.id.select).setVisible(false);
        } else {
            menu.findItem(R.id.deselect).setVisible(false);
        }

        if (read) {
            menu.findItem(R.id.mark_as_read).setVisible(false);
        } else {
            menu.findItem(R.id.mark_as_unread).setVisible(false);
        }

        if (flagged) {
            menu.findItem(R.id.flag).setVisible(false);
        } else {
            menu.findItem(R.id.unflag).setVisible(false);
        }

        if (!messagingController.isCopyCapable(account)) {
            menu.findItem(R.id.copy).setVisible(false);
        }

        if (!messagingController.isMoveCapable(account)) {
            menu.findItem(R.id.move).setVisible(false);
            menu.findItem(R.id.archive).setVisible(false);
            menu.findItem(R.id.spam).setVisible(false);
        }

        if (!account.hasArchiveFolder()) {
            menu.findItem(R.id.archive).setVisible(false);
        }

        if (!account.hasSpamFolder()) {
            menu.findItem(R.id.spam).setVisible(false);
        }

    }

    public void onSwipeRightToLeft(final MotionEvent e1, final MotionEvent e2) {
        // Handle right-to-left as an un-select
        handleSwipe(e1, false);
    }

    public void onSwipeLeftToRight(final MotionEvent e1, final MotionEvent e2) {
        // Handle left-to-right as a select.
        handleSwipe(e1, true);
    }

    /**
     * Handle a select or unselect swipe event.
     *
     * @param downMotion
     *         Event that started the swipe
     * @param selected
     *         {@code true} if this was an attempt to select (i.e. left to right).
     */
    private void handleSwipe(final MotionEvent downMotion, final boolean selected) {
        int x = (int) downMotion.getRawX();
        int y = (int) downMotion.getRawY();

        Rect headerRect = new Rect();
        listView.getGlobalVisibleRect(headerRect);

        // Only handle swipes in the visible area of the message list
        if (headerRect.contains(x, y)) {
            int[] listPosition = new int[2];
            listView.getLocationOnScreen(listPosition);

            int listX = x - listPosition[0];
            int listY = y - listPosition[1];

            int listViewPosition = listView.pointToPosition(listX, listY);

            toggleMessageSelect(listViewPosition);
        }
    }

    private int listViewToAdapterPosition(int position) {
        if (position >= 0 && position < adapter.getCount()) {
            return position;
        }

        return AdapterView.INVALID_POSITION;
    }

    private int adapterToListViewPosition(int position) {
        if (position >= 0 && position < adapter.getCount()) {
            return position;
        }

        return AdapterView.INVALID_POSITION;
    }

    class MessageListActivityListener extends ActivityListener {
        @Override
        public void remoteSearchFailed(String folder, final String err) {
            handler.post(new Runnable() {
                @Override
                public void run() {
                    Activity activity = getActivity();
                    if (activity != null) {
                        Toast.makeText(activity, R.string.remote_search_error,
                                Toast.LENGTH_LONG).show();
                    }
                }
            });
        }

        @Override
        public void remoteSearchStarted(String folder) {
            handler.progress(true);
            handler.updateFooter(context.getString(R.string.remote_search_sending_query));
        }

        @Override
        public void enableProgressIndicator(boolean enable) {
            handler.progress(enable);
        }

        @Override
        public void remoteSearchFinished(String folder, int numResults, int maxResults, List<Message> extraResults) {
            handler.progress(false);
            handler.remoteSearchFinished();
            extraSearchResults = extraResults;
            if (extraResults != null && extraResults.size() > 0) {
                handler.updateFooter(String.format(context.getString(R.string.load_more_messages_fmt), maxResults));
            } else {
                handler.updateFooter(null);
            }
            fragmentListener.setMessageListProgress(Window.PROGRESS_END);

        }

        @Override
        public void remoteSearchServerQueryComplete(String folderName, int numResults, int maxResults) {
            handler.progress(true);
            if (maxResults != 0 && numResults > maxResults) {
                handler.updateFooter(context.getResources().getQuantityString(R.plurals.remote_search_downloading_limited,
                        maxResults, maxResults, numResults));
            } else {
                handler.updateFooter(context.getResources().getQuantityString(R.plurals.remote_search_downloading,
                        numResults, numResults));
            }
            fragmentListener.setMessageListProgress(Window.PROGRESS_START);
        }

        @Override
        public void informUserOfStatus() {
            handler.refreshTitle();
        }

        @Override
        public void synchronizeMailboxStarted(Account account, String folder) {
            if (updateForMe(account, folder)) {
                handler.progress(true);
                handler.folderLoading(folder, true);
            }
            super.synchronizeMailboxStarted(account, folder);
        }

        @Override
        public void synchronizeMailboxFinished(Account account, String folder,
        int totalMessagesInMailbox, int numNewMessages) {

            if (updateForMe(account, folder)) {
                handler.progress(false);
                handler.folderLoading(folder, false);
            }
            super.synchronizeMailboxFinished(account, folder, totalMessagesInMailbox, numNewMessages);
        }

        @Override
        public void synchronizeMailboxFailed(Account account, String folder, String message) {

            if (updateForMe(account, folder)) {
                handler.progress(false);
                handler.folderLoading(folder, false);
            }
            super.synchronizeMailboxFailed(account, folder, message);
        }

        @Override
        public void folderStatusChanged(Account account, String folder, int unreadMessageCount) {
            if (isSingleAccountMode() && isSingleFolderMode() && MessageListFragment.this.account.equals(account) &&
                    folderName.equals(folder)) {
                MessageListFragment.this.unreadMessageCount = unreadMessageCount;
            }
            super.folderStatusChanged(account, folder, unreadMessageCount);
        }

        private boolean updateForMe(Account account, String folder) {
            if (account == null || folder == null) {
                return false;
            }

            if (!Utility.arrayContains(accountUuids, account.getUuid())) {
                return false;
            }

            List<String> folderNames = search.getFolderNames();
            return (folderNames.isEmpty() || folderNames.contains(folder));
        }
    }


    private View getFooterView(ViewGroup parent) {
        if (footerView == null) {
            footerView = layoutInflater.inflate(R.layout.message_list_item_footer, parent, false);
            FooterViewHolder holder = new FooterViewHolder();
            holder.main = (TextView) footerView.findViewById(R.id.main_text);
            footerView.setTag(holder);
        }

        return footerView;
    }

    private void updateFooterView() {
        if (!search.isManualSearch() && currentFolder != null && account != null) {
            if (currentFolder.loading) {
                updateFooter(context.getString(R.string.status_loading_more));
            } else if (!currentFolder.moreMessages) {
                updateFooter(null);
            } else {
                String message;
                if (!currentFolder.lastCheckFailed) {
                    if (account.getDisplayCount() == 0) {
                        message = context.getString(R.string.message_list_load_more_messages_action);
                    } else {
                        message = String.format(context.getString(R.string.load_more_messages_fmt),
                                account.getDisplayCount());
                    }
                } else {
                    message = context.getString(R.string.status_loading_more_failed);
                }
                updateFooter(message);
            }
        } else {
            updateFooter(null);
        }
    }

    public void updateFooter(final String text) {
        if (footerView == null) {
            return;
        }

        FooterViewHolder holder = (FooterViewHolder) footerView.getTag();

        if (text != null) {
            holder.main.setText(text);
            holder.main.setVisibility(View.VISIBLE);
        } else {
            holder.main.setVisibility(View.GONE);
        }
    }

    static class FooterViewHolder {
        public TextView main;
    }

    /**
     * Set selection state for all messages.
     *
     * @param selected
     *         If {@code true} all messages get selected. Otherwise, all messages get deselected and
     *         action mode is finished.
     */
    private void setSelectionState(boolean selected) {
        if (selected) {
            if (adapter.getCount() == 0) {
                // Nothing to do if there are no messages
                return;
            }

            selectedCount = 0;
            for (int i = 0, end = adapter.getCount(); i < end; i++) {
                Cursor cursor = (Cursor) adapter.getItem(i);
                long uniqueId = cursor.getLong(uniqueIdColumn);
                this.selected.add(uniqueId);

                if (showingThreadedList) {
                    int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
                    selectedCount += (threadCount > 1) ? threadCount : 1;
                } else {
                    selectedCount++;
                }
            }

            if (actionMode == null) {
                startAndPrepareActionMode();
            }
            computeBatchDirection();
            updateActionModeTitle();
            computeSelectAllVisibility();
        } else {
            this.selected.clear();
            selectedCount = 0;
            if (actionMode != null) {
                actionMode.finish();
                actionMode = null;
            }
        }

        adapter.notifyDataSetChanged();
    }

    private void toggleMessageSelect(int listViewPosition) {
        int adapterPosition = listViewToAdapterPosition(listViewPosition);
        if (adapterPosition == AdapterView.INVALID_POSITION) {
            return;
        }

        toggleMessageSelectWithAdapterPosition(adapterPosition);
    }

    void toggleMessageFlagWithAdapterPosition(int adapterPosition) {
        Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
        boolean flagged = (cursor.getInt(FLAGGED_COLUMN) == 1);

        setFlag(adapterPosition,Flag.FLAGGED, !flagged);
    }

    void toggleMessageSelectWithAdapterPosition(int adapterPosition) {
        Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
        long uniqueId = cursor.getLong(uniqueIdColumn);

        boolean selected = this.selected.contains(uniqueId);
        if (!selected) {
            this.selected.add(uniqueId);
        } else {
            this.selected.remove(uniqueId);
        }

        int selectedCountDelta = 1;
        if (showingThreadedList) {
            int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
            if (threadCount > 1) {
                selectedCountDelta = threadCount;
            }
        }

        if (actionMode != null) {
            if (selectedCount == selectedCountDelta && selected) {
                actionMode.finish();
                actionMode = null;
                return;
            }
        } else {
            startAndPrepareActionMode();
        }

        if (selected) {
            selectedCount -= selectedCountDelta;
        } else {
            selectedCount += selectedCountDelta;
        }

        computeBatchDirection();
        updateActionModeTitle();

        computeSelectAllVisibility();

        adapter.notifyDataSetChanged();
    }

    private void updateActionModeTitle() {
        actionMode.setTitle(String.format(getString(R.string.actionbar_selected), selectedCount));
    }

    private void computeSelectAllVisibility() {
        actionModeCallback.showSelectAll(selected.size() != adapter.getCount());
    }

    private void computeBatchDirection() {
        boolean isBatchFlag = false;
        boolean isBatchRead = false;

        for (int i = 0, end = adapter.getCount(); i < end; i++) {
            Cursor cursor = (Cursor) adapter.getItem(i);
            long uniqueId = cursor.getLong(uniqueIdColumn);

            if (selected.contains(uniqueId)) {
                boolean read = (cursor.getInt(READ_COLUMN) == 1);
                boolean flagged = (cursor.getInt(FLAGGED_COLUMN) == 1);

                if (!flagged) {
                    isBatchFlag = true;
                }
                if (!read) {
                    isBatchRead = true;
                }

                if (isBatchFlag && isBatchRead) {
                    break;
                }
            }
        }

        actionModeCallback.showMarkAsRead(isBatchRead);
        actionModeCallback.showFlag(isBatchFlag);
    }

    private void setFlag(int adapterPosition, final Flag flag, final boolean newState) {
        if (adapterPosition == AdapterView.INVALID_POSITION) {
            return;
        }

        Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
        Account account = preferences.getAccount(cursor.getString(ACCOUNT_UUID_COLUMN));

        if (showingThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
            long threadRootId = cursor.getLong(THREAD_ROOT_COLUMN);
            messagingController.setFlagForThreads(account,
                    Collections.singletonList(threadRootId), flag, newState);
        } else {
            long id = cursor.getLong(ID_COLUMN);
            messagingController.setFlag(account, Collections.singletonList(id), flag,
                    newState);
        }

        computeBatchDirection();
    }

    private void setFlagForSelected(final Flag flag, final boolean newState) {
        if (selected.isEmpty()) {
            return;
        }

        Map<Account, List<Long>> messageMap = new HashMap<>();
        Map<Account, List<Long>> threadMap = new HashMap<>();
        Set<Account> accounts = new HashSet<>();

        for (int position = 0, end = adapter.getCount(); position < end; position++) {
            Cursor cursor = (Cursor) adapter.getItem(position);
            long uniqueId = cursor.getLong(uniqueIdColumn);

            if (selected.contains(uniqueId)) {
                String uuid = cursor.getString(ACCOUNT_UUID_COLUMN);
                Account account = preferences.getAccount(uuid);
                accounts.add(account);

                if (showingThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
                    List<Long> threadRootIdList = threadMap.get(account);
                    if (threadRootIdList == null) {
                        threadRootIdList = new ArrayList<>();
                        threadMap.put(account, threadRootIdList);
                    }

                    threadRootIdList.add(cursor.getLong(THREAD_ROOT_COLUMN));
                } else {
                    List<Long> messageIdList = messageMap.get(account);
                    if (messageIdList == null) {
                        messageIdList = new ArrayList<>();
                        messageMap.put(account, messageIdList);
                    }

                    messageIdList.add(cursor.getLong(ID_COLUMN));
                }
            }
        }

        for (Account account : accounts) {
            List<Long> messageIds = messageMap.get(account);
            List<Long> threadRootIds = threadMap.get(account);

            if (messageIds != null) {
                messagingController.setFlag(account, messageIds, flag, newState);
            }

            if (threadRootIds != null) {
                messagingController.setFlagForThreads(account, threadRootIds, flag, newState);
            }
        }

        computeBatchDirection();
    }

    private void onMove(MessageReference message) {
        onMove(Collections.singletonList(message));
    }

    /**
     * Display the message move activity.
     *
     * @param messages
     *         Never {@code null}.
     */
    private void onMove(List<MessageReference> messages) {
        if (!checkCopyOrMovePossible(messages, FolderOperation.MOVE)) {
            return;
        }

        String folderName;
        if (isThreadDisplay) {
            folderName = messages.get(0).getFolderName();
        } else if (singleFolderMode) {
            folderName = currentFolder.folder.getName();
        } else {
            folderName = null;
        }


        displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, folderName,
                messages.get(0).getAccountUuid(), null,
                messages);
    }

    private void onCopy(MessageReference message) {
        onCopy(Collections.singletonList(message));
    }

    /**
     * Display the message copy activity.
     *
     * @param messages
     *         Never {@code null}.
     */
    private void onCopy(List<MessageReference> messages) {
        if (!checkCopyOrMovePossible(messages, FolderOperation.COPY)) {
            return;
        }

        String folderName;
        if (isThreadDisplay) {
            folderName = messages.get(0).getFolderName();
        } else if (singleFolderMode) {
            folderName = currentFolder.folder.getName();
        } else {
            folderName = null;
        }

        displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folderName,
                messages.get(0).getAccountUuid(),
                null,
                messages);
    }

    private void onDebugClearLocally(MessageReference message) {
        messagingController.debugClearMessagesLocally(Collections.singletonList(message));
    }

    /**
     * Helper method to manage the invocation of {@link #startActivityForResult(Intent, int)} for a
     * folder operation ({@link ChooseFolder} activity), while saving a list of associated messages.
     *
     * @param requestCode
     *         If {@code >= 0}, this code will be returned in {@code onActivityResult()} when the
     *         activity exits.
     *
     * @see #startActivityForResult(Intent, int)
     */
    private void displayFolderChoice(int requestCode, String sourceFolderName,
            String accountUuid, String lastSelectedFolderName,
            List<MessageReference> messages) {
        Intent intent = new Intent(getActivity(), ChooseFolder.class);
        intent.putExtra(ChooseFolder.EXTRA_ACCOUNT, accountUuid);
        intent.putExtra(ChooseFolder.EXTRA_SEL_FOLDER, lastSelectedFolderName);

        if (sourceFolderName == null) {
            intent.putExtra(ChooseFolder.EXTRA_SHOW_CURRENT, "yes");
        } else {
            intent.putExtra(ChooseFolder.EXTRA_CUR_FOLDER, sourceFolderName);
        }

        // remember the selected messages for #onActivityResult
        activeMessages = messages;
        startActivityForResult(intent, requestCode);
    }

    private void onArchive(MessageReference message) {
        onArchive(Collections.singletonList(message));
    }

    private void onArchive(final List<MessageReference> messages) {
        Map<Account, List<MessageReference>> messagesByAccount = groupMessagesByAccount(messages);

        for (Entry<Account, List<MessageReference>> entry : messagesByAccount.entrySet()) {
            Account account = entry.getKey();
            String archiveFolder = account.getArchiveFolderName();

            if (!K9.FOLDER_NONE.equals(archiveFolder)) {
                move(entry.getValue(), archiveFolder);
            }
        }
    }

    private Map<Account, List<MessageReference>> groupMessagesByAccount(final List<MessageReference> messages) {
        Map<Account, List<MessageReference>> messagesByAccount = new HashMap<>();
        for (MessageReference message : messages) {
            Account account = preferences.getAccount(message.getAccountUuid());

            List<MessageReference> msgList = messagesByAccount.get(account);
            if (msgList == null) {
                msgList = new ArrayList<>();
                messagesByAccount.put(account, msgList);
            }

            msgList.add(message);
        }
        return messagesByAccount;
    }

    private void onSpam(MessageReference message) {
        onSpam(Collections.singletonList(message));
    }

    /**
     * Move messages to the spam folder.
     *
     * @param messages
     *         The messages to move to the spam folder. Never {@code null}.
     */
    private void onSpam(List<MessageReference> messages) {
        if (K9.confirmSpam()) {
            // remember the message selection for #onCreateDialog(int)
            activeMessages = messages;
            showDialog(R.id.dialog_confirm_spam);
        } else {
            onSpamConfirmed(messages);
        }
    }

    private void onSpamConfirmed(List<MessageReference> messages) {
        Map<Account, List<MessageReference>> messagesByAccount = groupMessagesByAccount(messages);

        for (Entry<Account, List<MessageReference>> entry : messagesByAccount.entrySet()) {
            Account account = entry.getKey();
            String spamFolder = account.getSpamFolderName();

            if (!K9.FOLDER_NONE.equals(spamFolder)) {
                move(entry.getValue(), spamFolder);
            }
        }
    }

    private enum FolderOperation {
        COPY, MOVE
    }

    /**
     * Display a Toast message if any message isn't synchronized
     *
     * @param messages
     *         The messages to copy or move. Never {@code null}.
     * @param operation
     *         The type of operation to perform. Never {@code null}.
     *
     * @return {@code true}, if operation is possible.
     */
    private boolean checkCopyOrMovePossible(final List<MessageReference> messages,
            final FolderOperation operation) {

        if (messages.isEmpty()) {
            return false;
        }

        boolean first = true;
        for (MessageReference message : messages) {
            if (first) {
                first = false;
                Account account = preferences.getAccount(message.getAccountUuid());
                if ((operation == FolderOperation.MOVE && !messagingController.isMoveCapable(account)) ||
                        (operation == FolderOperation.COPY && !messagingController.isCopyCapable(account))) {
                    return false;
                }
            }
            // message check
            if ((operation == FolderOperation.MOVE && !messagingController.isMoveCapable(message)) ||
                    (operation == FolderOperation.COPY && !messagingController.isCopyCapable(message))) {
                final Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message,
                                                   Toast.LENGTH_LONG);
                toast.show();
                return false;
            }
        }
        return true;
    }

    /**
     * Copy the specified messages to the specified folder.
     *
     * @param messages
     *         List of messages to copy. Never {@code null}.
     * @param destination
     *         The name of the destination folder. Never {@code null}.
     */
    private void copy(List<MessageReference> messages, final String destination) {
        copyOrMove(messages, destination, FolderOperation.COPY);
    }

    /**
     * Move the specified messages to the specified folder.
     *
     * @param messages
     *         The list of messages to move. Never {@code null}.
     * @param destination
     *         The name of the destination folder. Never {@code null}.
     */
    private void move(List<MessageReference> messages, final String destination) {
        copyOrMove(messages, destination, FolderOperation.MOVE);
    }

    /**
     * The underlying implementation for {@link #copy(List, String)} and
     * {@link #move(List, String)}. This method was added mainly because those 2
     * methods share common behavior.
     *
     * @param messages
     *         The list of messages to copy or move. Never {@code null}.
     * @param destination
     *         The name of the destination folder. Never {@code null} or {@link K9#FOLDER_NONE}.
     * @param operation
     *         Specifies what operation to perform. Never {@code null}.
     */
    private void copyOrMove(List<MessageReference> messages, final String destination,
            final FolderOperation operation) {

        Map<String, List<MessageReference>> folderMap = new HashMap<>();

        for (MessageReference message : messages) {
            if ((operation == FolderOperation.MOVE && !messagingController.isMoveCapable(message)) ||
                    (operation == FolderOperation.COPY && !messagingController.isCopyCapable(message))) {

                Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message,
                        Toast.LENGTH_LONG).show();

                // XXX return meaningful error value?

                // message isn't synchronized
                return;
            }

            String folderName = message.getFolderName();
            if (folderName.equals(destination)) {
                // Skip messages already in the destination folder
                continue;
            }

            List<MessageReference> outMessages = folderMap.get(folderName);
            if (outMessages == null) {
                outMessages = new ArrayList<>();
                folderMap.put(folderName, outMessages);
            }

            outMessages.add(message);
        }

        for (Map.Entry<String, List<MessageReference>> entry : folderMap.entrySet()) {
            String folderName = entry.getKey();
            List<MessageReference> outMessages = entry.getValue();
            Account account = preferences.getAccount(outMessages.get(0).getAccountUuid());

            if (operation == FolderOperation.MOVE) {
                if (showingThreadedList) {
                    messagingController.moveMessagesInThread(account, folderName, outMessages, destination);
                } else {
                    messagingController.moveMessages(account, folderName, outMessages, destination);
                }
            } else {
                if (showingThreadedList) {
                    messagingController.copyMessagesInThread(account, folderName, outMessages, destination);
                } else {
                    messagingController.copyMessages(account, folderName, outMessages, destination);
                }
            }
        }
    }


    class ActionModeCallback implements ActionMode.Callback {
        private MenuItem mSelectAll;
        private MenuItem mMarkAsRead;
        private MenuItem mMarkAsUnread;
        private MenuItem mFlag;
        private MenuItem mUnflag;

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            mSelectAll = menu.findItem(R.id.select_all);
            mMarkAsRead = menu.findItem(R.id.mark_as_read);
            mMarkAsUnread = menu.findItem(R.id.mark_as_unread);
            mFlag = menu.findItem(R.id.flag);
            mUnflag = menu.findItem(R.id.unflag);

            // we don't support cross account actions atm
            if (!singleAccountMode) {
                // show all
                menu.findItem(R.id.move).setVisible(true);
                menu.findItem(R.id.archive).setVisible(true);
                menu.findItem(R.id.spam).setVisible(true);
                menu.findItem(R.id.copy).setVisible(true);

                Set<String> accountUuids = getAccountUuidsForSelected();

                for (String accountUuid : accountUuids) {
                    Account account = preferences.getAccount(accountUuid);
                    if (account != null) {
                        setContextCapabilities(account, menu);
                    }
                }

            }
            return true;
        }

        /**
         * Get the set of account UUIDs for the selected messages.
         */
        private Set<String> getAccountUuidsForSelected() {
            int maxAccounts = accountUuids.length;
            Set<String> accountUuids = new HashSet<>(maxAccounts);

            for (int position = 0, end = adapter.getCount(); position < end; position++) {
                Cursor cursor = (Cursor) adapter.getItem(position);
                long uniqueId = cursor.getLong(uniqueIdColumn);

                if (selected.contains(uniqueId)) {
                    String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
                    accountUuids.add(accountUuid);

                    if (accountUuids.size() == MessageListFragment.this.accountUuids.length) {
                        break;
                    }
                }
            }

            return accountUuids;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            actionMode = null;
            mSelectAll = null;
            mMarkAsRead = null;
            mMarkAsUnread = null;
            mFlag = null;
            mUnflag = null;
            setSelectionState(false);
        }

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.message_list_context, menu);

            // check capabilities
            setContextCapabilities(account, menu);

            return true;
        }

        /**
         * Disables menu options not supported by the account type or current "search view".
         *
         * @param account
         *         The account to query for its capabilities.
         * @param menu
         *         The menu to adapt.
         */
        private void setContextCapabilities(Account account, Menu menu) {
            if (!singleAccountMode) {
                // We don't support cross-account copy/move operations right now
                menu.findItem(R.id.move).setVisible(false);
                menu.findItem(R.id.copy).setVisible(false);

                //TODO: we could support the archive and spam operations if all selected messages
                // belong to non-POP3 accounts
                menu.findItem(R.id.archive).setVisible(false);
                menu.findItem(R.id.spam).setVisible(false);

            } else {
                // hide unsupported
                if (!messagingController.isCopyCapable(account)) {
                    menu.findItem(R.id.copy).setVisible(false);
                }

                if (!messagingController.isMoveCapable(account)) {
                    menu.findItem(R.id.move).setVisible(false);
                    menu.findItem(R.id.archive).setVisible(false);
                    menu.findItem(R.id.spam).setVisible(false);
                }

                if (!account.hasArchiveFolder()) {
                    menu.findItem(R.id.archive).setVisible(false);
                }

                if (!account.hasSpamFolder()) {
                    menu.findItem(R.id.spam).setVisible(false);
                }
            }
        }

        public void showSelectAll(boolean show) {
            if (actionMode != null) {
                mSelectAll.setVisible(show);
            }
        }

        public void showMarkAsRead(boolean show) {
            if (actionMode != null) {
                mMarkAsRead.setVisible(show);
                mMarkAsUnread.setVisible(!show);
            }
        }

        public void showFlag(boolean show) {
            if (actionMode != null) {
                mFlag.setVisible(show);
                mUnflag.setVisible(!show);
            }
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            /*
             * In the following we assume that we can't move or copy
             * mails to the same folder. Also that spam isn't available if we are
             * in the spam folder,same for archive.
             *
             * This is the case currently so safe assumption.
             */
            switch (item.getItemId()) {
            case R.id.delete: {
                List<MessageReference> messages = getCheckedMessages();
                onDelete(messages);
                selectedCount = 0;
                break;
            }
            case R.id.mark_as_read: {
                setFlagForSelected(Flag.SEEN, true);
                break;
            }
            case R.id.mark_as_unread: {
                setFlagForSelected(Flag.SEEN, false);
                break;
            }
            case R.id.flag: {
                setFlagForSelected(Flag.FLAGGED, true);
                break;
            }
            case R.id.unflag: {
                setFlagForSelected(Flag.FLAGGED, false);
                break;
            }
            case R.id.select_all: {
                selectAll();
                break;
            }

            // only if the account supports this
            case R.id.archive: {
                onArchive(getCheckedMessages());
                selectedCount = 0;
                break;
            }
            case R.id.spam: {
                onSpam(getCheckedMessages());
                selectedCount = 0;
                break;
            }
            case R.id.move: {
                onMove(getCheckedMessages());
                selectedCount = 0;
                break;
            }
            case R.id.copy: {
                onCopy(getCheckedMessages());
                selectedCount = 0;
                break;
            }
            }
            if (selectedCount == 0) {
                actionMode.finish();
            }

            return true;
        }
    }

    @Override
    public void doPositiveClick(int dialogId) {
        switch (dialogId) {
            case R.id.dialog_confirm_spam: {
                onSpamConfirmed(activeMessages);
                // No further need for this reference
                activeMessages = null;
                break;
            }
            case R.id.dialog_confirm_delete: {
                onDeleteConfirmed(activeMessages);
                activeMessage = null;
                break;
            }
            case R.id.dialog_confirm_mark_all_as_read: {
                markAllAsRead();
                break;
            }
        }
    }

    @Override
    public void doNegativeClick(int dialogId) {
        switch (dialogId) {
            case R.id.dialog_confirm_spam:
            case R.id.dialog_confirm_delete: {
                // No further need for this reference
                activeMessages = null;
                break;
            }
        }
    }

    @Override
    public void dialogCancelled(int dialogId) {
        doNegativeClick(dialogId);
    }

    public void checkMail() {
        if (isSingleAccountMode() && isSingleFolderMode()) {
            messagingController.synchronizeMailbox(account, folderName, activityListener, null);
            messagingController.sendPendingMessages(account, activityListener);
        } else if (allAccounts) {
            messagingController.checkMail(context, null, true, true, activityListener);
        } else {
            for (String accountUuid : accountUuids) {
                Account account = preferences.getAccount(accountUuid);
                messagingController.checkMail(context, account, true, true, activityListener);
            }
        }
    }

    /**
     * We need to do some special clean up when leaving a remote search result screen. If no
     * remote search is in progress, this method does nothing special.
     */
    @Override
    public void onStop() {
        // If we represent a remote search, then kill that before going back.
        if (isRemoteSearch() && remoteSearchFuture != null) {
            try {
                Timber.i("Remote search in progress, attempting to abort...");
                // Canceling the future stops any message fetches in progress.
                final boolean cancelSuccess = remoteSearchFuture.cancel(true);   // mayInterruptIfRunning = true
                if (!cancelSuccess) {
                    Timber.e("Could not cancel remote search future.");
                }
                // Closing the folder will kill off the connection if we're mid-search.
                final Account searchAccount = account;
                final Folder remoteFolder = currentFolder.folder;
                remoteFolder.close();
                // Send a remoteSearchFinished() message for good measure.
                activityListener
                        .remoteSearchFinished(currentFolder.name, 0, searchAccount.getRemoteSearchNumResults(), null);
            } catch (Exception e) {
                // Since the user is going back, log and squash any exceptions.
                Timber.e(e, "Could not abort remote search before going back");
            }
        }

        // Workaround for Android bug https://issuetracker.google.com/issues/37008170
        if (swipeRefreshLayout != null) {
            swipeRefreshLayout.setRefreshing(false);
            swipeRefreshLayout.destroyDrawingCache();
            swipeRefreshLayout.clearAnimation();
        }

        super.onStop();
    }

    public void selectAll() {
        setSelectionState(true);
    }

    public void onMoveUp() {
        int currentPosition = listView.getSelectedItemPosition();
        if (currentPosition == AdapterView.INVALID_POSITION || listView.isInTouchMode()) {
            currentPosition = listView.getFirstVisiblePosition();
        }
        if (currentPosition > 0) {
            listView.setSelection(currentPosition - 1);
        }
    }

    public void onMoveDown() {
        int currentPosition = listView.getSelectedItemPosition();
        if (currentPosition == AdapterView.INVALID_POSITION || listView.isInTouchMode()) {
            currentPosition = listView.getFirstVisiblePosition();
        }

        if (currentPosition < listView.getCount()) {
            listView.setSelection(currentPosition + 1);
        }
    }

    public boolean openPrevious(MessageReference messageReference) {
        int position = getPosition(messageReference);
        if (position <= 0) {
            return false;
        }

        openMessageAtPosition(position - 1);
        return true;
    }

    public boolean openNext(MessageReference messageReference) {
        int position = getPosition(messageReference);
        if (position < 0 || position == adapter.getCount() - 1) {
            return false;
        }

        openMessageAtPosition(position + 1);
        return true;
    }

    public boolean isFirst(MessageReference messageReference) {
        return adapter.isEmpty() || messageReference.equals(getReferenceForPosition(0));
    }

    public boolean isLast(MessageReference messageReference) {
        return adapter.isEmpty() || messageReference.equals(getReferenceForPosition(adapter.getCount() - 1));
    }

    private MessageReference getReferenceForPosition(int position) {
        Cursor cursor = (Cursor) adapter.getItem(position);

        String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
        String folderName = cursor.getString(FOLDER_NAME_COLUMN);
        String messageUid = cursor.getString(UID_COLUMN);
        return new MessageReference(accountUuid, folderName, messageUid, null);
    }

    private void openMessageAtPosition(int position) {
        // Scroll message into view if necessary
        int listViewPosition = adapterToListViewPosition(position);
        if (listViewPosition != AdapterView.INVALID_POSITION &&
                (listViewPosition < listView.getFirstVisiblePosition() ||
                listViewPosition > listView.getLastVisiblePosition())) {
            listView.setSelection(listViewPosition);
        }

        MessageReference ref = getReferenceForPosition(position);

        // For some reason the listView.setSelection() above won't do anything when we call
        // onOpenMessage() (and consequently adapter.notifyDataSetChanged()) right away. So we
        // defer the call using MessageListHandler.
        handler.openMessage(ref);
    }

    private int getPosition(MessageReference messageReference) {
        for (int i = 0, len = adapter.getCount(); i < len; i++) {
            Cursor cursor = (Cursor) adapter.getItem(i);

            String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
            String folderName = cursor.getString(FOLDER_NAME_COLUMN);
            String uid = cursor.getString(UID_COLUMN);

            if (accountUuid.equals(messageReference.getAccountUuid()) &&
                    folderName.equals(messageReference.getFolderName()) &&
                    uid.equals(messageReference.getUid())) {
                return i;
            }
        }

        return -1;
    }

    public interface MessageListFragmentListener {
        void enableActionBarProgress(boolean enable);
        void setMessageListProgress(int level);
        void showThread(Account account, String folderName, long rootId);
        void showMoreFromSameSender(String senderAddress);
        void onResendMessage(MessageReference message);
        void onForward(MessageReference message);
        void onReply(MessageReference message);
        void onReplyAll(MessageReference message);
        void openMessage(MessageReference messageReference);
        void setMessageListTitle(String title);
        void setMessageListSubTitle(String subTitle);
        void setUnreadCount(int unread);
        void onCompose(Account account);
        boolean startSearch(Account account, String folderName);
        void remoteSearchStarted();
        void goBack();
        void updateMenu();
    }

    public void onReverseSort() {
        changeSort(sortType);
    }

    private MessageReference getSelectedMessage() {
        int listViewPosition = listView.getSelectedItemPosition();
        int adapterPosition = listViewToAdapterPosition(listViewPosition);

        return getMessageAtPosition(adapterPosition);
    }

    private int getAdapterPositionForSelectedMessage() {
        int listViewPosition = listView.getSelectedItemPosition();
        return listViewToAdapterPosition(listViewPosition);
    }

    private int getPositionForUniqueId(long uniqueId) {
        for (int position = 0, end = adapter.getCount(); position < end; position++) {
            Cursor cursor = (Cursor) adapter.getItem(position);
            if (cursor.getLong(uniqueIdColumn) == uniqueId) {
                return position;
            }
        }

        return AdapterView.INVALID_POSITION;
    }

    private MessageReference getMessageAtPosition(int adapterPosition) {
        if (adapterPosition == AdapterView.INVALID_POSITION) {
            return null;
        }

        Cursor cursor = (Cursor) adapter.getItem(adapterPosition);

        String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
        String folderName = cursor.getString(FOLDER_NAME_COLUMN);
        String messageUid = cursor.getString(UID_COLUMN);

        return new MessageReference(accountUuid, folderName, messageUid, null);
    }

    private List<MessageReference> getCheckedMessages() {
        List<MessageReference> messages = new ArrayList<>(selected.size());
        for (int position = 0, end = adapter.getCount(); position < end; position++) {
            Cursor cursor = (Cursor) adapter.getItem(position);
            long uniqueId = cursor.getLong(uniqueIdColumn);

            if (selected.contains(uniqueId)) {
                MessageReference message = getMessageAtPosition(position);
                if (message != null) {
                    messages.add(message);
                }
            }
        }

        return messages;
    }

    public void onDelete() {
        MessageReference message = getSelectedMessage();
        if (message != null) {
            onDelete(Collections.singletonList(message));
        }
    }

    public void toggleMessageSelect() {
        toggleMessageSelect(listView.getSelectedItemPosition());
    }

    public void onToggleFlagged() {
        onToggleFlag(Flag.FLAGGED, FLAGGED_COLUMN);
    }

    public void onToggleRead() {
        onToggleFlag(Flag.SEEN, READ_COLUMN);
    }

    private void onToggleFlag(Flag flag, int flagColumn) {
        int adapterPosition = getAdapterPositionForSelectedMessage();
        if (adapterPosition == ListView.INVALID_POSITION) {
            return;
        }

        Cursor cursor = (Cursor) adapter.getItem(adapterPosition);
        boolean flagState = (cursor.getInt(flagColumn) == 1);
        setFlag(adapterPosition, flag, !flagState);
    }

    public void onMove() {
        MessageReference message = getSelectedMessage();
        if (message != null) {
            onMove(message);
        }
    }

    public void onArchive() {
        MessageReference message = getSelectedMessage();
        if (message != null) {
            onArchive(message);
        }
    }

    public void onCopy() {
        MessageReference message = getSelectedMessage();
        if (message != null) {
            onCopy(message);
        }
    }

    public boolean isOutbox() {
        return (folderName != null && folderName.equals(account.getOutboxFolderName()));
    }

    public boolean isRemoteFolder() {
        if (search.isManualSearch() || isOutbox()) {
            return false;
        }

        if (!messagingController.isMoveCapable(account)) {
            // For POP3 accounts only the Inbox is a remote folder.
            return (folderName != null && folderName.equals(account.getInboxFolderName()));
        }

        return true;
    }

    public boolean isManualSearch() {
        return search.isManualSearch();
    }

    public boolean isAccountExpungeCapable() {
        try {
            return (account != null && account.getRemoteStore().isExpungeCapable());
        } catch (Exception e) {
            return false;
        }
    }

    public void onRemoteSearch() {
        // Remote search is useless without the network.
        if (hasConnectivity) {
            onRemoteSearchRequested();
        } else {
            Toast.makeText(getActivity(), getText(R.string.remote_search_unavailable_no_network),
                    Toast.LENGTH_SHORT).show();
        }
    }

    public boolean isRemoteSearch() {
        return remoteSearchPerformed;
    }

    public boolean isRemoteSearchAllowed() {
        if (!search.isManualSearch() || remoteSearchPerformed || !singleFolderMode) {
            return false;
        }

        boolean allowRemoteSearch = false;
        final Account searchAccount = account;
        if (searchAccount != null) {
            allowRemoteSearch = searchAccount.allowRemoteSearch();
        }

        return allowRemoteSearch;
    }

    public boolean onSearchRequested() {
        String folderName = (currentFolder != null) ? currentFolder.name : null;
        return fragmentListener.startSearch(account, folderName);
   }

    @Override
    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
        String accountUuid = accountUuids[id];
        Account account = preferences.getAccount(accountUuid);

        String threadId = getThreadId(search);

        Uri uri;
        String[] projection;
        boolean needConditions;
        if (threadId != null) {
            uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/thread/" + threadId);
            projection = PROJECTION;
            needConditions = false;
        } else if (showingThreadedList) {
            uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages/threaded");
            projection = THREADED_PROJECTION;
            needConditions = true;
        } else {
            uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages");
            projection = PROJECTION;
            needConditions = true;
        }

        StringBuilder query = new StringBuilder();
        List<String> queryArgs = new ArrayList<>();
        if (needConditions) {
            boolean selectActive = activeMessage != null && activeMessage.getAccountUuid().equals(accountUuid);

            if (selectActive) {
                query.append("(" + MessageColumns.UID + " = ? AND " + SpecialColumns.FOLDER_NAME + " = ?) OR (");
                queryArgs.add(activeMessage.getUid());
                queryArgs.add(activeMessage.getFolderName());
            }

            SqlQueryBuilder.buildWhereClause(account, search.getConditions(), query, queryArgs);

            if (selectActive) {
                query.append(')');
            }
        }

        String selection = query.toString();
        String[] selectionArgs = queryArgs.toArray(new String[0]);

        String sortOrder = buildSortOrder();

        return new CursorLoader(getActivity(), uri, projection, selection, selectionArgs,
                sortOrder);
    }

    private String getThreadId(LocalSearch search) {
        for (ConditionsTreeNode node : search.getLeafSet()) {
            SearchCondition condition = node.mCondition;
            if (condition.field == SearchField.THREAD_ID) {
                return condition.value;
            }
        }

        return null;
    }

    private String buildSortOrder() {
        String sortColumn;
        switch (sortType) {
            case SORT_ARRIVAL: {
                sortColumn = MessageColumns.INTERNAL_DATE;
                break;
            }
            case SORT_ATTACHMENT: {
                sortColumn = "(" + MessageColumns.ATTACHMENT_COUNT + " < 1)";
                break;
            }
            case SORT_FLAGGED: {
                sortColumn = "(" + MessageColumns.FLAGGED + " != 1)";
                break;
            }
            case SORT_SENDER: {
                //FIXME
                sortColumn = MessageColumns.SENDER_LIST;
                break;
            }
            case SORT_SUBJECT: {
                sortColumn = MessageColumns.SUBJECT + " COLLATE NOCASE";
                break;
            }
            case SORT_UNREAD: {
                sortColumn = MessageColumns.READ;
                break;
            }
            case SORT_DATE:
            default: {
                sortColumn = MessageColumns.DATE;
            }
        }

        String sortDirection = (sortAscending) ? " ASC" : " DESC";
        String secondarySort;
        if (sortType == SortType.SORT_DATE || sortType == SortType.SORT_ARRIVAL) {
            secondarySort = "";
        } else {
            secondarySort = MessageColumns.DATE + ((sortDateAscending) ? " ASC, " : " DESC, ");
        }

        return sortColumn + sortDirection + ", " + secondarySort + MessageColumns.ID + " DESC";
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
        if (isThreadDisplay && data.getCount() == 0) {
            handler.goBack();
            return;
        }

        swipeRefreshLayout.setRefreshing(false);
        swipeRefreshLayout.setEnabled(isPullToRefreshAllowed());

        final int loaderId = loader.getId();
        cursors[loaderId] = data;
        cursorValid[loaderId] = true;

        Cursor cursor;
        if (cursors.length > 1) {
            cursor = new MergeCursorWithUniqueId(cursors, getComparator());
            uniqueIdColumn = cursor.getColumnIndex("_id");
        } else {
            cursor = data;
            uniqueIdColumn = ID_COLUMN;
        }

        if (isThreadDisplay) {
            if (cursor.moveToFirst()) {
                title = cursor.getString(SUBJECT_COLUMN);
                if (!TextUtils.isEmpty(title)) {
                    title = Utility.stripSubject(title);
                }
                if (TextUtils.isEmpty(title)) {
                    title = getString(R.string.general_no_subject);
                }
                updateTitle();
            } else {
                //TODO: empty thread view -> return to full message list
            }
        }

        cleanupSelected(cursor);
        updateContextMenu(cursor);

        adapter.swapCursor(cursor);

        resetActionMode();
        computeBatchDirection();

        if (isLoadFinished()) {
            if (savedListState != null) {
                handler.restoreListPosition();
            }

            fragmentListener.updateMenu();
        }
    }

    private void updateMoreMessagesOfCurrentFolder() {
        if (folderName != null) {
            try {
                LocalFolder folder = MlfUtils.getOpenFolder(folderName, account);
                currentFolder.setMoreMessagesFromFolder(folder);
            } catch (MessagingException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public boolean isLoadFinished() {
        if (cursorValid == null) {
            return false;
        }

        for (boolean cursorValid : this.cursorValid) {
            if (!cursorValid) {
                return false;
            }
        }

        return true;
    }

    /**
     * Close the context menu when the message it was opened for is no longer in the message list.
     */
    private void updateContextMenu(Cursor cursor) {
        if (contextMenuUniqueId == 0) {
            return;
        }

        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
            long uniqueId = cursor.getLong(uniqueIdColumn);
            if (uniqueId == contextMenuUniqueId) {
                return;
            }
        }

        contextMenuUniqueId = 0;
        Activity activity = getActivity();
        if (activity != null) {
            activity.closeContextMenu();
        }
    }

    private void cleanupSelected(Cursor cursor) {
        if (selected.isEmpty()) {
            return;
        }

        Set<Long> selected = new HashSet<>();
        for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
            long uniqueId = cursor.getLong(uniqueIdColumn);
            if (this.selected.contains(uniqueId)) {
                selected.add(uniqueId);
            }
        }

        this.selected = selected;
    }

    /**
     * Starts or finishes the action mode when necessary.
     */
    private void resetActionMode() {
        if (selected.isEmpty()) {
            if (actionMode != null) {
                actionMode.finish();
            }
            return;
        }

        if (actionMode == null) {
            startAndPrepareActionMode();
        }

        recalculateSelectionCount();
        updateActionModeTitle();
    }

    private void startAndPrepareActionMode() {
        actionMode = getActivity().startActionMode(actionModeCallback);
        actionMode.invalidate();
    }

    /**
     * Recalculates the selection count.
     *
     * <p>
     * For non-threaded lists this is simply the number of visibly selected messages. If threaded
     * view is enabled this method counts the number of messages in the selected threads.
     * </p>
     */
    private void recalculateSelectionCount() {
        if (!showingThreadedList) {
            selectedCount = selected.size();
            return;
        }

        selectedCount = 0;
        for (int i = 0, end = adapter.getCount(); i < end; i++) {
            Cursor cursor = (Cursor) adapter.getItem(i);
            long uniqueId = cursor.getLong(uniqueIdColumn);

            if (selected.contains(uniqueId)) {
                int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
                selectedCount += (threadCount > 1) ? threadCount : 1;
            }
        }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {
        selected.clear();
        adapter.swapCursor(null);
    }

    Account getAccountFromCursor(Cursor cursor) {
        String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
        return preferences.getAccount(accountUuid);
    }

    void remoteSearchFinished() {
        remoteSearchFuture = null;
    }

    /**
     * Mark a message as 'active'.
     *
     * <p>
     * The active message is the one currently displayed in the message view portion of the split
     * view.
     * </p>
     *
     * @param messageReference
     *         {@code null} to not mark any message as being 'active'.
     */
    public void setActiveMessage(MessageReference messageReference) {
        activeMessage = messageReference;

        // Reload message list with modified query that always includes the active message
        if (isAdded()) {
            restartLoader();
        }

        // Redraw list immediately
        if (adapter != null) {
            adapter.notifyDataSetChanged();
        }
    }

    public boolean isSingleAccountMode() {
        return singleAccountMode;
    }

    public boolean isSingleFolderMode() {
        return singleFolderMode;
    }

    public boolean isInitialized() {
        return initialized;
    }

    public boolean isMarkAllAsReadSupported() {
        return (isSingleAccountMode() && isSingleFolderMode());
    }

    public void confirmMarkAllAsRead() {
        if (K9.confirmMarkAllRead()) {
            showDialog(R.id.dialog_confirm_mark_all_as_read);
        } else {
            markAllAsRead();
        }
    }

    private void markAllAsRead() {
        if (isMarkAllAsReadSupported()) {
            messagingController.markAllMessagesRead(account, folderName);
        }
    }

    public boolean isCheckMailSupported() {
        return (allAccounts || !isSingleAccountMode() || !isSingleFolderMode() ||
                isRemoteFolder());
    }

    private boolean isCheckMailAllowed() {
        return (!isManualSearch() && isCheckMailSupported());
    }

    private boolean isPullToRefreshAllowed() {
        return (isRemoteSearchAllowed() || isCheckMailAllowed());
    }

    LayoutInflater getK9LayoutInflater() {
        return layoutInflater;
    }
}
