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

import org.teleal.cling.binding.annotations.AnnotationLocalServiceBinder;
import org.teleal.cling.controlpoint.ActionCallback;
import org.teleal.cling.model.DefaultServiceManager;
import org.teleal.cling.model.action.ActionInvocation;
import org.teleal.cling.model.message.UpnpResponse;
import org.teleal.cling.model.meta.LocalService;
import org.teleal.cling.support.avtransport.callback.GetCurrentTransportActions;
import org.teleal.cling.support.avtransport.callback.GetDeviceCapabilities;
import org.teleal.cling.support.avtransport.callback.GetMediaInfo;
import org.teleal.cling.support.avtransport.callback.GetPositionInfo;
import org.teleal.cling.support.avtransport.callback.GetTransportInfo;
import org.teleal.cling.support.avtransport.callback.Play;
import org.teleal.cling.support.avtransport.callback.SetAVTransportURI;
import org.teleal.cling.support.avtransport.callback.Stop;
import org.teleal.cling.support.avtransport.impl.AVTransportService;
import org.teleal.cling.support.avtransport.lastchange.AVTransportLastChangeParser;
import org.teleal.cling.support.avtransport.lastchange.AVTransportVariable;
import org.teleal.cling.support.lastchange.LastChange;
import org.teleal.cling.support.model.DeviceCapabilities;
import org.teleal.cling.support.model.MediaInfo;
import org.teleal.cling.support.model.PositionInfo;
import org.teleal.cling.support.model.TransportAction;
import org.teleal.cling.support.model.TransportInfo;
import org.teleal.cling.support.model.TransportState;
import org.testng.annotations.Test;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.net.URI;
import java.util.Arrays;
import java.util.List;

import static org.testng.Assert.assertEquals;

/**
 * Creating a renderer from scratch
 * <p>
 * Cling Support provides a state machine for managing the current state of your
 * playback engine. This feature simplifies writing a media player with a UPnP
 * renderer control interface. There are several steps involved
 * </p>
 * <div class="section">
 * <div class="title">Defining the states of the player</div>
 * <div class="content">
 * <p>
 * First, define your state machine and what states are supported by your player:
 * </p>
 * <a class="citation" href="javacode://example.mediarenderer.MyRendererStateMachine"/>
 * <p>
 * This is a very simple player with only three states: The initial state when no
 * media is present, and the Playing and Stopped states. You can also support
 * additional states, such as Paused and Recording but we want to keep this example
 * as simple as possible. (Also compare the "Theory of Operation" chapter and state
 * chart in the <em>AVTransport:1</em> specification document, section 2.5.)
 * </p>
 * <p>
 * Next, implement the states and the actions that trigger a transition from one
 * state to the other.
 * </p>
 * <a class="citation" href="javadoc://example.mediarenderer.MyRendererNoMediaPresent" style="read-title: false;"/>
 * <a class="citation" href="javadoc://example.mediarenderer.MyRendererStopped" style="read-title: false;"/>
 * <a class="citation" href="javadoc://example.mediarenderer.MyRendererPlaying" style="read-title: false;"/>
 * <p>
 * So far there wasn't much UPnP involved in writing your player - Cling just provided
 * a state machine for you and a way to signal state changes to clients through
 * the <code>LastEvent</code> interface.
 * </p>
 * </div>
 * </div>
 * <div class="section">
 * <div class="title">Registering the AVTransportService</div>
 * <div class="content">
 * <p>
 * Your next step is wiring the state machine into the UPnP service, so you can add the
 * service to a device and finally the Cling registry. First, bind the service and define
 * how the service manager will obtain an instance of your player:
 * </p>
 * <a class="citation" href="javacode://this#testCustomPlayer" style="include: INC1;"/>
 * <p>
 * The constructor takes two classes, one is your state machine definition, the other the
 * initial state of the machine after it has been created.
 * </p>
 * <p>
 * That's it - you are ready to add this service to a <em>MediaRenderer:1</em> device and
 * control points will see it and be able to call actions.
 * </p>
 * <p>
 * However, there is one more detail you have to consider: Propagation of <code>LastChange</code>
 * events. Whenever any player state or transition adds a "change" to <code>LastChange</code>, this
 * data will be accumulated. It will <em>not</em> be send to GENA subscribers immediately or
 * automatically! It's up to you how and when you want to flush all accumulated changes to
 * control points. A common approach would be a background thread that executes this operation every
 * second (or even more frequently):
 * </p>
 * <a class="citation" id="avtransport_flushlastchange" href="javacode://this#testCustomPlayer" style="include: INC2;"/>
 * <p>
 * Finally, note that the <em>AVTransport:1</em> specification also defines "logical"
 * player instances. For examle, a renderer that can play two URIs simultaneously would have
 * two <em>AVTransport</em> instances, each with its own identifier. The reserved identifier
 * "0" is the default for a renderer that only supports playback of a single URI at a time.
 * In Cling, each logical <em>AVTransport</em> instance is represented by one instance of a
 * state machine (with all its states) associated with one instance of the <code>AVTransport</code>
 * type. All of these objects are never shared, and they are not thread-safe. Read the documentation and
 * code of the <code>AVTransportService</code> class for more information on this feature -
 * by default it supports only a single transport instance with ID "0", you have to override
 * the <code>findInstance()</code> methods to create and support several parallel playback
 * instances.
 * </p>
 * </div>
 * </div>
 */
