/*
 * Copyright (C) 2019 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 android.net;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.ipmemorystore.Blob;
import android.net.ipmemorystore.NetworkAttributes;
import android.net.ipmemorystore.OnBlobRetrievedListener;
import android.net.ipmemorystore.OnDeleteStatusListener;
import android.net.ipmemorystore.OnL2KeyResponseListener;
import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener;
import android.net.ipmemorystore.OnNetworkEventCountRetrievedListener;
import android.net.ipmemorystore.OnSameL3NetworkResponseListener;
import android.net.ipmemorystore.OnStatusListener;
import android.net.ipmemorystore.Status;
import android.os.RemoteException;
import android.util.Log;

import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;

/**
 * service used to communicate with the ip memory store service in network stack,
 * which is running in a separate module.
 * @hide
 */
public abstract class IpMemoryStoreClient {
    private static final String TAG = IpMemoryStoreClient.class.getSimpleName();
    private final Context mContext;

    public IpMemoryStoreClient(@NonNull final Context context) {
        if (context == null) throw new IllegalArgumentException("missing context");
        mContext = context;
    }

    protected abstract void runWhenServiceReady(Consumer<IIpMemoryStore> cb)
            throws ExecutionException;

    @FunctionalInterface
    private interface ThrowingRunnable {
        void run() throws RemoteException;
    }

    private void ignoringRemoteException(ThrowingRunnable r) {
        ignoringRemoteException("Failed to execute remote procedure call", r);
    }

    private void ignoringRemoteException(String message, ThrowingRunnable r) {
        try {
            r.run();
        } catch (RemoteException e) {
            Log.e(TAG, message, e);
        }
    }

    /**
     * Store network attributes for a given L2 key.
     * If L2Key is null, choose automatically from the attributes ; passing null is equivalent to
     * calling findL2Key with the attributes and storing in the returned value.
     *
     * @param l2Key The L2 key for the L2 network. Clients that don't know or care about the L2
     *              key and only care about grouping can pass a unique ID here like the ones
     *              generated by {@code java.util.UUID.randomUUID()}, but keep in mind the low
     *              relevance of such a network will lead to it being evicted soon if it's not
     *              refreshed. Use findL2Key to try and find a similar L2Key to these attributes.
     * @param attributes The attributes for this network.
     * @param listener A listener that will be invoked to inform of the completion of this call,
     *                 or null if the client is not interested in learning about success/failure.
     * Through the listener, returns the L2 key. This is useful if the L2 key was not specified.
     * If the call failed, the L2 key will be null.
     */
    public void storeNetworkAttributes(@NonNull final String l2Key,
            @NonNull final NetworkAttributes attributes,
            @Nullable final OnStatusListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.storeNetworkAttributes(l2Key, attributes.toParcelable(),
                            OnStatusListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            if (null == listener) return;
            ignoringRemoteException("Error storing network attributes",
                    () -> listener.onComplete(new Status(Status.ERROR_UNKNOWN)));
        }
    }

    /**
     * Store a binary blob associated with an L2 key and a name.
     *
     * @param l2Key The L2 key for this network.
     * @param clientId The ID of the client.
     * @param name The name of this data.
     * @param data The data to store.
     * @param listener A listener to inform of the completion of this call, or null if the client
     *        is not interested in learning about success/failure.
     * Through the listener, returns a status to indicate success or failure.
     */
    public void storeBlob(@NonNull final String l2Key, @NonNull final String clientId,
            @NonNull final String name, @NonNull final Blob data,
            @Nullable final OnStatusListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.storeBlob(l2Key, clientId, name, data,
                            OnStatusListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            if (null == listener) return;
            ignoringRemoteException("Error storing blob",
                    () -> listener.onComplete(new Status(Status.ERROR_UNKNOWN)));
        }
    }

