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

import java.lang.reflect.Array;
import java.util.Objects;

import org.eclipse.milo.opcua.sdk.server.events.FilterContext;
import org.eclipse.milo.opcua.sdk.server.events.OperatorContext;
import org.eclipse.milo.opcua.sdk.server.events.ValidationException;
import org.eclipse.milo.opcua.sdk.server.events.conversions.ImplicitConversions;
import org.eclipse.milo.opcua.sdk.server.model.nodes.objects.BaseEventTypeNode;
import org.eclipse.milo.opcua.stack.core.BuiltinDataType;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.structured.FilterOperand;
import org.eclipse.milo.opcua.stack.core.util.ArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

public class Equals implements Operator<Boolean> {

    Equals() {}

    @Override
    public void validate(FilterContext context, FilterOperand[] operands) throws ValidationException {
        if (operands.length < 2) {
            throw new ValidationException(StatusCodes.Bad_FilterOperandCountMismatch);
        }
    }

    @Nullable
    @Override
    public Boolean apply(
        OperatorContext context,
        BaseEventTypeNode eventNode,
        FilterOperand[] operands) throws UaException {

        validate(context, operands);

        FilterOperand op0 = operands[0];
        FilterOperand op1 = operands[1];

        Object value0 = context.resolve(op0, eventNode);
        Object value1 = context.resolve(op1, eventNode);

        if (value0 == null || value1 == null) {
            return null;
        }

        BuiltinDataType dt0 = getType(value0);
        BuiltinDataType dt1 = getType(value1);

        if (dt0 == null || dt1 == null) {
            throw new UaException(StatusCodes.Bad_UnexpectedError);
        }

        int p0 = ImplicitConversions.getPrecedence(dt0);
        int p1 = ImplicitConversions.getPrecedence(dt1);

        if (p0 == p1) {
            assert dt0 == dt1;

            return equals(value0, value1);
        } else if (p0 >= p1) {
            // convert value1 to type of value0 (dt0)
            Object converted1 = convert(value1, dt0);

            return equals(value0, converted1);
        } else {
            // convert value0 to type of value1 (dt1)
            Object converted0 = convert(value0, dt1);

            return equals(converted0, value1);
        }
    }

    @Nullable
    private static Object convert(@NotNull Object value, BuiltinDataType targetType) {
        if (value.getClass().isArray()) {
            return convertArray(value, targetType);
        } else {
            return ImplicitConversions.convert(value, targetType);
        }
    }

    private static Object convertArray(@NotNull Object array, BuiltinDataType targetType) {
        int[] dimensions = ArrayUtil.getDimensions(array);

        Object flattened = ArrayUtil.flatten(array);
        int length = Array.getLength(flattened);

        Object transformed = Array.newInstance(targetType.getBackingClass(), length);

        for (int i = 0; i < length; i++) {
            Object sourceValue = Array.get(flattened, i);
            Object targetValue = ImplicitConversions.convert(sourceValue, targetType);
            Array.set(transformed, i, targetValue);
        }

        return ArrayUtil.unflatten(transformed, dimensions);
    }

    private static boolean equals(Object lhs, Object rhs) {
        if (lhs == null || rhs == null) {
            return Objects.equals(lhs, rhs);
        } else if (lhs.getClass().isArray()) {
            return Objects.deepEquals(lhs, rhs);
        } else {
            return Objects.equals(lhs, rhs);
        }
    }

    private static BuiltinDataType getType(@NotNull Object o) {
        if (o.getClass().isArray()) {
            return BuiltinDataType.fromBackingClass(ArrayUtil.getType(o));
        } else {
            return BuiltinDataType.fromBackingClass(o.getClass());
        }
    }

}
