// Remote
//
// Douglas
//
// Remote.java

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

package net.douglasthrift.bigscreenbot;

import java.io.Closeable;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.UnknownHostException;
import java.net.URL;

import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;

import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.jmdns.JmDNS;
import javax.jmdns.ServiceInfo;

import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;

import com.google.anymote.common.AnymoteFactory;
import com.google.anymote.common.ErrorListener;

import com.google.anymote.device.DeviceAdapter;
import com.google.anymote.device.MessageReceiver;

import com.google.polo.exception.PoloException;

import com.google.polo.pairing.ClientPairingSession;
import com.google.polo.pairing.PairingContext;
import com.google.polo.pairing.PairingListener;
import com.google.polo.pairing.PairingSession;

import com.google.polo.pairing.message.EncodingOption;

import com.google.polo.ssl.DummySSLSocketFactory;
import com.google.polo.ssl.SslUtil;

import com.google.polo.wire.WireFormat;

public class Remote implements Closeable
{
    public static interface Pairing
    {
        public void prompt();
        public void interrupted(InterruptedException exception);
    }

    private static final String STORE = "bigscreenbot.keystore";
    private static final char[] PASSWORD = "b1GsSC33Nb0T".toCharArray();
    private static final char[] NULL = new char[]{};
    private static final String TYPE = "_anymote._tcp.local.";
    private static final String LOCAL_ALIAS = "anymote-remote";
    private static final String REMOTE_ALIAS = "anymote-server-%1$X";

    private static class Secret
    {
        private String secret;
        private Thread thread;

        public Secret()
        {
            thread = Thread.currentThread();
        }

        public synchronized void set(String secret)
        {
            this.secret = secret;

            notify();
        }

        public synchronized String get() throws InterruptedException
        {
            while (secret == null)
                wait();

            return secret;
        }

        public void interrupt()
        {
            thread.interrupt();
        }
    }

    static
    {
        new Fixer();
    }

    private boolean verbose;
    private Settings settings;
    private JmDNS mdns;
    private KeyStore store;
    private SSLSocketFactory factory;
    private Map<InetSocketAddress, Secret> secrets = Collections.synchronizedMap(new HashMap<InetSocketAddress, Secret>());

    public Remote(boolean verbose, Settings settings) throws UnknownHostException, IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException, KeyManagementException, GeneralSecurityException
    {
        this.verbose = verbose;
        this.settings = settings;

        String interfaze = settings.getProperty("interface");

        if (interfaze != null)
            mdns = JmDNS.create(InetAddress.getByName(interfaze));
        else
            mdns = JmDNS.create();

        store = KeyStore.getInstance(KeyStore.getDefaultType());

        FileInputStream stream = null;

        try
        {
            stream = new FileInputStream(STORE);

            store.load(stream, PASSWORD);
        }
        catch (FileNotFoundException exception)
        {
            store.load(null, PASSWORD);
        }
        finally
        {
            if (stream != null)
                stream.close();
        }

        if (!store.containsAlias(LOCAL_ALIAS))
        {
            KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
            KeyPair pair = generator.generateKeyPair();
            X509Certificate certificate = SslUtil.generateX509V3Certificate(pair, "CN=anymote/bigscreenbot/" + System.getProperty("os.name") + "/" + System.getProperty("os.arch") + "/" + System.getProperty("user.name") + "@" + InetAddress.getLocalHost().getHostName());

            store.setKeyEntry(LOCAL_ALIAS, pair.getPrivate(), NULL, new Certificate[]{ certificate });

            store();
        }

        KeyManagerFactory factory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());

        factory.init(store, NULL);

        this.factory = DummySSLSocketFactory.fromKeyManagers(factory.getKeyManagers());
    }

    @Override
    public void close() throws IOException
    {
        mdns.close();
    }

    public List<String> listDevices()
    {
        List<String> devices = new ArrayList<String>();

        for (ServiceInfo info : mdns.list(TYPE))
            devices.add(info.getName());

        return devices;
    }

    public boolean startPairDevice(String device, final Pairing pairing) throws MalformedURLException, UnknownHostException, IOException, PoloException
    {
        SSLSocket socket = (SSLSocket)factory.createSocket();
        final InetSocketAddress address = getDevice(device);

        socket.connect(address);

        PairingContext context = PairingContext.fromSslSocket(socket, false);
        ClientPairingSession session = new ClientPairingSession(WireFormat.PROTOCOL_BUFFERS.getWireInterface(context), context, "AnyMote", "Big Screen Bot");
        EncodingOption option = new EncodingOption(EncodingOption.EncodingType.ENCODING_HEXADECIMAL, 4);

        session.addInputEncoding(option);
        session.addOutputEncoding(option);

        return session.doPair(new PairingListener()
        {
            @Override
            public void onLogMessage(LogLevel level, String message)
            {
                if (verbose)
                    System.out.println(level + ": " + message);
            }

            @Override
            public void onPerformInputDeviceRole(PairingSession session)
            {
                pairing.prompt();

                Secret secret = new Secret(), oldSecret = secrets.put(address, secret);

                if (oldSecret != null)
                    oldSecret.interrupt();

                try
                {
                    session.setSecret(session.getEncoder().decodeToBytes(secret.get()));
                }
                catch (InterruptedException exception)
                {
                    session.teardown();
                    pairing.interrupted(exception);
                }
            }

            @Override
            public void onPerformOutputDeviceRole(PairingSession session, byte[] gamma) throws PoloException
            {
                throw new PoloException("This should not happen!");
            }

            @Override
            public void onSessionCreated(PairingSession session)
            {
                if (verbose)
                    System.out.println(session + " created.");
            }

            @Override
            public void onSessionEnded(PairingSession session)
            {
                if (verbose)
                    System.out.println(session + " ended.");
            }
        });

        // TODO: store server certificate
    }

    public boolean finishPairDevice(String device, String code) throws MalformedURLException
    {
        Secret secret = secrets.remove(getDevice(device));

        if (secret != null)
            secret.set(code);
        else
            return false;

        return true;
    }

    public void fling(String url)
    {
    }

    public void fling(String device, String url)
    {
    }

    private InetSocketAddress getDevice(String device) throws MalformedURLException
    {
        ServiceInfo info = mdns.getServiceInfo(TYPE, device);

        if (info != null)
            return new InetSocketAddress(info.getInetAddresses()[0], info.getPort());
        else
        {
            URL url = new URL("http://" + device);
            int port = url.getPort();

            return new InetSocketAddress(url.getHost(), port == -1 ? 9551 : port);
        }
    }

    private void store() throws FileNotFoundException, IOException, KeyStoreException, NoSuchAlgorithmException, CertificateException
    {
        FileOutputStream stream = null;

        try
        {
            stream = new FileOutputStream(STORE);

            store.store(stream, PASSWORD);
        }
        finally
        {
            if (stream != null)
                stream.close();
        }
    }
}

// vim: expandtab
