/*
 * Copyright 2011-17 Fraunhofer ISE, energy & meteo Systems GmbH and other contributors
 *
 * 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.openmuc.openiec61850;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.openmuc.jasn1.ber.types.BerBitString;
import org.openmuc.openiec61850.internal.mms.asn1.AccessResult;
import org.openmuc.openiec61850.internal.mms.asn1.Data;
import org.openmuc.openiec61850.internal.mms.asn1.Identifier;
import org.openmuc.openiec61850.internal.mms.asn1.InformationReport;
import org.openmuc.openiec61850.internal.mms.asn1.MMSpdu;
import org.openmuc.openiec61850.internal.mms.asn1.ObjectName;
import org.openmuc.openiec61850.internal.mms.asn1.UnconfirmedPDU;
import org.openmuc.openiec61850.internal.mms.asn1.UnconfirmedService;
import org.openmuc.openiec61850.internal.mms.asn1.VariableAccessSpecification;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Urcb extends Rcb {

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

    ServerAssociation reserved = null;
    boolean enabled = false;
    private Timer integrityTimer;
    // private ScheduledFuture<?> integrityFuture = null;
    private ScheduledFuture<?> bufTmFuture = null;
    final HashMap<FcModelNode, BdaReasonForInclusion> membersToBeReported = new LinkedHashMap<>();

    public Urcb(ObjectReference objectReference, List<FcModelNode> children) {
        super(objectReference, Fc.RP, children);
    }

    /**
     * Reserve URCB - The attribute Resv (if set to TRUE) shall indicate that the URCB is currently exclusively reserved
     * for the client that has set the value to TRUE. Other clients shall not be allowed to set any attribute of that
     * URCB.
     * 
     * @return the Resv child
     */
    public BdaBoolean getResv() {
        return (BdaBoolean) children.get("Resv");
    }

    void enable() {

        for (FcModelNode dataSetMember : dataSet) {
            for (BasicDataAttribute bda : dataSetMember.getBasicDataAttributes()) {
                if (bda.dchg) {
                    if (getTrgOps().isDataChange()) {
                        synchronized (bda.chgRcbs) {
                            bda.chgRcbs.add(this);
                        }
                    }
                }
                else if (bda.qchg) {
                    if (getTrgOps().isQualityChange()) {
                        synchronized (bda.chgRcbs) {
                            bda.chgRcbs.add(this);
                        }
                    }
                }
                if (bda.dupd) {
                    if (getTrgOps().isDataUpdate()) {
                        synchronized (bda.dupdRcbs) {
                            bda.dupdRcbs.add(this);
                        }
                    }
                }
            }
        }

        if (getTrgOps().isIntegrity() && !(getIntgPd().getValue() < 10l)) {
            integrityTimer = new Timer();

            integrityTimer.schedule(new TimerTask() {
                // integrityFuture = reserved.executor.scheduleAtFixedRate(new Runnable() {
                @Override
                public void run() {
                    synchronized (Urcb.this) {
                        if (!enabled) {
                            return;
                        }
                        reserved.sendAnMmsPdu(getMmsReport(true, false));
                    }
                }
                // }, getIntgPd().getValue(), getIntgPd().getValue(), TimeUnit.MILLISECONDS);
            }, getIntgPd().getValue(), getIntgPd().getValue());

        }

        enabled = true;

    }

    void disable() {

        for (FcModelNode dataSetMember : dataSet) {
            for (BasicDataAttribute bda : dataSetMember.getBasicDataAttributes()) {
                if (bda.dchg) {
                    if (getTrgOps().isDataChange()) {
                        synchronized (bda.chgRcbs) {
                            bda.chgRcbs.remove(this);
                        }
                    }
                }
                else if (bda.qchg) {
                    if (getTrgOps().isQualityChange()) {
                        synchronized (bda.chgRcbs) {
                            bda.chgRcbs.remove(this);
                        }
                    }
                }
                if (bda.dupd) {
                    if (getTrgOps().isDataUpdate()) {
                        synchronized (bda.dupdRcbs) {
                            bda.dupdRcbs.remove(this);
                        }
                    }
                }
            }
        }

        // if (integrityFuture != null) {
        // integrityFuture.cancel(false);
        // }
        if (integrityTimer != null) {
            integrityTimer.cancel();
        }

        enabled = false;

    }

    void generalInterrogation() {
        reserved.executor.execute(new Runnable() {
            @Override
            public void run() {
                synchronized (Urcb.this) {
                    if (!enabled) {
                        return;
                    }
                    reserved.sendAnMmsPdu(getMmsReport(false, true));
                }
            }
        });
    }

    private MMSpdu getMmsReport(boolean integrity, boolean gi) {

        InformationReport.ListOfAccessResult listOfAccessResult = new InformationReport.ListOfAccessResult();

        List<AccessResult> accessResults = listOfAccessResult.getAccessResult();

        AccessResult accessResult = new AccessResult();
        accessResult.setSuccess(getRptId().getMmsDataObj());
        accessResults.add(accessResult);

        accessResult = new AccessResult();
        accessResult.setSuccess(getOptFlds().getMmsDataObj());
        accessResults.add(accessResult);

        if (getOptFlds().isSequenceNumber()) {
            accessResult = new AccessResult();
            accessResult.setSuccess(getSqNum().getMmsDataObj());
            accessResults.add(accessResult);
        }
        getSqNum().setValue((short) (getSqNum().getValue() + 1));

        if (getOptFlds().isReportTimestamp()) {
            BdaEntryTime entryTime = new BdaEntryTime(null, null, null, false, false);
            entryTime.setTimestamp(System.currentTimeMillis());

            accessResult = new AccessResult();
            accessResult.setSuccess(entryTime.getMmsDataObj());
            accessResults.add(accessResult);
        }

        if (getOptFlds().isDataSetName()) {
            accessResult = new AccessResult();
            accessResult.setSuccess(getDatSet().getMmsDataObj());
            accessResults.add(accessResult);
        }

        if (getOptFlds().isConfigRevision()) {
            accessResult = new AccessResult();
            accessResult.setSuccess(getConfRev().getMmsDataObj());
            accessResults.add(accessResult);
        }

        // segmentation not supported

        List<FcModelNode> dataSetMembers = dataSet.getMembers();
        int dataSetSize = dataSetMembers.size();

        // inclusion bitstring
        byte[] inclusionStringArray = new byte[(dataSetSize - 1) / 8 + 1];

        if (integrity || gi) {

            for (int i = 0; i < dataSetSize; i++) {
                inclusionStringArray[i / 8] |= 1 << (7 - i % 8);
            }
            BerBitString inclusionString = new BerBitString(inclusionStringArray, dataSetSize);

            Data data = new Data();
            data.setBitString(inclusionString);
            accessResult = new AccessResult();
            accessResult.setSuccess(data);
            accessResults.add(accessResult);

            // data reference sending not supported for now

            for (FcModelNode dataSetMember : dataSetMembers) {
                accessResult = new AccessResult();
                accessResult.setSuccess(dataSetMember.getMmsDataObj());
                accessResults.add(accessResult);
            }

            BdaReasonForInclusion reasonForInclusion = new BdaReasonForInclusion(null);
            if (integrity) {
                reasonForInclusion.setIntegrity(true);
            }
            else {
                reasonForInclusion.setGeneralInterrogation(true);
            }

            if (getOptFlds().isReasonForInclusion()) {
                for (int i = 0; i < dataSetMembers.size(); i++) {
                    accessResult = new AccessResult();
                    accessResult.setSuccess(reasonForInclusion.getMmsDataObj());
                    accessResults.add(accessResult);
                }
            }

        }
        else {

            int index = 0;
            for (FcModelNode dataSetMember : dataSet) {
                if (membersToBeReported.get(dataSetMember) != null) {
                    inclusionStringArray[index / 8] |= 1 << (7 - index % 8);
                }
                index++;
            }
            BerBitString inclusionString = new BerBitString(inclusionStringArray, dataSetSize);

            Data data = new Data();
            data.setBitString(inclusionString);
            accessResult = new AccessResult();
            accessResult.setSuccess(data);
            accessResults.add(accessResult);

            // data reference sending not supported for now

            for (FcModelNode dataSetMember : dataSetMembers) {
                if (membersToBeReported.get(dataSetMember) != null) {
                    accessResult = new AccessResult();
                    accessResult.setSuccess(dataSetMember.getMmsDataObj());
                    accessResults.add(accessResult);
                }
            }

            if (getOptFlds().isReasonForInclusion()) {
                for (FcModelNode dataSetMember : dataSetMembers) {
                    BdaReasonForInclusion reasonForInclusion = membersToBeReported.get(dataSetMember);
                    if (reasonForInclusion != null) {
                        accessResult = new AccessResult();
                        accessResult.setSuccess(reasonForInclusion.getMmsDataObj());
                        accessResults.add(accessResult);
                    }
                }
            }

            membersToBeReported.clear();
            bufTmFuture = null;

        }

        ObjectName objectName = new ObjectName();
        objectName.setVmdSpecific(new Identifier("RPT".getBytes()));

        VariableAccessSpecification varAccSpec = new VariableAccessSpecification();
        varAccSpec.setVariableListName(objectName);
        // null,
        // new ObjectName(new Identifier("RPT".getBytes()), null, null));

        InformationReport infoReport = new InformationReport();
        infoReport.setVariableAccessSpecification(varAccSpec);
        infoReport.setListOfAccessResult(listOfAccessResult);
        // varAccSpec,
        // new InformationReport.ListOfAccessResult(listOfAccessResult));

        UnconfirmedService unconfirmedService = new UnconfirmedService();
        unconfirmedService.setInformationReport(infoReport);

        UnconfirmedPDU unconfirmedPDU = new UnconfirmedPDU();
        unconfirmedPDU.setService(unconfirmedService);

        MMSpdu mmsPdu = new MMSpdu();
        mmsPdu.setUnconfirmedPDU(unconfirmedPDU);

        return mmsPdu;
    }

    @Override
    public FcDataObject copy() {
        List<FcModelNode> childCopies = new ArrayList<>(children.size());
        for (ModelNode childNode : children.values()) {
            childCopies.add((FcModelNode) childNode.copy());
        }
        Urcb urcb = new Urcb(objectReference, childCopies);
        urcb.dataSet = dataSet;
        return urcb;
    }

    void report(BasicDataAttribute bda, boolean dchg, boolean qchg, boolean dupd) {

        synchronized (this) {

            if (!enabled) {
                return;
            }

            FcModelNode memberFound = null;
            FcModelNode fcModelNode = bda;
            while (memberFound == null) {
                for (FcModelNode member : dataSet) {
                    if (member == fcModelNode) {
                        memberFound = fcModelNode;
                        break;
                    }
                }
                if (memberFound != null) {
                    break;
                }
                if (!(fcModelNode.parent instanceof FcModelNode)) {
                    logger.error(
                            "Unable to report Basic Data Attribute because it is not part of the referenced data set: "
                                    + bda.getReference());
                    return;
                }
                fcModelNode = (FcModelNode) fcModelNode.parent;
            }

            BdaReasonForInclusion reasonForInclusion = membersToBeReported.get(fcModelNode);
            if (reasonForInclusion == null) {
                reasonForInclusion = new BdaReasonForInclusion(null);
                membersToBeReported.put(fcModelNode, reasonForInclusion);
            }

            if (dchg) {
                reasonForInclusion.setDataChange(true);
            }
            if (dupd) {
                reasonForInclusion.setDataUpdate(true);
            }
            else if (qchg) {
                reasonForInclusion.setQualityChange(true);
            }

            // if bufTmFuture is not null then it is already scheduled and will send the combined report
            if (bufTmFuture == null) {
                bufTmFuture = reserved.executor.schedule(new Runnable() {
                    @Override
                    public void run() {
                        synchronized (Urcb.this) {
                            if (!enabled) {
                                return;
                            }
                            reserved.sendAnMmsPdu(getMmsReport(false, false));
                        }
                    }
                }, getBufTm().getValue(), TimeUnit.MILLISECONDS);
            }

        }

    }
}
