/*
 * Copyright (C) 2021 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.NetworkProvider.NetworkOfferCallback;
import android.os.Looper;
import android.os.Message;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executor;

/**
 * A NetworkFactory is an entity that creates NetworkAgent objects.
 * The bearers register with ConnectivityService using {@link #register} and
 * their factory will start receiving scored NetworkRequests.  NetworkRequests
 * can be filtered 3 ways: by NetworkCapabilities, by score and more complexly by
 * overridden function.  All of these can be dynamic - changing NetworkCapabilities
 * or score forces re-evaluation of all current requests.
 *
 * If any requests pass the filter some overrideable functions will be called.
 * If the bearer only cares about very simple start/stopNetwork callbacks, those
 * functions can be overridden.  If the bearer needs more interaction, it can
 * override addNetworkRequest and removeNetworkRequest which will give it each
 * request that passes their current filters.
 * @hide
 **/
// TODO(b/187083878): factor out common code between this and NetworkFactoryLegacyImpl
class NetworkFactoryImpl extends NetworkFactoryLegacyImpl {
    private static final boolean DBG = NetworkFactory.DBG;
    private static final boolean VDBG = NetworkFactory.VDBG;

    // A score that will win against everything, so that score filtering will let all requests
    // through
    // TODO : remove this and replace with an API to listen to all requests.
    @NonNull
    private static final NetworkScore INVINCIBLE_SCORE =
            new NetworkScore.Builder().setLegacyInt(1000).build();

    // TODO(b/187082970): Replace CMD_* with Handler.post(() -> { ... }) since all the CMDs do is to
    //  run the tasks asynchronously on the Handler thread.

    /**
     * Pass a network request to the bearer.  If the bearer believes it can
     * satisfy the request it should connect to the network and create a
     * NetworkAgent.  Once the NetworkAgent is fully functional it will
     * register itself with ConnectivityService using registerNetworkAgent.
     * If the bearer cannot immediately satisfy the request (no network,
     * user disabled the radio, lower-scored network) it should remember
     * any NetworkRequests it may be able to satisfy in the future.  It may
     * disregard any that it will never be able to service, for example
     * those requiring a different bearer.
     * msg.obj = NetworkRequest
     */
    // TODO : this and CANCEL_REQUEST are only used by telephony tests. Replace it in the tests
    // and remove them and the associated code.
    private static final int CMD_REQUEST_NETWORK = NetworkFactory.CMD_REQUEST_NETWORK;

    /**
     * Cancel a network request
     * msg.obj = NetworkRequest
     */
    private static final int CMD_CANCEL_REQUEST = NetworkFactory.CMD_CANCEL_REQUEST;

    /**
     * Internally used to set our best-guess score.
     * msg.obj = new score
     */
    private static final int CMD_SET_SCORE = 3;

    /**
     * Internally used to set our current filter for coarse bandwidth changes with
     * technology changes.
     * msg.obj = new filter
     */
    private static final int CMD_SET_FILTER = 4;

    /**
     * Internally used to send the network offer associated with this factory.
     * No arguments, will read from members
     */
    private static final int CMD_OFFER_NETWORK = 5;

    /**
     * Internally used to send the request to listen to all requests.
     * No arguments, will read from members
     */
    private static final int CMD_LISTEN_TO_ALL_REQUESTS = 6;

    private final Map<NetworkRequest, NetworkRequestInfo> mNetworkRequests =
            new LinkedHashMap<>();

    @NonNull private NetworkScore mScore = new NetworkScore.Builder().setLegacyInt(0).build();

    private final NetworkOfferCallback mRequestCallback = new NetworkOfferCallback() {
        @Override
        public void onNetworkNeeded(@NonNull final NetworkRequest request) {
            handleAddRequest(request);
        }

        @Override
        public void onNetworkUnneeded(@NonNull final NetworkRequest request) {
            handleRemoveRequest(request);
        }
    };
    @NonNull private final Executor mExecutor = command -> post(command);


    // Ideally the filter argument would be non-null, but null has historically meant to see
    // no requests and telephony passes null.
    NetworkFactoryImpl(NetworkFactory parent, Looper looper, Context context,
            @Nullable final NetworkCapabilities filter) {
        super(parent, looper, context,
                null != filter ? filter :
                        NetworkCapabilities.Builder.withoutDefaultCapabilities().build());
    }

    /* Registers this NetworkFactory with the system. May only be called once per factory. */
    @Override public void register(final String logTag) {
        register(logTag, false);
    }

    /**
     * Registers this NetworkFactory with the system ignoring the score filter. This will let
     * the factory always see all network requests matching its capabilities filter.
     * May only be called once per factory.
     */
    @Override public void registerIgnoringScore(final String logTag) {
        register(logTag, true);
    }

