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

import example.binarylight.BinaryLightSampleData;
import org.teleal.cling.binding.LocalServiceBinder;
import org.teleal.cling.binding.annotations.AnnotationLocalServiceBinder;
import org.teleal.cling.binding.annotations.UpnpAction;
import org.teleal.cling.binding.annotations.UpnpInputArgument;
import org.teleal.cling.binding.annotations.UpnpOutputArgument;
import org.teleal.cling.binding.annotations.UpnpStateVariable;
import org.teleal.cling.controlpoint.ActionCallback;
import org.teleal.cling.mock.MockUpnpService;
import org.teleal.cling.model.DefaultServiceManager;
import org.teleal.cling.model.action.ActionArgumentValue;
import org.teleal.cling.model.action.ActionInvocation;
import org.teleal.cling.model.message.UpnpResponse;
import org.teleal.cling.model.meta.Action;
import org.teleal.cling.model.meta.LocalDevice;
import org.teleal.cling.model.meta.LocalService;
import org.teleal.cling.model.meta.Service;
import org.teleal.cling.model.types.BooleanDatatype;
import org.teleal.cling.model.types.Datatype;
import org.teleal.cling.model.types.UDAServiceId;
import org.teleal.cling.model.types.UDAServiceType;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;

import static org.testng.Assert.assertEquals;

/**
 * Invoking an action
 * <p>
 * UPnP services expose state variables and actions. While the state variables represent the
 * current state of the service, actions are the operations used to query or maniuplate the
 * service's state. You have to obtain a <code>Service</code> instance from a
 * <code>Device</code> to access any <code>Action</code>. The target device can be local
 * to the same UPnP stack as your control point, or it can be remote of another device anywhere
 * on the network. We'll discuss later in this chapter how to access devices through the
 * local stack's <code>Registry</code>.
 * </p>
 * <p>
 * Once you have the device, access the <code>Service</code> through the metadata model, for example:
 * </p>
 * <a class="citation" href="javacode://this#invokeActions(LocalDevice)" id="ai_findservice" style="include: FINDSERVICE"/>
 * <p>
 * This method will search the device and all its embedded devices for a service with the given
 * identififer and returns either the found <code>Service</code> or <code>null</code>. The Cling
 * metamodel is thread-safe, so you can share an instance of <code>Service</code> or
 * <code>Action</code> and access it concurrently.
 * </p>
 * <p>
 * Invoking an action is the job of an instance of <code>ActionInvocation</code>, note that this
 * instance is <em>NOT</em> thread-safe and each thread that wishes to execute an action has to
 * obtain its own invocation from the <code>Action</code> metamodel:
 * </p>
 * <a class="citation" href="javacode://this#invokeActions(LocalDevice)" id="ai_getstatus" style="include: GETSTATUS; exclude: EXC1"/>
 * <p>
 * Execution is asynchronous, your <code>ActionCallback</code> has two methods which will be called
 * by the UPnP stack when the execution completes. If the action is successful, you can obtain any
 * output argument values from the invocation instance, which is conveniently passed into the
 * <code>success()</code> method. You can inspect the named output argument values and their datatypes to
 * continue processing the result.
 * </p>
 * <p>
 * Action execution doesn't have to be processed asynchronously, after all, the underlying HTTP/SOAP protocol
 * is a request waiting for a response. The callback programming model however fits nicely into a typical
 * UPnP client, which also has to process event notifications and device registrations asynchronously. If
 * you want to execute an <code>ActionInvocation</code> directly, within the current thread, use the empty
 * <code>ActionCallback.Default</code> implementation:
 * </p>
 * <a class="citation" href="javacode://this#invokeActions(LocalDevice)" id="ai_synchronous" style="include: SYNCHRONOUS"/>
 * <p>
 * When invocation fails you can access the failure details through
 * <code>invocation.getFailure()</code>, or use the shown convenience method to create a simple error
 * message. See the Javadoc of <code>ActionCallback</code> for more details.
 * </p>
 * <p>
 * When an action requires input argument values, you have to provide them. Like output arguments, any
 * input arguments of actions are also named, so you can set them by calling <code>setInput("MyArgumentName", value)</code>:
 * </p>
 * <a class="citation" href="javacode://this#invokeActions(LocalDevice)" id="ai_settarget" style="include: SETTARGET; exclude: EXC2"/>
 * <p>
 * This action has one input argument of UPnP type "boolean". You can set a Java <code>boolean</code>
 * primitive or <code>Boolean</code> instance and it will be automatically converted. If you set an
 * invalid value for a particular argument, such as an instance with the wrong type,
 * an <code>InvalidValueException</code> will be thrown immediately.
 * </p>
 * <div class="note">
 * <div class="title">Empty values and null in Cling</div>
 * There is no difference between empty string <code>""</code> and <code>null</code> in Cling,
 * because the UPnP specification does not address this issue. The SOAP  message of an action call
 * or an event message must contain an element {@code <SomeVar></SomeVar>} for all arguments, even if
 * it is an empty XML element. If you provide  an empty string or a null value when preparing a message,
 * it will always be a <code>null</code> on the receiving end because we can only transmit one
 * thing, an empty XML element. If you forget to set an input argument's value, it will be null/empty element.
 * </div>
 */
