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

package com.android.settings.intelligence.search.indexing;

import android.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.provider.SearchIndexableData;
import android.provider.SearchIndexableResource;
import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.collection.ArraySet;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.util.Xml;

import com.android.settings.intelligence.search.ResultPayload;
import com.android.settings.intelligence.search.SearchFeatureProvider;
import com.android.settings.intelligence.search.SearchIndexableRaw;
import com.android.settings.intelligence.search.sitemap.HighlightableMenu;
import com.android.settings.intelligence.search.sitemap.SiteMapManager;
import com.android.settings.intelligence.search.sitemap.SiteMapPair;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Helper class to convert {@link PreIndexData} to {@link IndexData}.
 */
public class IndexDataConverter {

    private static final String TAG = "IndexDataConverter";

    private static final String SETTINGS_PACKAGE_NAME = "com.android.settings";
    private static final String NODE_NAME_PREFERENCE_SCREEN = "PreferenceScreen";
    private static final String NODE_NAME_CHECK_BOX_PREFERENCE = "CheckBoxPreference";
    private static final String NODE_NAME_LIST_PREFERENCE = "ListPreference";
    private static final List<String> SKIP_NODES = Arrays.asList("intent", "extra");

    public IndexDataConverter() {
    }

    @Deprecated
    public IndexDataConverter(Context context) {
    }

    /**
     * Return the collection of {@param preIndexData} converted into {@link IndexData}.
     *
     * @param preIndexData a collection of {@link SearchIndexableResource},
     *                     {@link SearchIndexableRaw} and non-indexable keys.
     */
    public List<IndexData> convertPreIndexDataToIndexData(PreIndexData preIndexData) {
        final long startConversion = System.currentTimeMillis();
        final Map<String, List<SearchIndexableData>> indexableDataMap =
                preIndexData.getDataToUpdate();
        final Map<String, Set<String>> nonIndexableKeys = preIndexData.getNonIndexableKeys();
        final List<IndexData> indexData = new ArrayList<>();

        for (Map.Entry<String, List<SearchIndexableData>> entry : indexableDataMap.entrySet()) {
            final String authority = entry.getKey();
            final List<SearchIndexableData> indexableData = entry.getValue();

            for (SearchIndexableData data : indexableData) {
                if (data instanceof SearchIndexableRaw) {
                    final SearchIndexableRaw rawData = (SearchIndexableRaw) data;
                    final Set<String> rawNonIndexableKeys = nonIndexableKeys.get(authority);
                    final IndexData convertedRaw = convertRaw(authority, rawData,
                            rawNonIndexableKeys);
                    if (convertedRaw != null) {
                        indexData.add(convertedRaw);
                    }
                } else if (data instanceof SearchIndexableResource) {
                    final SearchIndexableResource sir = (SearchIndexableResource) data;
                    final Set<String> resourceNonIndexableKeys =
                            getNonIndexableKeysForResource(nonIndexableKeys, authority);
                    final List<IndexData> resourceData = convertResource(sir, authority,
                            resourceNonIndexableKeys);
                    indexData.addAll(resourceData);
                }
            }
        }

        final long endConversion = System.currentTimeMillis();
        Log.d(TAG, "Converting pre-index data to index data took: "
                + (endConversion - startConversion));

        return indexData;
    }

    /**
     * Returns a full list of site map pairs based on metadata from all data sources.
     *
     * The content schema follows {@link IndexDatabaseHelper.Tables#TABLE_SITE_MAP}
     */
    public List<SiteMapPair> convertSiteMapPairs(List<IndexData> indexData,
            List<Pair<String, String>> siteMapClassNames) {
        final List<SiteMapPair> pairs = new ArrayList<>();
        if (indexData == null) {
            return pairs;
        }
        // Step 1: loop indexData and build all static site map pairs.
        final Map<String, String> classToTitleMap = new TreeMap<>();
        for (IndexData row : indexData) {
            if (TextUtils.isEmpty(row.className)) {
                continue;
            }
            // Build a map of [class, title] for the next step.
            classToTitleMap.put(row.className, row.screenTitle);
            if (!TextUtils.isEmpty(row.childClassName)) {
                pairs.add(new SiteMapPair(row.className, row.screenTitle,
                        row.childClassName, row.updatedTitle, row.highlightableMenuKey));
            }
        }
        // Step 2: Extend the sitemap pairs by adding dynamic pairs provided by
        // SearchIndexableProvider. The provider only tells us class name so we need to finish
        // the mapping by looking up display title for each class.
        for (Pair<String, String> pair : siteMapClassNames) {
            final String parentName = classToTitleMap.get(pair.first);
            final String childName = classToTitleMap.get(pair.second);
            if (TextUtils.isEmpty(parentName) || TextUtils.isEmpty(childName)) {
                Log.w(TAG, "Cannot build sitemap pair for incomplete names "
                        + pair + parentName + childName);
            } else {
                pairs.add(new SiteMapPair(pair.first, parentName, pair.second, childName,
                        null /* highlightableMenuKey*/));
            }
        }
        // Done
        return pairs;
    }

