package org.openmuc.openiec61850.integrationtests;

import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.naming.ConfigurationException;

import org.junit.Assert;
import org.junit.Test;
import org.openmuc.openiec61850.BasicDataAttribute;
import org.openmuc.openiec61850.BdaBoolean;
import org.openmuc.openiec61850.BdaFloat32;
import org.openmuc.openiec61850.BdaInt32;
import org.openmuc.openiec61850.BdaInt32U;
import org.openmuc.openiec61850.BdaQuality;
import org.openmuc.openiec61850.BdaQuality.Validity;
import org.openmuc.openiec61850.BdaReasonForInclusion;
import org.openmuc.openiec61850.BdaTimestamp;
import org.openmuc.openiec61850.BdaVisibleString;
import org.openmuc.openiec61850.ClientAssociation;
import org.openmuc.openiec61850.ClientEventListener;
import org.openmuc.openiec61850.ClientSap;
import org.openmuc.openiec61850.DataSet;
import org.openmuc.openiec61850.Fc;
import org.openmuc.openiec61850.FcModelNode;
import org.openmuc.openiec61850.ModelNode;
import org.openmuc.openiec61850.Report;
import org.openmuc.openiec61850.SclParseException;
import org.openmuc.openiec61850.ServerEventListener;
import org.openmuc.openiec61850.ServerModel;
import org.openmuc.openiec61850.ServerSap;
import org.openmuc.openiec61850.ServiceError;
import org.openmuc.openiec61850.Urcb;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ClientServerITest extends Thread implements ServerEventListener, ClientEventListener {

    private static final Logger logger = LoggerFactory.getLogger(ClientServerITest.class);

    int port = 54321;
    String host = "127.0.0.1";
    ClientSap clientSap = new ClientSap();
    ServerSap serverSap = null;
    ClientAssociation clientAssociation = null;
    ClientAssociation clientAssociation2 = null;
    ServerModel serversServerModel = null;
    private static int numReports = 0;
    private static int numSuccess = 0;
    private static int numAssociationClosed = 0;

    // Get the Java runtime
    public static Runtime runtime = Runtime.getRuntime();

    @Test
    public void testClientServerCom() throws IOException, ServiceError, ConfigurationException,
            javax.naming.ConfigurationException, SclParseException, InterruptedException {

        clientSap.setTSelRemote(new byte[] { 0, 1 });
        clientSap.setTSelLocal(new byte[] { 0, 0 });
        clientSap.setApTitleCalled(new int[] { 1, 1, 999, 1 });
        BdaTimestamp timestamp;
        BdaVisibleString namePlateVendor;

        // ---------------------------------------------------
        // ------------------1st test server------------------
        runServer("src/test/resources/openiec61850sample01.icd", port);
        logger.info("IED Server is running");

        // -----------------------------------------------------------
        // Client
        // -----------------------------------------------------------

        logger.debug("Attempting to connect to server " + host + " on port " + port);

        clientAssociation = clientSap.associate(InetAddress.getByName(host), port, null, this);

        ServerModel serverModel = clientAssociation.retrieveModel();
        getAllBdas(serverModel);

        timestamp = (BdaTimestamp) serverModel.findModelNode("ied1lDevice1/MMXU1.TotW.t", Fc.MX);
        clientAssociation.getDataValues(timestamp);

        namePlateVendor = (BdaVisibleString) serverModel.findModelNode("ied1lDevice1/LLN0.NamPlt.vendor", Fc.DC);
        namePlateVendor.setValue("Fraunhofer ISE".getBytes());
        clientAssociation.setDataValues(namePlateVendor);
        namePlateVendor.setDefault();
        clientAssociation.getDataValues(namePlateVendor);

        Assert.assertEquals("Fraunhofer ISE", new String(namePlateVendor.getValue()));

        // -------------Test DataSets-Start---------------------

        Collection<DataSet> dataSets = serverModel.getDataSets();
        Assert.assertEquals(2, dataSets.size());

        DataSet dataSet1 = serverModel.getDataSet("ied1lDevice1/LLN0.dataset1");

        Assert.assertEquals("ied1lDevice1/LLN0.dataset1", dataSet1.getReferenceStr());

        DataSet dataSet2 = serverModel.getDataSet("ied1lDevice1/LLN0.dataset2");

        Assert.assertEquals("ied1lDevice1/LLN0.dataset2", dataSet2.getReferenceStr());

        for (FcModelNode dataSet2Member : dataSet2) {
            ((BdaInt32) dataSet2Member).setValue(9);
        }

        clientAssociation.setDataSetValues(dataSet2);

        dataSet1 = serverModel.getDataSet(dataSet1.getReferenceStr());

        clientAssociation.getDataSetValues(dataSet1);

        List<FcModelNode> nonPersistentDataSetMembers = new ArrayList<>(2);
        nonPersistentDataSetMembers.add(namePlateVendor);
        nonPersistentDataSetMembers.add(timestamp);

        DataSet nonPersistentDataSet = new DataSet("@nonPersistentDataSet", nonPersistentDataSetMembers);

        clientAssociation.createDataSet(nonPersistentDataSet);

        namePlateVendor.setDefault();

        List<ServiceError> serviceErrors = clientAssociation.getDataSetValues(nonPersistentDataSet);

        for (ServiceError serviceError : serviceErrors) {
            Assert.assertNull(serviceError);
        }

        Assert.assertTrue("Fraunhofer ISE".equals(new String(namePlateVendor.getValue())));

        clientAssociation.deleteDataSet(nonPersistentDataSet);

        serviceErrors = clientAssociation.getDataSetValues(nonPersistentDataSet);

        Assert.assertEquals(2, serviceErrors.size());

        for (ServiceError serviceError : serviceErrors) {
            Assert.assertNotNull(serviceError);
        }

        // -------------Test DataSets-End---------------------

        serverModel.findModelNode("ied1lDevice1/MMXU1.TotW", Fc.MX);

        BdaFloat32 f = (BdaFloat32) serverModel.findModelNode("ied1lDevice1/MMXU1.W.phsA.cVal.mag.f", Fc.MX);

        f.setFloat(3.0f);

        Assert.assertEquals(f.getFloat(), 3.0f, 0.00001);

        clientAssociation.getDataSetValues(dataSet1);

        Assert.assertEquals(f.getFloat(), 0.0f, 0.00001);

        /* Test selecting a controllable Data Object */

        BdaInt32U sboTimeout = (BdaInt32U) serverModel.findModelNode("ied1lDevice1/CSWI1.PosA.sboTimeout", Fc.CF);
        sboTimeout.setValue(2000l);

        BdaVisibleString sbo = (BdaVisibleString) serverModel.findModelNode("ied1lDevice1/CSWI1.PosA.SBO", Fc.CO);

        Assert.assertNotNull(sbo);

        clientAssociation.getDataValues(sbo);

        Assert.assertTrue(sbo.getStringValue().equals("success"));

        clientAssociation.getDataValues(sbo);

        Assert.assertEquals(sbo.getStringValue(), "success");

        /* select with second connection */

        clientAssociation2 = clientSap.associate(InetAddress.getByName(host), port, null, this);
        ServerModel serverModel2 = clientAssociation2.retrieveModel();

        BdaVisibleString sbo2 = (BdaVisibleString) serverModel2.findModelNode("ied1lDevice1/CSWI1.PosA.SBO", Fc.CO);

        clientAssociation2.getDataValues(sbo2);
        Assert.assertEquals(sbo2.getStringValue(), "");

        /* select with second connection after the sboTimeout of 1000ms should have been run out */

        // clientAssociation.close();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        clientAssociation2.getDataValues(sbo2);
        Assert.assertEquals(sbo2.getStringValue(), "success");

        /* select with first connnection after the second was quit */

        clientAssociation.getDataValues(sbo);
        Assert.assertEquals(sbo.getStringValue(), "");

        clientAssociation2.close();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        clientAssociation.getDataValues(sbo);
        Assert.assertEquals(sbo.getStringValue(), "success");

        FcModelNode switchPosAFcDo = (FcModelNode) serverModel.findModelNode("ied1lDevice1/CSWI1.PosA", Fc.CO);

        Assert.assertNotNull(switchPosAFcDo);

        clientAssociation.operate(switchPosAFcDo);

        // -------------Test Reporting-Start-------------------

        Urcb urcb1 = serverModel.getUrcb("ied1lDevice1/LLN0.urcb1");
        Assert.assertNotNull(urcb1);

        Urcb urcb2 = serverModel.getUrcb("ied1lDevice1/LLN0.urcb2");
        Assert.assertNotNull(urcb2);

        clientAssociation.getRcbValues(urcb1);
        clientAssociation.getRcbValues(urcb2);

        BdaBoolean resv = (BdaBoolean) urcb1.getChild("Resv");
        Assert.assertNotNull(resv);
        clientAssociation.getDataValues(resv);
        Assert.assertFalse(resv.getValue());
        clientAssociation.reserveUrcb(urcb1);
        clientAssociation.getDataValues(resv);
        Assert.assertTrue(resv.getValue());

        Assert.assertEquals("urcb1", urcb1.getRptId().getStringValue());
        Assert.assertEquals("ied1lDevice1/LLN0.urcb2", urcb2.getRptId().getStringValue());

        Assert.assertEquals("ied1lDevice1/LLN0$dataset1", urcb1.getDatSet().getStringValue());
        Assert.assertEquals("ied1lDevice1/LLN0$dataset1", urcb2.getDatSet().getStringValue());

        Assert.assertEquals(true, urcb1.getOptFlds().isDataSetName());
        Assert.assertEquals(false, urcb1.getOptFlds().isBufferOverflow());
        Assert.assertEquals(false, urcb1.getOptFlds().isConfigRevision());
        Assert.assertEquals(false, urcb1.getOptFlds().isDataReference());
        Assert.assertEquals(false, urcb1.getOptFlds().isEntryId());
        Assert.assertEquals(true, urcb1.getOptFlds().isReasonForInclusion());
        Assert.assertEquals(true, urcb1.getOptFlds().isReportTimestamp());
        Assert.assertEquals(false, urcb1.getOptFlds().isSegmentation());
        Assert.assertEquals(true, urcb1.getOptFlds().isSequenceNumber());

        Assert.assertEquals(false, urcb2.getOptFlds().isDataSetName());
        Assert.assertEquals(false, urcb2.getOptFlds().isBufferOverflow());
        Assert.assertEquals(false, urcb2.getOptFlds().isConfigRevision());
        Assert.assertEquals(false, urcb2.getOptFlds().isDataReference());
        Assert.assertEquals(false, urcb2.getOptFlds().isEntryId());
        Assert.assertEquals(false, urcb2.getOptFlds().isReasonForInclusion());
        Assert.assertEquals(false, urcb2.getOptFlds().isReportTimestamp());
        Assert.assertEquals(false, urcb2.getOptFlds().isSegmentation());
        Assert.assertEquals(false, urcb2.getOptFlds().isSequenceNumber());

        Assert.assertEquals(50L, urcb1.getBufTm().getValue());

        Assert.assertEquals(true, urcb1.getTrgOps().isDataChange());
        Assert.assertEquals(true, urcb1.getTrgOps().isDataUpdate());
        Assert.assertEquals(true, urcb1.getTrgOps().isGeneralInterrogation());
        Assert.assertEquals(false, urcb1.getTrgOps().isIntegrity());
        Assert.assertEquals(true, urcb1.getTrgOps().isQualityChange());

        Assert.assertEquals(false, urcb2.getTrgOps().isDataChange());
        Assert.assertEquals(false, urcb2.getTrgOps().isDataUpdate());
        Assert.assertEquals(true, urcb2.getTrgOps().isGeneralInterrogation());
        Assert.assertEquals(false, urcb2.getTrgOps().isIntegrity());
        Assert.assertEquals(false, urcb2.getTrgOps().isQualityChange());

        Assert.assertEquals(5000L, urcb1.getIntgPd().getValue());

        Assert.assertEquals(0, urcb1.getSqNum().getValue());

        Assert.assertEquals(0L, urcb1.getConfRev().getValue());

        urcb1.getRptId().setValue("myurcb1");
        urcb1.getTrgOps().setGeneralInterrogation(false);
        urcb1.getTrgOps().setDataUpdate(false);
        urcb1.getTrgOps().setDataChange(false);

        clientAssociation.setRcbValues(urcb1, true, true, true, true, true, true, true, true);

        urcb1.getRptId().setValue("fasdfsadf");

        clientAssociation.getRcbValues(urcb1);

        Assert.assertEquals("myurcb1", urcb1.getRptId().getStringValue());

        clientAssociation.reserveUrcb(urcb1);
        clientAssociation.enableReporting(urcb1);

        boolean thrown = false;
        try {
            clientAssociation.startGi(urcb1);
        } catch (ServiceError e) {
            thrown = true;
        }
        Assert.assertTrue(thrown);

        urcb1.getTrgOps().setGeneralInterrogation(true);
        urcb1.getTrgOps().setDataChange(true);
        urcb1.getIntgPd().setValue(1000);

        serviceErrors = clientAssociation.setRcbValues(urcb1, false, false, false, false, true, true, false, false);

        Assert.assertNotNull(serviceErrors.get(0));
        clientAssociation.disableReporting(urcb1);

        serviceErrors = clientAssociation.setRcbValues(urcb1, false, false, false, false, true, true, false, false);
        Assert.assertNull(serviceErrors.get(0));

        clientAssociation.enableReporting(urcb1);

        clientAssociation.startGi(urcb1);

        Thread.sleep(4000);

        Assert.assertEquals(2, numReports);
        Assert.assertEquals(2, numSuccess);

        clientAssociation.disableReporting(urcb1);

        urcb1.getTrgOps().setIntegrity(true);
        serviceErrors = clientAssociation.setRcbValues(urcb1, false, false, false, false, true, false, false, false);
        Assert.assertNull(serviceErrors.get(0));

        clientAssociation.enableReporting(urcb1);
        Thread.sleep(6500);

        Assert.assertEquals(8, numReports);
        Assert.assertEquals(8, numSuccess);

        clientAssociation.disableReporting(urcb1);

        // -------------Test Reporting-End---------------------

        clientAssociation.disconnect();

        Thread.sleep(500);
        Assert.assertEquals(2, numAssociationClosed);

        serverSap.stop();

    }

    private void getAllBdas(ServerModel serverModel) throws ServiceError, IOException {

        for (ModelNode ld : serverModel) {
            for (ModelNode ln : ld) {
                getDataRecursive(ln, clientAssociation);
            }
        }
    }

    private void runServer(String sclFilePath, int port) throws SclParseException, IOException {

        List<ServerSap> serverSaps = null;
        serverSaps = ServerSap.getSapsFromSclFile(sclFilePath);

        serverSap = serverSaps.get(0);
        serverSap.setPort(port);
        serverSap.startListening(this);
        serversServerModel = serverSap.getModelCopy();
        start();
    }

    private static void getDataRecursive(ModelNode modelNode, ClientAssociation clientAssociation)
            throws ServiceError, IOException {
        if (modelNode.getChildren() == null) {
            return;
        }
        for (ModelNode childNode : modelNode) {
            FcModelNode fcChildNode = (FcModelNode) childNode;
            if (fcChildNode.getFc() != Fc.CO) {
                logger.debug("calling GetDataValues(" + childNode.getReference() + ")");
                clientAssociation.getDataValues(fcChildNode);
            }
            // clientAssociation.setDataValues(fcChildNode);
            getDataRecursive(childNode, clientAssociation);
        }
    }

    public static int findArray(Byte[] array, Byte[] subArray) {
        return Collections.indexOfSubList(Arrays.asList(array), Arrays.asList(subArray));
    }

    public static String getByteArrayString(byte[] byteArray) {
        StringBuilder builder = new StringBuilder();
        int l = 1;
        for (byte b : byteArray) {
            if ((l != 1) && ((l - 1) % 8 == 0)) {
                builder.append(' ');
            }
            if ((l != 1) && ((l - 1) % 16 == 0)) {
                builder.append('\n');
            }
            l++;
            builder.append("0x");
            String hexString = Integer.toHexString(b & 0xff);
            if (hexString.length() == 1) {
                builder.append(0);
            }
            builder.append(hexString + " ");
        }
        return builder.toString();
    }

    @Override
    public void serverStoppedListening(ServerSap serverSAP) {
        // TODO Auto-generated method stub
    }

    @Override
    public List<ServiceError> write(List<BasicDataAttribute> bdas) {
        logger.debug("DataSource: got write request");
        return null;
    }

    @Override
    public void run() {

        BdaFloat32 totWMag = (BdaFloat32) serversServerModel.findModelNode("ied1lDevice1/MMXU1.TotW.mag.f", Fc.MX);
        BdaQuality q = (BdaQuality) serversServerModel.findModelNode("ied1lDevice1/MMXU1.TotW.q", Fc.MX);
        BdaTimestamp t = (BdaTimestamp) serversServerModel.findModelNode("ied1lDevice1/MMXU1.TotW.t", Fc.MX);

        List<BasicDataAttribute> totWBdas = new ArrayList<>(3);
        totWBdas.add(totWMag);
        totWBdas.add(q);
        totWBdas.add(t);

        float totWMagVal = 0.0f;
        q.setValidity(BdaQuality.Validity.GOOD);

        // for (int i = 0; i < 500000; i++) {

        totWMagVal += 1.0;

        logger.info("setting totWmag to: " + totWMagVal);
        totWMag.setFloat(totWMagVal);
        t.setCurrentTime();

        if (q.getValidity() == Validity.GOOD) {
            q.setValidity(BdaQuality.Validity.INVALID);
        }
        else {
            q.setValidity(BdaQuality.Validity.GOOD);
        }

        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
        }
        serverSap.setValues(totWBdas);

        // // Run the garbage collector
        // runtime.gc();
        // // Calculate the used memory
        // long memory = runtime.totalMemory() - runtime.freeMemory();
        // System.out.println("Used memory is bytes: " + memory);
        // System.out.println("Used memory is megabytes: " + bytesToMegabytes(memory));

        // try {
        // Thread.sleep(2000);
        // } catch (InterruptedException e) {
        // }
        // }

    }

    private static final long MEGABYTE = 1024L * 1024L;

    public static long bytesToMegabytes(long bytes) {
        return bytes / MEGABYTE;
    }

    @Override
    public void newReport(Report report) {
        logger.info("got report!");
        numReports++;

        if (numReports == 1) {
            List<BdaReasonForInclusion> reasons = report.getReasonCodes();
            Assert.assertEquals(2, reasons.size());
            Assert.assertTrue(reasons.get(0).isGeneralInterrogation());
            Assert.assertFalse(reasons.get(0).isDataUpdate());
        }
        else if (numReports == 2) {
            List<BdaReasonForInclusion> reasons = report.getReasonCodes();
            Assert.assertEquals(1, reasons.size());
            Assert.assertFalse(reasons.get(0).isGeneralInterrogation());
            Assert.assertTrue(reasons.get(0).isDataChange());
        }
        else if (numReports >= 3) {
            List<BdaReasonForInclusion> reasons = report.getReasonCodes();
            Assert.assertEquals(2, reasons.size());
            Assert.assertTrue(reasons.get(0).isIntegrity());
            Assert.assertTrue(reasons.get(1).isIntegrity());
        }

        numSuccess++;

    }

    @Override
    public void associationClosed(IOException e) {
        logger.debug("Association closed!");
        numAssociationClosed++;
    }
}