public class ActionInvocationTest {

    protected LocalService bindService(Class<?> clazz) throws Exception {
        LocalServiceBinder binder = new AnnotationLocalServiceBinder();
        // Let's also test the overloaded reader
        LocalService svc = binder.read(
                clazz,
                new UDAServiceId("SwitchPower"),
                new UDAServiceType("SwitchPower", 1),
                true,
                new Class[]{MyString.class}
        );
        svc.setManager(
                new DefaultServiceManager(svc, clazz)
        );
        return svc;
    }

    @DataProvider(name = "devices")
    public Object[][] getDevices() throws Exception {
        return new LocalDevice[][]{
                {BinaryLightSampleData.createDevice(bindService(TestServiceOne.class))},
                {BinaryLightSampleData.createDevice(bindService(TestServiceTwo.class))},
                {BinaryLightSampleData.createDevice(bindService(TestServiceThree.class))},
        };
    }

    @Test(dataProvider = "devices")
    public void invokeActions(LocalDevice device) throws Exception {

        MockUpnpService upnpService = new MockUpnpService();

        Service service = device.findService(new UDAServiceId("SwitchPower")); // DOC: FINDSERVICE
        Action getStatusAction = service.getAction("GetStatus");               // DOC: FINDSERVICE

        final boolean[] tests = new boolean[3];

        ActionInvocation getStatusInvocation = new ActionInvocation(getStatusAction);   // DOC: GETSTATUS

        ActionCallback getStatusCallback = new ActionCallback(getStatusInvocation) {

            @Override
            public void success(ActionInvocation invocation) {
                ActionArgumentValue status  = invocation.getOutput("ResultStatus");

                assert status != null;

                assertEquals(status.getArgument().getName(), "ResultStatus");

                assertEquals(status.getDatatype().getClass(), BooleanDatatype.class);
                assertEquals(status.getDatatype().getBuiltin(), Datatype.Builtin.BOOLEAN);

                assertEquals((Boolean) status.getValue(), Boolean.valueOf(false));
                assertEquals(status.toString(), "0"); // '0' is 'false' in UPnP
                tests[0] = true; // DOC: EXC1
            }

            @Override
            public void failure(ActionInvocation invocation,
                                UpnpResponse operation,
                                String defaultMsg) {
                System.err.println(defaultMsg);
            }
        };

        upnpService.getControlPoint().execute(getStatusCallback);                       // DOC: GETSTATUS


        Action action = service.getAction("SetTarget");                                 // DOC: SETTARGET

        ActionInvocation setTargetInvocation = new ActionInvocation(action);

        setTargetInvocation.setInput("NewTargetValue", true); // Can throw InvalidValueException

        // Alternative:
        //
        // setTargetInvocation.setInput(
        //         new ActionArgumentValue(
        //                 action.getInputArgument("NewTargetValue"),
        //                 true
        //         )
        // );

        ActionCallback setTargetCallback = new ActionCallback(setTargetInvocation) {

            @Override
            public void success(ActionInvocation invocation) {
                ActionArgumentValue[] output = invocation.getOutput();
                assertEquals(output.length, 0);
                tests[1] = true; // DOC: EXC2
            }

            @Override
            public void failure(ActionInvocation invocation,
                                UpnpResponse operation,
                                String defaultMsg) {
                System.err.println(defaultMsg);
            }
        };

        upnpService.getControlPoint().execute(setTargetCallback);                       // DOC: SETTARGET

        getStatusInvocation = new ActionInvocation(getStatusAction);
        new ActionCallback.Default(getStatusInvocation, upnpService.getControlPoint()).run(); // DOC: SYNCHRONOUS
        ActionArgumentValue status  = getStatusInvocation.getOutput("ResultStatus");
        if (Boolean.valueOf(true).equals(status.getValue())) {
            tests[2] = true;
        }

        for (boolean test : tests) {
            assertEquals(test, true);
        }


        LocalService svc = (LocalService) service;

        ActionInvocation getTargetInvocation = new ActionInvocation(svc.getAction("GetTarget"));
        svc.getExecutor(getTargetInvocation.getAction()).execute(getTargetInvocation);
        assertEquals(getTargetInvocation.getFailure(), null);
        assertEquals(getTargetInvocation.getOutput().length, 1);
        assertEquals(getTargetInvocation.getOutput()[0].toString(), "1");

        ActionInvocation setMyStringInvocation = new ActionInvocation(svc.getAction("SetMyString"));
        setMyStringInvocation.setInput("MyString", "foo");
        svc.getExecutor(setMyStringInvocation.getAction()).execute(setMyStringInvocation);
        assertEquals(setMyStringInvocation.getFailure(), null);
        assertEquals(setMyStringInvocation.getOutput().length, 0);

        ActionInvocation getMyStringInvocation = new ActionInvocation(svc.getAction("GetMyString"));
        svc.getExecutor(getMyStringInvocation.getAction()).execute(getMyStringInvocation);
        assertEquals(getTargetInvocation.getFailure(), null);
        assertEquals(getMyStringInvocation.getOutput().length, 1);
        assertEquals(getMyStringInvocation.getOutput()[0].toString(), "foo");

    }

