/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.email.activity;

import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Loader;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.CursorAdapter;
import android.widget.TextView;

import com.android.email.FolderProperties;
import com.android.email.R;
import com.android.email.ResourceHelper;
import com.android.email.data.ClosingMatrixCursor;
import com.android.email.data.ThrottlingCursorLoader;
import com.android.emailcommon.provider.Account;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.AccountColumns;
import com.android.emailcommon.provider.EmailContent.MailboxColumns;
import com.android.emailcommon.provider.Mailbox;
import com.android.emailcommon.utility.Utility;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;

import java.util.ArrayList;
import java.util.Collection;

/**
 * Account selector spinner.
 *
 * TODO Test it!
 */
public class AccountSelectorAdapter extends CursorAdapter {
    /** meta data column for an message count (unread or total, depending on row) */
    private static final String MESSAGE_COUNT = "unreadCount";

    /** meta data column for the row type; used for display purposes */
    private static final String ROW_TYPE = "rowType";

    /** meta data position of the currently selected account in the drop-down list */
    private static final String ACCOUNT_POSITION = "accountPosition";

    /** "account id" virtual column name for the matrix cursor */
    private static final String ACCOUNT_ID = "accountId";

    private static final int ROW_TYPE_HEADER = AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
    @SuppressWarnings("unused")
    private static final int ROW_TYPE_MAILBOX = 0;
    private static final int ROW_TYPE_ACCOUNT = 1;
    private static final int ITEM_VIEW_TYPE_ACCOUNT = 0;
    static final int UNKNOWN_POSITION = -1;
    /** Projection for account database query */
    private static final String[] ACCOUNT_PROJECTION = new String[] {
        EmailContent.RECORD_ID,
        Account.DISPLAY_NAME,
        Account.EMAIL_ADDRESS,
    };
    /**
     * Projection used for the selector display; we add meta data that doesn't exist in the
     * account database, so, this should be a super-set of {@link #ACCOUNT_PROJECTION}.
     */
    private static final String[] ADAPTER_PROJECTION = new String[] {
        ROW_TYPE,
        EmailContent.RECORD_ID,
        Account.DISPLAY_NAME,
        Account.EMAIL_ADDRESS,
        MESSAGE_COUNT,
        ACCOUNT_POSITION, // TODO Probably we don't really need this
        ACCOUNT_ID,
    };

    /** Sort order.  Show the default account first. */
    private static final String ORDER_BY = Account.IS_DEFAULT + " desc, " + Account.RECORD_ID;

    @SuppressWarnings("hiding")
    private final Context mContext;
    private final LayoutInflater mInflater;
    private final ResourceHelper mResourceHelper;

    /**
     * Returns a loader that can populate the account spinner.
     * @param context a context
     * @param accountId the ID of the currently viewed account
     */
    public static Loader<Cursor> createLoader(Context context, long accountId, long mailboxId) {
        return new AccountsLoader(context, accountId, mailboxId, UiUtilities.useTwoPane(context));
    }

    public AccountSelectorAdapter(Context context) {
        super(context, null, 0 /* no auto-requery */);
        mContext = context;
        mInflater = LayoutInflater.from(context);
        mResourceHelper = ResourceHelper.getInstance(context);
    }

