/**
 *
 * Copyright 2016 Florian Schmaus
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.igniterealtime.smack.smackrepl;

import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;

import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.roster.Roster;
import org.jivesoftware.smack.roster.RosterUtil;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jivesoftware.smack.util.StringUtils;

import org.jivesoftware.smackx.iot.IoTDiscoveryIntegrationTest;
import org.jivesoftware.smackx.iot.Thing;
import org.jivesoftware.smackx.iot.data.IoTDataManager;
import org.jivesoftware.smackx.iot.data.ThingMomentaryReadOutRequest;
import org.jivesoftware.smackx.iot.data.ThingMomentaryReadOutResult;
import org.jivesoftware.smackx.iot.data.element.IoTDataField;
import org.jivesoftware.smackx.iot.data.element.IoTDataField.IntField;
import org.jivesoftware.smackx.iot.data.element.IoTFieldsExtension;
import org.jivesoftware.smackx.iot.discovery.AbstractThingStateChangeListener;
import org.jivesoftware.smackx.iot.discovery.IoTDiscoveryManager;
import org.jivesoftware.smackx.iot.discovery.ThingState;
import org.jivesoftware.smackx.iot.provisioning.BecameFriendListener;
import org.jivesoftware.smackx.iot.provisioning.IoTProvisioningManager;

import org.igniterealtime.smack.inttest.util.SimpleResultSyncPoint;
import org.jxmpp.jid.BareJid;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.impl.JidCreate;

public class IoT {

    // A 10 minute timeout.
    private static final long TIMEOUT = 10 * 60 * 1000;

    private interface IotScenario {
        void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readinThingConnection) throws XMPPException, SmackException, IOException, InterruptedException, TimeoutException, Exception;
    }

    public static void iotScenario(String dataThingJidString, String dataThingPassword, String readingThingJidString,
            String readingThingPassword, IotScenario scenario) throws TimeoutException, Exception {
        final EntityBareJid dataThingJid = JidCreate.entityBareFrom(dataThingJidString);
        final EntityBareJid readingThingJid = JidCreate.entityBareFrom(readingThingJidString);

        final XMPPTCPConnectionConfiguration dataThingConnectionConfiguration = XMPPTCPConnectionConfiguration.builder()
                .setUsernameAndPassword(dataThingJid.getLocalpart(), dataThingPassword)
                .setXmppDomain(dataThingJid.asDomainBareJid()).setSecurityMode(SecurityMode.disabled)
                .enableDefaultDebugger().build();
        final XMPPTCPConnectionConfiguration readingThingConnectionConfiguration = XMPPTCPConnectionConfiguration
                .builder().setUsernameAndPassword(readingThingJid.getLocalpart(), readingThingPassword)
                .setXmppDomain(readingThingJid.asDomainBareJid()).setSecurityMode(SecurityMode.disabled)
                .enableDefaultDebugger().build();

        final XMPPTCPConnection dataThingConnection = new XMPPTCPConnection(dataThingConnectionConfiguration);
        final XMPPTCPConnection readingThingConnection = new XMPPTCPConnection(readingThingConnectionConfiguration);

        dataThingConnection.setReplyTimeout(TIMEOUT);
        readingThingConnection.setReplyTimeout(TIMEOUT);

        dataThingConnection.setUseStreamManagement(false);
        readingThingConnection.setUseStreamManagement(false);

        try {
            dataThingConnection.connect().login();
            readingThingConnection.connect().login();
            scenario.iotScenario(dataThingConnection, readingThingConnection);
        } finally {
            dataThingConnection.disconnect();
            readingThingConnection.disconnect();
        }
    }

    public static void iotReadOutScenario(String dataThingJidString, String dataThingPassword, String readingThingJidString,
                    String readingThingPassword)
                    throws Exception {
        iotScenario(dataThingJidString, dataThingPassword, readingThingJidString, readingThingPassword, READ_OUT_SCENARIO);
    }

    public static final IotScenario READ_OUT_SCENARIO = new IotScenario() {
        @Override
        public void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) throws TimeoutException, Exception {
            ThingState dataThingState = actAsDataThing(dataThingConnection);

            final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint();
            dataThingState.setThingStateChangeListener(new AbstractThingStateChangeListener() {
                @Override
                public void owned(BareJid jid) {
                    syncPoint.signal();
                }
            });
            // Wait until the thing is owned.
            syncPoint.waitForResult(TIMEOUT);
            printStatus("OWNED - Thing now onwed by " + dataThingState.getOwner());

            // Make sure things are befriended.
            IoTProvisioningManager readingThingProvisioningManager = IoTProvisioningManager.getInstanceFor(readingThingConnection);
            readingThingProvisioningManager.sendFriendshipRequestIfRequired(dataThingConnection.getUser().asBareJid());

            Roster dataThingRoster = Roster.getInstanceFor(dataThingConnection);
            RosterUtil.waitUntilOtherEntityIsSubscribed(dataThingRoster, readingThingConnection.getUser().asBareJid(), TIMEOUT);
            printStatus("FRIENDSHIP ACCEPTED - Trying to read out data");

            IoTDataManager readingThingDataManager = IoTDataManager.getInstanceFor(readingThingConnection);
            List<IoTFieldsExtension> values = readingThingDataManager.requestMomentaryValuesReadOut(dataThingConnection.getUser());
            if (values.size() != 1) {
                throw new IllegalStateException("Unexpected number of values returned: " + values.size());
            }
            IoTFieldsExtension field = values.get(0);
            printStatus("DATA READ-OUT SUCCESS: " + field.toXML());
            printStatus("IoT SCENARIO FINISHED SUCCESSFULLY");
        }
    };

    public static void iotOwnerApprovesFriendScenario(String dataThingJidString, String dataThingPassword,
            String readingThingJidString, String readingThingPassword) throws Exception {
        iotScenario(dataThingJidString, dataThingPassword, readingThingJidString, readingThingPassword,
                OWNER_APPROVES_FRIEND_SCENARIO);
    }

    public static final IotScenario OWNER_APPROVES_FRIEND_SCENARIO = new IotScenario() {
        @Override
        public void iotScenario(XMPPTCPConnection dataThingConnection, XMPPTCPConnection readingThingConnection) throws TimeoutException, Exception {
            // First ensure that the two XMPP entities are not already subscribed to each other presences.
            RosterUtil.ensureNotSubscribedToEachOther(dataThingConnection, readingThingConnection);

            final BareJid dataThingBareJid = dataThingConnection.getUser().asBareJid();
            final BareJid readingThingBareJid = readingThingConnection.getUser().asBareJid();
            final ThingState dataThingState = actAsDataThing(dataThingConnection);

            printStatus("WAITING for 'claimed' notification. Please claim thing now");
            final SimpleResultSyncPoint syncPoint = new SimpleResultSyncPoint();
            dataThingState.setThingStateChangeListener(new AbstractThingStateChangeListener() {
                @Override
                public void owned(BareJid jid) {
                    syncPoint.signal();
                }
            });
            // Wait until the thing is owned.
            syncPoint.waitForResult(TIMEOUT);
            printStatus("OWNED - Thing now onwed by " + dataThingState.getOwner());

            // Now, ReadingThing sends a friendship request to data thing, which
            // will proxy the request to its provisioning service, which will
            // likely return that both a not friends since the owner did not
            // authorize the friendship yet.
            final SimpleResultSyncPoint friendshipApprovedSyncPoint = new SimpleResultSyncPoint();
            final IoTProvisioningManager readingThingProvisioningManager = IoTProvisioningManager.getInstanceFor(readingThingConnection);
            final BecameFriendListener becameFriendListener = new BecameFriendListener() {
                @Override
                public void becameFriend(BareJid jid, Presence presence) {
                    if (jid.equals(dataThingBareJid)) {
                        friendshipApprovedSyncPoint.signal();
                    }
                }
            };
            readingThingProvisioningManager.addBecameFriendListener(becameFriendListener);

            try {
                readingThingProvisioningManager
                        .sendFriendshipRequestIfRequired(dataThingConnection.getUser().asBareJid());
                friendshipApprovedSyncPoint.waitForResult(TIMEOUT);
            } finally {
                readingThingProvisioningManager.removeBecameFriendListener(becameFriendListener);
            }

            printStatus("FRIENDSHIP APPROVED - ReadingThing " + readingThingBareJid + " is now a friend of DataThing " + dataThingBareJid);
        }
    };

    private static ThingState actAsDataThing(XMPPTCPConnection connection) throws XMPPException, SmackException, InterruptedException {
        final String key = StringUtils.randomString(12);
        final String sn = StringUtils.randomString(12);
        Thing dataThing = Thing.builder()
                        .setKey(key)
                        .setSerialNumber(sn)
                        .setManufacturer("IgniteRealtime")
                        .setModel("Smack")
                        .setVersion("0.1")
                        .setMomentaryReadOutRequestHandler(new ThingMomentaryReadOutRequest() {
            @Override
            public void momentaryReadOutRequest(ThingMomentaryReadOutResult callback) {
                IoTDataField.IntField field = new IntField("timestamp", (int) (System.currentTimeMillis() / 1000));
                callback.momentaryReadOut(Collections.singletonList(field));
            }
        })
                        .build();
        IoTDiscoveryManager iotDiscoveryManager = IoTDiscoveryManager.getInstanceFor(connection);
        ThingState state = IoTDiscoveryIntegrationTest.registerThing(iotDiscoveryManager, dataThing);
        printStatus("SUCCESS: Thing registered:" + dataThing);
        return state;
    }

    private static void printStatus(CharSequence status) {
        // CHECKSTYLE:OFF
        System.out.println(status);
        // CHECKSTYLE:ON
    }

    public static void main(String[] args) throws TimeoutException, Exception {
        if (args.length != 4) {
            throw new IllegalArgumentException();
        }
        iotOwnerApprovesFriendScenario(args[0], args[1], args[2], args[3]);
    }

}
