/*
 * Copyright (C) 2008 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.wifi.p2p;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.net.wifi.p2p.WifiP2pConfig;
import android.net.wifi.p2p.WifiP2pDevice;
import android.net.wifi.p2p.WifiP2pGroup;
import android.net.wifi.p2p.WifiP2pProvDiscEvent;
import android.net.wifi.p2p.nsd.WifiP2pServiceResponse;
import android.os.Handler;
import android.os.Message;
import android.util.ArraySet;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Protocol;
import com.android.server.wifi.p2p.WifiP2pServiceImpl.P2pStatus;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Listens for events from the wpa_supplicant, and passes them on
 * to the {@link WifiP2pServiceImpl} for handling.
 */
public class WifiP2pMonitor {
    private static final String TAG = "WifiP2pMonitor";

    /* Supplicant events reported to a state machine */
    private static final int BASE = Protocol.BASE_WIFI_MONITOR;

    /* Connection to supplicant established */
    public static final int SUP_CONNECTION_EVENT                 = BASE + 1;
    /* Connection to supplicant lost */
    public static final int SUP_DISCONNECTION_EVENT              = BASE + 2;

    /* P2P events */
    public static final int P2P_DEVICE_FOUND_EVENT               = BASE + 21;
    public static final int P2P_DEVICE_LOST_EVENT                = BASE + 22;
    public static final int P2P_GO_NEGOTIATION_REQUEST_EVENT     = BASE + 23;
    public static final int P2P_GO_NEGOTIATION_SUCCESS_EVENT     = BASE + 25;
    public static final int P2P_GO_NEGOTIATION_FAILURE_EVENT     = BASE + 26;
    public static final int P2P_GROUP_FORMATION_SUCCESS_EVENT    = BASE + 27;
    public static final int P2P_GROUP_FORMATION_FAILURE_EVENT    = BASE + 28;
    public static final int P2P_GROUP_STARTED_EVENT              = BASE + 29;
    public static final int P2P_GROUP_REMOVED_EVENT              = BASE + 30;
    public static final int P2P_INVITATION_RECEIVED_EVENT        = BASE + 31;
    public static final int P2P_INVITATION_RESULT_EVENT          = BASE + 32;
    public static final int P2P_PROV_DISC_PBC_REQ_EVENT          = BASE + 33;
    public static final int P2P_PROV_DISC_PBC_RSP_EVENT          = BASE + 34;
    public static final int P2P_PROV_DISC_ENTER_PIN_EVENT        = BASE + 35;
    public static final int P2P_PROV_DISC_SHOW_PIN_EVENT         = BASE + 36;
    public static final int P2P_FIND_STOPPED_EVENT               = BASE + 37;
    public static final int P2P_SERV_DISC_RESP_EVENT             = BASE + 38;
    public static final int P2P_PROV_DISC_FAILURE_EVENT          = BASE + 39;
    public static final int P2P_FREQUENCY_CHANGED_EVENT          = BASE + 40;

    /* hostap events */
    public static final int AP_STA_DISCONNECTED_EVENT            = BASE + 41;
    public static final int AP_STA_CONNECTED_EVENT               = BASE + 42;

    public static final int PROV_DISC_STATUS_SUCCESS             = 0;
    public static final int PROV_DISC_STATUS_TIMEOUT             = 1;
    public static final int PROV_DISC_STATUS_REJECTED            = 2;
    public static final int PROV_DISC_STATUS_TIMEOUT_JOIN        = 3;
    public static final int PROV_DISC_STATUS_INFO_UNAVAILABLE    = 4;
    public static final int PROV_DISC_STATUS_UNKNOWN             = 5;
    @IntDef(prefix = {"PROV_DISC_STATUS_"}, value = {
            PROV_DISC_STATUS_SUCCESS,
            PROV_DISC_STATUS_TIMEOUT,
            PROV_DISC_STATUS_REJECTED,
            PROV_DISC_STATUS_TIMEOUT_JOIN,
            PROV_DISC_STATUS_INFO_UNAVAILABLE,
            PROV_DISC_STATUS_UNKNOWN})
    @Retention(RetentionPolicy.SOURCE)
    public @interface P2pProvDiscStatus {
    }