    /**
     * {@inheritDoc}
     *
     * The account selector view can contain one of four types of row data:
     * <ol>
     * <li>headers</li>
     * <li>accounts</li>
     * <li>recent mailboxes</li>
     * <li>"show all folders"</li>
     * </ol>
     * Headers are handled separately as they have a unique layout and cannot be interacted with.
     * Accounts, recent mailboxes and "show all folders" all have the same interaction model and
     * share a very similar layout. The single difference is that both accounts and recent
     * mailboxes display an unread count; whereas "show all folders" does not. To determine
     * if a particular row is "show all folders" verify that a) it's not an account row and
     * b) it's ID is {@link Mailbox#NO_MAILBOX}.
     *
     * TODO Use recycled views.  ({@link #getViewTypeCount} and {@link #getItemViewType})
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        Cursor c = getCursor();
        c.moveToPosition(position);
        View view;
        if (c.getInt(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER) {
            view = mInflater.inflate(R.layout.action_bar_spinner_dropdown_header, parent, false);
            final TextView displayNameView = (TextView) view.findViewById(R.id.display_name);
            final String displayName = getDisplayName(c);
            displayNameView.setText(displayName);
        } else {
            view = mInflater.inflate(R.layout.action_bar_spinner_dropdown, parent, false);
            final TextView displayNameView = (TextView) view.findViewById(R.id.display_name);
            final TextView emailAddressView = (TextView) view.findViewById(R.id.email_address);
            final TextView unreadCountView = (TextView) view.findViewById(R.id.unread_count);
            final View chipView = view.findViewById(R.id.color_chip);

            final String displayName = getDisplayName(c);
            final String emailAddress = getAccountEmailAddress(c);

            displayNameView.setText(displayName);

            // Show the email address only when it's different from the display name.
            boolean isAccount = isAccountItem(c);
            if (displayName.equals(emailAddress) || !isAccount) {
                emailAddressView.setVisibility(View.GONE);
            } else {
                emailAddressView.setVisibility(View.VISIBLE);
                emailAddressView.setText(emailAddress);
            }

            long id = getId(c);
            if (isAccount || id != Mailbox.NO_MAILBOX) {
                unreadCountView.setVisibility(View.VISIBLE);
                unreadCountView.setText(UiUtilities.getMessageCountForUi(mContext,
                        getAccountUnreadCount(c), true));

                // If we're on a combined account, show the color chip indicators for all real
                // accounts so it can be used as a legend.
                boolean isCombinedActive =
                        ((CursorWithExtras) c).getAccountId() == Account.ACCOUNT_ID_COMBINED_VIEW;

                if (isCombinedActive && Account.isNormalAccount(id)) {
                    chipView.setBackgroundColor(mResourceHelper.getAccountColor(id));
                    chipView.setVisibility(View.VISIBLE);
                } else {
                    chipView.setVisibility(View.GONE);
                }
            } else {
                unreadCountView.setVisibility(View.INVISIBLE);
                chipView.setVisibility(View.GONE);
            }

        }
        return view;
    }

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        return null; // we don't reuse views.  This method never gets called.
    }

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        // we don't reuse views.  This method never gets called.
    }

    @Override
    public int getViewTypeCount() {
        return 2;
    }

    @Override
    public int getItemViewType(int position) {
        Cursor c = getCursor();
        c.moveToPosition(position);
        return c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_HEADER
                ? AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER
                : ITEM_VIEW_TYPE_ACCOUNT;
    }

    @Override
    public boolean areAllItemsEnabled() {
        return false;
    }

    @Override
    public boolean isEnabled(int position) {
        return (getItemViewType(position) != AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER);
    }

    public boolean isAccountItem(int position) {
        Cursor c = getCursor();
        c.moveToPosition(position);
        return isAccountItem(c);
    }

    public boolean isAccountItem(Cursor c) {
        return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_ACCOUNT);
    }

    public boolean isMailboxItem(int position) {
        Cursor c = getCursor();
        c.moveToPosition(position);
        return (c.getLong(c.getColumnIndex(ROW_TYPE)) == ROW_TYPE_MAILBOX);
    }

    private int getAccountUnreadCount(Cursor c) {
        return getMessageCount(c);
    }

    /**
     * Returns the account/mailbox ID extracted from the given cursor.
     */
    private static long getId(Cursor c) {
        return c.getLong(c.getColumnIndex(EmailContent.RECORD_ID));
    }

    /**
     * @return ID of the account / mailbox for a row
     */
    public long getId(int position) {
        final Cursor c = getCursor();
        return c.moveToPosition(position) ? getId(c) : Account.NO_ACCOUNT;
    }

    /**
     * @return ID of the account for a row
     */
    public long getAccountId(int position) {
        final Cursor c = getCursor();
        return c.moveToPosition(position)
                ? c.getLong(c.getColumnIndex(ACCOUNT_ID))
                : Account.NO_ACCOUNT;
    }

    /** Returns the account name extracted from the given cursor. */
    static String getDisplayName(Cursor cursor) {
        return cursor.getString(cursor.getColumnIndex(Account.DISPLAY_NAME));
    }

    /** Returns the email address extracted from the given cursor. */
    private static String getAccountEmailAddress(Cursor cursor) {
        return cursor.getString(cursor.getColumnIndex(Account.EMAIL_ADDRESS));
    }

