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

import android.content.Context;
import android.net.wifi.ScanResult;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
import android.util.Log;

import com.android.internal.util.AsyncChannel;
import com.android.internal.util.Protocol;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;

/**
 * Provides APIs for managing Wifi Passpoint credentials.
 * @hide
 */
public class WifiPasspointManager {

    private static final String TAG = "PasspointManager";

    private static final boolean DBG = true;

    /* Passpoint states values */

    /** Passpoint is in an unknown state. This should only occur in boot time */
    public static final int PASSPOINT_STATE_UNKNOWN = 0;

    /** Passpoint is disabled. This occurs when wifi is disabled */
    public static final int PASSPOINT_STATE_DISABLED = 1;

    /** Passpoint is enabled and in discovery state */
    public static final int PASSPOINT_STATE_DISCOVERY = 2;

    /** Passpoint is enabled and in access state */
    public static final int PASSPOINT_STATE_ACCESS = 3;

    /** Passpoint is enabled and in provisioning state */
    public static final int PASSPOINT_STATE_PROVISION = 4;

    /* Passpoint callback error codes */

    /** Indicates that the operation failed due to an internal error */
    public static final int REASON_ERROR = 0;

    /** Indicates that the operation failed because wifi is disabled */
    public static final int REASON_WIFI_DISABLED = 1;

    /** Indicates that the operation failed because the framework is busy */
    public static final int REASON_BUSY = 2;

    /** Indicates that the operation failed because parameter is invalid */
    public static final int REASON_INVALID_PARAMETER = 3;

    /** Indicates that the operation failed because the server is not trusted */
    public static final int REASON_NOT_TRUSTED = 4;

    /**
     * protocol supported for Passpoint
     */
    public static final String PROTOCOL_DM = "OMA-DM-ClientInitiated";

    /**
     * protocol supported for Passpoint
     */
    public static final String PROTOCOL_SOAP = "SPP-ClientInitiated";

    /* Passpoint broadcasts */

    /**
     * Broadcast intent action indicating that the state of Passpoint
     * connectivity has changed
     */
    public static final String PASSPOINT_STATE_CHANGED_ACTION =
            "android.net.wifi.passpoint.STATE_CHANGE";

    /**
     * Broadcast intent action indicating that the saved Passpoint credential
     * list has changed
     */
    public static final String PASSPOINT_CRED_CHANGED_ACTION =
            "android.net.wifi.passpoint.CRED_CHANGE";

    /**
     * Broadcast intent action indicating that Passpoint online sign up is
     * avaiable.
     */
    public static final String PASSPOINT_OSU_AVAILABLE_ACTION =
            "android.net.wifi.passpoint.OSU_AVAILABLE";

    /**
     * Broadcast intent action indicating that user remediation is required
     */
    public static final String PASSPOINT_USER_REM_REQ_ACTION =
            "android.net.wifi.passpoint.USER_REM_REQ";

    /**
     * Interface for callback invocation when framework channel is lost
     */
    public interface ChannelListener {
        /**
         * The channel to the framework has been disconnected. Application could
         * try re-initializing using {@link #initialize}
         */
        public void onChannelDisconnected();
    }

    /**
     * Interface for callback invocation on an application action
     */
    public interface ActionListener {
        /** The operation succeeded */
        public void onSuccess();

        /**
         * The operation failed
         *
         * @param reason The reason for failure could be one of
         *            {@link #WIFI_DISABLED}, {@link #ERROR} or {@link #BUSY}
         */
        public void onFailure(int reason);
    }

    /**
     * Interface for callback invocation when doing OSU or user remediation
     */
    public interface OsuRemListener {
        /** The operation succeeded */
        public void onSuccess();

        /**
         * The operation failed
         *
         * @param reason The reason for failure could be one of
         *            {@link #WIFI_DISABLED}, {@link #ERROR} or {@link #BUSY}
         */
        public void onFailure(int reason);

        /**
         * Browser launch is requried for user interaction. When this callback
         * is called, app should launch browser / webview to the given URI.
         *
         * @param uri URI for browser launch
         */
        public void onBrowserLaunch(String uri);

