/*
 * 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 example.mediaserver;

import org.teleal.cling.binding.annotations.AnnotationLocalServiceBinder;
import org.teleal.cling.controlpoint.ControlPoint;
import org.teleal.cling.model.DefaultServiceManager;
import org.teleal.cling.model.ServiceReference;
import org.teleal.cling.model.ValidationException;
import org.teleal.cling.model.action.ActionException;
import org.teleal.cling.model.action.ActionInvocation;
import org.teleal.cling.model.message.UpnpResponse;
import org.teleal.cling.model.meta.DeviceDetails;
import org.teleal.cling.model.meta.DeviceIdentity;
import org.teleal.cling.model.meta.LocalDevice;
import org.teleal.cling.model.meta.LocalService;
import org.teleal.cling.model.types.UDADeviceType;
import org.teleal.cling.model.types.UDN;
import org.teleal.cling.support.connectionmanager.AbstractPeeringConnectionManagerService;
import org.teleal.cling.support.model.ConnectionInfo;
import org.teleal.cling.support.model.ProtocolInfo;
import org.teleal.cling.support.model.ProtocolInfos;
import org.testng.annotations.Test;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import static org.testng.Assert.assertEquals;


/**
 * Managing connections between peers
 * <p>
 * You'd probably agree that the <em>ConnectionManager</em> is unnecessary when the
 * media player <strong>pulls</strong> the media data with a HTTP GET request on the provided URL.
 * Understand that the UPnP <em>MediaServer</em> device provides the URL; if it also serves the
 * file named in the URL, that is outside of the scope of UPnP although a common system architecture.
 * </p>
 * <p>
 * Then again, when the source of the media data has to <strong>push</strong> the data to
 * the player, or prepare the connection with the player beforehand, the <em>ConnectionManager</em>
 * service becomes useful. In this situation two connection managers would first negotiate a
 * connection with the <code>PrepareForConnection</code> action - which side initiates this is
 * up to you. Once the media finished playing, one of the connection managers will then
 * call the <code>ConnectionComplete</code> action. A connection has a unique identifier and
 * some associated protocol information, the connection managers handle the connection as peers.
 * </p>
 * <p>
 * Cling Support provides an <code>AbstractPeeringConnectionManagerService</code> that will do
 * all the heavy lifting for you, all you have to do is implement the creation and closing of
 * connections. Although we are still discussing this in the context of a media server, this
 * peer negotiation of a connection naturally also has to be implemented on the media renderer/player
 * side. The following examples are therefore also relevant for the connection manager of
 * a <em>MediaRenderer</em>.
 * </p>
 * <p>
 * First, implement how you want to manage the connection on both ends of the connection
 * (this is just one side):
 * </p>
 * <a class="citation" href="javadoc://example.mediaserver.ConnectionManagerPeerTest.PeeringConnectionManager" style="read-title: false;"/>
 * <p>
 * Let's create a connection between two connection manager peers. First, create the service
 * acting as the source (let's also assume that this is the media server representing the source
 * of the media data):
 * </p>
 * <a class="citation" href="javacode://this#createDestroyConnections" style="include: INC1;"/>
 * <p>
 * You can see that it provides media metadata with several protocols. The sink (or
 * media renderer) is the peer connection manager:
 * </p>
 * <a class="citation" href="javacode://this#createDestroyConnections" style="include: INC2;" id="conmgr_sink"/>
 * <p>
 * It plays only one particular protocol.
 * </p>
 * <p>
 * The <code>createService()</code> method is simply setting the connection manager
 * instance on the service, after reading the service metadata from (already provided) annotations:
 * </p>
 * <a class="citation" href="javacode://this#createService(example.mediaserver.ConnectionManagerPeerTest.PeeringConnectionManager)"/>
 * <p>
 * Now one of the peers has to initiate the connection. It has to create a connection identifier, store this
 * identifier ("managing" the connection), and call the <code>PrepareForConnection</code> service of the
 * other peer. All of this is provided and encapsulated in the <code>createConnectionWithPeer()</code>
 * method:
 * </p>
 * <a class="citation" href="javacode://this#createDestroyConnections" style="include: INC3;" id="conmgr_prepare"/>
 * <p>
 * You have to provide a reference to the local service, a <code>ControlPoint</code>
 * to execute the action, and the protocol information you want to use for this connection. The
 * direction (<code>Input</code> in this case) is how the remote peer should handle the data
 * transmitted on this connection (again, we assume the peer is the data sink). The method returns
 * the identifer of the new connection. You can use this identifier to obtain
 * more information about the connection, for example the identifier of the connection assigned by
 * the other peer, or the logical service identifier for the AV Transport service, also assigned
 * by the remote peer.
 * </p>
 * <p>
 * When you are done with the connection, close it with the peer:
 * </p>
 * <a class="citation" href="javacode://this#createDestroyConnections" style="include: INC4;" id="conmgr_close"/>
 * <p>
 * The <code>peerFailure()</code> method shown earlier will be called when
 * an invocation of <code>createConnectionWithPeer()</code> or
 * <code>closeConnectionWithPeer()</code> fails.
 * </p>
 */