    /**
     * Returns the message count (unread or total, depending on row) extracted from the given
     * cursor.
     */
    private static int getMessageCount(Cursor cursor) {
        return cursor.getInt(cursor.getColumnIndex(MESSAGE_COUNT));
    }

    private static String sCombinedViewDisplayName;
    private static String getCombinedViewDisplayName(Context c) {
        if (sCombinedViewDisplayName == null) {
            sCombinedViewDisplayName = c.getResources().getString(
                    R.string.mailbox_list_account_selector_combined_view);
        }
        return sCombinedViewDisplayName;
    }

    /**
     * Load the account list.  The resulting cursor contains
     * - Account info
     * - # of unread messages in inbox
     * - The "Combined view" row if there's more than one account.
     */
    @VisibleForTesting
    static class AccountsLoader extends ThrottlingCursorLoader {
        private final Context mContext;
        private final long mAccountId;
        private final long mMailboxId;
        private final boolean mUseTwoPane; // Injectable for test
        private final FolderProperties mFolderProperties;

        @VisibleForTesting
        AccountsLoader(Context context, long accountId, long mailboxId, boolean useTwoPane) {
            // Super class loads a regular account cursor, but we replace it in loadInBackground().
            super(context, Account.CONTENT_URI, ACCOUNT_PROJECTION, null, null,
                    ORDER_BY);
            mContext = context;
            mAccountId = accountId;
            mMailboxId = mailboxId;
            mFolderProperties = FolderProperties.getInstance(mContext);
            mUseTwoPane = useTwoPane;
        }

        @Override
        public Cursor loadInBackground() {
            final Cursor accountsCursor = super.loadInBackground();
            // Use ClosingMatrixCursor so that accountsCursor gets closed too when it's closed.
            final CursorWithExtras resultCursor
                    = new CursorWithExtras(ADAPTER_PROJECTION, accountsCursor);
            final int accountPosition = addAccountsToCursor(resultCursor, accountsCursor);
            addMailboxesToCursor(resultCursor, accountPosition);

            resultCursor.setAccountMailboxInfo(getContext(), mAccountId, mMailboxId);
            return resultCursor;
        }

        /** Adds the account list [with extra meta data] to the given matrix cursor */
        private int addAccountsToCursor(CursorWithExtras matrixCursor, Cursor accountCursor) {
            int accountPosition = UNKNOWN_POSITION;
            accountCursor.moveToPosition(-1);

            matrixCursor.mAccountCount = accountCursor.getCount();
            int totalUnread = 0;
            while (accountCursor.moveToNext()) {
                // Add account, with its unread count.
                final long accountId = accountCursor.getLong(0);
                final int unread = Mailbox.getUnreadCountByAccountAndMailboxType(
                        mContext, accountId, Mailbox.TYPE_INBOX);
                final String name = getDisplayName(accountCursor);
                final String emailAddress = getAccountEmailAddress(accountCursor);
                addRow(matrixCursor, ROW_TYPE_ACCOUNT, accountId, name, emailAddress, unread,
                    UNKNOWN_POSITION, accountId);
                totalUnread += unread;
                if (accountId == mAccountId) {
                    accountPosition = accountCursor.getPosition();
                }
            }
            // Add "combined view" if more than one account exists
            final int countAccounts = accountCursor.getCount();
            if (countAccounts > 1) {
                final String accountCount = mContext.getResources().getQuantityString(
                        R.plurals.number_of_accounts, countAccounts, countAccounts);
                addRow(matrixCursor, ROW_TYPE_ACCOUNT, Account.ACCOUNT_ID_COMBINED_VIEW,
                        getCombinedViewDisplayName(mContext),
                        accountCount, totalUnread, UNKNOWN_POSITION,
                        Account.ACCOUNT_ID_COMBINED_VIEW);

                // Increment the account count for the combined account.
                matrixCursor.mAccountCount++;
            }
            return accountPosition;
        }