        /**
         * When this is called, app should dismiss the previously lanched browser.
         */
        public void onBrowserDismiss();
    }

    /**
     * A channel that connects the application to the wifi passpoint framework.
     * Most passpoint operations require a Channel as an argument.
     * An instance of Channel is obtained by doing a call on {@link #initialize}
     */
    public static class Channel {
        private final static int INVALID_LISTENER_KEY = 0;

        private ChannelListener mChannelListener;

        private HashMap<Integer, Object> mListenerMap = new HashMap<Integer, Object>();
        private HashMap<Integer, Integer> mListenerMapCount = new HashMap<Integer, Integer>();
        private Object mListenerMapLock = new Object();
        private int mListenerKey = 0;

        private List<ScanResult> mAnqpRequest = new LinkedList<ScanResult>();
        private Object mAnqpRequestLock = new Object();

        private AsyncChannel mAsyncChannel;
        private PasspointHandler mHandler;
        Context mContext;

        Channel(Context context, Looper looper, ChannelListener l) {
            mAsyncChannel = new AsyncChannel();
            mHandler = new PasspointHandler(looper);
            mChannelListener = l;
            mContext = context;
        }

        private int putListener(Object listener) {
            return putListener(listener, 1);
        }

        private int putListener(Object listener, int count) {
            if (listener == null || count <= 0)
                return INVALID_LISTENER_KEY;
            int key;
            synchronized (mListenerMapLock) {
                do {
                    key = mListenerKey++;
                } while (key == INVALID_LISTENER_KEY);
                mListenerMap.put(key, listener);
                mListenerMapCount.put(key, count);
            }
            return key;
        }

        private Object peekListener(int key) {
            Log.d(TAG, "peekListener() key=" + key);
            if (key == INVALID_LISTENER_KEY)
                return null;
            synchronized (mListenerMapLock) {
                return mListenerMap.get(key);
            }
        }


        private Object getListener(int key, boolean forceRemove) {
            Log.d(TAG, "getListener() key=" + key + " force=" + forceRemove);
            if (key == INVALID_LISTENER_KEY)
                return null;
            synchronized (mListenerMapLock) {
                if (!forceRemove) {
                    int count = mListenerMapCount.get(key);
                    Log.d(TAG, "count=" + count);
                    mListenerMapCount.put(key, --count);
                    if (count > 0)
                        return null;
                }
                Log.d(TAG, "remove key");
                mListenerMapCount.remove(key);
                return mListenerMap.remove(key);
            }
        }

        private void anqpRequestStart(ScanResult sr) {
            Log.d(TAG, "anqpRequestStart sr.bssid=" + sr.BSSID);
            synchronized (mAnqpRequestLock) {
                mAnqpRequest.add(sr);
            }
        }

        private void anqpRequestFinish(WifiPasspointInfo result) {
            Log.d(TAG, "anqpRequestFinish pi.bssid=" + result.bssid);
            synchronized (mAnqpRequestLock) {
                for (ScanResult sr : mAnqpRequest)
                    if (sr.BSSID.equals(result.bssid)) {
                        Log.d(TAG, "find hit " + result.bssid);
                        /* sr.passpoint = result; */
                        mAnqpRequest.remove(sr);
                        Log.d(TAG, "mAnqpRequest.len=" + mAnqpRequest.size());
                        break;
                    }
            }
        }

        private void anqpRequestFinish(ScanResult sr) {
            Log.d(TAG, "anqpRequestFinish sr.bssid=" + sr.BSSID);
            synchronized (mAnqpRequestLock) {
                for (ScanResult sr1 : mAnqpRequest)
                    if (sr1.BSSID.equals(sr.BSSID)) {
                        mAnqpRequest.remove(sr1);
                        break;
                    }
            }
        }

        class PasspointHandler extends Handler {
            PasspointHandler(Looper looper) {
                super(looper);
            }

