/*
 * Copyright 2018 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.internal.app;

import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;

import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.Message;
import android.os.UserHandle;
import android.util.Log;

import com.android.internal.app.ResolverActivity.ResolvedComponentInfo;
import com.android.internal.app.chooser.TargetInfo;

import com.google.android.collect.Lists;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;

/**
 * Uses an {@link AppPredictor} to sort Resolver targets. If the AppPredictionService appears to be
 * disabled by returning an empty sorted target list, {@link AppPredictionServiceResolverComparator}
 * will fallback to using a {@link ResolverRankerServiceResolverComparator}.
 */
class AppPredictionServiceResolverComparator extends AbstractResolverComparator {

    private static final String TAG = "APSResolverComparator";

    private final AppPredictor mAppPredictor;
    private final Context mContext;
    private final UserHandle mUser;
    private final Intent mIntent;
    private final String mReferrerPackage;

    private final ModelBuilder mModelBuilder;
    private ResolverComparatorModel mComparatorModel;

    // If this is non-null (and this is not destroyed), it means APS is disabled and we should fall
    // back to using the ResolverRankerService.
    // TODO: responsibility for this fallback behavior can live outside of the AppPrediction client.
    private ResolverRankerServiceResolverComparator mResolverRankerService;

    AppPredictionServiceResolverComparator(
            Context context,
            Intent intent,
            String referrerPackage,
            AppPredictor appPredictor,
            UserHandle user,
            ChooserActivityLogger chooserActivityLogger) {
        super(context, intent, Lists.newArrayList(user));
        mContext = context;
        mIntent = intent;
        mAppPredictor = appPredictor;
        mUser = user;
        mReferrerPackage = referrerPackage;
        setChooserActivityLogger(chooserActivityLogger);

        mModelBuilder = new ModelBuilder(appPredictor, user);
        mComparatorModel = mModelBuilder.buildFromRankedList(Collections.emptyList());
    }

    @Override
    void destroy() {
        if (mResolverRankerService != null) {
            mResolverRankerService.destroy();
            mResolverRankerService = null;

            // TODO: may not be necessary to build a new model, since we're destroying anyways.
            mComparatorModel = mModelBuilder.buildFallbackModel(mResolverRankerService);
        }
    }

    @Override
    int compare(ResolveInfo lhs, ResolveInfo rhs) {
        return mComparatorModel.getComparator().compare(lhs, rhs);
    }

    @Override
    float getScore(TargetInfo targetInfo) {
        return mComparatorModel.getScore(targetInfo);
    }

    @Override
    void updateModel(TargetInfo targetInfo) {
        mComparatorModel.notifyOnTargetSelected(targetInfo);
    }

    @Override
    void handleResultMessage(Message msg) {
        // Null value is okay if we have defaulted to the ResolverRankerService.
        if (msg.what == RANKER_SERVICE_RESULT && msg.obj != null) {
            // TODO: this probably never happens? The sorting callback circumvents the Handler
            // design to call handleResult() directly instead of sending the list through a Message.
            // (OK to leave as-is since the Handler design is going away soon.)
            mComparatorModel = mModelBuilder.buildFromRankedList((List<AppTarget>) msg.obj);
        } else if (msg.obj == null && mResolverRankerService == null) {
            Log.e(TAG, "Unexpected null result");
        }
    }