        /**
         * Adds the recent mailbox list / "show all folders" to the given cursor.
         *
         * @param matrixCursor the cursor to add the list to
         * @param accountPosition the cursor position of the currently selected account
         */
        private void addMailboxesToCursor(CursorWithExtras matrixCursor, int accountPosition) {
            if (mAccountId == Account.NO_ACCOUNT) {
                return; // Account not selected
            }
            if (mAccountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
                if (!mUseTwoPane) {
                    // TODO We may want a header for this to separate it from the account list
                    addShowAllFoldersRow(matrixCursor, accountPosition);
                }
                return;
            }
            String emailAddress = null;
            if (accountPosition != UNKNOWN_POSITION) {
                matrixCursor.moveToPosition(accountPosition);
                emailAddress =
                        matrixCursor.getString(matrixCursor.getColumnIndex(Account.EMAIL_ADDRESS));
            }
            RecentMailboxManager mailboxManager = RecentMailboxManager.getInstance(mContext);
            ArrayList<Long> recentMailboxes = null;
            if (!mUseTwoPane) {
                // Do not display recent mailboxes in the account spinner for the two pane view
                recentMailboxes = mailboxManager.getMostRecent(mAccountId, mUseTwoPane);
            }
            final int recentCount = (recentMailboxes == null) ? 0 : recentMailboxes.size();
            matrixCursor.mRecentCount = recentCount;

            if (!mUseTwoPane) {
                // "Recent mailboxes" header
                addHeaderRow(matrixCursor, mContext.getString(
                        R.string.mailbox_list_account_selector_mailbox_header_fmt, emailAddress));
            }

            if (recentCount > 0) {
                addMailboxRows(matrixCursor, accountPosition, recentMailboxes);
            }

            if (!mUseTwoPane) {
                addShowAllFoldersRow(matrixCursor, accountPosition);
            }
        }

        private void addShowAllFoldersRow(CursorWithExtras matrixCursor, int accountPosition) {
            matrixCursor.mHasShowAllFolders = true;
            String name = mContext.getString(
                    R.string.mailbox_list_account_selector_show_all_folders);
            addRow(matrixCursor, ROW_TYPE_MAILBOX, Mailbox.NO_MAILBOX, name, null, 0,
                    accountPosition, mAccountId);
        }


        private static final String[] RECENT_MAILBOX_INFO_PROJECTION = new String[] {
            MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
            MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
        };

        private void addMailboxRows(MatrixCursor matrixCursor, int accountPosition,
                Collection<Long> mailboxIds) {
            Cursor c = mContext.getContentResolver().query(
                    Mailbox.CONTENT_URI, RECENT_MAILBOX_INFO_PROJECTION,
                    Utility.buildInSelection(MailboxColumns.ID, mailboxIds), null,
                    RecentMailboxManager.RECENT_MAILBOXES_SORT_ORDER);
            try {
                c.moveToPosition(-1);
                while (c.moveToNext()) {
                    addRow(matrixCursor, ROW_TYPE_MAILBOX,
                            c.getLong(c.getColumnIndex(MailboxColumns.ID)),
                            mFolderProperties.getDisplayName(c), null,
                            mFolderProperties.getMessageCount(c), accountPosition, mAccountId);
                }
            } finally {
                c.close();
            }
        }

        private void addHeaderRow(MatrixCursor cursor, String name) {
            addRow(cursor, ROW_TYPE_HEADER, 0L, name, null, 0, UNKNOWN_POSITION,
                    Account.NO_ACCOUNT);
        }

        /** Adds a row to the given cursor */
        private void addRow(MatrixCursor cursor, int rowType, long id, String name,
                String emailAddress, int messageCount, int listPosition, long accountId) {
            cursor.newRow()
                .add(rowType)
                .add(id)
                .add(name)
                .add(emailAddress)
                .add(messageCount)
                .add(listPosition)
                .add(accountId);
        }
    }

    /** Cursor with some extra meta data. */
    static class CursorWithExtras extends ClosingMatrixCursor {

        /** Number of account elements, including the combined account row. */
        private int mAccountCount;
        /** Number of recent mailbox elements */
        private int mRecentCount;
        private boolean mHasShowAllFolders;

        private boolean mAccountExists;

        /**
         * Account ID that's loaded.
         */
        private long mAccountId;
        private String mAccountDisplayName;

        /**
         * Mailbox ID that's loaded.
         */
        private long mMailboxId;
        private String mMailboxDisplayName;
        private int mMailboxMessageCount;

        @VisibleForTesting
        CursorWithExtras(String[] columnNames, Cursor innerCursor) {
            super(columnNames, innerCursor);
        }

