/*
 * Copyright (C) 2010 Teleal GmbH, Switzerland
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program 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
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.teleal.cling.android;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
import org.teleal.cling.UpnpServiceConfiguration;
import org.teleal.cling.model.ModelUtil;
import org.teleal.cling.model.message.IncomingDatagramMessage;
import org.teleal.cling.model.message.OutgoingDatagramMessage;
import org.teleal.cling.model.message.StreamRequestMessage;
import org.teleal.cling.model.message.StreamResponseMessage;
import org.teleal.cling.protocol.ProtocolFactory;
import org.teleal.cling.transport.Router;
import org.teleal.cling.transport.RouterImpl;
import org.teleal.cling.transport.spi.InitializationException;
import org.teleal.cling.transport.spi.NetworkAddressFactory;
import org.teleal.cling.transport.spi.UpnpStream;
import org.teleal.common.util.Exceptions;

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.logging.Logger;

/**
 * Wrapping a regular {@link org.teleal.cling.transport.RouterImpl}, making it on/off switchable.
 * <p>
 * This implementation listens to connectivity changes in an Android environment (register for
 * intent <code>"android.net.conn.CONNECTIVITY_CHANGE"</code>). It then enables/disables UPnP
 * network transport whenever the WiFi network interface is turned on/off.
 * </p>
 * <p>
 * While WiFi is switched off, only mock responses (mostly <code>null</code>) will be returned
 * from this network transport layer, and all operations are NOOPs.
 * </p>
 * <p>
 * If the router can't be switched on after a connectivity change (e.g. an exception occurs during
 * socket binding), a warning log message will be printed. You can customize this behavior by
 * overriding {@link #handleRouterFailure(org.teleal.cling.transport.spi.InitializationException)}.
 * </p>
 *
 * @author Christian Bauer
 */
public class AndroidWifiSwitchingRouter extends BroadcastReceiver implements Router {

    private static Logger log = Logger.getLogger(AndroidWifiSwitchingRouter.class.getName());

    final UpnpServiceConfiguration configuration;
    final ProtocolFactory protocolFactory;
    final WifiManager wifiManager;
    final ConnectivityManager connectivityManager;

    private Router router;
    private WifiManager.MulticastLock multicastLock;

    public AndroidWifiSwitchingRouter(UpnpServiceConfiguration configuration, ProtocolFactory protocolFactory,
                                      WifiManager wifiManager, ConnectivityManager connectivityManager) {
        this.configuration = configuration;
        this.protocolFactory = protocolFactory;
        this.wifiManager = wifiManager;
        this.connectivityManager = connectivityManager;

        if (ModelUtil.ANDROID_EMULATOR) {
            // Start router immediately on emulator because there will never be a "WiFi switched on" event
            log.info("Running on Android emulator, not waiting for real WiFi to become available");
            enableRouter();
        }
    }

    public UpnpServiceConfiguration getConfiguration() {
        return configuration;
    }

    public ProtocolFactory getProtocolFactory() {
        return protocolFactory;
    }

    protected WifiManager getWifiManager() {
        return wifiManager;
    }

    protected ConnectivityManager getConnectivityManager() {
        return connectivityManager;
    }

    @Override
    synchronized public void onReceive(Context c, Intent intent) {
        if (!intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION)) return;
        NetworkInfo wifiInfo = getConnectivityManager().getNetworkInfo(ConnectivityManager.TYPE_WIFI);
        // We can't listen to "is available" or simply "is switched on", we have to make sure it's connected
        if (!wifiInfo.isConnected()) {
            disableRouter();
        } else {
            enableRouter();
        }
    }

    synchronized void enableRouter() {
        if (router == null) {
            try {
                log.info("WiFi state changed, enabling UPnP network transport router");
                // Enable multicast on the WiFi network interface, requires android.permission.CHANGE_WIFI_MULTICAST_STATE
                multicastLock = getWifiManager().createMulticastLock(getClass().getSimpleName());
                multicastLock.acquire();
                router = new RouterImpl(getConfiguration(), getProtocolFactory());
            } catch (InitializationException ex) {
                handleRouterFailure(ex);
            }
        }
    }

    synchronized protected void handleRouterFailure(InitializationException ex) {
        log.severe("Unable to initialize network router: " + ex);
        log.severe("Cause: " + Exceptions.unwrap(ex));
    }

    synchronized void disableRouter() {
        if (router != null) {
            log.info("WiFi state changed, disabling UPnP network transport router");
            shutdown();
            router = null;
            multicastLock.release();
        }
    }

    synchronized public NetworkAddressFactory getNetworkAddressFactory() {
        if (router != null) return router.getNetworkAddressFactory();
        return new NetworkAddressFactory() {
            public InetAddress getMulticastGroup() {
                return null;
            }

            public int getMulticastPort() {
                return 0;
            }

            public int getStreamListenPort() {
                return 0;
            }

            public NetworkInterface[] getNetworkInterfaces() {
                return new NetworkInterface[0];
            }

            public InetAddress[] getBindAddresses() {
                return new InetAddress[0];
            }

            public byte[] getHardwareAddress(InetAddress inetAddress) {
                return new byte[0];
            }

            public InetAddress getBroadcastAddress(InetAddress inetAddress) {
                return null;
            }

            public InetAddress getLocalAddress(NetworkInterface networkInterface,
                                               boolean isIPv6,
                                               InetAddress remoteAddress)
                    throws IllegalStateException {
                return null;
            }
        };
    }

    synchronized public int getLocalStreamPort(InetAddress address) {
        return router != null ? router.getLocalStreamPort(address) : 0;
    }

    synchronized public void shutdown() {
        if (router != null) router.shutdown();
    }

    synchronized public void received(IncomingDatagramMessage msg) {
        if (router != null) router.received(msg);
    }

    synchronized public void received(UpnpStream stream) {
        if (router != null) router.received(stream);
    }

    synchronized public void send(OutgoingDatagramMessage msg) {
        if (router != null) router.send(msg);
    }

    synchronized public StreamResponseMessage send(StreamRequestMessage msg) {
        if (router != null) return router.send(msg);
        return null;
    }

    synchronized public void broadcast(byte[] bytes) {
        if (router != null) router.broadcast(bytes);
    }

}