    private void register(final String logTag, final boolean listenToAllRequests) {
        if (mProvider != null) {
            throw new IllegalStateException("A NetworkFactory must only be registered once");
        }
        if (DBG) mParent.log("Registering NetworkFactory");

        mProvider = new NetworkProvider(mContext, NetworkFactoryImpl.this.getLooper(), logTag) {
            @Override
            public void onNetworkRequested(@NonNull NetworkRequest request, int score,
                    int servingProviderId) {
                handleAddRequest(request);
            }

            @Override
            public void onNetworkRequestWithdrawn(@NonNull NetworkRequest request) {
                handleRemoveRequest(request);
            }
        };

        ((ConnectivityManager) mContext.getSystemService(
                Context.CONNECTIVITY_SERVICE)).registerNetworkProvider(mProvider);

        // The mScore and mCapabilityFilter members can only be accessed on the handler thread.
        // TODO : offer a separate API to listen to all requests instead
        if (listenToAllRequests) {
            sendMessage(obtainMessage(CMD_LISTEN_TO_ALL_REQUESTS));
        } else {
            sendMessage(obtainMessage(CMD_OFFER_NETWORK));
        }
    }

    private void handleOfferNetwork(@NonNull final NetworkScore score) {
        mProvider.registerNetworkOffer(score, mCapabilityFilter, mExecutor, mRequestCallback);
    }

    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case CMD_REQUEST_NETWORK: {
                handleAddRequest((NetworkRequest) msg.obj);
                break;
            }
            case CMD_CANCEL_REQUEST: {
                handleRemoveRequest((NetworkRequest) msg.obj);
                break;
            }
            case CMD_SET_SCORE: {
                handleSetScore((NetworkScore) msg.obj);
                break;
            }
            case CMD_SET_FILTER: {
                handleSetFilter((NetworkCapabilities) msg.obj);
                break;
            }
            case CMD_OFFER_NETWORK: {
                handleOfferNetwork(mScore);
                break;
            }
            case CMD_LISTEN_TO_ALL_REQUESTS: {
                handleOfferNetwork(INVINCIBLE_SCORE);
                break;
            }
        }
    }

    private static class NetworkRequestInfo {
        @NonNull public final NetworkRequest request;
        public boolean requested; // do we have a request outstanding, limited by score

        NetworkRequestInfo(@NonNull final NetworkRequest request) {
            this.request = request;
            this.requested = false;
        }

        @Override
        public String toString() {
            return "{" + request + ", requested=" + requested + "}";
        }
    }

    /**
     * Add a NetworkRequest that the bearer may want to attempt to satisfy.
     * @see #CMD_REQUEST_NETWORK
     *
     * @param request the request to handle.
     */
    private void handleAddRequest(@NonNull final NetworkRequest request) {
        NetworkRequestInfo n = mNetworkRequests.get(request);
        if (n == null) {
            if (DBG) mParent.log("got request " + request);
            n = new NetworkRequestInfo(request);
            mNetworkRequests.put(n.request, n);
        } else {
            if (VDBG) mParent.log("handle existing request " + request);
        }
        if (VDBG) mParent.log("  my score=" + mScore + ", my filter=" + mCapabilityFilter);

        if (mParent.acceptRequest(request)) {
            n.requested = true;
            mParent.needNetworkFor(request);
        }
    }

    private void handleRemoveRequest(NetworkRequest request) {
        NetworkRequestInfo n = mNetworkRequests.get(request);
        if (n != null) {
            mNetworkRequests.remove(request);
            if (n.requested) mParent.releaseNetworkFor(n.request);
        }
    }

    private void handleSetScore(@NonNull final NetworkScore score) {
        if (mScore.equals(score)) return;
        mScore = score;
        mParent.reevaluateAllRequests();
    }

    private void handleSetFilter(@NonNull final NetworkCapabilities netCap) {
        if (netCap.equals(mCapabilityFilter)) return;
        mCapabilityFilter = netCap;
        mParent.reevaluateAllRequests();
    }

    @Override public final void reevaluateAllRequests() {
        if (mProvider == null) return;
        mProvider.registerNetworkOffer(mScore, mCapabilityFilter, mExecutor, mRequestCallback);
    }

    /**
     * @deprecated this method was never part of the API (system or public) and is only added
     *   for migration of existing clients.
     */
    @Deprecated
    public void setScoreFilter(final int score) {
        setScoreFilter(new NetworkScore.Builder().setLegacyInt(score).build());
    }

    /**
     * Set a score filter for this factory.
     *
     * This should include the transports the factory knows its networks will have, and
     * an optimistic view of the attributes it may have. This does not commit the factory
     * to being able to bring up such a network ; it only lets it avoid hearing about
     * requests that it has no chance of fulfilling.
     *
     * @param score the filter
     */
    @Override public void setScoreFilter(@NonNull final NetworkScore score) {
        sendMessage(obtainMessage(CMD_SET_SCORE, score));
    }

    @Override public void setCapabilityFilter(NetworkCapabilities netCap) {
        sendMessage(obtainMessage(CMD_SET_FILTER, new NetworkCapabilities(netCap)));
    }

    @Override public int getRequestCount() {
        return mNetworkRequests.size();
    }

    @Override public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
        writer.println(toString());
        for (NetworkRequestInfo n : mNetworkRequests.values()) {
            writer.println("  " + n);
        }
    }

    @Override public String toString() {
        return "providerId=" + (mProvider != null ? mProvider.getProviderId() : "null")
                + ", ScoreFilter=" + mScore + ", Filter=" + mCapabilityFilter
                + ", requests=" + mNetworkRequests.size();
    }
}