public class ConnectionManagerPeerTest {

    @Test
    public void createDestroyConnections() throws Exception {

        // Ignore this
        ControlPoint controlPoint = null;

        CountingListener listener = new CountingListener();

        PeeringConnectionManager peerOne =                                                      // DOC: INC1
            new PeeringConnectionManager(
                    new ProtocolInfos("http-get:*:video/mpeg:*,http-get:*:audio/mpeg:*"),
                    null
            );
        LocalService<PeeringConnectionManager> peerOneService = createService(peerOne);        // DOC: INC1

        peerOne.getPropertyChangeSupport().addPropertyChangeListener(listener);
        createDevice("MEDIASERVER-AAA-AAA-AAA", "MediaServer", peerOneService);

        PeeringConnectionManager peerTwo =                                                     // DOC: INC2
            new PeeringConnectionManager(
                    null,
                    new ProtocolInfos("http-get:*:video/mpeg:*")
            );
        LocalService<PeeringConnectionManager> peerTwoService = createService(peerTwo);     // DOC: INC2

        peerTwo.getPropertyChangeSupport().addPropertyChangeListener(listener);
        createDevice("MEDIARENDERER-BBB-BBB-BBB", "MediaRenderer", peerTwoService);

        int peerOneConnectionID = peerOne.createConnectionWithPeer(                                  // DOC: INC3
            peerOneService.getReference(),
            controlPoint,
            peerTwoService,
            new ProtocolInfo("http-get:*:video/mpeg:*"),
            ConnectionInfo.Direction.Input
        );

        if (peerOneConnectionID == -1) {
            // Connection establishment failed, the peerFailure()
            // method has been called already. It's up to you
            // how you'd like to continue at this point.
        }
        
        int peerTwoConnectionID =
                peerOne.getCurrentConnectionInfo(peerOneConnectionID) .getPeerConnectionID();

        int peerTwoAVTransportID =
                peerOne.getCurrentConnectionInfo(peerOneConnectionID).getAvTransportID();           // DOC: INC3

        assertEquals(peerOne.getCurrentConnectionIDs().size(), 1);
        assertEquals(peerTwo.getCurrentConnectionIDs().size(), 1);

        assertEquals(peerOne.getCurrentConnectionInfo(peerOneConnectionID).getDirection(), ConnectionInfo.Direction.Output);
        assertEquals(peerTwo.getCurrentConnectionInfo(peerTwoConnectionID).getDirection(), ConnectionInfo.Direction.Input);

        assertEquals(peerOne.getCurrentConnectionInfo(peerOneConnectionID).getRcsID(), 111);
        assertEquals(peerTwo.getCurrentConnectionInfo(peerTwoConnectionID).getRcsID(), 111);
        assertEquals(peerOne.getCurrentConnectionInfo(peerOneConnectionID).getAvTransportID(), 333);
        assertEquals(peerTwo.getCurrentConnectionInfo(peerTwoConnectionID).getAvTransportID(), 333);

        assertEquals(peerOne.getCurrentConnectionInfo(peerOneConnectionID).getConnectionStatus(), ConnectionInfo.Status.OK);
        assertEquals(peerTwo.getCurrentConnectionInfo(peerTwoConnectionID).getConnectionStatus(), ConnectionInfo.Status.OK);

        // Another connection
        int anotherID = peerOne.createConnectionWithPeer(
            peerOneService.getReference(),
            controlPoint,
            peerTwoService,
            new ProtocolInfo("http-get:*:video/mpeg:*"),
            ConnectionInfo.Direction.Input
        );
        int anotherPeerID = peerOne.getCurrentConnectionInfo(anotherID).getPeerConnectionID();

        assertEquals(peerOne.getCurrentConnectionIDs().size(), 2);
        assertEquals(peerTwo.getCurrentConnectionIDs().size(), 2);

        assertEquals(peerOne.getCurrentConnectionInfo(anotherID).getRcsID(), 222);
        assertEquals(peerTwo.getCurrentConnectionInfo(anotherPeerID).getRcsID(), 222);
        assertEquals(peerOne.getCurrentConnectionInfo(anotherID).getAvTransportID(), 444);
        assertEquals(peerTwo.getCurrentConnectionInfo(anotherPeerID).getAvTransportID(), 444);

        // Close one
        peerOne.closeConnectionWithPeer(                                            // DOC: INC4
                controlPoint,
                peerTwoService,
                peerOneConnectionID
        );                                                                          // DOC: INC4

        assertEquals(peerOne.getCurrentConnectionIDs().size(), 1);
        assertEquals(peerTwo.getCurrentConnectionIDs().size(), 1);

        // The other is still there
        assertEquals(peerOne.getCurrentConnectionInfo(anotherID).getPeerConnectionID(), 1);

        // Should have 2 + 2 + 2 (connect, connect, disconnect on both connectionmanagers) events
        assertEquals(listener.count, 6);
    }