    public List<IndexData> updateIndexDataPayload(Context context, List<IndexData> indexData) {
        final long startTime = System.currentTimeMillis();
        final List<IndexData> updatedIndexData = new ArrayList<>(indexData);
        for (IndexData row : indexData) {
            String menuKey = row.highlightableMenuKey;
            if (!TextUtils.isEmpty(menuKey)) {
                // top level settings
                continue;
            }
            menuKey = HighlightableMenu.getMenuKey(context, row);
            if (TextUtils.isEmpty(menuKey)) {
                continue;
            }
            updatedIndexData.remove(row);
            updatedIndexData.add(row.mutate().setTopLevelMenuKey(menuKey).build());
        }
        Log.d(TAG, "Updating index data payload took: " + (System.currentTimeMillis() - startTime));
        return updatedIndexData;
    }

    /**
     * Return the conversion of {@link SearchIndexableRaw} to {@link IndexData}.
     * The fields of {@link SearchIndexableRaw} are a subset of {@link IndexData},
     * and there is some data sanitization in the conversion.
     */
    @Nullable
    private IndexData convertRaw(String authority, SearchIndexableRaw raw,
            Set<String> nonIndexableKeys) {
        if (TextUtils.isEmpty(raw.key)) {
            Log.w(TAG, "Skipping null key for raw indexable " + authority + "/" + raw.title);
            return null;
        }
        // A row is enabled if it does not show up as an nonIndexableKey
        boolean enabled = !(nonIndexableKeys != null && nonIndexableKeys.contains(raw.key));

        final IndexData.Builder builder = getIndexDataBuilder();
        builder.setTitle(raw.title)
                .setSummaryOn(raw.summaryOn)
                .setEntries(raw.entries)
                .setKeywords(raw.keywords)
                .setClassName(raw.className)
                .setScreenTitle(raw.screenTitle)
                .setIconResId(raw.iconResId)
                .setIntentAction(raw.intentAction)
                .setIntentTargetPackage(raw.intentTargetPackage)
                .setIntentTargetClass(raw.intentTargetClass)
                .setEnabled(enabled)
                .setPackageName(raw.packageName)
                .setAuthority(authority)
                .setKey(raw.key);

        return builder.build();
    }

    /**
     * Return the conversion of the {@link SearchIndexableResource} to {@link IndexData}.
     * Each of the elements in the xml layout attribute of {@param sir} is a candidate to be
     * converted (including the header element).
     *
     * TODO (b/33577327) simplify this method.
     */
    private List<IndexData> convertResource(SearchIndexableResource sir, String authority,
            Set<String> nonIndexableKeys) {
        final Context context = sir.context;
        XmlResourceParser parser = null;

        List<IndexData> resourceIndexData = new ArrayList<>();
        try {
            parser = context.getResources().getXml(sir.xmlResId);

            int type;
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                    && type != XmlPullParser.START_TAG) {
                // Parse next until start tag is found
            }

            String nodeName = parser.getName();
            if (!NODE_NAME_PREFERENCE_SCREEN.equals(nodeName)) {
                throw new RuntimeException(
                        "XML document must start with <PreferenceScreen> tag; found"
                                + nodeName + " at " + parser.getPositionDescription());
            }

            final int outerDepth = parser.getDepth();
            final AttributeSet attrs = Xml.asAttributeSet(parser);

            final String screenTitle = XmlParserUtils.getDataTitle(context, attrs);
            final String headerKey = XmlParserUtils.getDataKey(context, attrs);

            String title;
            String key;
            String headerTitle;
            String summary;
            String headerSummary;
            String keywords;
            String headerKeywords;
            String childFragment;
            String highlightableMenuKey = null;
            @DrawableRes int iconResId;
            ResultPayload payload;
            boolean enabled;

            // TODO REFACTOR (b/62807132) Add proper inline support
//            Map<String, PreferenceControllerMixin> controllerUriMap = null;
//
//            if (fragmentName != null) {
//                controllerUriMap = DatabaseIndexingUtils
//                        .getPreferenceControllerUriMap(fragmentName, context);
//            }

            headerTitle = XmlParserUtils.getDataTitle(context, attrs);
            headerSummary = XmlParserUtils.getDataSummary(context, attrs);
            headerKeywords = XmlParserUtils.getDataKeywords(context, attrs);
            enabled = !nonIndexableKeys.contains(headerKey);
            // TODO: Set payload type for header results
            IndexData.Builder headerBuilder = getIndexDataBuilder();
            headerBuilder.setTitle(headerTitle)
                    .setSummaryOn(headerSummary)
                    .setScreenTitle(screenTitle)
                    .setKeywords(headerKeywords)
                    .setClassName(sir.className)
                    .setPackageName(sir.packageName)
                    .setAuthority(authority)
                    .setIntentAction(sir.intentAction)
                    .setIntentTargetPackage(sir.intentTargetPackage)
                    .setIntentTargetClass(sir.intentTargetClass)
                    .setEnabled(enabled)
                    .setKey(headerKey);

            // Flag for XML headers which a child element's title.
            boolean isHeaderUnique = true;
            IndexData.Builder builder;

            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                    continue;
                }

