/*
 * Copyright 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 com.android.managedprovisioning.task;

import static com.android.internal.util.Preconditions.checkNotNull;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Handler;

import com.android.internal.annotations.VisibleForTesting;
import com.android.managedprovisioning.analytics.MetricsWriterFactory;
import com.android.managedprovisioning.analytics.ProvisioningAnalyticsTracker;
import com.android.managedprovisioning.common.ManagedProvisioningSharedPreferences;
import com.android.managedprovisioning.common.ProvisionLogger;
import com.android.managedprovisioning.common.SettingsFacade;
import com.android.managedprovisioning.common.Utils;
import com.android.managedprovisioning.model.ProvisioningParams;
import com.android.managedprovisioning.task.wifi.NetworkMonitor;
import com.android.managedprovisioning.task.wifi.WifiConfigurationProvider;

/**
 * Adds a wifi network to the system and waits for it to successfully connect. If the system does
 * not support wifi, the adding or connection times out {@link #error(int)} will be called.
 */
public class AddWifiNetworkTask extends AbstractProvisioningTask
        implements NetworkMonitor.NetworkConnectedCallback {
    private static final int RETRY_SLEEP_DURATION_BASE_MS = 500;
    private static final int RETRY_SLEEP_MULTIPLIER = 2;
    private static final int MAX_RETRIES = 6;
    private static final int RECONNECT_TIMEOUT_MS = 60000;
    @VisibleForTesting  static final int ADD_NETWORK_FAIL = -1;

    private final WifiConfigurationProvider mWifiConfigurationProvider;
    private final WifiManager mWifiManager;
    private final NetworkMonitor mNetworkMonitor;

    private Handler mHandler;
    private boolean mTaskDone = false;

    private final Utils mUtils;
    private Runnable mTimeoutRunnable;
    private Injector mInjector;

    public AddWifiNetworkTask(
            Context context,
            ProvisioningParams provisioningParams,
            Callback callback) {
        this(
                new NetworkMonitor(context, /* waitForValidated */ false),
                new WifiConfigurationProvider(),
                context, provisioningParams, callback, new Utils(), new Injector(),
                new ProvisioningAnalyticsTracker(
                        MetricsWriterFactory.getMetricsWriter(context, new SettingsFacade()),
                        new ManagedProvisioningSharedPreferences(context)));
    }

    @VisibleForTesting
    AddWifiNetworkTask(
            NetworkMonitor networkMonitor,
            WifiConfigurationProvider wifiConfigurationProvider,
            Context context,
            ProvisioningParams provisioningParams,
            Callback callback,
            Utils utils,
            Injector injector,
            ProvisioningAnalyticsTracker provisioningAnalyticsTracker) {
        super(context, provisioningParams, callback, provisioningAnalyticsTracker);

        mNetworkMonitor = checkNotNull(networkMonitor);
        mWifiConfigurationProvider = checkNotNull(wifiConfigurationProvider);
        mWifiManager  = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
        mUtils = checkNotNull(utils);
        mInjector = checkNotNull(injector);
    }

    @Override
    public void run(int userId) {
        if (mProvisioningParams.wifiInfo == null) {
            success();
            return;
        }

        if (mWifiManager == null || !enableWifi()) {
            ProvisionLogger.loge("Failed to enable wifi");
            error(0);
            return;
        }

        if (isConnectedToSpecifiedWifi()) {
            success();
            return;
        }

        mTaskDone = false;
        mHandler = new Handler();
        mNetworkMonitor.startListening(this);
        connectToProvidedNetwork();
    }

    private void connectToProvidedNetwork() {
        WifiConfiguration wifiConf =
                mWifiConfigurationProvider.generateWifiConfiguration(mProvisioningParams.wifiInfo);

        if (wifiConf == null) {
            ProvisionLogger.loge("WifiConfiguration is null");
            error(0);
            return;
        }

        int netId = tryAddingNetwork(wifiConf);

        if (netId == ADD_NETWORK_FAIL) {
            ProvisionLogger.loge("Unable to add network after trying " +  MAX_RETRIES + " times.");
            error(0);
            return;
        }

        // Setting disableOthers to 'true' should trigger a connection attempt.
        mWifiManager.enableNetwork(netId, true);
        mWifiManager.saveConfiguration();

        // Network was successfully saved, now connect to it.
        if (!mWifiManager.reconnect()) {
            ProvisionLogger.loge("Unable to connect to wifi");
            error(0);
            return;
        }

        // NetworkMonitor will call onNetworkConnected when in Wifi mode.
        // Post time out event in case the NetworkMonitor doesn't call back.
        mTimeoutRunnable = () -> finishTask(false);
        mHandler.postDelayed(mTimeoutRunnable, RECONNECT_TIMEOUT_MS);
    }

    private int tryAddingNetwork(WifiConfiguration wifiConf) {
        int netId = mWifiManager.addNetwork(wifiConf);
        int retriesLeft = MAX_RETRIES;
        int durationNextSleep = RETRY_SLEEP_DURATION_BASE_MS;

        while(netId == -1 && retriesLeft > 0) {
            ProvisionLogger.loge("Retrying in " + durationNextSleep + " ms.");
            try {
                mInjector.threadSleep(durationNextSleep);
            } catch (InterruptedException e) {
                ProvisionLogger.loge("Retry interrupted.");
            }
            durationNextSleep *= RETRY_SLEEP_MULTIPLIER;
            retriesLeft--;
            netId = mWifiManager.addNetwork(wifiConf);
        }
        return netId;
    }

    private boolean enableWifi() {
        return mWifiManager.isWifiEnabled() || mWifiManager.setWifiEnabled(true);
    }

    @Override
    public void onNetworkConnected() {
        ProvisionLogger.logd("onNetworkConnected");
        if (isConnectedToSpecifiedWifi()) {
            ProvisionLogger.logd("Connected to the correct network");
            finishTask(true);
            // Remove time out callback.
            mHandler.removeCallbacks(mTimeoutRunnable);
        }
    }

    private synchronized void finishTask(boolean isSuccess) {
        if (mTaskDone) {
            return;
        }

        mTaskDone = true;
        mNetworkMonitor.stopListening();
        if (isSuccess) {
            success();
        } else {
            error(0);
        }
    }

    private boolean isConnectedToSpecifiedWifi() {
        if (!mUtils.isNetworkTypeConnected(mContext, ConnectivityManager.TYPE_WIFI)) {
            ProvisionLogger.logd("Not connected to WIFI");
            return false;
        }
        WifiInfo connectionInfo = mWifiManager.getConnectionInfo();
        if (connectionInfo == null) {
            ProvisionLogger.logd("connection info is null");
            return false;
        }
        String connectedSSID = mWifiManager.getConnectionInfo().getSSID();
        if (!mProvisioningParams.wifiInfo.ssid.equals(connectedSSID)) {
            ProvisionLogger.logd("Wanted to connect SSID " + mProvisioningParams.wifiInfo.ssid
                    + ", but it is now connected to " + connectedSSID);
            return false;
        }
        return true;
    }

    @VisibleForTesting
    static class Injector {
        public void threadSleep(long milliseconds) throws InterruptedException {
            Thread.sleep(milliseconds);
        }
    }
}