    @Override
    void doCompute(List<ResolvedComponentInfo> targets) {
        if (targets.isEmpty()) {
            mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT);
            return;
        }
        List<AppTarget> appTargets = new ArrayList<>();
        for (ResolvedComponentInfo target : targets) {
            appTargets.add(
                    new AppTarget.Builder(
                        new AppTargetId(target.name.flattenToString()),
                            target.name.getPackageName(),
                            mUser)
                    .setClassName(target.name.getClassName())
                    .build());
        }
        mAppPredictor.sortTargets(appTargets, Executors.newSingleThreadExecutor(),
                sortedAppTargets -> {
                    if (sortedAppTargets.isEmpty()) {
                        Log.i(TAG, "AppPredictionService disabled. Using resolver.");
                        setupFallbackModel(targets);
                    } else {
                        Log.i(TAG, "AppPredictionService response received");
                        // Skip sending to Handler which takes extra time to dispatch messages.
                        // TODO: the Handler guards some concurrency conditions, so this could
                        // probably result in a race (we're not currently on the Handler thread?).
                        // We'll leave this as-is since we intend to remove the Handler design
                        // shortly, but this is still an unsound shortcut.
                        handleResult(sortedAppTargets);
                    }
                }
        );
    }

    private void setupFallbackModel(List<ResolvedComponentInfo> targets) {
        mResolverRankerService =
                new ResolverRankerServiceResolverComparator(
                        mContext,
                        mIntent,
                        mReferrerPackage,
                        () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
                        getChooserActivityLogger(),
                        mUser);
        mComparatorModel = mModelBuilder.buildFallbackModel(mResolverRankerService);
        mResolverRankerService.compute(targets);
    }

    private void handleResult(List<AppTarget> sortedAppTargets) {
        if (mHandler.hasMessages(RANKER_RESULT_TIMEOUT)) {
            mComparatorModel = mModelBuilder.buildFromRankedList(sortedAppTargets);
            mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
            afterCompute();
        }
    }

    static class ModelBuilder {
        private final AppPredictor mAppPredictor;
        private final UserHandle mUser;

        ModelBuilder(AppPredictor appPredictor, UserHandle user) {
            mAppPredictor = appPredictor;
            mUser = user;
        }

        ResolverComparatorModel buildFromRankedList(List<AppTarget> sortedAppTargets) {
            return new AppPredictionServiceComparatorModel(
                    mAppPredictor, mUser, buildTargetRanksMapFromSortedTargets(sortedAppTargets));
        }

        ResolverComparatorModel buildFallbackModel(
                ResolverRankerServiceResolverComparator fallback) {
            return adaptLegacyResolverComparatorToComparatorModel(fallback);
        }

        // The remaining methods would be static if this weren't an inner class (i.e., they don't
        // depend on any ivars, not even the ones in ModelBuilder).

        private Map<ComponentName, Integer> buildTargetRanksMapFromSortedTargets(
                List<AppTarget> sortedAppTargets) {
            Map<ComponentName, Integer> targetRanks = new HashMap<>();
            for (int i = 0; i < sortedAppTargets.size(); i++) {
                ComponentName componentName = new ComponentName(
                        sortedAppTargets.get(i).getPackageName(),
                        sortedAppTargets.get(i).getClassName());
                targetRanks.put(componentName, i);
                Log.i(TAG, "handleSortedAppTargets, sortedAppTargets #" + i + ": " + componentName);
            }
            return targetRanks;
        }

        // TODO: when the refactoring is further along we'll probably have access to the
        // comparator's new ResolverComparatorModel API, so we won't have to adapt from the legacy
        // interface here. On the other hand, AppPredictionServiceResolverComparatorModel (or its
        // replacement counterpart) shouldn't still be responsible for implementing the
        // ResolverRankerService fallback logic at that time.
        private ResolverComparatorModel adaptLegacyResolverComparatorToComparatorModel(
                AbstractResolverComparator comparator) {
            return new ResolverComparatorModel() {
                    @Override
                    public Comparator<ResolveInfo> getComparator() {
                        // Adapt the base type, which doesn't declare itself to be an implementation
                        // of {@code Comparator<ResolveInfo>} even though it has the one method.
                        return (lhs, rhs) -> comparator.compare(lhs, rhs);
                    }

                    @Override
                    public float getScore(TargetInfo targetInfo) {
                        return comparator.getScore(targetInfo);
                    }

                    @Override
                    public void notifyOnTargetSelected(TargetInfo targetInfo) {
                        comparator.updateModel(targetInfo);
                    }
                };
        }
    }

    // TODO: Finish separating behaviors of AbstractResolverComparator, then (probably) make this a
    // standalone class once clients are written in terms of ResolverComparatorModel.
    static class AppPredictionServiceComparatorModel implements ResolverComparatorModel {
        private final AppPredictor mAppPredictor;
        private final UserHandle mUser;
        private final Map<ComponentName, Integer> mTargetRanks;  // Treat as immutable.

        AppPredictionServiceComparatorModel(
                AppPredictor appPredictor,
                UserHandle user,
                Map<ComponentName, Integer> targetRanks) {
            mAppPredictor = appPredictor;
            mUser = user;
            mTargetRanks = targetRanks;
        }

        @Override
        public Comparator<ResolveInfo> getComparator() {
            return (lhs, rhs) -> {
                Integer lhsRank = mTargetRanks.get(new ComponentName(lhs.activityInfo.packageName,
                        lhs.activityInfo.name));
                Integer rhsRank = mTargetRanks.get(new ComponentName(rhs.activityInfo.packageName,
                        rhs.activityInfo.name));
                if (lhsRank == null && rhsRank == null) {
                    return 0;
                } else if (lhsRank == null) {
                    return -1;
                } else if (rhsRank == null) {
                    return 1;
                }
                return lhsRank - rhsRank;
            };
        }

        @Override
        public float getScore(TargetInfo targetInfo) {
            Integer rank = mTargetRanks.get(targetInfo.getResolvedComponentName());
            if (rank == null) {
                Log.w(TAG, "Score requested for unknown component. Did you call compute yet?");
                return 0f;
            }
            int consecutiveSumOfRanks = (mTargetRanks.size() - 1) * (mTargetRanks.size()) / 2;
            return 1.0f - (((float) rank) / consecutiveSumOfRanks);
        }

        @Override
        public void notifyOnTargetSelected(TargetInfo targetInfo) {
            mAppPredictor.notifyAppTargetEvent(
                    new AppTargetEvent.Builder(
                        new AppTarget.Builder(
                            new AppTargetId(targetInfo.getResolvedComponentName().toString()),
                                targetInfo.getResolvedComponentName().getPackageName(), mUser)
                            .setClassName(targetInfo.getResolvedComponentName()
                                    .getClassName()).build(),
                        ACTION_LAUNCH).build());
        }
    }
}