            @Override
            public void handleMessage(Message message) {
                Object listener = null;

                switch (message.what) {
                    case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
                        if (mChannelListener != null) {
                            mChannelListener.onChannelDisconnected();
                            mChannelListener = null;
                        }
                        break;

                    case REQUEST_ANQP_INFO_SUCCEEDED:
                        WifiPasspointInfo result = (WifiPasspointInfo) message.obj;
                        anqpRequestFinish(result);
                        listener = getListener(message.arg2, false);
                        if (listener != null) {
                            ((ActionListener) listener).onSuccess();
                        }
                        break;

                    case REQUEST_ANQP_INFO_FAILED:
                        anqpRequestFinish((ScanResult) message.obj);
                        listener = getListener(message.arg2, false);
                        if (listener == null)
                            getListener(message.arg2, true);
                        if (listener != null) {
                            ((ActionListener) listener).onFailure(message.arg1);
                        }
                        break;

                    case START_OSU_SUCCEEDED:
                        listener = getListener(message.arg2, true);
                        if (listener != null) {
                            ((OsuRemListener) listener).onSuccess();
                        }
                        break;

                    case START_OSU_FAILED:
                        listener = getListener(message.arg2, true);
                        if (listener != null) {
                            ((OsuRemListener) listener).onFailure(message.arg1);
                        }
                        break;

                    case START_OSU_BROWSER:
                        listener = peekListener(message.arg2);
                        if (listener != null) {
                            ParcelableString str = (ParcelableString) message.obj;
                            if (str == null || str.string == null)
                                ((OsuRemListener) listener).onBrowserDismiss();
                            else
                                ((OsuRemListener) listener).onBrowserLaunch(str.string);
                        }
                        break;

                    default:
                        Log.d(TAG, "Ignored " + message);
                        break;
                }
            }
        }

    }

    public static class ParcelableString implements Parcelable {
        public String string;

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel out, int flags) {
            out.writeString(string);
        }

