/*
 * Copyright 2015 by AVM GmbH <info@avm.de>
 *
 * This software contains free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License ("License") as
 * published by the Free Software Foundation  (version 3 of the License).
 * This software is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the copy of the
 * License you received along with this software for more details.
 */

package de.avm.android.fritzapp.service;

import android.annotation.TargetApi;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;

import java.net.Inet4Address;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;

import de.avm.android.fritzapp.com.ConnectionProblem;
import de.avm.fundamentals.logger.FileLog;

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class NetworkChangedLollipopHandler extends NetworkChangedHandler
{
    private static final String TAG = "NetworkChangedLollipopH";

    private static final String VPN_CONNECTIVITY_ACTION = "de.avm.android.vpn.CONNECTION_STATE";
    private static final String EXTRA_VPN_STATE = "state";
    private static final long POLLING_INTERVAL = 30 * 1000;

    public NetworkChangedLollipopHandler(Context context)
    {
        super(context);
        mHandler = new Handler(Looper.getMainLooper());
        mWifiManager = (WifiManager)context.getSystemService(Context.WIFI_SERVICE);
    }

    public void registerCallback(OnNetworkChangedListener listener)
    {
        if (DEBUG) FileLog.d(TAG, "registerCallback()");
        mRecentlyPropagatedNetworkState = getNetworkState();

        mChangedListener = listener;
        if (mNetworkCallback == null)
        {
            mNetworkCallback = new LNetworkCallback();
            getConnectivityManager().registerNetworkCallback(new NetworkRequest.Builder()
                    .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
                    .addTransportType(NetworkCapabilities.TRANSPORT_VPN)
                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                    .build(), mNetworkCallback);
        }

        // on SDK21 mNetworkCallback is never called for TRANSPORT_VPN!
        // receive broadcast from MyFRITZ!App for connectivity changes to its VPN
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(VPN_CONNECTIVITY_ACTION);
        getContext().registerReceiver(mVpnConnectivityReceiver, intentFilter);


        // on SDK21 mNetworkCallback is never called for TRANSPORT_VPN!
        // check connected networks periodically
        if (mPollingTimer == null)
        {
            mPollingTimer = new Timer();
            mPollingTimer.schedule(new TimerTask()
            {
                public void run()
                {
                    onConnectivityChanged();
                }
            }, POLLING_INTERVAL, POLLING_INTERVAL);
        }
    }

    public void unregisterCallback()
    {
        if (DEBUG) FileLog.d(TAG, "unregisterCallback()");

        if (mPollingTimer != null)
        {
            mPollingTimer.cancel();
            mPollingTimer = null;
        }

        getContext().unregisterReceiver(mVpnConnectivityReceiver);
        if (mNetworkCallback != null)
        {
            getConnectivityManager().unregisterNetworkCallback(mNetworkCallback);
            mNetworkCallback = null;
            mChangedListener = null;
        }
    }

    public ConnectionProblem checkValidConnectivity()
    {
        if (DEBUG) FileLog.d(TAG, "checkForValidConnectivity()");
        NetworkState state = getNetworkState();
        return (state.isConnected()) ?
                ConnectionProblem.OK : ConnectionProblem.NETWORK_DISCONNECTED;
    }

    private Handler mHandler;
    private WifiManager mWifiManager;
    private LNetworkCallback mNetworkCallback = null;
    private OnNetworkChangedListener mChangedListener = null;
    private BroadcastReceiver mVpnConnectivityReceiver = new VpnConnectivityReceiver();
    private NetworkState mRecentlyPropagatedNetworkState = null;
    private Timer mPollingTimer = null;

    private NetworkState getNetworkState()
    {
        NetworkState result = new NetworkState();
        Network[] networks = getConnectivityManager().getAllNetworks();
        for (Network network : networks)
        {
            NetworkInfo networkInfo = getConnectivityManager().getNetworkInfo(network);
            LinkProperties linkProperties = getConnectivityManager().getLinkProperties(network);
            if (DEBUG) dumpNetwork(network, networkInfo, linkProperties);
            if ((networkInfo != null) && networkInfo.isConnectedOrConnecting())
                result.addNetwork(networkInfo, linkProperties);
        }

        if (DEBUG)
            FileLog.d(TAG, "getNetworkState: " + (result.isConnected() ? "" : "not ") + "connected");
        return result;
    }

    private void dumpNetwork(Network network, NetworkInfo networkInfo,
            LinkProperties linkProperties)
    {
        StringBuilder message = new StringBuilder();
        if ((networkInfo != null) && networkInfo.isConnectedOrConnecting())
        {
            NetworkCapabilities networkCapabilities = getConnectivityManager()
                    .getNetworkCapabilities(network);
            boolean hasEthernet = networkCapabilities
                    .hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET);
            boolean hasVpn = networkCapabilities
                    .hasTransport(NetworkCapabilities.TRANSPORT_VPN);
            boolean hasWifi = networkCapabilities
                    .hasTransport(NetworkCapabilities.TRANSPORT_WIFI);

            message.append("\tnetwork: ")
                    .append(networkInfo.getTypeName())
                    .append(hasEthernet ? ", eth" : "")
                    .append(hasVpn ? ", vpn" : "")
                    .append(hasWifi ? ", wifi" : "");
        }
        else
        {
            message.append("\tnetwork not connected");
        }

        if (linkProperties == null)
        {
            message.append(", no link properties");
        }
        else
        {
            message.append(", iface == \"")
                    .append(linkProperties.getInterfaceName())
                    .append("\"");
        }

        FileLog.d(TAG, message.toString());
    }

    private void onConnectivityChanged()
    {
        try
        {
            NetworkState networkState = getNetworkState();
            if (!mRecentlyPropagatedNetworkState.equals(networkState))
            {
                if (DEBUG)
                {
                    FileLog.d(TAG, "Connection state has changed");
                    FileLog.d(TAG, networkState.toString());
                }
                mRecentlyPropagatedNetworkState = networkState;
                if (mChangedListener != null)
                    mChangedListener.onChanged(!networkState.isConnected());
            }
        }
        catch (Throwable e)
        {
            FileLog.w(TAG, e.getMessage(), e);
        }
    }

    private class NetworkState
    {
        private List<String> mIfaces = new ArrayList<>();

        public boolean isConnected()
        {
            return !mIfaces.isEmpty();
        }

        public void addNetwork(NetworkInfo networkInfo, LinkProperties linkProperties)
        {
            if ((networkInfo != null) && (linkProperties != null))
            {
                String iface = null;
                switch (networkInfo.getType())
                {
                    case ConnectivityManager.TYPE_VPN:
                        iface = linkProperties.getInterfaceName();
                        if (!TextUtils.isEmpty(iface))
                        {
                            List<LinkAddress> addresses = linkProperties.getLinkAddresses();
                            for (LinkAddress linkAddress : addresses)
                                if (linkAddress.getAddress() instanceof Inet4Address)
                                {
                                    iface += "," + linkAddress.toString();
                                    break;
                                }
                        }
                        break;

                    case ConnectivityManager.TYPE_ETHERNET:
                        iface = linkProperties.getInterfaceName();
                        break;

                    case ConnectivityManager.TYPE_WIFI:
                        iface = linkProperties.getInterfaceName();
                        if (!TextUtils.isEmpty(iface))
                        {
                            WifiInfo wifiInfo = mWifiManager.getConnectionInfo();
                            if (wifiInfo != null)
                            {
                                int id = wifiInfo.getNetworkId();
                                if (id != -1)
                                    iface += "," + Integer.toString(id);
                            }
                        }
                        break;
                    }

                if (!TextUtils.isEmpty(iface) && !mIfaces.contains(iface))
                {
                    mIfaces.add(iface);
                    Collections.sort(mIfaces);
                }
            }
        }

        @Override
        public boolean equals(Object object)
        {
            return (object instanceof NetworkState) &&
                    mIfaces.equals(((NetworkState)object).mIfaces);
        }

        @Override
        public String toString()
        {
            StringBuilder builder = new StringBuilder();
            builder.append("NetworkState: ");
            for (String iface : mIfaces)
                if (!TextUtils.isEmpty(iface))
                    builder.append("\"").append(iface).append("\";");
            return builder.toString();
        }
    }

    private class LNetworkCallback extends ConnectivityManager.NetworkCallback
    {
        @Override
        public void onAvailable(Network network)
        {
            super.onAvailable(network);
            if (DEBUG) FileLog.d(TAG, "NetworkCallback.onAvailable()");
            mHandler.post(new Runnable()
            {
                public void run()
                {
                    onConnectivityChanged();
                }
            });
        }

        @Override
        public void onLost(Network network)
        {
            super.onLost(network);
            if (DEBUG) FileLog.d(TAG, "NetworkCallback.onLost()");
            mHandler.post(new Runnable()
            {
                public void run()
                {
                    onConnectivityChanged();
                }
            });
        }

        @Override
        public void onCapabilitiesChanged(Network network,
                NetworkCapabilities networkCapabilities)
        {
            super.onCapabilitiesChanged(network, networkCapabilities);
            if (DEBUG)
            {
                FileLog.d(TAG, "NetworkCallback.onCapabilitiesChanged()");
                try
                {
                    dumpNetwork(network, getConnectivityManager().getNetworkInfo(network),
                            getConnectivityManager().getLinkProperties(network));
                }
                catch (Throwable ignored)
                {
                }
            }
        }

        @Override
        public void onLinkPropertiesChanged(Network network, LinkProperties linkProperties)
        {
            super.onLinkPropertiesChanged(network, linkProperties);
            if (DEBUG)
            {
                FileLog.d(TAG, "NetworkCallback.onLinkPropertiesChanged()");
                try
                {
                    dumpNetwork(network, getConnectivityManager().getNetworkInfo(network),
                            getConnectivityManager().getLinkProperties(network));
                }
                catch (Throwable ignored)
                {
                }
            }
        }
    }

    private class VpnConnectivityReceiver extends BroadcastReceiver
    {
        public void onReceive(Context context, Intent intent)
        {
            if (intent.getAction().equals(VPN_CONNECTIVITY_ACTION))
            {
                if (DEBUG)
                    FileLog.d(TAG, "onReceive: \"" + intent.getStringExtra(EXTRA_VPN_STATE) + "\"");
                onConnectivityChanged();
            }
        }
    }
}