    @Test(dataProvider = "devices")
    public void invokeActionsWithAlias(LocalDevice device) throws Exception {

        MockUpnpService upnpService = new MockUpnpService();

        Service service = device.findService(new UDAServiceId("SwitchPower"));
        Action getStatusAction = service.getAction("GetStatus");

        final boolean[] tests = new boolean[1];

        Action action = service.getAction("SetTarget");
        ActionInvocation setTargetInvocation = new ActionInvocation(action);
        setTargetInvocation.setInput("NewTargetValue1", true);
        ActionCallback setTargetCallback = new ActionCallback(setTargetInvocation) {

            @Override
            public void success(ActionInvocation invocation) {
                ActionArgumentValue[] output = invocation.getOutput();
                assertEquals(output.length, 0);
                tests[0] = true;
            }

            @Override
            public void failure(ActionInvocation invocation,
                                UpnpResponse operation,
                                String defaultMsg) {
                System.err.println(defaultMsg);
            }
        };
        upnpService.getControlPoint().execute(setTargetCallback);

        for (boolean test : tests) {
            assertEquals(test, true);
        }

        LocalService svc = (LocalService) service;

        ActionInvocation getTargetInvocation = new ActionInvocation(svc.getAction("GetTarget"));
        svc.getExecutor(getTargetInvocation.getAction()).execute(getTargetInvocation);
        assertEquals(getTargetInvocation.getFailure(), null);
        assertEquals(getTargetInvocation.getOutput().length, 1);
        assertEquals(getTargetInvocation.getOutput()[0].toString(), "1");

        ActionInvocation setMyStringInvocation = new ActionInvocation(svc.getAction("SetMyString"));
        setMyStringInvocation.setInput("MyString1", "foo");
        svc.getExecutor(setMyStringInvocation.getAction()).execute(setMyStringInvocation);
        assertEquals(setMyStringInvocation.getFailure(), null);
        assertEquals(setMyStringInvocation.getOutput().length, 0);

        ActionInvocation getMyStringInvocation = new ActionInvocation(svc.getAction("GetMyString"));
        svc.getExecutor(getMyStringInvocation.getAction()).execute(getMyStringInvocation);
        assertEquals(getTargetInvocation.getFailure(), null);
        assertEquals(getMyStringInvocation.getOutput().length, 1);
        assertEquals(getMyStringInvocation.getOutput()[0].toString(), "foo");

    }