        public static final Parcelable.Creator<ParcelableString> CREATOR =
                new Parcelable.Creator<ParcelableString>() {
                    @Override
                    public ParcelableString createFromParcel(Parcel in) {
                        ParcelableString ret = new ParcelableString();
                        ret.string = in.readString();
                        return ret;
                    }
                    @Override
                    public ParcelableString[] newArray(int size) {
                        return new ParcelableString[size];
                    }
        };
    }

    private static final int BASE = Protocol.BASE_WIFI_PASSPOINT_MANAGER;

    public static final int REQUEST_ANQP_INFO                   = BASE + 1;
    public static final int REQUEST_ANQP_INFO_FAILED            = BASE + 2;
    public static final int REQUEST_ANQP_INFO_SUCCEEDED         = BASE + 3;
    public static final int REQUEST_OSU_ICON                    = BASE + 4;
    public static final int REQUEST_OSU_ICON_FAILED             = BASE + 5;
    public static final int REQUEST_OSU_ICON_SUCCEEDED          = BASE + 6;
    public static final int START_OSU                           = BASE + 7;
    public static final int START_OSU_BROWSER                   = BASE + 8;
    public static final int START_OSU_FAILED                    = BASE + 9;
    public static final int START_OSU_SUCCEEDED                 = BASE + 10;

    private Context mContext;
    IWifiPasspointManager mService;

    /**
     * TODO: doc
     * @param context
     * @param service
     */
    public WifiPasspointManager(Context context, IWifiPasspointManager service) {
        mContext = context;
        mService = service;
    }

    /**
     * Registers the application with the framework. This function must be the
     * first to be called before any async passpoint operations are performed.
     *
     * @param srcContext is the context of the source
     * @param srcLooper is the Looper on which the callbacks are receivied
     * @param listener for callback at loss of framework communication. Can be
     *            null.
     * @return Channel instance that is necessary for performing any further
     *         passpoint operations
     *
     */
    public Channel initialize(Context srcContext, Looper srcLooper, ChannelListener listener) {
        Messenger messenger = getMessenger();
        if (messenger == null)
            return null;

        Channel c = new Channel(srcContext, srcLooper, listener);
        if (c.mAsyncChannel.connectSync(srcContext, c.mHandler, messenger)
                == AsyncChannel.STATUS_SUCCESSFUL) {
            return c;
        } else {
            return null;
        }
    }

    /**
     * STOPSHIP: temp solution, should use supplicant manager instead, check
     * with b/13931972
     */
    public Messenger getMessenger() {
        try {
            return mService.getMessenger();
        } catch (RemoteException e) {
            return null;
        }
    }

    public int getPasspointState() {
        try {
            return mService.getPasspointState();
        } catch (RemoteException e) {
            return PASSPOINT_STATE_UNKNOWN;
        }
    }

    public void requestAnqpInfo(Channel c, List<ScanResult> requested, int mask,
            ActionListener listener) {
        Log.d(TAG, "requestAnqpInfo start");
        Log.d(TAG, "requested.size=" + requested.size());
        checkChannel(c);
        List<ScanResult> list = new ArrayList<ScanResult>();
        for (ScanResult sr : requested)
            if (sr.capabilities.contains("[HS20]")) {
                list.add(sr);
                c.anqpRequestStart(sr);
                Log.d(TAG, "adding " + sr.BSSID);
            }
        int count = list.size();
        Log.d(TAG, "after filter, count=" + count);
        if (count == 0) {
            if (DBG)
                Log.d(TAG, "ANQP info request contains no HS20 APs, skipped");
            listener.onSuccess();
            return;
        }
        int key = c.putListener(listener, count);
        for (ScanResult sr : list)
            c.mAsyncChannel.sendMessage(REQUEST_ANQP_INFO, mask, key, sr);
        Log.d(TAG, "requestAnqpInfo end");
    }

    public void requestOsuIcons(Channel c, List<WifiPasspointOsuProvider> requested,
            int resolution, ActionListener listener) {
    }

    public List<WifiPasspointPolicy> requestCredentialMatch(List<ScanResult> requested) {
        try {
            return mService.requestCredentialMatch(requested);
        } catch (RemoteException e) {
            return null;
        }
    }

    /**
     * Get a list of saved Passpoint credentials. Only those credentials owned
     * by the caller will be returned.
     *
     * @return The list of credentials
     */
    public List<WifiPasspointCredential> getCredentials() {
        try {
            return mService.getCredentials();
        } catch (RemoteException e) {
            return null;
        }
    }

    /**
     * Add a new Passpoint credential.
     *
     * @param cred The credential to be added
     * @return {@code true} if the operation succeeds, {@code false} otherwise
     */
    public boolean addCredential(WifiPasspointCredential cred) {
        try {
            return mService.addCredential(cred);
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * Update an existing Passpoint credential. Only system or the owner of this
     * credential has the permission to do this.
     *
     * @param cred The credential to be updated
     * @return {@code true} if the operation succeeds, {@code false} otherwise
     */
    public boolean updateCredential(WifiPasspointCredential cred) {
        try {
            return mService.updateCredential(cred);
        } catch (RemoteException e) {
            return false;
        }
    }

    /**
     * Remove an existing Passpoint credential. Only system or the owner of this
     * credential has the permission to do this.
     *
     * @param cred The credential to be removed
     * @return {@code true} if the operation succeeds, {@code false} otherwise
     */
    public boolean removeCredential(WifiPasspointCredential cred) {
        try {
            return mService.removeCredential(cred);
        } catch (RemoteException e) {
            return false;
        }
    }

    public void startOsu(Channel c, WifiPasspointOsuProvider osu, OsuRemListener listener) {
        Log.d(TAG, "startOsu start");
        checkChannel(c);
        int key = c.putListener(listener);
        c.mAsyncChannel.sendMessage(START_OSU, 0, key, osu);
        Log.d(TAG, "startOsu end");
    }

    public void startRemediation(Channel c, OsuRemListener listener) {
    }

    public void connect(WifiPasspointPolicy policy) {
    }

    private static void checkChannel(Channel c) {
        if (c == null) throw new IllegalArgumentException("Channel needs to be initialized");
    }
}
