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

import java.util.Optional;

import com.google.common.collect.ImmutableList;
import org.eclipse.milo.opcua.sdk.core.NumericRange;
import org.eclipse.milo.opcua.sdk.core.Reference;
import org.eclipse.milo.opcua.sdk.core.nodes.Node;
import org.eclipse.milo.opcua.sdk.server.nodes.delegates.AttributeDelegate;
import org.eclipse.milo.opcua.sdk.server.util.AttributeReader;
import org.eclipse.milo.opcua.sdk.server.util.AttributeWriter;
import org.eclipse.milo.opcua.stack.core.AttributeId;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
import org.eclipse.milo.opcua.stack.core.types.builtin.QualifiedName;
import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
import org.eclipse.milo.opcua.stack.core.types.enumerated.TimestampsToReturn;
import org.jetbrains.annotations.Nullable;

public interface UaServerNode extends Node {

    /**
     * @return the {@link UaNodeContext} for this {@link UaServerNode}.
     */
    UaNodeContext getNodeContext();

    /**
     * Add a {@link Reference} to this node.
     *
     * @param reference the {@link Reference} to add.
     */
    void addReference(Reference reference);

    /**
     * Remove a {@link Reference} from this node.
     *
     * @param reference to remove.
     */
    void removeReference(Reference reference);

    /**
     * @return this node's {@link Reference}s.
     */
    ImmutableList<Reference> getReferences();

    /**
     * Read the specified attribute.
     * <p>
     * If the attribute is not specified on this node, a value with status {@link StatusCodes#Bad_AttributeIdInvalid}
     * will be returned.
     *
     * @param attribute the id of the attribute to read.
     * @return the value of the specified attribute.
     */
    default DataValue readAttribute(AttributeContext context, UInteger attribute) {
        return readAttribute(context, attribute, null, null, null);
    }

    /**
     * Read the specified attribute.
     * <p>
     * If the attribute is not specified on this node, a value with status {@link StatusCodes#Bad_AttributeIdInvalid}
     * will be returned.
     *
     * @param attribute    the id of the attribute to read.
     * @param timestamps   the {@link TimestampsToReturn}.
     * @param indexRange   the index range to read. Must be a parseable by {@link NumericRange}.
     * @param dataEncoding the requested data encoding.
     * @return the value of the specified attribute.
     */
    default DataValue readAttribute(
        AttributeContext context,
        UInteger attribute,
        @Nullable TimestampsToReturn timestamps,
        @Nullable String indexRange,
        @Nullable QualifiedName dataEncoding) {

        return AttributeId.from(attribute)
            .map(attributeId -> readAttribute(context, attributeId, timestamps, indexRange, dataEncoding))
            .orElse(new DataValue(StatusCodes.Bad_AttributeIdInvalid));
    }

    /**
     * Read the specified attribute, applying {@code timestamps} and {@code indexRange} if specified.
     * <p>
     * If the attribute is not specified on this node, a value with status {@link StatusCodes#Bad_AttributeIdInvalid}
     * will be returned.
     *
     * @param attributeId  the id of the attribute to read.
     * @param timestamps   the {@link TimestampsToReturn}.
     * @param indexRange   the index range to read. Must be a parseable by {@link NumericRange}.
     * @param dataEncoding the requested data encoding.
     * @return the value of the specified attribute.
     */
    default DataValue readAttribute(
        AttributeContext context,
        AttributeId attributeId,
        @Nullable TimestampsToReturn timestamps,
        @Nullable String indexRange,
        @Nullable QualifiedName dataEncoding) {

        return AttributeReader.readAttribute(context, this, attributeId, timestamps, indexRange, dataEncoding);
    }

    /**
     * Write to the specified attribute.
     *
     * @param context    the {@link AttributeContext}.
     * @param attribute  the id of the attribute to write.
     * @param value      the {@link DataValue} write.
     * @param indexRange the index range to write. Must be a parseable by {@link NumericRange}.
     * @throws UaException if writing to the attribute fails.
     */
    default void writeAttribute(
        AttributeContext context,
        UInteger attribute,
        DataValue value,
        String indexRange) throws UaException {

        Optional<AttributeId> attributeId = AttributeId.from(attribute);

        if (attributeId.isPresent()) {
            writeAttribute(context, attributeId.get(), value, indexRange);
        } else {
            throw new UaException(StatusCodes.Bad_AttributeIdInvalid);
        }
    }

    /**
     * Write to the specified attribute.
     *
     * @param context     the {@link AttributeContext}.
     * @param attributeId the {@link AttributeId} of the attribute to write.
     * @param value       the {@link DataValue} write.
     * @param indexRange  the index range to write. Must be a parseable by {@link NumericRange}.
     * @throws UaException if writing to the attribute fails.
     */
    default void writeAttribute(
        AttributeContext context,
        AttributeId attributeId,
        DataValue value,
        String indexRange) throws UaException {

        AttributeWriter.writeAttribute(context, this, attributeId, value, indexRange);
    }

    /**
     * Get an attribute of this node, taking the {@link AttributeContext} into account and respecting any
     * {@link AttributeDelegate} this node may have.
     *
     * @param context     the {@link AttributeContext} to get the attribute in.
     * @param attributeId the {@link AttributeId} to get.
     * @return a {@link DataValue} containing the attribute value or a {@link StatusCode} describing any failure.
     */
    DataValue getAttribute(AttributeContext context, AttributeId attributeId);

    /**
     * Set an attribute of this node, taking the {@link AttributeContext} into account and respecting any
     * {@link AttributeDelegate} this node may have.
     *
     * @param context     the {@link AttributeContext} to set the attribute in.
     * @param attributeId the {@link AttributeId} to set.
     * @param value       the new {@link DataValue} to set for the attribute.
     * @throws UaException if setting the attribute failed for any reason.
     */
    void setAttribute(AttributeContext context, AttributeId attributeId, DataValue value) throws UaException;

}