    /* ####################################################################################################### */

    public static class TestServiceOne {

        @UpnpStateVariable(sendEvents = false)
        private boolean target = false;

        @UpnpStateVariable
        private boolean status = false;

        @UpnpStateVariable(sendEvents = false)
        private MyString myString;

        @UpnpAction
        public void setTarget(@UpnpInputArgument(name = "NewTargetValue", aliases ={"NewTargetValue1"}) boolean newTargetValue) {
            target = newTargetValue;
            status = newTargetValue;
        }

        @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
        public boolean getTarget() {
            return target;
        }

        @UpnpAction(name = "GetStatus", out = @UpnpOutputArgument(name = "ResultStatus", getterName = "getStatus"))
        public void dummyStatus() {
            // NOOP
        }

        public boolean getStatus() {
            return status;
        }

        @UpnpAction
        public void setMyString(@UpnpInputArgument(name = "MyString", aliases ={"MyString1"}) MyString myString) {
            this.myString = myString;
        }

        @UpnpAction(name = "GetMyString", out = @UpnpOutputArgument(name = "MyString", getterName = "getMyString"))
        public void getMyStringDummy() {
        }

        public MyString getMyString() {
            return myString;
        }
    }

    public static class TestServiceTwo {

        @UpnpStateVariable(sendEvents = false)
        private boolean target = false;

        @UpnpStateVariable
        private boolean status = false;

        @UpnpStateVariable(sendEvents = false)
        private MyString myString;

        @UpnpAction
        public void setTarget(@UpnpInputArgument(name = "NewTargetValue", aliases ={"NewTargetValue1"}) boolean newTargetValue) {
            target = newTargetValue;
            status = newTargetValue;
        }

        @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
        public boolean getTarget() {
            return target;
        }

        @UpnpAction(name = "GetStatus", out = @UpnpOutputArgument(name = "ResultStatus", getterName = "getStatus"))
        public StatusHolder dummyStatus() {
            return new StatusHolder(status);
        }

        @UpnpAction
        public void setMyString(@UpnpInputArgument(name = "MyString", aliases ={"MyString1"}) MyString myString) {
            this.myString = myString;
        }

        @UpnpAction(out = @UpnpOutputArgument(name = "MyString", getterName = "getMyString"))
        public MyStringHolder getMyString() {
            return new MyStringHolder(myString);
        }

        public class StatusHolder {
            boolean st;

            public StatusHolder(boolean st) {
                this.st = st;
            }

            public boolean getStatus() {
                return st;
            }
        }

        public class MyStringHolder {
            MyString myString;

            public MyStringHolder(MyString myString) {
                this.myString = myString;
            }

            public MyString getMyString() {
                return myString;
            }
        }

    }

    public static class TestServiceThree {

        @UpnpStateVariable(sendEvents = false)
        private boolean target = false;

        @UpnpStateVariable
        private boolean status = false;

        @UpnpStateVariable(sendEvents = false)
        private MyString myString;

        @UpnpAction
        public void setTarget(@UpnpInputArgument(name = "NewTargetValue", aliases ={"NewTargetValue1"}) boolean newTargetValue) {
            target = newTargetValue;
            status = newTargetValue;
        }

        @UpnpAction(out = @UpnpOutputArgument(name = "RetTargetValue"))
        public boolean getTarget() {
            return target;
        }

        @UpnpAction(out = @UpnpOutputArgument(name = "ResultStatus"))
        public boolean getStatus() {
            return status;
        }

        @UpnpAction
        public void setMyString(@UpnpInputArgument(name = "MyString", aliases ={"MyString1"}) MyString myString) {
            this.myString = myString;
        }

        @UpnpAction(out = @UpnpOutputArgument(name = "MyString"))
        public MyString getMyString() {
            return myString;
        }
    }

    public static class MyString {
        private String s;

        public MyString(String s) {
            this.s = s;
        }

        public String getS() {
            return s;
        }

        @Override
        public String toString() {
            return s;
        }
    }

}