    /**
     * Returns the best L2 key associated with the attributes.
     *
     * This will find a record that would be in the same group as the passed attributes. This is
     * useful to choose the key for storing a sample or private data when the L2 key is not known.
     * If multiple records are group-close to these attributes, the closest match is returned.
     * If multiple records have the same closeness, the one with the smaller (unicode codepoint
     * order) L2 key is returned.
     * If no record matches these attributes, null is returned.
     *
     * @param attributes The attributes of the network to find.
     * @param listener The listener that will be invoked to return the answer.
     * Through the listener, returns the L2 key if one matched, or null.
     */
    public void findL2Key(@NonNull final NetworkAttributes attributes,
            @NonNull final OnL2KeyResponseListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.findL2Key(attributes.toParcelable(),
                            OnL2KeyResponseListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            ignoringRemoteException("Error finding L2 Key",
                    () -> listener.onL2KeyResponse(new Status(Status.ERROR_UNKNOWN),
                            null /* l2Key */));
        }
    }

    /**
     * Returns whether, to the best of the store's ability to tell, the two specified L2 keys point
     * to the same L3 network. Group-closeness is used to determine this.
     *
     * @param l2Key1 The key for the first network.
     * @param l2Key2 The key for the second network.
     * @param listener The listener that will be invoked to return the answer.
     * Through the listener, a SameL3NetworkResponse containing the answer and confidence.
     */
    public void isSameNetwork(@NonNull final String l2Key1, @NonNull final String l2Key2,
            @NonNull final OnSameL3NetworkResponseListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.isSameNetwork(l2Key1, l2Key2,
                            OnSameL3NetworkResponseListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            ignoringRemoteException("Error checking for network sameness",
                    () -> listener.onSameL3NetworkResponse(new Status(Status.ERROR_UNKNOWN),
                            null /* response */));
        }
    }

    /**
     * Retrieve the network attributes for a key.
     * If no record is present for this key, this will return null attributes.
     *
     * @param l2Key The key of the network to query.
     * @param listener The listener that will be invoked to return the answer.
     * Through the listener, returns the network attributes and the L2 key associated with
     *         the query.
     */
    public void retrieveNetworkAttributes(@NonNull final String l2Key,
            @NonNull final OnNetworkAttributesRetrievedListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.retrieveNetworkAttributes(l2Key,
                            OnNetworkAttributesRetrievedListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            ignoringRemoteException("Error retrieving network attributes",
                    () -> listener.onNetworkAttributesRetrieved(new Status(Status.ERROR_UNKNOWN),
                            null /* l2Key */, null /* attributes */));
        }
    }

    /**
     * Retrieve previously stored private data.
     * If no data was stored for this L2 key and name this will return null.
     *
     * @param l2Key The L2 key.
     * @param clientId The id of the client that stored this data.
     * @param name The name of the data.
     * @param listener The listener that will be invoked to return the answer.
     * Through the listener, returns the private data (or null), with the L2 key
     *         and the name of the data associated with the query.
     */
    public void retrieveBlob(@NonNull final String l2Key, @NonNull final String clientId,
            @NonNull final String name, @NonNull final OnBlobRetrievedListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.retrieveBlob(l2Key, clientId, name,
                            OnBlobRetrievedListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            ignoringRemoteException("Error retrieving blob",
                    () -> listener.onBlobRetrieved(new Status(Status.ERROR_UNKNOWN),
                            null /* l2Key */, null /* name */, null /* blob */));
        }
    }