    public LocalDevice createDevice(String udn, String type, LocalService service) throws ValidationException {
        return new LocalDevice(
                new DeviceIdentity(new UDN(udn)),
                new UDADeviceType(type),
                new DeviceDetails(type),
                service
        );
    }

    public LocalService<PeeringConnectionManager> createService(final PeeringConnectionManager peer) {

        LocalService<PeeringConnectionManager> service =
                new AnnotationLocalServiceBinder().read(
                        AbstractPeeringConnectionManagerService.class
                );

        service.setManager(
                new DefaultServiceManager<PeeringConnectionManager>(service, null) {
                    @Override
                    protected PeeringConnectionManager createServiceInstance() throws Exception {
                        return peer;
                    }
                }
        );
        return service;
    }

    /**
     * <a class="citation" href="javacode://this" style="exclude: EXC1"/>
     * <p>
     * In the <code>createConnection()</code> method you have to provide the identifiers of your
     * Rendering Control and A/V Transport logical service, responsible for the created connection.
     * The connection ID has already been stored for you, so all you have to do is return the
     * connection information with these identifiers.
     * </p>
     * <p>
     * The <code>closeConnection()</code> method is the counterpart, here you would tear down
     * your logical services for this connection, or do whatever cleanup is necessary.
     * </p>
     * <p>
     * The <code>peerFailure()</code> message is not related to the two previous messages. It is
     * only used by a connection manager that invokes the actions, not on the receiving side.
     * </p>
     */
    public class PeeringConnectionManager extends AbstractPeeringConnectionManagerService {

        PeeringConnectionManager(ProtocolInfos sourceProtocolInfo,
                                 ProtocolInfos sinkProtocolInfo) {
            super(sourceProtocolInfo, sinkProtocolInfo);
        }

        @Override
        protected ConnectionInfo createConnection(int connectionID,
                                                  int peerConnectionId,
                                                  ServiceReference peerConnectionManager,
                                                  ConnectionInfo.Direction direction,
                                                  ProtocolInfo protocolInfo)
                throws ActionException {

            // Create the connection on "this" side with the given ID now...
            ConnectionInfo con = new ConnectionInfo(
                    connectionID,
                    123, // Logical Rendering Control service ID
                    456, // Logical AV Transport service ID
                    protocolInfo,
                    peerConnectionManager,
                    peerConnectionId,
                    direction,
                    ConnectionInfo.Status.OK
            );

            // DOC: EXC1
            con = new ConnectionInfo(
                    connectionID,
                    connectionID == 0 ? 111 : 222,
                    connectionID == 0 ? 333 : 444,
                    protocolInfo,
                    peerConnectionManager,
                    peerConnectionId,
                    direction,
                    ConnectionInfo.Status.OK
            );
            // DOC: EXC1
            return con;
        }

        @Override
        protected void closeConnection(ConnectionInfo connectionInfo) {
            // Close the connection
        }

        @Override
        protected void peerFailure(ActionInvocation invocation,
                                   UpnpResponse operation,
                                   String defaultFailureMessage) {
            System.err.println("Error managing connection with peer: " + defaultFailureMessage);
        }
    }

    class CountingListener implements PropertyChangeListener {
        int count = 0;

        public void propertyChange(PropertyChangeEvent e) {
            if (e.getPropertyName().equals("CurrentConnectionIDs")) {
                count++;
            }
        }
    }

}