    private boolean mVerboseLoggingEnabled = false;

    /**
     * Enable verbose logging for all sub modules.
     */
    public void enableVerboseLogging(boolean verboseEnabled) {
        mVerboseLoggingEnabled = verboseEnabled;
    }

    private final Map<String, SparseArray<Set<Handler>>> mHandlerMap = new HashMap<>();

    /**
     * Registers a callback handler for the provided event.
     */
    public synchronized void registerHandler(String iface, int what, Handler handler) {
        SparseArray<Set<Handler>> ifaceHandlers = mHandlerMap.get(iface);
        if (ifaceHandlers == null) {
            ifaceHandlers = new SparseArray<>();
            mHandlerMap.put(iface, ifaceHandlers);
        }
        Set<Handler> ifaceWhatHandlers = ifaceHandlers.get(what);
        if (ifaceWhatHandlers == null) {
            ifaceWhatHandlers = new ArraySet<>();
            ifaceHandlers.put(what, ifaceWhatHandlers);
        }
        ifaceWhatHandlers.add(handler);
    }

    private final Map<String, Boolean> mMonitoringMap = new HashMap<>();
    private boolean isMonitoring(String iface) {
        Boolean val = mMonitoringMap.get(iface);
        if (val == null) {
            return false;
        } else {
            return val.booleanValue();
        }
    }

    /**
     * Enable/Disable monitoring for the provided iface.
     *
     * @param iface Name of the iface.
     * @param enabled true to enable, false to disable.
     */
    @VisibleForTesting
    public void setMonitoring(String iface, boolean enabled) {
        mMonitoringMap.put(iface, enabled);
    }

    /**
     * Start Monitoring for wpa_supplicant events.
     *
     * @param iface Name of iface.
     * TODO: Add unit tests for these once we remove the legacy code.
     */
    public synchronized void startMonitoring(String iface) {
        setMonitoring(iface, true);
        broadcastSupplicantConnectionEvent(iface);
    }

    /**
     * Stop Monitoring for wpa_supplicant events.
     *
     * @param iface Name of iface.
     * TODO: Add unit tests for these once we remove the legacy code.
     */
    public synchronized void stopMonitoring(String iface) {
        if (mVerboseLoggingEnabled) Log.d(TAG, "stopMonitoring(" + iface + ")");
        setMonitoring(iface, true);
        broadcastSupplicantDisconnectionEvent(iface);
        setMonitoring(iface, false);
    }

    /**
     * Similar functions to Handler#sendMessage that send the message to the registered handler
     * for the given interface and message what.
     * All of these should be called with the WifiMonitor class lock
     */
    private void sendMessage(String iface, int what) {
        sendMessage(iface, Message.obtain(null, what));
    }

    private void sendMessage(String iface, int what, Object obj) {
        sendMessage(iface, Message.obtain(null, what, obj));
    }

    private void sendMessage(String iface, int what, int arg1) {
        sendMessage(iface, Message.obtain(null, what, arg1, 0));
    }

    private void sendMessage(String iface, int what, int arg1, int arg2) {
        sendMessage(iface, Message.obtain(null, what, arg1, arg2));
    }

    private void sendMessage(String iface, int what, int arg1, int arg2, Object obj) {
        sendMessage(iface, Message.obtain(null, what, arg1, arg2, obj));
    }