public class AVTransportTest {

    /**
     * Controlling a renderer
     * <p>
     * Cling Support provides several action callbacks that simplify creating a control
     * point for the <em>AVTransport</em> service. This is the client side of your player,
     * the remote control.
     * </p>
     * <p>
     * This is how you set an URI for playback:
     * </p>
     * <a class="citation" id="avtransport_ctrl1" href="javacode://this#testCustomPlayer" style="include: CTRL1;"/>
     * <p>
     * This is how you actually start playback:
     * </p>
     * <a class="citation" id="avtransport_ctrl2" href="javacode://this#testCustomPlayer" style="include: CTRL2;"/>
     * <p>
     * Explore the package <code>org.teleal.cling.support.avtransport.callback</code> for more options.
     * </p>
     * <p>
     * Your control point can also subscribe with the service and listen for <code>LastChange</code>
     * events. Cling provides a parser so you get the same types and classes on the control point
     * as are available on the server - it's the same for sending and receiving the event data.
     * When you receive the "last change" string in your <code>SubscriptionCallback</code> you
     * can transform it, for example, this event could have been sent by the service after the
     * player transitioned from NoMediaPresent to Stopped state:
     * </p>
     * <a class="citation" id="avtransport_ctrl3" href="javacode://this#testCustomPlayer" style="include: CTRL3;"/>
     */
    @Test
    public void testCustomPlayer() throws Exception {

        LocalService<AVTransportService> service =                                      // DOC:INC1
                new AnnotationLocalServiceBinder().read(AVTransportService.class);

        service.setManager(
                new DefaultServiceManager<AVTransportService>(service, null) {
                    @Override
                    protected AVTransportService createServiceInstance() throws Exception {
                        return new AVTransportService(
                                MyRendererStateMachine.class,   // All states
                                MyRendererNoMediaPresent.class  // Initial state
                        );
                    }
                }
        );                                                                              // DOC:INC1

        // Yes, it's a bit awkward to get the LastChange without a controlpoint
        final String[] lcValue = new String[1];
        PropertyChangeSupport pcs = service.getManager().getPropertyChangeSupport();
        pcs.addPropertyChangeListener(new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent ev) {
                if (ev.getPropertyName().equals("LastChange"))
                    lcValue[0] = (String) ev.getNewValue();
            }
        });

        final boolean[] assertions = new boolean[5];

        ActionCallback getDeviceCapsAction =
                new GetDeviceCapabilities(service) {
                    @Override
                    public void received(ActionInvocation actionInvocation, DeviceCapabilities caps) {
                        assertEquals(caps.getPlayMedia()[0].toString(), "NETWORK");
                        assertEquals(caps.getRecMedia()[0].toString(), "NOT_IMPLEMENTED");
                        assertEquals(caps.getRecQualityModes()[0].toString(), "NOT_IMPLEMENTED");
                        assertions[0] = true;
                    }

                    @Override
                    public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                        // Something was wrong
                    }
                };
        getDeviceCapsAction.run();

        ActionCallback setAVTransportURIAction = // DOC: CTRL1
                new SetAVTransportURI(service, "http://10.0.0.1/file.mp3", "NO METADATA") {
                    @Override
                    public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                        // Something was wrong
                    }
                }; // DOC: CTRL1
        setAVTransportURIAction.run();

        ActionCallback getTransportInfo =
                new GetTransportInfo(service) {
                    @Override
                    public void received(ActionInvocation invocation, TransportInfo transportInfo) {
                        assertEquals(transportInfo.getCurrentTransportState(), TransportState.STOPPED);
                        assertions[1] = true;
                    }

                    @Override
                    public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                        // Something was wrong
                    }
                };
        getTransportInfo.run();

        ActionCallback getMediaInfoAction =
                new GetMediaInfo(service) {
                    @Override
                    public void received(ActionInvocation invocation, MediaInfo mediaInfo) {
                        assertEquals(mediaInfo.getCurrentURI(), "http://10.0.0.1/file.mp3");
                        assertEquals(mediaInfo.getCurrentURIMetaData(), "NO METADATA");
                        assertions[2] = true;
                    }

                    @Override
                    public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                        // Something was wrong
                    }
                };
        getMediaInfoAction.run();
        
        ActionCallback getPositionInfoAction =
                new GetPositionInfo(service) {
                    @Override
                    public void received(ActionInvocation invocation, PositionInfo positionInfo) {
                        assertEquals(positionInfo.getTrackURI(), "http://10.0.0.1/file.mp3");
                        assertEquals(positionInfo.getTrackMetaData(), "NO METADATA");
                        assertions[3] = true;
                    }

                    @Override
                    public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                        // Something was wrong
                        System.err.println(defaultMsg);
                    }
                };
        getPositionInfoAction.run();

        ActionCallback getCurrentTransportActions =
                new GetCurrentTransportActions(service) {
                    @Override
                    public void received(ActionInvocation invocation, TransportAction[] actions) {
                        List<TransportAction> currentActions = Arrays.asList(actions);
                        assert currentActions.contains(TransportAction.Play);
                        assert currentActions.contains(TransportAction.Stop);
                        assert currentActions.contains(TransportAction.Seek);
                        assertions[4] = true;
                    }

                    @Override
                    public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                        // Something was wrong
                    }
                };
        getCurrentTransportActions.run();

        AVTransportService avTransportService = service.getManager().getImplementation(); // DOC:INC2
        avTransportService.fireLastChange();                                              // DOC:INC2

        String lastChangeString = lcValue[0];
        LastChange lastChange = new LastChange( // DOC:CTRL3
                new AVTransportLastChangeParser(),
                lastChangeString
        );
        assertEquals(
                lastChange.getEventedValue(
                        0, // Instance ID!
                        AVTransportVariable.AVTransportURI.class
                ).getValue(),
                URI.create("http://10.0.0.1/file.mp3")
        );
        assertEquals(
                lastChange.getEventedValue(
                        0,
                        AVTransportVariable.CurrentTrackURI.class
                ).getValue(),
                URI.create("http://10.0.0.1/file.mp3")
        );
        assertEquals(
                lastChange.getEventedValue(
                        0,
                        AVTransportVariable.TransportState.class
                ).getValue(),
                TransportState.STOPPED
        );// DOC:CTRL3

        ActionCallback playAction = // DOC:CTRL2
                new Play(service) {
                    @Override
                    public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                        // Something was wrong
                    }
                }; // DOC:CTRL2
        playAction.run();

        avTransportService = service.getManager().getImplementation();
        avTransportService.fireLastChange();

        lastChangeString = lcValue[0];
        lastChange = new LastChange(
                new AVTransportLastChangeParser(),
                lastChangeString
        );
        assertEquals(
                lastChange.getEventedValue(
                        0,
                        AVTransportVariable.TransportState.class
                ).getValue(),
                TransportState.PLAYING
        );

        ActionCallback stopAction =
                new Stop(service) {
                    @Override
                    public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) {
                        // Something was wrong
                    }
                };
        stopAction.run();

        avTransportService = service.getManager().getImplementation();
        avTransportService.fireLastChange();

        lastChangeString = lcValue[0];
        lastChange = new LastChange(
                new AVTransportLastChangeParser(),
                lastChangeString
        );
        assertEquals(
                lastChange.getEventedValue(
                        0,
                        AVTransportVariable.TransportState.class
                ).getValue(),
                TransportState.STOPPED
        );

        for (boolean assertion : assertions) {
            assertEquals(assertion, true);
        }

    }

}