                nodeName = parser.getName();
                if (SKIP_NODES.contains(nodeName)) {
                    if (SearchFeatureProvider.DEBUG) {
                        Log.d(TAG, nodeName + " is not a valid entity to index, skip.");
                    }
                    continue;
                }

                title = XmlParserUtils.getDataTitle(context, attrs);
                key = XmlParserUtils.getDataKey(context, attrs);
                enabled = !nonIndexableKeys.contains(key);
                keywords = XmlParserUtils.getDataKeywords(context, attrs);
                iconResId = XmlParserUtils.getDataIcon(context, attrs);
                if (TextUtils.equals(sir.packageName, SETTINGS_PACKAGE_NAME)
                        && SiteMapManager.isTopLevelSettings(sir.className)) {
                    highlightableMenuKey = XmlParserUtils.getHighlightableMenuKey(context, attrs);
                }


                if (isHeaderUnique && TextUtils.equals(headerTitle, title)) {
                    isHeaderUnique = false;
                }

                builder = getIndexDataBuilder();
                builder.setTitle(title)
                        .setKeywords(keywords)
                        .setClassName(sir.className)
                        .setScreenTitle(screenTitle)
                        .setIconResId(iconResId)
                        .setPackageName(sir.packageName)
                        .setAuthority(authority)
                        .setIntentAction(sir.intentAction)
                        .setIntentTargetPackage(sir.intentTargetPackage)
                        .setIntentTargetClass(sir.intentTargetClass)
                        .setEnabled(enabled)
                        .setHighlightableMenuKey(highlightableMenuKey)
                        .setTopLevelMenuKey(highlightableMenuKey)
                        .setKey(key);

                if (!nodeName.equals(NODE_NAME_CHECK_BOX_PREFERENCE)) {
                    summary = XmlParserUtils.getDataSummary(context, attrs);

                    String entries = null;

                    if (nodeName.endsWith(NODE_NAME_LIST_PREFERENCE)) {
                        entries = XmlParserUtils.getDataEntries(context, attrs);
                    }

                    // TODO (b/62254931) index primitives instead of payload
                    // TODO (b/62807132) Add proper inline support
                    //payload = DatabaseIndexingUtils.getPayloadFromUriMap(controllerUriMap, key);
                    childFragment = XmlParserUtils.getDataChildFragment(context, attrs);

                    builder.setSummaryOn(summary)
                            .setEntries(entries)
                            .setChildClassName(childFragment);
                    tryAddIndexDataToList(resourceIndexData, builder);
                } else {
                    // TODO (b/33577327) We removed summary off here. We should check if we can
                    // merge this 'else' section with the one above. Put a break point to
                    // investigate.
                    String summaryOn = XmlParserUtils.getDataSummaryOn(context, attrs);

                    if (TextUtils.isEmpty(summaryOn)) {
                        summaryOn = XmlParserUtils.getDataSummary(context, attrs);
                    }

                    builder.setSummaryOn(summaryOn);

                    tryAddIndexDataToList(resourceIndexData, builder);
                }
            }

            // The xml header's title does not match the title of one of the child settings.
            if (isHeaderUnique) {
                tryAddIndexDataToList(resourceIndexData, headerBuilder);
            }
        } catch (XmlPullParserException e) {
            Log.w(TAG, "XML Error parsing PreferenceScreen: " + sir.className, e);
        } catch (IOException e) {
            Log.w(TAG, "IO Error parsing PreferenceScreen: " + sir.className, e);
        } catch (Resources.NotFoundException e) {
            Log.w(TAG, "Resoucre not found error parsing PreferenceScreen: " + sir.className, e);
        } finally {
            if (parser != null) {
                parser.close();
            }
        }
        return resourceIndexData;
    }

    private void tryAddIndexDataToList(List<IndexData> list, IndexData.Builder data) {
        if (!TextUtils.isEmpty(data.getKey())) {
            list.add(data.build());
        } else {
            Log.w(TAG, "Skipping index for null-key item " + data);
        }
    }

    private Set<String> getNonIndexableKeysForResource(Map<String, Set<String>> nonIndexableKeys,
            String authority) {
        final Set<String> result = nonIndexableKeys.get(authority);
        return result != null ? result : new ArraySet<>();
    }

    protected IndexData.Builder getIndexDataBuilder() {
        return new IndexData.Builder();
    }
}