    private void sendMessage(String iface, Message message) {
        SparseArray<Set<Handler>> ifaceHandlers = mHandlerMap.get(iface);
        if (iface != null && ifaceHandlers != null) {
            if (isMonitoring(iface)) {
                Set<Handler> ifaceWhatHandlers = ifaceHandlers.get(message.what);
                if (ifaceWhatHandlers != null) {
                    for (Handler handler : ifaceWhatHandlers) {
                        if (handler != null) {
                            sendMessage(handler, Message.obtain(message));
                        }
                    }
                }
            } else {
                if (mVerboseLoggingEnabled) {
                    Log.d(TAG, "Dropping event because (" + iface + ") is stopped");
                }
            }
        } else {
            if (mVerboseLoggingEnabled) {
                Log.d(TAG, "Sending to all monitors because there's no matching iface");
            }
            for (Map.Entry<String, SparseArray<Set<Handler>>> entry : mHandlerMap.entrySet()) {
                if (isMonitoring(entry.getKey())) {
                    Set<Handler> ifaceWhatHandlers = entry.getValue().get(message.what);
                    for (Handler handler : ifaceWhatHandlers) {
                        if (handler != null) {
                            sendMessage(handler, Message.obtain(message));
                        }
                    }
                }
            }
        }

        message.recycle();
    }

    private void sendMessage(Handler handler, Message message) {
        message.setTarget(handler);
        message.sendToTarget();
    }

    /**
     * Broadcast the connection to wpa_supplicant event to all the handlers registered for
     * this event.
     *
     * @param iface Name of iface on which this occurred.
     */
    public void broadcastSupplicantConnectionEvent(String iface) {
        sendMessage(iface, SUP_CONNECTION_EVENT);
    }

    /**
     * Broadcast the loss of connection to wpa_supplicant event to all the handlers registered for
     * this event.
     *
     * @param iface Name of iface on which this occurred.
     */
    public void broadcastSupplicantDisconnectionEvent(String iface) {
        sendMessage(iface, SUP_DISCONNECTION_EVENT);
    }

    /**
     * Broadcast new p2p device discovered event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param device Device that has been discovered during recent scan.
     */
    public void broadcastP2pDeviceFound(String iface, WifiP2pDevice device) {
        if (device != null) {
            sendMessage(iface, P2P_DEVICE_FOUND_EVENT, device);
        }
    }

    /**
     * Broadcast p2p device lost event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param device Device that has been lost in recent scan.
     */
    public void broadcastP2pDeviceLost(String iface, WifiP2pDevice device) {
        if (device != null) {
            sendMessage(iface, P2P_DEVICE_LOST_EVENT, device);
        }
    }

    /**
     * Broadcast scan termination event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     */
    public void broadcastP2pFindStopped(String iface) {
        sendMessage(iface, P2P_FIND_STOPPED_EVENT);
    }

    /**
     * Broadcast group owner negotiation request event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param config P2p configuration.
     */
    public void broadcastP2pGoNegotiationRequest(String iface, WifiP2pConfig config) {
        if (config != null) {
            sendMessage(iface, P2P_GO_NEGOTIATION_REQUEST_EVENT, config);
        }
    }

    /**
     * Broadcast group owner negotiation success event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     */
    public void broadcastP2pGoNegotiationSuccess(String iface) {
        sendMessage(iface, P2P_GO_NEGOTIATION_SUCCESS_EVENT);
    }

    /**
     * Broadcast group owner negotiation failure event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param reason Failure reason.
     */
    public void broadcastP2pGoNegotiationFailure(String iface, P2pStatus reason) {
        sendMessage(iface, P2P_GO_NEGOTIATION_FAILURE_EVENT, reason);
    }

    /**
     * Broadcast group formation success event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     */
    public void broadcastP2pGroupFormationSuccess(String iface) {
        sendMessage(iface, P2P_GROUP_FORMATION_SUCCESS_EVENT);
    }

    /**
     * Broadcast group formation failure event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param reason Failure reason.
     */
    public void broadcastP2pGroupFormationFailure(String iface, String reason) {
        P2pStatus err = P2pStatus.UNKNOWN;
        if (reason.equals("FREQ_CONFLICT")) {
            err = P2pStatus.NO_COMMON_CHANNEL;
        }
        sendMessage(iface, P2P_GROUP_FORMATION_FAILURE_EVENT, err);
    }

    /**
     * Broadcast group started event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param group Started group.
     */
    public void broadcastP2pGroupStarted(String iface, WifiP2pGroup group) {
        if (group != null) {
            sendMessage(iface, P2P_GROUP_STARTED_EVENT, group);
        }
    }