        private static final String[] ACCOUNT_INFO_PROJECTION = new String[] {
            AccountColumns.DISPLAY_NAME,
        };
        private static final String[] MAILBOX_INFO_PROJECTION = new String[] {
            MailboxColumns.ID, MailboxColumns.DISPLAY_NAME, MailboxColumns.TYPE,
            MailboxColumns.UNREAD_COUNT, MailboxColumns.MESSAGE_COUNT
        };

        /**
         * Set the current account/mailbox info.
         */
        @VisibleForTesting
        void setAccountMailboxInfo(Context context, long accountId, long mailboxId) {
            mAccountId = accountId;
            mMailboxId = mailboxId;

            // Get account info
            if (accountId == Account.ACCOUNT_ID_COMBINED_VIEW) {
                // We need to treat ACCOUNT_ID_COMBINED_VIEW specially...
                mAccountExists = true;
                mAccountDisplayName = getCombinedViewDisplayName(context);
                if (mailboxId != Mailbox.NO_MAILBOX) {
                    setCombinedMailboxInfo(context, mailboxId);
                }
                return;
            }

            mAccountDisplayName = Utility.getFirstRowString(context,
                    ContentUris.withAppendedId(Account.CONTENT_URI, accountId),
                    ACCOUNT_INFO_PROJECTION, null, null, null, 0, null);
            if (mAccountDisplayName == null) {
                // Account gone!
                mAccountExists = false;
                return;
            }
            mAccountExists = true;

            // If mailbox not specified, done.
            if (mMailboxId == Mailbox.NO_MAILBOX) {
                return;
            }
            // Combined mailbox?
            // Unfortunately this can happen even when account != ACCOUNT_ID_COMBINED_VIEW,
            // when you open "starred" on 2-pane on non-combined view.
            if (mMailboxId < 0) {
                setCombinedMailboxInfo(context, mailboxId);
                return;
            }

            // Get mailbox info
            final ContentResolver r = context.getContentResolver();
            final Cursor mailboxCursor = r.query(
                    ContentUris.withAppendedId(Mailbox.CONTENT_URI, mailboxId),
                    MAILBOX_INFO_PROJECTION, null, null, null);
            try {
                if (mailboxCursor.moveToFirst()) {
                    final FolderProperties fp = FolderProperties.getInstance(context);
                    mMailboxDisplayName = fp.getDisplayName(mailboxCursor);
                    mMailboxMessageCount = fp.getMessageCount(mailboxCursor);
                }
            } finally {
                mailboxCursor.close();
            }
        }

        private void setCombinedMailboxInfo(Context context, long mailboxId) {
            Preconditions.checkState(mailboxId < -1, "Not combined mailbox");
            mMailboxDisplayName = FolderProperties.getInstance(context)
                    .getCombinedMailboxName(mMailboxId);

            mMailboxMessageCount = FolderProperties.getMessageCountForCombinedMailbox(
                    context, mailboxId);
        }

        /**
         * Returns the cursor position of the item with the given ID. Or {@link #UNKNOWN_POSITION}
         * if the given ID does not exist.
         */
        int getPosition(long id) {
            moveToPosition(-1);
            while(moveToNext()) {
                if (id == getId(this)) {
                    return getPosition();
                }
            }
            return UNKNOWN_POSITION;
        }

        public int getAccountCount() {
            return mAccountCount;
        }

        @VisibleForTesting
        public int getRecentMailboxCount() {
            return mRecentCount;
        }

        /**
         * @return true if the cursor has more than one selectable item so we should enable the
         *     spinner.
         */
        public boolean shouldEnableSpinner() {
            return mHasShowAllFolders || (mAccountCount + mRecentCount > 1);
        }

        public long getAccountId() {
            return mAccountId;
        }

        public String getAccountDisplayName() {
            return mAccountDisplayName;
        }

        @VisibleForTesting
        public long getMailboxId() {
            return mMailboxId;
        }

        public String getMailboxDisplayName() {
            return mMailboxDisplayName;
        }

        public int getMailboxMessageCount() {
            return mMailboxMessageCount;
        }

        /**
         * @return {@code true} if the specified accuont exists.
         */
        public boolean accountExists() {
            return mAccountExists;
        }
    }
}
