/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.gecko;

import org.mozilla.gecko.AwesomeBar.ContextMenuSubject;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.BrowserDB.URLColumns;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.util.GamepadUtils;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UiAsyncTask;
import org.mozilla.gecko.widget.FaviconView;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.AdapterView;
import android.widget.FilterQueryProvider;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.List;

public class AllPagesTab extends AwesomeBarTab implements GeckoEventListener {
    public static final String LOGTAG = "GeckoAllPagesTab";
    private static final String TAG = "allPages";

    private static final int SUGGESTION_TIMEOUT = 3000;
    private static final int SUGGESTION_MAX = 3;
    private static final int ANIMATION_DURATION = 250;
    // The maximum number of rows deep in a search we'll dig for an autocomplete result
    private static final int MAX_AUTOCOMPLETE_SEARCH = 20;

    private String mSearchTerm;
    private ArrayList<SearchEngine> mSearchEngines;
    private SuggestClient mSuggestClient;
    private boolean mSuggestionsEnabled;
    private AsyncTask<String, Void, ArrayList<String>> mSuggestTask;
    private AwesomeBarCursorAdapter mCursorAdapter = null;
    private boolean mTelemetrySent = false;
    private LinearLayout mAllPagesView;
    private boolean mAnimateSuggestions;
    private View mSuggestionsOptInPrompt;
    private Handler mHandler;
    private ListView mListView;
    private volatile AutocompleteHandler mAutocompleteHandler = null;

    private static final int MESSAGE_LOAD_FAVICONS = 1;
    private static final int MESSAGE_UPDATE_FAVICONS = 2;
    private static final int DELAY_SHOW_THUMBNAILS = 550;

    private class SearchEntryViewHolder {
        public FlowLayout suggestionView;
        public FaviconView iconView;
        public LinearLayout userEnteredView;
        public TextView userEnteredTextView;
    }

    public AllPagesTab(Context context) {
        super(context);
        mSearchEngines = new ArrayList<SearchEngine>();

        registerEventListener("SearchEngines:Data");
        GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:Get", null));

        mHandler = new AllPagesHandler();
    }

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

    @Override
    public int getTitleStringId() {
        return R.string.awesomebar_all_pages_title;
    }

    @Override
    public String getTag() {
        return TAG;
    }

    private ListView getListView() {
        if (mListView == null && mView != null) {
            mListView = (ListView) mView.findViewById(R.id.awesomebar_list);
        }
        return mListView;
    }