    /**
     * Broadcast group removed event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param group Removed group.
     */
    public void broadcastP2pGroupRemoved(String iface, WifiP2pGroup group) {
        if (group != null) {
            sendMessage(iface, P2P_GROUP_REMOVED_EVENT, group);
        }
    }

    /**
     * Broadcast invitation received event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param group Group to which invitation has been received.
     */
    public void broadcastP2pInvitationReceived(String iface, WifiP2pGroup group) {
        if (group != null) {
            sendMessage(iface, P2P_INVITATION_RECEIVED_EVENT, group);
        }
    }

    /**
     * Broadcast invitation result event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param result Result of invitation.
     */
    public void broadcastP2pInvitationResult(String iface, P2pStatus result) {
        sendMessage(iface, P2P_INVITATION_RESULT_EVENT, result);
    }

    /**
     * Broadcast PB discovery request event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param event Provision discovery request event.
     */
    public void broadcastP2pProvisionDiscoveryPbcRequest(String iface, WifiP2pProvDiscEvent event) {
        if (event != null) {
            sendMessage(iface, P2P_PROV_DISC_PBC_REQ_EVENT, event);
        }
    }

    /**
     * Broadcast PB discovery response event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param event Provision discovery response event.
     */
    public void broadcastP2pProvisionDiscoveryPbcResponse(
            String iface, WifiP2pProvDiscEvent event) {
        if (event != null) {
            sendMessage(iface, P2P_PROV_DISC_PBC_RSP_EVENT, event);
        }
    }

    /**
     * Broadcast PIN discovery request event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param event Provision discovery request event.
     */
    public void broadcastP2pProvisionDiscoveryEnterPin(String iface, WifiP2pProvDiscEvent event) {
        if (event != null) {
            sendMessage(iface, P2P_PROV_DISC_ENTER_PIN_EVENT, event);
        }
    }

    /**
     * Broadcast PIN discovery response event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param event Provision discovery response event.
     */
    public void broadcastP2pProvisionDiscoveryShowPin(String iface, WifiP2pProvDiscEvent event) {
        if (event != null) {
            sendMessage(iface, P2P_PROV_DISC_SHOW_PIN_EVENT, event);
        }
    }

    /**
     * Broadcast P2P discovery failure event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param status Indicate the reason of this failure.
     * @param event The information about the provision discovery.
     */
    public void broadcastP2pProvisionDiscoveryFailure(@NonNull String iface,
            @P2pProvDiscStatus int status, @NonNull WifiP2pProvDiscEvent event) {
        sendMessage(iface, P2P_PROV_DISC_FAILURE_EVENT, status, 0, event);
    }

    /**
     * Broadcast service discovery response event to all handlers registered for this event.
     *
     * @param iface Name of iface on which this occurred.
     * @param services List of discovered services.
     */
    public void broadcastP2pServiceDiscoveryResponse(
            String iface, List<WifiP2pServiceResponse> services) {
        sendMessage(iface, P2P_SERV_DISC_RESP_EVENT, services);
    }

    /**
     * Broadcast AP STA connection event.
     *
     * @param iface Name of iface on which this occurred.
     */
    public void broadcastP2pApStaConnected(String iface, WifiP2pDevice device) {
        sendMessage(iface, AP_STA_CONNECTED_EVENT, device);
    }

    /**
     * Broadcast AP STA disconnection event.
     *
     * @param iface Name of iface on which this occurred.
     */
    public void broadcastP2pApStaDisconnected(String iface, WifiP2pDevice device) {
        sendMessage(iface, AP_STA_DISCONNECTED_EVENT, device);
    }

    /**
     * Broadcast frequency changed event.
     *
     * @param iface Name of iface on which this occurred.
     * @param frequency New operating frequency.
     */
    public void broadcastP2pFrequencyChanged(String iface,  int frequency) {
        sendMessage(iface, P2P_FREQUENCY_CHANGED_EVENT, frequency);
    }
}
