/*
 * Copyright (c) 2019 the Eclipse Milo Authors
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */

package org.eclipse.milo.opcua.sdk.server.util;

import java.util.Objects;

import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.enumerated.DataChangeTrigger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.DeadbandType;
import org.eclipse.milo.opcua.stack.core.types.structured.DataChangeFilter;

public class DataChangeMonitoringFilter {

    public static boolean filter(DataValue lastValue, DataValue currentValue, DataChangeFilter filter) {
        return triggerFilter(lastValue, currentValue, filter) && deadbandFilter(lastValue, currentValue, filter);
    }

    private static boolean triggerFilter(DataValue lastValue, DataValue currentValue, DataChangeFilter filter) {
        if (lastValue == null) return true;

        DataChangeTrigger trigger = filter.getTrigger();

        if (trigger == DataChangeTrigger.Status) {
            return statusChanged(lastValue, currentValue);
        } else if (trigger == DataChangeTrigger.StatusValue) {
            return valueChanged(lastValue, currentValue) || statusChanged(lastValue, currentValue);
        } else {
            // DataChangeTrigger.StatusValueTimestamp
            return timestampChanged(lastValue, currentValue) ||
                valueChanged(lastValue, currentValue) ||
                statusChanged(lastValue, currentValue);
        }
    }

    private static boolean deadbandFilter(DataValue lastValue, DataValue currentValue, DataChangeFilter filter) {
        if (lastValue == null) return true;

        int index = filter.getDeadbandType().intValue();
        if (index < 0 || index >= DeadbandType.values().length) return true;
        DeadbandType deadbandType = DeadbandType.values()[index];

        if (deadbandType != DeadbandType.Absolute) return true;

        Object last = lastValue.getValue().getValue();
        Object current = currentValue.getValue().getValue();

        if (last == null || current == null) {
            return true;
        } else if (last.getClass().isArray() && current.getClass().isArray()) {
            return compareArrayDeadband(last, current, filter.getDeadbandValue());
        } else {
            return compareScalarDeadband(last, current, filter.getDeadbandValue());
        }
    }

    private static boolean compareArrayDeadband(Object last, Object current, double deadband) {
        Object[] lastA = (Object[]) last;
        Object[] currentA = (Object[]) current;

        if (lastA.length != currentA.length) {
            return true;
        } else {
            boolean exceeds = false;

            for (int i = 0; i < lastA.length; i++) {
                exceeds = exceeds || exceedsDeadband(lastA[i], currentA[i], deadband);
            }

            return exceeds;
        }
    }

    private static boolean compareScalarDeadband(Object last, Object current, double deadband) {
        return exceedsDeadband(last, current, deadband);
    }

    private static boolean exceedsDeadband(Object last, Object current, double deadband) {
        try {
            double lastD = ((Number) last).doubleValue();
            double currentD = ((Number) current).doubleValue();

            return Math.abs(lastD - currentD) > deadband;
        } catch (Throwable t) {
            return true;
        }
    }

    private static boolean statusChanged(DataValue lastValue, DataValue currentValue) {
        return !Objects.equals(lastValue.getStatusCode(), currentValue.getStatusCode());
    }

    private static boolean valueChanged(DataValue lastValue, DataValue currentValue) {
        return !Objects.equals(lastValue.getValue(), currentValue.getValue());
    }

    private static boolean timestampChanged(DataValue lastValue, DataValue currentValue) {
        return !Objects.equals(lastValue.getSourceTime(), currentValue.getSourceTime());
    }

}