    @Override
    public View getView() {
        if (mView == null) {
            mView = (LinearLayout) (LayoutInflater.from(mContext).inflate(R.layout.awesomebar_allpages_list, null));
            mView.setTag(TAG);

            ListView list = getListView();
            list.setTag(TAG);
            ((Activity)mContext).registerForContextMenu(list);
            list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
                @Override
                public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                     handleItemClick(parent, view, position, id);
                }
            });
            list.setOnKeyListener(GamepadUtils.getListItemClickDispatcher());

            AwesomeBarCursorAdapter adapter = getCursorAdapter();
            list.setAdapter(adapter);
            list.setOnTouchListener(mListListener);
        }

        return mView;
    }

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

        unregisterEventListener("SearchEngines:Data");

        mHandler.removeMessages(MESSAGE_UPDATE_FAVICONS);
        mHandler.removeMessages(MESSAGE_LOAD_FAVICONS);
        mHandler = null;

        // Can't use getters for adapter or listview. They will create them if null.
        if (mCursorAdapter != null && mListView != null) {
            mListView.setAdapter(null);
            final Cursor cursor = mCursorAdapter.getCursor();
            // Gingerbread locks the DB when closing a cursor, so do it in the
            // background.
            ThreadUtils.postToBackgroundThread(new Runnable() {
                @Override
                public void run() {
                    if (cursor != null && !cursor.isClosed())
                        cursor.close();
                }
            });
        }
    }

    public void filter(String searchTerm, AutocompleteHandler handler) {
        mAutocompleteHandler = handler;

        AwesomeBarCursorAdapter adapter = getCursorAdapter();
        adapter.filter(searchTerm);

        filterSuggestions(searchTerm);
        if (mSuggestionsOptInPrompt != null) {
            int visibility = TextUtils.isEmpty(searchTerm) ? View.GONE : View.VISIBLE;
            if (mSuggestionsOptInPrompt.getVisibility() != visibility) {
                mSuggestionsOptInPrompt.setVisibility(visibility);
            }
        }
    }

    private void findAutocompleteFor(String searchTerm, Cursor cursor) {
        if (TextUtils.isEmpty(searchTerm) || cursor == null || mAutocompleteHandler == null)
            return;

        // avoid searching the path if we don't have to. Currently just decided by if there is
        // a '/' character in the string
        final String res = searchHosts(searchTerm, cursor, searchTerm.indexOf("/") > 0);

        if (res != null) {
            ThreadUtils.postToUiThread(new Runnable() {
                @Override
                public void run() {
                    // Its possible that mAutocompleteHandler has been destroyed
                    if (mAutocompleteHandler != null) {
                        mAutocompleteHandler.onAutocomplete(res);
                        mAutocompleteHandler = null;
                    }
                }
            });
        }
    }

    private String searchHosts(String searchTerm, Cursor cursor, boolean searchPath) {
        int i = 0;
        if (cursor.moveToFirst()) {
            int urlIndex = cursor.getColumnIndexOrThrow(URLColumns.URL);
            do {
                final Uri url = Uri.parse(cursor.getString(urlIndex));
                String host = StringUtils.stripCommonSubdomains(url.getHost());
                // host may be null for about pages
                if (host == null)
                    continue;

                StringBuilder hostBuilder = new StringBuilder(host);
               if (hostBuilder.indexOf(searchTerm) == 0) {
                    return hostBuilder.append("/").toString();
                }

                if (searchPath) {
                    List<String> path = url.getPathSegments();

                    for (String seg : path) {
                        hostBuilder.append("/").append(seg);
                        if (hostBuilder.indexOf(searchTerm) == 0) {
                            return hostBuilder.append("/").toString();
                        }
                    }
                }

                i++;
            } while (i < MAX_AUTOCOMPLETE_SEARCH && cursor.moveToNext());
        }
        return null;
    }

    /**
     * Query for suggestions, but don't show them yet.
     */
    private void primeSuggestions() {
        ThreadUtils.postToBackgroundThread(new Runnable() {
            @Override
            public void run() {
                mSuggestClient.query(mSearchTerm);
            }
        });
    }

    private void filterSuggestions(String searchTerm) {
        // cancel previous query
        if (mSuggestTask != null) {
            mSuggestTask.cancel(true);
        }

        if (mSuggestClient != null && mSuggestionsEnabled) {
            mSuggestTask = new AsyncTask<String, Void, ArrayList<String>>() {
                @Override
                protected ArrayList<String> doInBackground(String... query) {
                    return mSuggestClient.query(query[0]);
                }

                @Override
                protected void onPostExecute(ArrayList<String> suggestions) {
                    setSuggestions(suggestions);
                }
            };
            mSuggestTask.execute(searchTerm);
        }
    }

    protected AwesomeBarCursorAdapter getCursorAdapter() {
        if (mCursorAdapter == null) {
            // Load the list using a custom adapter so we can create the bitmaps
            mCursorAdapter = new AwesomeBarCursorAdapter(mContext);

            mCursorAdapter.setFilterQueryProvider(new FilterQueryProvider() {
                @Override
                public Cursor runQuery(CharSequence constraint) {
                    long start = SystemClock.uptimeMillis();

                    Cursor c = BrowserDB.filter(getContentResolver(), constraint, MAX_RESULTS);
                    c.getCount();

                    postLoadFavicons();

                    long end = SystemClock.uptimeMillis();
                    if (!mTelemetrySent && TextUtils.isEmpty(constraint)) {
                        int time = (int)(end - start);
                        Telemetry.HistogramAdd("FENNEC_AWESOMEBAR_ALLPAGES_EMPTY_TIME", time);
                        mTelemetrySent = true;
                    }

                    findAutocompleteFor(constraint.toString(), c);
                    return c;
                }
            });
        }
        return mCursorAdapter;
    }

    private interface AwesomeBarItem {
        public void onClick();
        public ContextMenuSubject getSubject();
    }

    private class AwesomeBarCursorItem implements AwesomeBarItem {
        private Cursor mCursor;

        public AwesomeBarCursorItem(Cursor cursor) {
            mCursor = cursor;
        }

        @Override
        public void onClick() {
            String url = mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.URL));
            String title = mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.TITLE));
            sendToListener(url, title);
        }

        @Override
        public ContextMenuSubject getSubject() {
            // Use the history id in order to allow removing history entries
            int id = mCursor.getInt(mCursor.getColumnIndexOrThrow(Combined.HISTORY_ID));

            String keyword = null;
            int keywordCol = mCursor.getColumnIndex(URLColumns.KEYWORD);
            if (keywordCol != -1)
                keyword = mCursor.getString(keywordCol);

            final String url = mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.URL));

            Bitmap bitmap = Favicons.getInstance().getFaviconFromMemCache(url);
            byte[] favicon = null;

            if (bitmap != null) {
                ByteArrayOutputStream stream = new ByteArrayOutputStream();
                if (bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)) {
                    favicon = stream.toByteArray();
                } else {
                    Log.w(LOGTAG, "Favicon compression failed.");
                }
            }

            return new ContextMenuSubject(id, url, favicon,
                                          mCursor.getString(mCursor.getColumnIndexOrThrow(URLColumns.TITLE)),
                                          keyword,
                                          mCursor.getInt(mCursor.getColumnIndexOrThrow(Combined.DISPLAY)));
        }
    }

    private class AwesomeBarSearchEngineItem implements AwesomeBarItem {
        private SearchEngine mSearchEngine;

        public AwesomeBarSearchEngineItem(SearchEngine searchEngine) {
            mSearchEngine = searchEngine;
        }

        @Override
        public void onClick() {
            AwesomeBarTabs.OnUrlOpenListener listener = getUrlListener();
            if (listener != null)
                listener.onSearch(mSearchEngine, mSearchTerm);
        }

        @Override
        public ContextMenuSubject getSubject() {
            // Do not show context menu for search engine items
            return null;
        }
    }

    private class AwesomeBarCursorAdapter extends SimpleCursorAdapter {
        private static final int ROW_SEARCH = 0;
        private static final int ROW_STANDARD = 1;
        private static final int ROW_SUGGEST = 2;

        public AwesomeBarCursorAdapter(Context context) {
            super(context, -1, null, new String[] {}, new int[] {});
            mSearchTerm = "";
        }

        public void filter(String searchTerm) {
            boolean changed = !mSearchTerm.equals(searchTerm);
            mSearchTerm = searchTerm;

            if (changed)
                mCursorAdapter.notifyDataSetChanged();

            getFilter().filter(searchTerm);
        }

        private int getSuggestEngineCount() {
            return (mSearchTerm.length() == 0 || mSuggestClient == null || !mSuggestionsEnabled) ? 0 : 1;
        }

        // Add the search engines to the number of reported results.
        @Override
        public int getCount() {
            final int resultCount = super.getCount();

            // don't show search engines or suggestions if search field is empty
            if (mSearchTerm.length() == 0)
                return resultCount;

            return resultCount + mSearchEngines.size();
        }

        // If an item is part of the cursor result set, return that entry.
        // Otherwise, return the search engine data.
        @Override
        public Object getItem(int position) {
            int engineIndex = getEngineIndex(position);

            if (engineIndex == -1) {
                // return awesomebar result
                position -= getSuggestEngineCount();
                return new AwesomeBarCursorItem((Cursor) super.getItem(position));
            }

            // return search engine
            return new AwesomeBarSearchEngineItem(mSearchEngines.get(engineIndex));
        }

        private int getEngineIndex(int position) {
            final int resultCount = super.getCount();
            final int suggestEngineCount = getSuggestEngineCount();

            // return suggest engine index
            if (position < suggestEngineCount)
                return position;

            // not an engine
            if (position - suggestEngineCount < resultCount)
                return -1;

            // return search engine index
            return position - resultCount;
        }

        @Override
        public int getItemViewType(int position) {
            int engine = getEngineIndex(position);
            if (engine == -1) {
                return ROW_STANDARD;
            } else if (engine == 0 && mSuggestionsEnabled) {
                // Give suggestion views their own type to prevent them from
                // sharing other recycled search engine views. Using other
                // recycled views for the suggestion row can break animations
                // (bug 815937).
                return ROW_SUGGEST;
            }
            return ROW_SEARCH;
        }

        @Override
        public int getViewTypeCount() {
            // view can be either a standard awesomebar row, a search engine
            // row, or a suggestion row
            return 3;
        }

        @Override
        public boolean isEnabled(int position) {
            // If the suggestion row only contains one item (the user-entered
            // query), allow the entire row to be clickable; clicking the row
            // has the same effect as clicking the single suggestion. If the
            // row contains multiple items, clicking the row will do nothing.
            int index = getEngineIndex(position);
            if (index != -1)
                return mSearchEngines.get(index).suggestions.isEmpty();
            return true;
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            int type = getItemViewType(position);
            if (type == ROW_SEARCH || type == ROW_SUGGEST) {
                SearchEntryViewHolder viewHolder = null;

                if (convertView == null || !(convertView.getTag() instanceof SearchEntryViewHolder)) {
                    convertView = getInflater().inflate(R.layout.awesomebar_suggestion_row, getListView(), false);

                    viewHolder = new SearchEntryViewHolder();
                    viewHolder.suggestionView = (FlowLayout) convertView.findViewById(R.id.suggestion_layout);
                    viewHolder.iconView = (FaviconView) convertView.findViewById(R.id.suggestion_icon);
                    viewHolder.userEnteredView = (LinearLayout) convertView.findViewById(R.id.suggestion_user_entered);
                    viewHolder.userEnteredTextView = (TextView) convertView.findViewById(R.id.suggestion_text);

                    convertView.setTag(viewHolder);
                } else {
                    viewHolder = (SearchEntryViewHolder) convertView.getTag();
                }

                bindSearchEngineView(mSearchEngines.get(getEngineIndex(position)), viewHolder);
            } else {
                AwesomeEntryViewHolder viewHolder = null;

                if (convertView == null || !(convertView.getTag() instanceof AwesomeEntryViewHolder)) {
                    convertView = getInflater().inflate(R.layout.awesomebar_row, null);

                    viewHolder = new AwesomeEntryViewHolder();
                    viewHolder.titleView = (TextView) convertView.findViewById(R.id.title);
                    viewHolder.urlView = (TextView) convertView.findViewById(R.id.url);
                    viewHolder.faviconView = (FaviconView) convertView.findViewById(R.id.favicon);
                    viewHolder.bookmarkIconView = (ImageView) convertView.findViewById(R.id.bookmark_icon);

                    convertView.setTag(viewHolder);
                } else {
                    viewHolder = (AwesomeEntryViewHolder) convertView.getTag();
                }

                position -= getSuggestEngineCount();
                Cursor cursor = getCursor();
                if (!cursor.moveToPosition(position))
                    throw new IllegalStateException("Couldn't move cursor to position " + position);

                updateTitle(viewHolder.titleView, cursor);
                updateUrl(viewHolder, cursor);
                updateBookmarkIcon(viewHolder.bookmarkIconView, cursor);
                displayFavicon(viewHolder);
            }

            return convertView;
        }

        private void bindSearchEngineView(final SearchEngine engine, final SearchEntryViewHolder viewHolder) {
            // when a suggestion is clicked, do a search
            OnClickListener clickListener = new OnClickListener() {
                @Override
                public void onClick(View v) {
                    AwesomeBarTabs.OnUrlOpenListener listener = getUrlListener();
                    if (listener != null) {
                        String suggestion = ((TextView) v.findViewById(R.id.suggestion_text)).getText().toString();

                        // If we're not clicking the user-entered view (the
                        // first suggestion item) and the search matches a URL
                        // pattern, go to that URL. Otherwise, do a search for
                        // the term.
                        if (v != viewHolder.userEnteredView && !StringUtils.isSearchQuery(suggestion, false)) {
                            listener.onUrlOpen(suggestion, null);
                        } else {
                            listener.onSearch(engine, suggestion);
                        }
                    }
                }
            };

            // when a suggestion is long-clicked, copy the suggestion into the URL EditText
            OnLongClickListener longClickListener = new OnLongClickListener() {
                @Override
                public boolean onLongClick(View v) {
                    AwesomeBarTabs.OnUrlOpenListener listener = getUrlListener();
                    if (listener != null) {
                        String suggestion = ((TextView) v.findViewById(R.id.suggestion_text)).getText().toString();
                        listener.onEditSuggestion(suggestion);
                        return true;
                    }
                    return false;
                }
            };

            // set the search engine icon (e.g., Google) for the row
            FlowLayout suggestionView = viewHolder.suggestionView;
            updateFavicon(viewHolder.iconView, engine.icon, engine.name);

            // user-entered search term is first suggestion
            viewHolder.userEnteredTextView.setText(mSearchTerm);
            viewHolder.userEnteredView.setOnClickListener(clickListener);
            
            // add additional suggestions given by this engine
            int recycledSuggestionCount = suggestionView.getChildCount();
            int suggestionCount = engine.suggestions.size();
            boolean showedSuggestions = false;

            for (int i = 0; i < suggestionCount; i++) {
                String suggestion = engine.suggestions.get(i);
                View suggestionItem = null;

                // reuse suggestion views from recycled view, if possible
                if (i+1 < recycledSuggestionCount) {
                    suggestionItem = suggestionView.getChildAt(i+1);
                    suggestionItem.setVisibility(View.VISIBLE);
                } else {
                    suggestionItem = getInflater().inflate(R.layout.awesomebar_suggestion_item, null);
                    ((ImageView) suggestionItem.findViewById(R.id.suggestion_magnifier)).setVisibility(View.GONE);
                    suggestionView.addView(suggestionItem);
                }
                ((TextView) suggestionItem.findViewById(R.id.suggestion_text)).setText(suggestion);

                suggestionItem.setOnClickListener(clickListener);
                suggestionItem.setOnLongClickListener(longClickListener);

                if (mAnimateSuggestions) {
                    showedSuggestions = true;
                    AlphaAnimation anim = new AlphaAnimation(0, 1);
                    anim.setDuration(ANIMATION_DURATION);
                    anim.setStartOffset(i * ANIMATION_DURATION);
                    suggestionItem.startAnimation(anim);
                }
            }
            
            // hide extra suggestions that have been recycled
            for (int i = suggestionCount + 1; i < recycledSuggestionCount; i++) {
                suggestionView.getChildAt(i).setVisibility(View.GONE);
            }

            if (showedSuggestions)
                mAnimateSuggestions = false;
        }
    };

    /**
     * Sets suggestions associated with the current suggest engine.
     * If there is no suggest engine, this does nothing.
     */
    private void setSuggestions(final ArrayList<String> suggestions) {
        if (mSuggestClient != null) {
            mSearchEngines.get(0).suggestions = suggestions;
            getCursorAdapter().notifyDataSetChanged();
        }
    }

    /**
     * Sets search engines to be shown for user-entered queries.
     */
    private void setSearchEngines(JSONObject data) {
        try {
            JSONObject suggest = data.getJSONObject("suggest");
            String suggestEngine = suggest.isNull("engine") ? null : suggest.getString("engine");
            String suggestTemplate = suggest.isNull("template") ? null : suggest.getString("template");
            mSuggestionsEnabled = suggest.getBoolean("enabled");
            boolean suggestionsPrompted = suggest.getBoolean("prompted");
            JSONArray engines = data.getJSONArray("searchEngines");

            ArrayList<SearchEngine> searchEngines = new ArrayList<SearchEngine>();
            for (int i = 0; i < engines.length(); i++) {
                JSONObject engineJSON = engines.getJSONObject(i);
                String name = engineJSON.getString("name");
                String identifier = engineJSON.getString("identifier");
                String iconURI = engineJSON.getString("iconURI");
                Bitmap icon = BitmapUtils.getBitmapFromDataURI(iconURI);
                if (name.equals(suggestEngine) && suggestTemplate != null) {
                    // suggest engine should be at the front of the list
                    searchEngines.add(0, new SearchEngine(name, identifier, icon));

                    // The only time Tabs.getInstance().getSelectedTab() should
                    // be null is when we're restoring after a crash. We should
                    // never restore private tabs when that happens, so it
                    // should be safe to assume that null means non-private.
                    Tab tab = Tabs.getInstance().getSelectedTab();
                    if (tab == null || !tab.isPrivate())
                        mSuggestClient = new SuggestClient(getView().getContext(), suggestTemplate, SUGGESTION_TIMEOUT, SUGGESTION_MAX);
                } else {
                    searchEngines.add(new SearchEngine(name, identifier, icon));
                }
            }

            mSearchEngines = searchEngines;
            mCursorAdapter.notifyDataSetChanged();

            // show suggestions opt-in if user hasn't been prompted
            if (!suggestionsPrompted && mSuggestClient != null) {
                showSuggestionsOptIn();
            }
        } catch (JSONException e) {
            Log.e(LOGTAG, "Error getting search engine JSON", e);
        }

        filterSuggestions(mSearchTerm);
    }

    private void showSuggestionsOptIn() {
        mSuggestionsOptInPrompt = LayoutInflater.from(mContext).inflate(R.layout.awesomebar_suggestion_prompt, (LinearLayout)getView(), false);
        GeckoTextView promptText = (GeckoTextView) mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_title);
        promptText.setText(getResources().getString(R.string.suggestions_prompt, mSearchEngines.get(0).name));
        Tab tab = Tabs.getInstance().getSelectedTab();
        if (tab != null)
            promptText.setPrivateMode(tab.isPrivate());

        final View yesButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_yes);
        final View noButton = mSuggestionsOptInPrompt.findViewById(R.id.suggestions_prompt_no);
        OnClickListener listener = new OnClickListener() {
            @Override
            public void onClick(View v) {
                // Prevent the buttons from being clicked multiple times (bug 816902)
                yesButton.setOnClickListener(null);
                noButton.setOnClickListener(null);

                setSuggestionsEnabled(v == yesButton);
            }
        };
        yesButton.setOnClickListener(listener);
        noButton.setOnClickListener(listener);

        mSuggestionsOptInPrompt.setVisibility(View.GONE);
        ((LinearLayout)getView()).addView(mSuggestionsOptInPrompt, 0);
    }

    private void setSuggestionsEnabled(final boolean enabled) {
        // Clicking the yes/no buttons quickly can cause the click events be
        // queued before the listeners are removed above, so it's possible
        // setSuggestionsEnabled() can be called twice. mSuggestionsOptInPrompt
        // can be null if this happens (bug 828480).
        if (mSuggestionsOptInPrompt == null) {
            return;
        }

        // Make suggestions appear immediately after the user opts in
        primeSuggestions();

        // Pref observer in gecko will also set prompted = true
        PrefsHelper.setPref("browser.search.suggest.enabled", enabled);

        TranslateAnimation anim1 = new TranslateAnimation(0, mSuggestionsOptInPrompt.getWidth(), 0, 0);
        anim1.setDuration(ANIMATION_DURATION);
        anim1.setInterpolator(new AccelerateInterpolator());
        anim1.setFillAfter(true);
        final View promptContainer = mSuggestionsOptInPrompt.findViewById(R.id.prompt_container);

        TranslateAnimation anim2 = new TranslateAnimation(0, 0, 0, -1 * mSuggestionsOptInPrompt.getHeight());
        anim2.setDuration(ANIMATION_DURATION);
        anim2.setFillAfter(true);
        anim2.setStartOffset(anim1.getDuration());
        final LinearLayout view = (LinearLayout)getView();
        anim2.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation a) {
                // Increase the height of the view so a gap isn't shown during animation
                view.getLayoutParams().height = view.getHeight() +
                        mSuggestionsOptInPrompt.getHeight();
                view.requestLayout();
            }
            @Override
            public void onAnimationRepeat(Animation a) {}
            @Override
            public void onAnimationEnd(Animation a) {
                // Removing the view immediately results in a NPE in
                // dispatchDraw(), possibly because this callback executes
                // before drawing is finished. Posting this as a Runnable fixes
                // the issue.
                view.post(new Runnable() {
                    @Override
                    public void run() {
                        view.removeView(mSuggestionsOptInPrompt);
                        getListView().clearAnimation();
                        mSuggestionsOptInPrompt = null;

                        if (enabled) {
                            // Reset the view height
                            view.getLayoutParams().height = LayoutParams.FILL_PARENT;

                            mSuggestionsEnabled = enabled;
                            mAnimateSuggestions = true;
                            getCursorAdapter().notifyDataSetChanged();
                            filterSuggestions(mSearchTerm);
                        }
                    }
                });
            }
        });

        promptContainer.startAnimation(anim1);
        mSuggestionsOptInPrompt.startAnimation(anim2);
        getListView().startAnimation(anim2);
    }

    @Override
    public void handleMessage(String event, final JSONObject message) {
        if (event.equals("SearchEngines:Data")) {
            ThreadUtils.postToUiThread(new Runnable() {
                @Override
                public void run() {
                    setSearchEngines(message);
                }
            });
        }
    }

    public void handleItemClick(AdapterView<?> parent, View view, int position, long id) {
        ListView listview = getListView();
        if (listview == null)
            return;

        AwesomeBarItem item = (AwesomeBarItem)listview.getItemAtPosition(position);
        item.onClick();
    }

    protected void updateBookmarkIcon(ImageView bookmarkIconView, Cursor cursor) {
        int bookmarkIdIndex = cursor.getColumnIndexOrThrow(Combined.BOOKMARK_ID);
        long id = cursor.getLong(bookmarkIdIndex);

        int displayIndex = cursor.getColumnIndexOrThrow(Combined.DISPLAY);
        int display = cursor.getInt(displayIndex);

        // The bookmark id will be 0 (null in database) when the url
        // is not a bookmark.
        int visibility = (id == 0 ? View.GONE : View.VISIBLE);
        bookmarkIconView.setVisibility(visibility);

        if (display == Combined.DISPLAY_READER) {
            bookmarkIconView.setImageResource(R.drawable.ic_awesomebar_reader);
        } else {
            bookmarkIconView.setImageResource(R.drawable.ic_awesomebar_star);
        }
    }

    @Override
    public ContextMenuSubject getSubject(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
        ContextMenuSubject subject = null;

        if (!(menuInfo instanceof AdapterView.AdapterContextMenuInfo)) {
            Log.e(LOGTAG, "menuInfo is not AdapterContextMenuInfo");
            return subject;
        }

        ListView list = (ListView)view;
        AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo;
        subject = ((AwesomeBarItem) list.getItemAtPosition(info.position)).getSubject();

        if (subject == null)
            return subject;

        setupMenu(menu, subject);

        menu.findItem(R.id.remove_bookmark).setVisible(false);
        menu.findItem(R.id.edit_bookmark).setVisible(false);
        menu.findItem(R.id.open_in_reader).setVisible(subject.display == Combined.DISPLAY_READER);

        return subject;
    }

    private void registerEventListener(String event) {
        GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
    }

    private void unregisterEventListener(String event) {
        GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
    }

    private List<String> getUrlsWithoutFavicon() {
        List<String> urls = new ArrayList<String>();

        Cursor c = mCursorAdapter.getCursor();
        if (c == null || !c.moveToFirst())
            return urls;

        do {
            final String url = c.getString(c.getColumnIndexOrThrow(URLColumns.URL));

            // We only want to load favicons from DB if they are not in the
            // memory cache yet.
            if (Favicons.getInstance().getFaviconFromMemCache(url) != null)
                continue;

            urls.add(url);
        } while (c.moveToNext());

        return urls;
    }

    public void storeFaviconsInMemCache(Cursor c) {
        try {
            if (c == null || !c.moveToFirst())
                return;

            do {
                final String url = c.getString(c.getColumnIndexOrThrow(Combined.URL));
                final byte[] b = c.getBlob(c.getColumnIndexOrThrow(Combined.FAVICON));
                if (b == null)
                    continue;

                Bitmap favicon = BitmapUtils.decodeByteArray(b);
                if (favicon == null)
                    continue;

                favicon = Favicons.getInstance().scaleImage(favicon);
                Favicons.getInstance().putFaviconInMemCache(url, favicon);
            } while (c.moveToNext());
        } finally {
            if (c != null)
                c.close();
        }
    }

    private void loadFaviconsForCurrentResults() {
        final List<String> urls = getUrlsWithoutFavicon();
        if (urls.size() == 0)
            return;

        (new UiAsyncTask<Void, Void, Void>(ThreadUtils.getBackgroundHandler()) {
            @Override
            public Void doInBackground(Void... params) {
                Cursor cursor = BrowserDB.getFaviconsForUrls(getContentResolver(), urls);
                storeFaviconsInMemCache(cursor);
                return null;
            }

            @Override
            public void onPostExecute(Void result) {
                postUpdateFavicons();
            }
        }).execute();
    }

    private void displayFavicon(AwesomeEntryViewHolder viewHolder) {
        final String url = viewHolder.url;
        Bitmap bitmap = Favicons.getInstance().getFaviconFromMemCache(url);
        updateFavicon(viewHolder.faviconView, bitmap, url);
    }

    private void updateFavicons() {
        ListView listView = getListView();
        AwesomeBarCursorAdapter adapter = getCursorAdapter();
        Cursor cursor = adapter.getCursor();
        if (cursor == null)
            return;

        for (int i = 0; i < listView.getChildCount(); i++) {
            final View view = listView.getChildAt(i);
            final Object tag = view.getTag();

            if (tag == null || !(tag instanceof AwesomeEntryViewHolder))
                continue;

            final AwesomeEntryViewHolder viewHolder = (AwesomeEntryViewHolder) tag;
            displayFavicon(viewHolder);
        }

        mView.invalidate();
    }

    private void postUpdateFavicons() {
        if (mHandler == null)
            return;

        Message msg = mHandler.obtainMessage(MESSAGE_UPDATE_FAVICONS,
                                             AllPagesTab.this);

        mHandler.removeMessages(MESSAGE_UPDATE_FAVICONS);
        mHandler.sendMessage(msg);
    }

    private void postLoadFavicons() {
        if (mHandler == null)
            return;

        Message msg = mHandler.obtainMessage(MESSAGE_LOAD_FAVICONS,
                                             AllPagesTab.this);

        mHandler.removeMessages(MESSAGE_LOAD_FAVICONS);
        mHandler.sendMessageDelayed(msg, 200);
    }

    private class AllPagesHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_LOAD_FAVICONS:
                    loadFaviconsForCurrentResults();
                    break;
                case MESSAGE_UPDATE_FAVICONS:
                    updateFavicons();
                    break;
            }
        }
    }
}