    /**
     * Delete a single entry.
     *
     * @param l2Key The L2 key of the entry to delete.
     * @param needWipe Whether the data must be wiped from disk immediately. This makes the
     *                 operation vastly more expensive as the database files will have to be copied
     *                 and created again from the old files (see sqlite3 VACUUM operation for
     *                 details) and makes no functional difference; only pass true if security or
     *                 privacy demands this data must be removed from disk immediately.
     *                 Note that this can fail for storage reasons. The passed listener will then
     *                 receive an appropriate error status with the number of deleted rows.
     * @param listener A listener that will be invoked to inform of the completion of this call,
     *                 or null if the client is not interested in learning about success/failure.
     * returns (through the listener) A status to indicate success and the number of deleted records
     */
    public void delete(@NonNull final String l2Key, final boolean needWipe,
            @Nullable final OnDeleteStatusListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(() ->
                    service.delete(l2Key, needWipe, OnDeleteStatusListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            if (null == listener) return;
            ignoringRemoteException("Error deleting from the memory store",
                    () -> listener.onComplete(new Status(Status.ERROR_UNKNOWN),
                            0 /* deletedRecords */));
        }
    }

    /**
     * Delete all entries in a cluster.
     *
     * This method will delete all entries in the memory store that have the cluster attribute
     * passed as an argument.
     *
     * @param cluster The cluster to delete.
     * @param needWipe Whether the data must be wiped from disk immediately. This makes the
     *                 operation vastly more expensive as the database files will have to be copied
     *                 and created again from the old files (see sqlite3 VACUUM operation for
     *                 details) and makes no functional difference; only pass true if security or
     *                 privacy demands this data must be removed from disk immediately.
     *                 Note that this can fail for storage reasons. The passed listener will then
     *                 receive an appropriate error status with the number of deleted rows.
     * @param listener A listener that will be invoked to inform of the completion of this call,
     *                 or null if the client is not interested in learning about success/failure.
     * returns (through the listener) A status to indicate success and the number of deleted records
     */
    public void deleteCluster(@NonNull final String cluster, final boolean needWipe,
            @Nullable final OnDeleteStatusListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.deleteCluster(cluster, needWipe,
                            OnDeleteStatusListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            if (null == listener) return;
            ignoringRemoteException("Error deleting from the memory store",
                    () -> listener.onComplete(new Status(Status.ERROR_UNKNOWN),
                            0 /* deletedRecords */));
        }
    }

    /**
     * Wipe the data in the database upon network factory reset.
     */
    public void factoryReset() {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.factoryReset()));
        } catch (ExecutionException m) {
            Log.e(TAG, "Error executing factory reset", m);
        }
    }

    /**
     * Retrieve the specific network event counts for a given cluster and event type since one or
     * more timestamps in the past.
     *
     * @param cluster The cluster to query.
     * @param sinceTimes An array of timestamps in the past. The query will return an array of
     *                   equal size. Each element in the array will contain the number of network
     *                   events between the corresponding timestamp and the current time, e.g. query
     *                   since the last week and/or the last day.
     * @param eventTypes An array of network event types to query, which can be one or more of the
     *                   above NETWORK_EVENT constants.
     * @param listener The listener that will be invoked to return the answer.
     * returns (through the listener) The event counts associated with the query, or an empty array
     *                                if the query failed.
     */
    public void retrieveNetworkEventCount(@NonNull final String cluster,
            @NonNull final long[] sinceTimes,
            @NonNull final int[] eventTypes,
            @Nullable final OnNetworkEventCountRetrievedListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.retrieveNetworkEventCount(cluster, sinceTimes, eventTypes,
                            OnNetworkEventCountRetrievedListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            ignoringRemoteException("Error retrieving network event count",
                    () -> listener.onNetworkEventCountRetrieved(
                            new Status(Status.ERROR_UNKNOWN),
                            new int[0]) /* empty counts */);
        }
    }

    /**
     * Store a specific network event to database for a given cluster.
     *
     * @param cluster The cluster representing a notion of network group (e.g., BSSIDs with the
     *                same SSID).
     * @param timestamp The timestamp {@link System.currentTimeMillis} when a specific network
     *                  event occurred.
     * @param expiry The timestamp {@link System.currentTimeMillis} when a specific network
     *               event stored in the database expires, e.g. it might be one week from now.
     * @param eventType One of the NETWORK_EVENT constants above.
     * @param listener A listener that will be invoked to inform of the completion of this call.
     * returns (through the listener) A status to indicate success or failure.
     */
    public void storeNetworkEvent(@NonNull final String cluster,
            final long timestamp,
            final long expiry,
            final int eventType,
            @Nullable final OnStatusListener listener) {
        try {
            runWhenServiceReady(service -> ignoringRemoteException(
                    () -> service.storeNetworkEvent(cluster, timestamp, expiry, eventType,
                            OnStatusListener.toAIDL(listener))));
        } catch (ExecutionException m) {
            if (null == listener) return;
            ignoringRemoteException("Error storing network event",
                    () -> listener.onComplete(new Status(Status.ERROR_UNKNOWN)));
        }
    }
}
