/*
 * Copyright (C) 2020 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.server.location.contexthub;

import android.annotation.Nullable;
import android.hardware.contexthub.V1_2.HubAppInfo;
import android.hardware.location.NanoAppInstanceInfo;
import android.util.Log;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.function.Consumer;

/**
 * Manages the state of loaded nanoapps at the Context Hubs.
 *
 * This class maintains a list of nanoapps that have been informed as loaded at the hubs. The state
 * should be updated based on the hub callbacks (defined in IContexthubCallback.hal), as a result
 * of either loadNanoApp, unloadNanoApp, or queryApps.
 *
 * The state tracked by this manager is used by clients of ContextHubService that use the old APIs.
 *
 * TODO(b/69270990): Remove this class and its logic once the old API is deprecated.
 *
 * @hide
 */
/* package */ class NanoAppStateManager {
    private static final String TAG = "NanoAppStateManager";

    /*
     * Enables verbose debug logs for this class.
     */
    private static final boolean ENABLE_LOG_DEBUG = true;

    /*
     * Service cache maintaining of handle to nanoapp infos.
     */
    private final HashMap<Integer, NanoAppInstanceInfo> mNanoAppHash = new HashMap<>();

    /*
     * The next nanoapp handle to use.
     */
    private int mNextHandle = 0;

    /**
     * @param nanoAppHandle the nanoapp handle
     * @return the NanoAppInstanceInfo for the given nanoapp, or null if the nanoapp does not exist
     *         in the cache
     */
    @Nullable
    /* package */
    synchronized NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) {
        return mNanoAppHash.get(nanoAppHandle);
    }

    /**
     * Invokes a Consumer operation for each NanoAppInstanceInfo entry in the cache
     *
     * @param consumer the Consumer operation to perform
     */
    /* package */
    synchronized void foreachNanoAppInstanceInfo(Consumer<NanoAppInstanceInfo> consumer) {
        for (NanoAppInstanceInfo info : mNanoAppHash.values()) {
            consumer.accept(info);
        }
    }

    /**
     * @param contextHubId the ID of the hub to search for the instance
     * @param nanoAppId the unique 64-bit ID of the nanoapp
     * @return the nanoapp handle, -1 if the nanoapp is not in the cache
     */
    /* package */
    synchronized int getNanoAppHandle(int contextHubId, long nanoAppId) {
        for (NanoAppInstanceInfo info : mNanoAppHash.values()) {
            if (info.getContexthubId() == contextHubId && info.getAppId() == nanoAppId) {
                return info.getHandle();
            }
        }

        return -1;
    }

    /**
     * Adds a nanoapp instance to the cache.
     *
     * If the cache already contained the nanoapp, the entry is removed and a new nanoapp handle is
     * generated.
     *
     * @param contextHubId the ID of the hub the nanoapp is loaded in
     * @param nanoAppId the unique 64-bit ID of the nanoapp
     * @param nanoAppVersion the version of the nanoapp
     */
    /* package */
    synchronized void addNanoAppInstance(int contextHubId, long nanoAppId, int nanoAppVersion) {
        removeNanoAppInstance(contextHubId, nanoAppId);
        if (mNanoAppHash.size() == Integer.MAX_VALUE) {
            Log.e(TAG, "Error adding nanoapp instance: max limit exceeded");
            return;
        }

        int nanoAppHandle = mNextHandle;
        for (int i = 0; i <= Integer.MAX_VALUE; i++) {
            if (!mNanoAppHash.containsKey(nanoAppHandle)) {
                mNanoAppHash.put(nanoAppHandle, new NanoAppInstanceInfo(
                        nanoAppHandle, nanoAppId, nanoAppVersion, contextHubId));
                mNextHandle = (nanoAppHandle == Integer.MAX_VALUE) ? 0 : nanoAppHandle + 1;
                break;
            }
            nanoAppHandle = (nanoAppHandle == Integer.MAX_VALUE) ? 0 : nanoAppHandle + 1;
        }

        if (ENABLE_LOG_DEBUG) {
            Log.v(TAG, "Added app instance with handle " + nanoAppHandle + " to hub "
                    + contextHubId + ": ID=0x" + Long.toHexString(nanoAppId)
                    + ", version=0x" + Integer.toHexString(nanoAppVersion));
        }
    }

    /**
     * Removes a nanoapp instance from the cache.
     *
     * @param nanoAppId the ID of the nanoapp to remove the instance of
     */
    /* package */
    synchronized void removeNanoAppInstance(int contextHubId, long nanoAppId) {
        int nanoAppHandle = getNanoAppHandle(contextHubId, nanoAppId);
        mNanoAppHash.remove(nanoAppHandle);
    }

    /**
     * Performs a batch update of the nanoapp cache given a nanoapp query response.
     *
     * @param contextHubId    the ID of the hub the response came from
     * @param nanoAppInfoList the list of loaded nanoapps
     */
    /* package */
    synchronized void updateCache(int contextHubId, List<HubAppInfo> nanoAppInfoList) {
        HashSet<Long> nanoAppIdSet = new HashSet<>();
        for (HubAppInfo appInfo : nanoAppInfoList) {
            handleQueryAppEntry(contextHubId, appInfo.info_1_0.appId, appInfo.info_1_0.version);
            nanoAppIdSet.add(appInfo.info_1_0.appId);
        }

        Iterator<NanoAppInstanceInfo> iterator = mNanoAppHash.values().iterator();
        while (iterator.hasNext()) {
            NanoAppInstanceInfo info = iterator.next();
            if (info.getContexthubId() == contextHubId &&
                    !nanoAppIdSet.contains(info.getAppId())) {
                iterator.remove();
            }
        }
    }

    /**
     * If the nanoapp exists in the cache, then the entry is updated. Otherwise, inserts a new
     * instance of the nanoapp in the cache. This method should only be invoked from updateCache.
     *
     * @param contextHubId the ID of the hub the nanoapp is loaded in
     * @param nanoAppId the unique 64-bit ID of the nanoapp
     * @param nanoAppVersion the version of the nanoapp
     */
    private void handleQueryAppEntry(int contextHubId, long nanoAppId, int nanoAppVersion) {
        int nanoAppHandle = getNanoAppHandle(contextHubId, nanoAppId);
        if (nanoAppHandle == -1) {
            addNanoAppInstance(contextHubId, nanoAppId, nanoAppVersion);
        } else {
            NanoAppInstanceInfo info = mNanoAppHash.get(nanoAppHandle);
            if (info.getAppVersion() != nanoAppVersion) {
                mNanoAppHash.put(nanoAppHandle, new NanoAppInstanceInfo(
                        nanoAppHandle, nanoAppId, nanoAppVersion, contextHubId));
                if (ENABLE_LOG_DEBUG) {
                    Log.v(TAG, "Updated app instance with handle " + nanoAppHandle + " at hub "
                            + contextHubId + ": ID=0x" + Long.toHexString(nanoAppId)
                            + ", version=0x" + Integer.toHexString(nanoAppVersion));
                }
            }
        }
    }
}
