/* -*- 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.home;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.GeckoBundle;
import org.mozilla.gecko.util.ThreadUtils;

import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
import android.util.Pair;

public final class HomeConfig {
    public static final String PREF_KEY_BOOKMARKS_PANEL_ENABLED = "bookmarksPanelEnabled";
    public static final String PREF_KEY_HISTORY_PANEL_ENABLED = "combinedHistoryPanelEnabled";

    /**
     * Used to determine what type of HomeFragment subclass to use when creating
     * a given panel. With the exception of DYNAMIC, all of these types correspond
     * to a default set of built-in panels. The DYNAMIC panel type is used by
     * third-party services to create panels with varying types of content.
     */
    @RobocopTarget
    public static enum PanelType implements Parcelable {
        TOP_SITES("top_sites", TopSitesPanel.class),
        BOOKMARKS("bookmarks", BookmarksPanel.class),
        COMBINED_HISTORY("combined_history", CombinedHistoryPanel.class),
        DYNAMIC("dynamic", DynamicPanel.class),
        // Deprecated panels that should no longer exist but are kept around for
        // migration code. Class references have been replaced with new version of the panel.
        DEPRECATED_REMOTE_TABS("remote_tabs", CombinedHistoryPanel.class),
        DEPRECATED_HISTORY("history", CombinedHistoryPanel.class),
        DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class),
        DEPRECATED_RECENT_TABS("recent_tabs", CombinedHistoryPanel.class);

        private final String mId;
        private final Class<?> mPanelClass;

        PanelType(String id, Class<?> panelClass) {
            mId = id;
            mPanelClass = panelClass;
        }

        public static PanelType fromId(String id) {
            if (id == null) {
                throw new IllegalArgumentException("Could not convert null String to PanelType");
            }

            for (PanelType panelType : PanelType.values()) {
                if (TextUtils.equals(panelType.mId, id.toLowerCase())) {
                    return panelType;
                }
            }

            throw new IllegalArgumentException("Could not convert String id to PanelType");
        }

        @Override
        public String toString() {
            return mId;
        }

        public Class<?> getPanelClass() {
            return mPanelClass;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(ordinal());
        }

        public static final Creator<PanelType> CREATOR = new Creator<PanelType>() {
            @Override
            public PanelType createFromParcel(final Parcel source) {
                return PanelType.values()[source.readInt()];
            }

            @Override
            public PanelType[] newArray(final int size) {
                return new PanelType[size];
            }
        };
    }

    public static class PanelConfig implements Parcelable {
        private final PanelType mType;
        private final String mTitle;
        private final String mId;
        private final LayoutType mLayoutType;
        private final List<ViewConfig> mViews;
        private final AuthConfig mAuthConfig;
        private final EnumSet<Flags> mFlags;
        private final int mPosition;

        static final String JSON_KEY_TYPE = "type";
        static final String JSON_KEY_TITLE = "title";
        static final String JSON_KEY_ID = "id";
        static final String JSON_KEY_LAYOUT = "layout";
        static final String JSON_KEY_VIEWS = "views";
        static final String JSON_KEY_AUTH_CONFIG = "authConfig";
        static final String JSON_KEY_DEFAULT = "default";
        static final String JSON_KEY_DISABLED = "disabled";
        static final String JSON_KEY_POSITION = "position";

        public enum Flags {
            DEFAULT_PANEL,
            DISABLED_PANEL
        }

        public PanelConfig(JSONObject json) throws JSONException, IllegalArgumentException {
            final String panelType = json.optString(JSON_KEY_TYPE, null);
            if (TextUtils.isEmpty(panelType)) {
                mType = PanelType.DYNAMIC;
            } else {
                mType = PanelType.fromId(panelType);
            }

            mTitle = json.getString(JSON_KEY_TITLE);
            mId = json.getString(JSON_KEY_ID);

            final String layoutTypeId = json.optString(JSON_KEY_LAYOUT, null);
            if (layoutTypeId != null) {
                mLayoutType = LayoutType.fromId(layoutTypeId);
            } else {
                mLayoutType = null;
            }

            final JSONArray jsonViews = json.optJSONArray(JSON_KEY_VIEWS);
            if (jsonViews != null) {
                mViews = new ArrayList<ViewConfig>();

                final int viewCount = jsonViews.length();
                for (int i = 0; i < viewCount; i++) {
                    final JSONObject jsonViewConfig = (JSONObject) jsonViews.get(i);
                    final ViewConfig viewConfig = new ViewConfig(i, jsonViewConfig);
                    mViews.add(viewConfig);
                }
            } else {
                mViews = null;
            }

            final JSONObject jsonAuthConfig = json.optJSONObject(JSON_KEY_AUTH_CONFIG);
            if (jsonAuthConfig != null) {
                mAuthConfig = new AuthConfig(jsonAuthConfig);
            } else {
                mAuthConfig = null;
            }

            mFlags = EnumSet.noneOf(Flags.class);

            if (json.optBoolean(JSON_KEY_DEFAULT, false)) {
                mFlags.add(Flags.DEFAULT_PANEL);
            }

            if (json.optBoolean(JSON_KEY_DISABLED, false)) {
                mFlags.add(Flags.DISABLED_PANEL);
            }

            mPosition = json.optInt(JSON_KEY_POSITION, -1);

            validate();
        }

        @SuppressWarnings("unchecked")
        public PanelConfig(Parcel in) {
            mType = (PanelType) in.readParcelable(getClass().getClassLoader());
            mTitle = in.readString();
            mId = in.readString();
            mLayoutType = (LayoutType) in.readParcelable(getClass().getClassLoader());

            mViews = new ArrayList<ViewConfig>();
            in.readTypedList(mViews, ViewConfig.CREATOR);

            mAuthConfig = (AuthConfig) in.readParcelable(getClass().getClassLoader());

            mFlags = (EnumSet<Flags>) in.readSerializable();
            mPosition = in.readInt();

            validate();
        }

        public PanelConfig(PanelConfig panelConfig) {
            mType = panelConfig.mType;
            mTitle = panelConfig.mTitle;
            mId = panelConfig.mId;
            mLayoutType = panelConfig.mLayoutType;

            mViews = new ArrayList<ViewConfig>();
            List<ViewConfig> viewConfigs = panelConfig.mViews;
            if (viewConfigs != null) {
                for (ViewConfig viewConfig : viewConfigs) {
                    mViews.add(new ViewConfig(viewConfig));
                }
            }

            mAuthConfig = panelConfig.mAuthConfig;
            mFlags = panelConfig.mFlags.clone();
            mPosition = panelConfig.mPosition;

            validate();
        }

        public PanelConfig(PanelType type, String title, String id) {
            this(type, title, id, EnumSet.noneOf(Flags.class));
        }

        public PanelConfig(PanelType type, String title, String id, EnumSet<Flags> flags) {
            this(type, title, id, null, null, null, flags, -1);
        }

        public PanelConfig(PanelType type, String title, String id, LayoutType layoutType,
                List<ViewConfig> views, AuthConfig authConfig, EnumSet<Flags> flags, int position) {
            mType = type;
            mTitle = title;
            mId = id;
            mLayoutType = layoutType;
            mViews = views;
            mAuthConfig = authConfig;
            mFlags = flags;
            mPosition = position;

            validate();
        }

        private void validate() {
            if (mType == null) {
                throw new IllegalArgumentException("Can't create PanelConfig with null type");
            }

            if (TextUtils.isEmpty(mTitle)) {
                throw new IllegalArgumentException("Can't create PanelConfig with empty title");
            }

            if (TextUtils.isEmpty(mId)) {
                throw new IllegalArgumentException("Can't create PanelConfig with empty id");
            }

            if (mLayoutType == null && mType == PanelType.DYNAMIC) {
                throw new IllegalArgumentException("Can't create a dynamic PanelConfig with null layout type");
            }

            if ((mViews == null || mViews.size() == 0) && mType == PanelType.DYNAMIC) {
                throw new IllegalArgumentException("Can't create a dynamic PanelConfig with no views");
            }

            if (mFlags == null) {
                throw new IllegalArgumentException("Can't create PanelConfig with null flags");
            }
        }

        public PanelType getType() {
            return mType;
        }

        public String getTitle() {
            return mTitle;
        }

        public String getId() {
            return mId;
        }

        public LayoutType getLayoutType() {
            return mLayoutType;
        }

        public int getViewCount() {
            return (mViews != null ? mViews.size() : 0);
        }

        public ViewConfig getViewAt(int index) {
            return (mViews != null ? mViews.get(index) : null);
        }

        public EnumSet<Flags> getFlags() {
            return mFlags.clone();
        }

        public boolean isDynamic() {
            return (mType == PanelType.DYNAMIC);
        }

        public boolean isDefault() {
            return mFlags.contains(Flags.DEFAULT_PANEL);
        }

        private void setIsDefault(boolean isDefault) {
            if (isDefault) {
                mFlags.add(Flags.DEFAULT_PANEL);
            } else {
                mFlags.remove(Flags.DEFAULT_PANEL);
            }
        }

        public boolean isDisabled() {
            return mFlags.contains(Flags.DISABLED_PANEL);
        }

        private void setIsDisabled(boolean isDisabled) {
            if (isDisabled) {
                mFlags.add(Flags.DISABLED_PANEL);
            } else {
                mFlags.remove(Flags.DISABLED_PANEL);
            }
        }

        public AuthConfig getAuthConfig() {
            return mAuthConfig;
        }

        public int getPosition() {
            return mPosition;
        }

        public JSONObject toJSON() throws JSONException {
            final JSONObject json = new JSONObject();

            json.put(JSON_KEY_TYPE, mType.toString());
            json.put(JSON_KEY_TITLE, mTitle);
            json.put(JSON_KEY_ID, mId);

            if (mLayoutType != null) {
                json.put(JSON_KEY_LAYOUT, mLayoutType.toString());
            }

            if (mViews != null) {
                final JSONArray jsonViews = new JSONArray();

                final int viewCount = mViews.size();
                for (int i = 0; i < viewCount; i++) {
                    final ViewConfig viewConfig = mViews.get(i);
                    final JSONObject jsonViewConfig = viewConfig.toJSON();
                    jsonViews.put(jsonViewConfig);
                }

                json.put(JSON_KEY_VIEWS, jsonViews);
            }

            if (mAuthConfig != null) {
                json.put(JSON_KEY_AUTH_CONFIG, mAuthConfig.toJSON());
            }

            if (mFlags.contains(Flags.DEFAULT_PANEL)) {
                json.put(JSON_KEY_DEFAULT, true);
            }

            if (mFlags.contains(Flags.DISABLED_PANEL)) {
                json.put(JSON_KEY_DISABLED, true);
            }

            json.put(JSON_KEY_POSITION, mPosition);

            return json;
        }

        @Override
        public boolean equals(Object o) {
            if (o == null) {
                return false;
            }

            if (this == o) {
                return true;
            }

            if (!(o instanceof PanelConfig)) {
                return false;
            }

            final PanelConfig other = (PanelConfig) o;
            return mId.equals(other.mId);
        }

        @Override
        public int hashCode() {
            return super.hashCode();
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeParcelable(mType, 0);
            dest.writeString(mTitle);
            dest.writeString(mId);
            dest.writeParcelable(mLayoutType, 0);
            dest.writeTypedList(mViews);
            dest.writeParcelable(mAuthConfig, 0);
            dest.writeSerializable(mFlags);
            dest.writeInt(mPosition);
        }

        public static final Creator<PanelConfig> CREATOR = new Creator<PanelConfig>() {
            @Override
            public PanelConfig createFromParcel(final Parcel in) {
                return new PanelConfig(in);
            }

            @Override
            public PanelConfig[] newArray(final int size) {
                return new PanelConfig[size];
            }
        };
    }

    public static enum LayoutType implements Parcelable {
        FRAME("frame");

        private final String mId;

        LayoutType(String id) {
            mId = id;
        }

        public static LayoutType fromId(String id) {
            if (id == null) {
                throw new IllegalArgumentException("Could not convert null String to LayoutType");
            }

            for (LayoutType layoutType : LayoutType.values()) {
                if (TextUtils.equals(layoutType.mId, id.toLowerCase())) {
                    return layoutType;
                }
            }

            throw new IllegalArgumentException("Could not convert String id to LayoutType");
        }

        @Override
        public String toString() {
            return mId;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(ordinal());
        }

        public static final Creator<LayoutType> CREATOR = new Creator<LayoutType>() {
            @Override
            public LayoutType createFromParcel(final Parcel source) {
                return LayoutType.values()[source.readInt()];
            }

            @Override
            public LayoutType[] newArray(final int size) {
                return new LayoutType[size];
            }
        };
    }

    public static enum ViewType implements Parcelable {
        LIST("list"),
        GRID("grid");

        private final String mId;

        ViewType(String id) {
            mId = id;
        }

        public static ViewType fromId(String id) {
            if (id == null) {
                throw new IllegalArgumentException("Could not convert null String to ViewType");
            }

            for (ViewType viewType : ViewType.values()) {
                if (TextUtils.equals(viewType.mId, id.toLowerCase())) {
                    return viewType;
                }
            }

            throw new IllegalArgumentException("Could not convert String id to ViewType");
        }

        @Override
        public String toString() {
            return mId;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(ordinal());
        }

        public static final Creator<ViewType> CREATOR = new Creator<ViewType>() {
            @Override
            public ViewType createFromParcel(final Parcel source) {
                return ViewType.values()[source.readInt()];
            }

            @Override
            public ViewType[] newArray(final int size) {
                return new ViewType[size];
            }
        };
    }

    public static enum ItemType implements Parcelable {
        ARTICLE("article"),
        IMAGE("image"),
        ICON("icon");

        private final String mId;

        ItemType(String id) {
            mId = id;
        }

        public static ItemType fromId(String id) {
            if (id == null) {
                throw new IllegalArgumentException("Could not convert null String to ItemType");
            }

            for (ItemType itemType : ItemType.values()) {
                if (TextUtils.equals(itemType.mId, id.toLowerCase())) {
                    return itemType;
                }
            }

            throw new IllegalArgumentException("Could not convert String id to ItemType");
        }

        @Override
        public String toString() {
            return mId;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(ordinal());
        }

        public static final Creator<ItemType> CREATOR = new Creator<ItemType>() {
            @Override
            public ItemType createFromParcel(final Parcel source) {
                return ItemType.values()[source.readInt()];
            }

            @Override
            public ItemType[] newArray(final int size) {
                return new ItemType[size];
            }
        };
    }

    public static enum ItemHandler implements Parcelable {
        BROWSER("browser"),
        INTENT("intent");

        private final String mId;

        ItemHandler(String id) {
            mId = id;
        }

        public static ItemHandler fromId(String id) {
            if (id == null) {
                throw new IllegalArgumentException("Could not convert null String to ItemHandler");
            }

            for (ItemHandler itemHandler : ItemHandler.values()) {
                if (TextUtils.equals(itemHandler.mId, id.toLowerCase())) {
                    return itemHandler;
                }
            }

            throw new IllegalArgumentException("Could not convert String id to ItemHandler");
        }

        @Override
        public String toString() {
            return mId;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(ordinal());
        }

        public static final Creator<ItemHandler> CREATOR = new Creator<ItemHandler>() {
            @Override
            public ItemHandler createFromParcel(final Parcel source) {
                return ItemHandler.values()[source.readInt()];
            }

            @Override
            public ItemHandler[] newArray(final int size) {
                return new ItemHandler[size];
            }
        };
    }

    public static class ViewConfig implements Parcelable {
        private final int mIndex;
        private final ViewType mType;
        private final String mDatasetId;
        private final ItemType mItemType;
        private final ItemHandler mItemHandler;
        private final String mBackImageUrl;
        private final String mFilter;
        private final EmptyViewConfig mEmptyViewConfig;
        private final HeaderConfig mHeaderConfig;
        private final EnumSet<Flags> mFlags;

        static final String JSON_KEY_TYPE = "type";
        static final String JSON_KEY_DATASET = "dataset";
        static final String JSON_KEY_ITEM_TYPE = "itemType";
        static final String JSON_KEY_ITEM_HANDLER = "itemHandler";
        static final String JSON_KEY_BACK_IMAGE_URL = "backImageUrl";
        static final String JSON_KEY_FILTER = "filter";
        static final String JSON_KEY_EMPTY = "empty";
        static final String JSON_KEY_HEADER = "header";
        static final String JSON_KEY_REFRESH_ENABLED = "refreshEnabled";

        public enum Flags {
            REFRESH_ENABLED
        }

        public ViewConfig(int index, JSONObject json) throws JSONException, IllegalArgumentException {
            mIndex = index;
            mType = ViewType.fromId(json.getString(JSON_KEY_TYPE));
            mDatasetId = json.getString(JSON_KEY_DATASET);
            mItemType = ItemType.fromId(json.getString(JSON_KEY_ITEM_TYPE));
            mItemHandler = ItemHandler.fromId(json.getString(JSON_KEY_ITEM_HANDLER));
            mBackImageUrl = json.optString(JSON_KEY_BACK_IMAGE_URL, null);
            mFilter = json.optString(JSON_KEY_FILTER, null);

            final JSONObject jsonEmptyViewConfig = json.optJSONObject(JSON_KEY_EMPTY);
            if (jsonEmptyViewConfig != null) {
                mEmptyViewConfig = new EmptyViewConfig(jsonEmptyViewConfig);
            } else {
                mEmptyViewConfig = null;
            }

            final JSONObject jsonHeaderConfig = json.optJSONObject(JSON_KEY_HEADER);
            mHeaderConfig = jsonHeaderConfig != null ? new HeaderConfig(jsonHeaderConfig) : null;

            mFlags = EnumSet.noneOf(Flags.class);
            if (json.optBoolean(JSON_KEY_REFRESH_ENABLED, false)) {
                mFlags.add(Flags.REFRESH_ENABLED);
            }

            validate();
        }

        @SuppressWarnings("unchecked")
        public ViewConfig(Parcel in) {
            mIndex = in.readInt();
            mType = (ViewType) in.readParcelable(getClass().getClassLoader());
            mDatasetId = in.readString();
            mItemType = (ItemType) in.readParcelable(getClass().getClassLoader());
            mItemHandler = (ItemHandler) in.readParcelable(getClass().getClassLoader());
            mBackImageUrl = in.readString();
            mFilter = in.readString();
            mEmptyViewConfig = (EmptyViewConfig) in.readParcelable(getClass().getClassLoader());
            mHeaderConfig = (HeaderConfig) in.readParcelable(getClass().getClassLoader());
            mFlags = (EnumSet<Flags>) in.readSerializable();

            validate();
        }

        public ViewConfig(ViewConfig viewConfig) {
            mIndex = viewConfig.mIndex;
            mType = viewConfig.mType;
            mDatasetId = viewConfig.mDatasetId;
            mItemType = viewConfig.mItemType;
            mItemHandler = viewConfig.mItemHandler;
            mBackImageUrl = viewConfig.mBackImageUrl;
            mFilter = viewConfig.mFilter;
            mEmptyViewConfig = viewConfig.mEmptyViewConfig;
            mHeaderConfig = viewConfig.mHeaderConfig;
            mFlags = viewConfig.mFlags.clone();

            validate();
        }

        private void validate() {
            if (mType == null) {
                throw new IllegalArgumentException("Can't create ViewConfig with null type");
            }

            if (TextUtils.isEmpty(mDatasetId)) {
                throw new IllegalArgumentException("Can't create ViewConfig with empty dataset ID");
            }

            if (mItemType == null) {
                throw new IllegalArgumentException("Can't create ViewConfig with null item type");
            }

            if (mItemHandler == null) {
                throw new IllegalArgumentException("Can't create ViewConfig with null item handler");
            }

            if (mFlags == null) {
               throw new IllegalArgumentException("Can't create ViewConfig with null flags");
            }
        }

        public int getIndex() {
            return mIndex;
        }

        public ViewType getType() {
            return mType;
        }

        public String getDatasetId() {
            return mDatasetId;
        }

        public ItemType getItemType() {
            return mItemType;
        }

        public ItemHandler getItemHandler() {
            return mItemHandler;
        }

        public String getBackImageUrl() {
            return mBackImageUrl;
        }

        public String getFilter() {
            return mFilter;
        }

        public EmptyViewConfig getEmptyViewConfig() {
            return mEmptyViewConfig;
        }

        public HeaderConfig getHeaderConfig() {
            return mHeaderConfig;
        }

        public boolean hasHeaderConfig() {
            return mHeaderConfig != null;
        }

        public boolean isRefreshEnabled() {
            return mFlags.contains(Flags.REFRESH_ENABLED);
        }

        public JSONObject toJSON() throws JSONException {
            final JSONObject json = new JSONObject();

            json.put(JSON_KEY_TYPE, mType.toString());
            json.put(JSON_KEY_DATASET, mDatasetId);
            json.put(JSON_KEY_ITEM_TYPE, mItemType.toString());
            json.put(JSON_KEY_ITEM_HANDLER, mItemHandler.toString());

            if (!TextUtils.isEmpty(mBackImageUrl)) {
                json.put(JSON_KEY_BACK_IMAGE_URL, mBackImageUrl);
            }

            if (!TextUtils.isEmpty(mFilter)) {
                json.put(JSON_KEY_FILTER, mFilter);
            }

            if (mEmptyViewConfig != null) {
                json.put(JSON_KEY_EMPTY, mEmptyViewConfig.toJSON());
            }

            if (mHeaderConfig != null) {
                json.put(JSON_KEY_HEADER, mHeaderConfig.toJSON());
            }

            if (mFlags.contains(Flags.REFRESH_ENABLED)) {
                json.put(JSON_KEY_REFRESH_ENABLED, true);
            }

            return json;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeInt(mIndex);
            dest.writeParcelable(mType, 0);
            dest.writeString(mDatasetId);
            dest.writeParcelable(mItemType, 0);
            dest.writeParcelable(mItemHandler, 0);
            dest.writeString(mBackImageUrl);
            dest.writeString(mFilter);
            dest.writeParcelable(mEmptyViewConfig, 0);
            dest.writeParcelable(mHeaderConfig, 0);
            dest.writeSerializable(mFlags);
        }

        public static final Creator<ViewConfig> CREATOR = new Creator<ViewConfig>() {
            @Override
            public ViewConfig createFromParcel(final Parcel in) {
                return new ViewConfig(in);
            }

            @Override
            public ViewConfig[] newArray(final int size) {
                return new ViewConfig[size];
            }
        };
    }

    public static class EmptyViewConfig implements Parcelable {
        private final String mText;
        private final String mImageUrl;

        static final String JSON_KEY_TEXT = "text";
        static final String JSON_KEY_IMAGE_URL = "imageUrl";

        public EmptyViewConfig(JSONObject json) throws JSONException, IllegalArgumentException {
            mText = json.optString(JSON_KEY_TEXT, null);
            mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null);
        }

        public EmptyViewConfig(Parcel in) {
            mText = in.readString();
            mImageUrl = in.readString();
        }

        public EmptyViewConfig(EmptyViewConfig emptyViewConfig) {
            mText = emptyViewConfig.mText;
            mImageUrl = emptyViewConfig.mImageUrl;
        }

        public EmptyViewConfig(String text, String imageUrl) {
            mText = text;
            mImageUrl = imageUrl;
        }

        public String getText() {
            return mText;
        }

        public String getImageUrl() {
            return mImageUrl;
        }

        public JSONObject toJSON() throws JSONException {
            final JSONObject json = new JSONObject();

            json.put(JSON_KEY_TEXT, mText);
            json.put(JSON_KEY_IMAGE_URL, mImageUrl);

            return json;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(mText);
            dest.writeString(mImageUrl);
        }

        public static final Creator<EmptyViewConfig> CREATOR = new Creator<EmptyViewConfig>() {
            @Override
            public EmptyViewConfig createFromParcel(final Parcel in) {
                return new EmptyViewConfig(in);
            }

            @Override
            public EmptyViewConfig[] newArray(final int size) {
                return new EmptyViewConfig[size];
            }
        };
    }

    public static class HeaderConfig implements Parcelable {
        static final String JSON_KEY_IMAGE_URL = "image_url";
        static final String JSON_KEY_URL = "url";

        private final String mImageUrl;
        private final String mUrl;

        public HeaderConfig(JSONObject json) {
            mImageUrl = json.optString(JSON_KEY_IMAGE_URL);
            mUrl = json.optString(JSON_KEY_URL);
        }

        public HeaderConfig(Parcel in) {
            mImageUrl = in.readString();
            mUrl = in.readString();
        }

        public String getImageUrl() {
            return mImageUrl;
        }

        public String getUrl() {
            return mUrl;
        }

        public JSONObject toJSON() throws JSONException {
            JSONObject json = new JSONObject();

            json.put(JSON_KEY_IMAGE_URL, mImageUrl);
            json.put(JSON_KEY_URL, mUrl);

            return json;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(mImageUrl);
            dest.writeString(mUrl);
        }

        public static final Creator<HeaderConfig> CREATOR = new Creator<HeaderConfig>() {
            @Override
            public HeaderConfig createFromParcel(Parcel source) {
                return new HeaderConfig(source);
            }

            @Override
            public HeaderConfig[] newArray(int size) {
                return new HeaderConfig[size];
            }
        };
    }

    public static class AuthConfig implements Parcelable {
        private final String mMessageText;
        private final String mButtonText;
        private final String mImageUrl;

        static final String JSON_KEY_MESSAGE_TEXT = "messageText";
        static final String JSON_KEY_BUTTON_TEXT = "buttonText";
        static final String JSON_KEY_IMAGE_URL = "imageUrl";

        public AuthConfig(JSONObject json) throws JSONException, IllegalArgumentException {
            mMessageText = json.optString(JSON_KEY_MESSAGE_TEXT);
            mButtonText = json.optString(JSON_KEY_BUTTON_TEXT);
            mImageUrl = json.optString(JSON_KEY_IMAGE_URL, null);
        }

        public AuthConfig(Parcel in) {
            mMessageText = in.readString();
            mButtonText = in.readString();
            mImageUrl = in.readString();

            validate();
        }

        public AuthConfig(AuthConfig authConfig) {
            mMessageText = authConfig.mMessageText;
            mButtonText = authConfig.mButtonText;
            mImageUrl = authConfig.mImageUrl;

            validate();
        }

        public AuthConfig(String messageText, String buttonText, String imageUrl) {
            mMessageText = messageText;
            mButtonText = buttonText;
            mImageUrl = imageUrl;

            validate();
        }

        private void validate() {
            if (mMessageText == null) {
                throw new IllegalArgumentException("Can't create AuthConfig with null message text");
            }

            if (mButtonText == null) {
                throw new IllegalArgumentException("Can't create AuthConfig with null button text");
            }
        }

        public String getMessageText() {
            return mMessageText;
        }

        public String getButtonText() {
            return mButtonText;
        }

        public String getImageUrl() {
            return mImageUrl;
        }

        public JSONObject toJSON() throws JSONException {
            final JSONObject json = new JSONObject();

            json.put(JSON_KEY_MESSAGE_TEXT, mMessageText);
            json.put(JSON_KEY_BUTTON_TEXT, mButtonText);
            json.put(JSON_KEY_IMAGE_URL, mImageUrl);

            return json;
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(mMessageText);
            dest.writeString(mButtonText);
            dest.writeString(mImageUrl);
        }

        public static final Creator<AuthConfig> CREATOR = new Creator<AuthConfig>() {
            @Override
            public AuthConfig createFromParcel(final Parcel in) {
                return new AuthConfig(in);
            }

            @Override
            public AuthConfig[] newArray(final int size) {
                return new AuthConfig[size];
            }
        };
    }
   /**
     * Immutable representation of the current state of {@code HomeConfig}.
     * This is what HomeConfig returns from a load() call and takes as
     * input to save a new state.
     *
     * Users of {@code State} should use an {@code Iterator} to iterate
     * through the contained {@code PanelConfig} instances.
     *
     * {@code State} is immutable i.e. you can't add, remove, or update
     * contained elements directly. You have to use an {@code Editor} to
     * change the state, which can be created through the {@code edit()}
     * method.
     */
    public static class State implements Iterable<PanelConfig> {
        private HomeConfig mHomeConfig;
        private final List<PanelConfig> mPanelConfigs;
        private final boolean mIsDefault;

        State(List<PanelConfig> panelConfigs, boolean isDefault) {
            this(null, panelConfigs, isDefault);
        }

        private State(HomeConfig homeConfig, List<PanelConfig> panelConfigs, boolean isDefault) {
            mHomeConfig = homeConfig;
            mPanelConfigs = Collections.unmodifiableList(panelConfigs);
            mIsDefault = isDefault;
        }

        private void setHomeConfig(HomeConfig homeConfig) {
            if (mHomeConfig != null) {
                throw new IllegalStateException("Can't set HomeConfig more than once");
            }

            mHomeConfig = homeConfig;
        }

        @Override
        public Iterator<PanelConfig> iterator() {
            return mPanelConfigs.iterator();
        }

        /**
         * Returns whether this {@code State} instance represents the default
         * {@code HomeConfig} configuration or not.
         */
        public boolean isDefault() {
            return mIsDefault;
        }

        /**
         * Creates an {@code Editor} for this state.
         */
        @RobocopTarget
        public Editor edit() {
            return new Editor(mHomeConfig, this);
        }
    }

    /**
     * {@code Editor} allows you to make changes to a {@code State}. You
     * can create {@code Editor} by calling {@code edit()} on the target
     * {@code State} instance.
     *
     * {@code Editor} works on a copy of the {@code State} that originated
     * it. This means that adding, removing, or updating panels in an
     * {@code Editor} will never change the {@code State} which you
     * created the {@code Editor} from. Calling {@code commit()} or
     * {@code apply()} will cause the new {@code State} instance to be
     * created and saved using the {@code HomeConfig} instance that
     * created the source {@code State}.
     *
     * {@code Editor} is *not* thread-safe. You can only make calls on it
     * from the thread where it was originally created. It will throw an
     * exception if you don't follow this invariant.
     */
    public static class Editor implements Iterable<PanelConfig> {
        private final HomeConfig mHomeConfig;
        private final Map<String, PanelConfig> mConfigMap;
        private final List<String> mConfigOrder;
        private final Thread mOriginalThread;

        // Each Pair represents parameters to a EventDispatcher.dispatch call;
        // the String is the event name and the GeckoBundle is the event data.
        private List<Pair<String, GeckoBundle>> mNotificationQueue;
        private PanelConfig mDefaultPanel;
        private int mEnabledCount;

        private boolean mHasChanged;
        private final boolean mIsFromDefault;

        private Editor(HomeConfig homeConfig, State configState) {
            mHomeConfig = homeConfig;
            mOriginalThread = Thread.currentThread();
            mConfigMap = new HashMap<String, PanelConfig>();
            mConfigOrder = new LinkedList<String>();
            mNotificationQueue = new ArrayList<>();

            mIsFromDefault = configState.isDefault();

            initFromState(configState);
        }

        /**
         * Initialize the initial state of the editor from the given
         * {@sode State}. A HashMap is used to represent the list of
         * panels as it provides fast access, and a LinkedList is used to
         * keep track of order. We keep a reference to the default panel
         * and the number of enabled panels to avoid iterating through the
         * map every time we need those.
         *
         * @param configState The source State to load the editor from.
         */
        private void initFromState(State configState) {
            for (PanelConfig panelConfig : configState) {
                final PanelConfig panelCopy = new PanelConfig(panelConfig);

                if (!panelCopy.isDisabled()) {
                    mEnabledCount++;
                }

                if (panelCopy.isDefault()) {
                    if (mDefaultPanel == null) {
                        mDefaultPanel = panelCopy;
                    } else {
                        throw new IllegalStateException("Multiple default panels in HomeConfig state");
                    }
                }

                final String panelId = panelConfig.getId();
                mConfigOrder.add(panelId);
                mConfigMap.put(panelId, panelCopy);
            }

            // We should always have a defined default panel if there's
            // at least one enabled panel around.
            if (mEnabledCount > 0 && mDefaultPanel == null) {
                throw new IllegalStateException("Default panel in HomeConfig state is undefined");
            }
        }

        private PanelConfig getPanelOrThrow(String panelId) {
            final PanelConfig panelConfig = mConfigMap.get(panelId);
            if (panelConfig == null) {
                throw new IllegalStateException("Tried to access non-existing panel: " + panelId);
            }

            return panelConfig;
        }

        private boolean isCurrentDefaultPanel(PanelConfig panelConfig) {
            if (mDefaultPanel == null) {
                return false;
            }

            return mDefaultPanel.equals(panelConfig);
        }

        private void findNewDefault() {
            // Pick the first panel that is neither disabled nor currently
            // set as default.
            for (PanelConfig panelConfig : makeOrderedCopy(false)) {
                if (!panelConfig.isDefault() && !panelConfig.isDisabled()) {
                    setDefault(panelConfig.getId());
                    return;
                }
            }

            mDefaultPanel = null;
        }

        /**
         * Makes an ordered list of PanelConfigs that can be references
         * or deep copied objects.
         *
         * @param deepCopy true to make deep-copied objects
         * @return ordered List of PanelConfigs
         */
        private List<PanelConfig> makeOrderedCopy(boolean deepCopy) {
            final List<PanelConfig> copiedList = new ArrayList<PanelConfig>(mConfigOrder.size());
            for (String panelId : mConfigOrder) {
                PanelConfig panelConfig = mConfigMap.get(panelId);
                if (deepCopy) {
                    panelConfig = new PanelConfig(panelConfig);
                }
                copiedList.add(panelConfig);
            }

            return copiedList;
        }

        private void setPanelIsDisabled(PanelConfig panelConfig, boolean disabled) {
            if (panelConfig.isDisabled() == disabled) {
                return;
            }

            panelConfig.setIsDisabled(disabled);
            mEnabledCount += (disabled ? -1 : 1);
        }

        /**
         * Gets the ID of the current default panel.
         */
        @RobocopTarget
        public String getDefaultPanelId() {
            ThreadUtils.assertOnThread(mOriginalThread);

            if (mDefaultPanel == null) {
                return null;
            }

            return mDefaultPanel.getId();
        }

        /**
         * Set a new default panel.
         *
         * @param panelId the ID of the new default panel.
         */
        @RobocopTarget
        public void setDefault(String panelId) {
            ThreadUtils.assertOnThread(mOriginalThread);

            final PanelConfig panelConfig = getPanelOrThrow(panelId);
            if (isCurrentDefaultPanel(panelConfig)) {
                return;
            }

            if (mDefaultPanel != null) {
                mDefaultPanel.setIsDefault(false);
            }

            panelConfig.setIsDefault(true);
            setPanelIsDisabled(panelConfig, false);

            mDefaultPanel = panelConfig;
            mHasChanged = true;
        }

        /**
         * Toggles disabled state for a panel.
         *
         * @param panelId the ID of the target panel.
         * @param disabled true to disable the panel.
         */
        public void setDisabled(String panelId, boolean disabled) {
            ThreadUtils.assertOnThread(mOriginalThread);

            final PanelConfig panelConfig = getPanelOrThrow(panelId);
            if (panelConfig.isDisabled() == disabled) {
                return;
            }

            setPanelIsDisabled(panelConfig, disabled);

            if (disabled) {
                if (isCurrentDefaultPanel(panelConfig)) {
                    panelConfig.setIsDefault(false);
                    findNewDefault();
                }
            } else if (mEnabledCount == 1) {
                setDefault(panelId);
            }

            mHasChanged = true;
        }

        /**
         * Adds a new {@code PanelConfig}. It will do nothing if the
         * {@code Editor} already contains a panel with the same ID.
         *
         * @param panelConfig the {@code PanelConfig} instance to be added.
         * @return true if the item has been added.
         */
        public boolean install(PanelConfig panelConfig) {
            ThreadUtils.assertOnThread(mOriginalThread);

            if (panelConfig == null) {
                throw new IllegalStateException("Can't install a null panel");
            }

            if (!panelConfig.isDynamic()) {
                throw new IllegalStateException("Can't install a built-in panel: " + panelConfig.getId());
            }

            if (panelConfig.isDisabled()) {
                throw new IllegalStateException("Can't install a disabled panel: " + panelConfig.getId());
            }

            boolean installed = false;

            final String id = panelConfig.getId();
            if (!mConfigMap.containsKey(id)) {
                mConfigMap.put(id, panelConfig);

                final int position = panelConfig.getPosition();
                if (position < 0 || position >= mConfigOrder.size()) {
                    mConfigOrder.add(id);
                } else {
                    mConfigOrder.add(position, id);
                }

                mEnabledCount++;
                if (mEnabledCount == 1 || panelConfig.isDefault()) {
                    setDefault(panelConfig.getId());
                }

                installed = true;

                // Add an event to the queue if a new panel is successfully installed.
                final GeckoBundle data = new GeckoBundle(1);
                data.putString("id", panelConfig.getId());
                mNotificationQueue.add(new Pair<String, GeckoBundle>(
                        "HomePanels:Installed", data));
            }

            mHasChanged = true;
            return installed;
        }

        /**
         * Removes an existing panel.
         *
         * @return true if the item has been removed.
         */
        public boolean uninstall(String panelId) {
            ThreadUtils.assertOnThread(mOriginalThread);

            final PanelConfig panelConfig = mConfigMap.get(panelId);
            if (panelConfig == null) {
                return false;
            }

            if (!panelConfig.isDynamic()) {
                throw new IllegalStateException("Can't uninstall a built-in panel: " + panelConfig.getId());
            }

            mConfigMap.remove(panelId);
            mConfigOrder.remove(panelId);

            if (!panelConfig.isDisabled()) {
                mEnabledCount--;
            }

            if (isCurrentDefaultPanel(panelConfig)) {
                findNewDefault();
            }

            // Add an event to the queue if a panel is successfully uninstalled.
            final GeckoBundle data = new GeckoBundle(1);
            data.putString("id", panelId);
            mNotificationQueue.add(new Pair<String, GeckoBundle>(
                    "HomePanels:Uninstalled", data));

            mHasChanged = true;
            return true;
        }

        /**
         * Moves panel associated with panelId to the specified position.
         *
         * @param panelId Id of panel
         * @param destIndex Destination position
         * @return true if move succeeded
         */
        public boolean moveTo(String panelId, int destIndex) {
            ThreadUtils.assertOnThread(mOriginalThread);

            if (!mConfigOrder.contains(panelId)) {
                return false;
            }

            mConfigOrder.remove(panelId);
            mConfigOrder.add(destIndex, panelId);
            mHasChanged = true;

            return true;
        }

        /**
         * Replaces an existing panel with a new {@code PanelConfig} instance.
         *
         * @return true if the item has been updated.
         */
        public boolean update(PanelConfig panelConfig) {
            ThreadUtils.assertOnThread(mOriginalThread);

            if (panelConfig == null) {
                throw new IllegalStateException("Can't update a null panel");
            }

            boolean updated = false;

            final String id = panelConfig.getId();
            if (mConfigMap.containsKey(id)) {
                final PanelConfig oldPanelConfig = mConfigMap.put(id, panelConfig);

                // The disabled and default states can't never be
                // changed by an update operation.
                panelConfig.setIsDefault(oldPanelConfig.isDefault());
                panelConfig.setIsDisabled(oldPanelConfig.isDisabled());

                updated = true;
            }

            mHasChanged = true;
            return updated;
        }

        /**
         * Saves the current {@code Editor} state asynchronously in the
         * background thread.
         *
         * @return the resulting {@code State} instance.
         */
        @RobocopTarget
        public State apply() {
            ThreadUtils.assertOnThread(mOriginalThread);

            // We're about to save the current state in the background thread
            // so we should use a deep copy of the PanelConfig instances to
            // avoid saving corrupted state.
            final State newConfigState =
                    new State(mHomeConfig, makeOrderedCopy(true), isDefault());

            // Copy the event queue to a new list, so that we only modify mNotificationQueue on
            // the original thread where it was created.
            final List<Pair<String, GeckoBundle>> copiedQueue = mNotificationQueue;
            mNotificationQueue = new ArrayList<>();

            ThreadUtils.getBackgroundHandler().post(new Runnable() {
                @Override
                public void run() {
                    mHomeConfig.save(newConfigState);

                    // Send pending events after the new config is saved.
                    sendNotificationsToGecko(copiedQueue);
                }
            });

            return newConfigState;
        }

        /**
         * Saves the current {@code Editor} state synchronously in the
         * current thread.
         *
         * @return the resulting {@code State} instance.
         */
        public State commit() {
            ThreadUtils.assertOnThread(mOriginalThread);

            final State newConfigState =
                    new State(mHomeConfig, makeOrderedCopy(false), isDefault());

            // This is a synchronous blocking operation, hence no
            // need to deep copy the current PanelConfig instances.
            mHomeConfig.save(newConfigState);

            // Send pending events after the new config is saved.
            sendNotificationsToGecko(mNotificationQueue);
            mNotificationQueue.clear();

            return newConfigState;
        }

        /**
         * Returns whether the {@code Editor} represents the default
         * {@code HomeConfig} configuration without any unsaved changes.
         */
        public boolean isDefault() {
            ThreadUtils.assertOnThread(mOriginalThread);

            return (!mHasChanged && mIsFromDefault);
        }

        public boolean isEmpty() {
            return mConfigMap.isEmpty();
        }

        private void sendNotificationsToGecko(List<Pair<String, GeckoBundle>> notifications) {
            final EventDispatcher dispatcher = EventDispatcher.getInstance();
            for (final Pair<String, GeckoBundle> p : notifications) {
                dispatcher.dispatch(p.first, p.second);
            }
        }

        private class EditorIterator implements Iterator<PanelConfig> {
            private final Iterator<String> mOrderIterator;

            public EditorIterator() {
                mOrderIterator = mConfigOrder.iterator();
            }

            @Override
            public boolean hasNext() {
                return mOrderIterator.hasNext();
            }

            @Override
            public PanelConfig next() {
                final String panelId = mOrderIterator.next();
                return mConfigMap.get(panelId);
            }

            @Override
            public void remove() {
                throw new UnsupportedOperationException("Can't 'remove' from on Editor iterator.");
            }
        }

        @Override
        public Iterator<PanelConfig> iterator() {
            ThreadUtils.assertOnThread(mOriginalThread);

            return new EditorIterator();
        }
    }

    public interface OnReloadListener {
        public void onReload();
    }

    public interface HomeConfigBackend {
        public State load();
        public void save(State configState);
        public String getLocale();
        public void setOnReloadListener(OnReloadListener listener);
    }

    // UUIDs used to create PanelConfigs for default built-in panels. These are
    // public because they can be used in "about:home?panel=UUID" query strings
    // to open specific panels without querying the active Home Panel
    // configuration. Because they don't consider the active configuration, it
    // is only sensible to do this for built-in panels (and not for dynamic
    // panels).
    /* package-private */ static final String TOP_SITES_PANEL_ID = "4becc86b-41eb-429a-a042-88fe8b5a094e";
    private static final String BOOKMARKS_PANEL_ID = "7f6d419a-cd6c-4e34-b26f-f68b1b551907";
    private static final String HISTORY_PANEL_ID = "f134bf20-11f7-4867-ab8b-e8e705d7fbe8";
    private static final String COMBINED_HISTORY_PANEL_ID = "4d716ce2-e063-486d-9e7c-b190d7b04dc6";
    private static final String RECENT_TABS_PANEL_ID = "5c2601a5-eedc-4477-b297-ce4cef52adf8";
    private static final String REMOTE_TABS_PANEL_ID = "72429afd-8d8b-43d8-9189-14b779c563d0";
    private static final String DEPRECATED_READING_LIST_PANEL_ID = "20f4549a-64ad-4c32-93e4-1dcef792733b";

    private final HomeConfigBackend mBackend;

    public HomeConfig(HomeConfigBackend backend) {
        mBackend = backend;
    }

    @RobocopTarget
    public State load() {
        final State configState = mBackend.load();
        configState.setHomeConfig(this);

        return configState;
    }

    public String getLocale() {
        return mBackend.getLocale();
    }

    public void save(State configState) {
        mBackend.save(configState);
    }

    public void setOnReloadListener(OnReloadListener listener) {
        mBackend.setOnReloadListener(listener);
    }

    public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType) {
        return createBuiltinPanelConfig(context, panelType, EnumSet.noneOf(PanelConfig.Flags.class));
    }

    public static int getTitleResourceIdForBuiltinPanelType(PanelType panelType) {
        switch (panelType) {
        case TOP_SITES:
            return R.string.home_top_sites_title;

        case BOOKMARKS:
        case DEPRECATED_READING_LIST:
            return R.string.bookmarks_title;

        case DEPRECATED_HISTORY:
        case DEPRECATED_REMOTE_TABS:
        case DEPRECATED_RECENT_TABS:
        case COMBINED_HISTORY:
            return R.string.home_history_title;

        default:
            throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
        }
    }

    @RobocopTarget
    public static String getIdForBuiltinPanelType(PanelType panelType) {
        switch (panelType) {
        case TOP_SITES:
            return TOP_SITES_PANEL_ID;

        case BOOKMARKS:
            return BOOKMARKS_PANEL_ID;

        case DEPRECATED_HISTORY:
            return HISTORY_PANEL_ID;

        case COMBINED_HISTORY:
            return COMBINED_HISTORY_PANEL_ID;

        case DEPRECATED_REMOTE_TABS:
            return REMOTE_TABS_PANEL_ID;

        case DEPRECATED_READING_LIST:
            return DEPRECATED_READING_LIST_PANEL_ID;

        case DEPRECATED_RECENT_TABS:
            return RECENT_TABS_PANEL_ID;

        default:
            throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
        }
    }

    public static PanelConfig createBuiltinPanelConfig(Context context, PanelType panelType, EnumSet<PanelConfig.Flags> flags) {
        final int titleId = getTitleResourceIdForBuiltinPanelType(panelType);
        final String id = getIdForBuiltinPanelType(panelType);

        return new PanelConfig(panelType, context.getString(titleId), id, flags);
    }

    @RobocopTarget
    public static HomeConfig getDefault(Context context) {
        return new HomeConfig(new HomeConfigPrefsBackend(context));
    }
}
