code: /*
   * Copyright (c) 2021 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.client.nodes;
  
  import java.util.List;
  import java.util.Objects;
  import java.util.Optional;
  import java.util.concurrent.CompletableFuture;
  import java.util.concurrent.ExecutionException;
  
  import org.eclipse.milo.opcua.sdk.client.AddressSpace;
  import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
  import org.eclipse.milo.opcua.sdk.client.methods.UaMethod;
  import org.eclipse.milo.opcua.sdk.core.nodes.ObjectNode;
  import org.eclipse.milo.opcua.sdk.core.nodes.ObjectNodeProperties;
  import org.eclipse.milo.opcua.stack.core.AttributeId;
  import org.eclipse.milo.opcua.stack.core.Identifiers;
  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.ByteString;
  import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
  import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
  import org.eclipse.milo.opcua.stack.core.types.builtin.NodeId;
  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.Variant;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UByte;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseDirection;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.BrowseResultMask;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.NamingRuleType;
  import org.eclipse.milo.opcua.stack.core.types.enumerated.NodeClass;
  import org.eclipse.milo.opcua.stack.core.types.structured.Argument;
  import org.eclipse.milo.opcua.stack.core.types.structured.BrowseDescription;
  import org.eclipse.milo.opcua.stack.core.types.structured.BrowseResult;
  import org.eclipse.milo.opcua.stack.core.types.structured.ReferenceDescription;
  import org.eclipse.milo.opcua.stack.core.util.FutureUtils;
  
  import static org.eclipse.milo.opcua.sdk.core.util.StreamUtil.opt2stream;
  import static org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.Unsigned.uint;
  import static org.eclipse.milo.opcua.stack.core.util.ConversionUtil.l;
  import static org.eclipse.milo.opcua.stack.core.util.FutureUtils.failedFuture;
  import static org.eclipse.milo.opcua.stack.core.util.FutureUtils.failedUaFuture;
  
  public class UaObjectNode extends UaNode implements ObjectNode {
  
      private UByte eventNotifier;
  
      public UaObjectNode(
          OpcUaClient client,
          NodeId nodeId,
          NodeClass nodeClass,
          QualifiedName browseName,
          LocalizedText displayName,
          LocalizedText description,
          UInteger writeMask,
          UInteger userWriteMask,
          UByte eventNotifier
      ) {
  
          super(client, nodeId, nodeClass, browseName, displayName, description, writeMask, userWriteMask);
  
          this.eventNotifier = eventNotifier;
      }
  
      /**
       * {@inheritDoc}
       * <p>
       * The returned attribute is the most recently seen value; it is not read live from the server.
       *
       * @see #readEventNotifier()
       */
      @Override
      public synchronized UByte getEventNotifier() {
          return eventNotifier;
      }
  
      /**
       * {@inheritDoc}
       * <p>
       * The attribute is only updated locally; it is not written to the server.
       *
       * @see #writeEventNotifier(UByte)
       */
      @Override
      public synchronized void setEventNotifier(UByte eventNotifier) {
          this.eventNotifier = eventNotifier;
      }
  
      /**
       * Read the EventNotifier attribute for this Node from the server and update the local
       * attribute if the operation succeeds.
       *
       * @return the {@link UByte} read from the server.
       * @throws UaException if a service- or operation-level error occurs.s
       */
      public UByte readEventNotifier() throws UaException {
          DataValue value = readAttribute(AttributeId.EventNotifier);
  
          StatusCode statusCode = value.getStatusCode();
  
          if (statusCode != null && statusCode.isBad()) {
              throw new UaException(statusCode, "read EventNotifier failed");
          } else {
              UByte eventNotifier = (UByte) value.getValue().getValue();
              setEventNotifier(eventNotifier);
              return eventNotifier;
          }
      }
  
      /**
       * Write a new EventNotifier attribute for this Node to the server and update the
       * local attribute if the operation succeeds.
       *
       * @param eventNotifier the {@link UByte} to write to the server.
       * @throws UaException if a service- or operation-level error occurs.
       */
      public void writeEventNotifier(UByte eventNotifier) throws UaException {
          DataValue value = DataValue.valueOnly(new Variant(eventNotifier));
          StatusCode statusCode = writeAttribute(AttributeId.EventNotifier, value);
  
          if (statusCode == null || statusCode.isGood()) {
              setEventNotifier(eventNotifier);
          } else {
              throw new UaException(statusCode, "write EventNotifier failed");
          }
      }
  
      /**
       * Call the method named {@code methodName} on this Object, if it exists, using {@code inputs}
       * as the input argument values.
       *
       * @param methodName the name of the method to call.
       * @param inputs     the input argument values.
       * @return the method's output argument values if the call was successful.
       * @throws UaException if an operation- or service-level error occurs or if a method named
       *                     {@code methodName} could not be found.
       */
      public Variant[] callMethod(String methodName, Variant[] inputs) throws UaException {
          return getMethod(methodName).call(inputs);
      }
  
      /**
       * Call the method named {@code methodName} on this Object, if it exists, using {@code inputs}
       * as the input argument values.
       *
       * @param methodName the name of the method to call.
       * @param inputs     the input argument values.
       * @return the method's output argument values if the call was successful.
       * @throws UaException if an operation- or service-level error occurs or if a method named
       *                     {@code methodName} could not be found.
       */
      public Variant[] callMethod(QualifiedName methodName, Variant[] inputs) throws UaException {
          return getMethod(methodName).call(inputs);
      }
  
      /**
       * Call the method named {@code methodName} on this Object, if it exists, using {@code inputs}
       * as the input argument values.
       * <p>
       * This call completes asynchronously.
       *
       * @param methodName the name of the method to call.
       * @param inputs     the input argument values.
       * @return a {@link CompletableFuture} that completes successfully with the method's output
       * argument values if the call was successful, or completes exceptionally if an operation- or
       * service-level error occurs or if a method named {@code methodName} could not be found.
       */
      public CompletableFuture<Variant[]> callMethodAsync(String methodName, Variant[] inputs) {
          return getMethodAsync(methodName).thenCompose(m -> m.callAsync(inputs));
      }
  
      /**
       * Call the method named {@code methodName} on this Object, if it exists, using {@code inputs}
       * as the input argument values.
       * <p>
       * This call completes asynchronously.
       *
       * @param methodName the name of the method to call.
       * @param inputs     the input argument values.
       * @return a {@link CompletableFuture} that completes successfully with the method's output
       * argument values if the call was successful, or completes exceptionally if an operation- or
       * service-level error occurs or if a method named {@code methodName} could not be found.
       */
      public CompletableFuture<Variant[]> callMethodAsync(QualifiedName methodName, Variant[] inputs) {
          return getMethodAsync(methodName).thenCompose(m -> m.callAsync(inputs));
      }
  
      /**
       * Get the method named {@code methodName} on this Object, if it exists.
       *
       * @param methodName the name of the method.
       * @return a {@link UaMethod} for the method named {@code methodName}.
       * @throws UaException if an operation- or service-level error occurs or a method named
       *                     {@code methodName} could not be found.
       */
      public UaMethod getMethod(String methodName) throws UaException {
          UShort namespaceIndex = getNodeId().getNamespaceIndex();
  
          return getMethod(new QualifiedName(namespaceIndex, methodName));
      }
  
      /**
       * Get the method named {@code methodName} on this Object, if it exists.
       *
       * @param methodName the name of the method.
       * @return a {@link UaMethod} for the method named {@code methodName}.
       * @throws UaException if an operation- or service-level error occurs or a method named
       *                     {@code methodName} could not be found.
       */
      public UaMethod getMethod(QualifiedName methodName) throws UaException {
          try {
              return getMethodAsync(methodName).get();
          } catch (ExecutionException | InterruptedException e) {
              throw UaException.extract(e)
                  .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
          }
      }
  
      /**
       * Get the method named {@code methodName} on this Object, if it exists.
       * <p>
       * This call completes asynchronously.
       *
       * @param methodName the name of the method.
       * @return a {@link CompletableFuture} that completes successfully with a {@link UaMethod} or
       * completes exceptionally if an operation- or service-level error occurs or if a method named
       * {@code methodName} could not be found.
       */
      public CompletableFuture<UaMethod> getMethodAsync(String methodName) {
          UShort namespaceIndex = getNodeId().getNamespaceIndex();
  
          return getMethodAsync(new QualifiedName(namespaceIndex, methodName));
      }
  
      /**
       * Get the method named {@code methodName} on this Object, if it exists.
       * <p>
       * This call completes asynchronously.
       *
       * @param methodName the name of the method.
       * @return a {@link CompletableFuture} that completes successfully with a {@link UaMethod} or
       * completes exceptionally if an operation- or service-level error occurs or if a method named
       * {@code methodName} could not be found.
       */
      public CompletableFuture<UaMethod> getMethodAsync(QualifiedName methodName) {
          UInteger nodeClassMask = uint(NodeClass.Method.getValue());
          UInteger resultMask = uint(BrowseResultMask.All.getValue());
  
          CompletableFuture<BrowseResult> future = client.browse(
              new BrowseDescription(
                  getNodeId(),
                  BrowseDirection.Forward,
                  Identifiers.HasComponent,
                  true,
                  nodeClassMask,
                  resultMask
              )
          );
  
          return future.thenCompose(result -> {
              StatusCode statusCode = result.getStatusCode();
  
              if (statusCode != null && statusCode.isGood()) {
                  Optional<ExpandedNodeId> methodNodeId = l(result.getReferences())
                      .stream()
                      .filter(rd -> Objects.equals(methodName, rd.getBrowseName()))
                      .findFirst()
                      .map(ReferenceDescription::getNodeId);
  
                  return methodNodeId
                      .map(xni -> {
                          AddressSpace addressSpace = client.getAddressSpace();
  
                          return addressSpace.toNodeIdAsync(xni).thenCompose(
                              nodeId ->
                                  addressSpace.getNodeAsync(nodeId).thenCompose(node -> {
                                      UaMethodNode methodNode = (UaMethodNode) node;
  
                                      CompletableFuture<Argument[]> inputArguments =
                                          methodNode.readInputArgumentsAsync()
                                              .exceptionally(t -> new Argument[0]);
  
                                      CompletableFuture<Argument[]> outputArguments =
                                          methodNode.readOutputArgumentsAsync()
                                              .exceptionally(t -> new Argument[0]);
  
                                      return inputArguments.thenCombine(
                                          outputArguments,
                                          (in, out) -> new UaMethod(client, this, methodNode, in, out)
                                      );
                                  })
                          );
                      })
                      .orElse(failedFuture(new UaException(StatusCodes.Bad_NotFound, "method not found: " + methodName)));
              } else {
                  return failedFuture(new UaException(statusCode, "browsing for MethodNodes failed"));
              }
          });
      }
  
      /**
       * Get the {@link UaObjectNode} referenced by a HasComponent reference type with a browse name
       * of {@code name}, if it exists.
       *
       * @param name the browse name of the {@link UaObjectNode} component.
       * @return the {@link UaObjectNode} component, if it exists.
       * @throws UaException if the node could not be found or an operation- or service-level error
       *                     occurs.
       */
      public UaObjectNode getObjectComponent(String name) throws UaException {
          try {
              return getObjectComponentAsync(name).get();
          } catch (InterruptedException | ExecutionException e) {
              throw UaException.extract(e)
                  .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
          }
      }
  
      /**
       * Get the {@link UaObjectNode} referenced by a HasComponent reference type with a browse name
       * of {@code name}, if it exists.
       *
       * @param namespaceUri the namespace URI the browse name belongs to.
       * @param name         the browse name of the {@link UaObjectNode} component.
       * @return the {@link UaObjectNode} component, if it exists.
       * @throws UaException if the node could not be found or an operation- or service-level error
       *                     occurs.
       */
      public UaObjectNode getObjectComponent(String namespaceUri, String name) throws UaException {
          try {
              return getObjectComponentAsync(namespaceUri, name).get();
          } catch (InterruptedException | ExecutionException e) {
              throw UaException.extract(e)
                  .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
          }
      }
  
      /**
       * Get the {@link UaObjectNode} referenced by a HasComponent reference type with a browse name
       * of {@code name}, if it exists.
       *
       * @param browseName the browse name of the {@link UaObjectNode} component.
       * @return the {@link UaObjectNode} component, if it exists.
       * @throws UaException if the node could not be found or an operation- or service-level error
       *                     occurs.
       */
      public UaObjectNode getObjectComponent(QualifiedName browseName) throws UaException {
          try {
              return getObjectComponentAsync(browseName).get();
          } catch (InterruptedException | ExecutionException e) {
              throw UaException.extract(e)
                  .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
          }
      }
  
      public CompletableFuture<? extends UaObjectNode> getObjectComponentAsync(String name) {
          return getObjectComponentAsync(new QualifiedName(getNodeId().getNamespaceIndex(), name));
      }
  
      public CompletableFuture<? extends UaObjectNode> getObjectComponentAsync(String namespaceUri, String name) {
          UShort namespaceIndex = client.getNamespaceTable().getIndex(namespaceUri);
  
          if (namespaceIndex != null) {
              return getObjectComponentAsync(new QualifiedName(namespaceIndex, name));
          } else {
              return failedUaFuture(StatusCodes.Bad_NotFound);
          }
      }
  
      public CompletableFuture<? extends UaObjectNode> getObjectComponentAsync(QualifiedName browseName) {
          return getComponentAsync(browseName, NodeClass.Object).thenApply(UaObjectNode.class::cast);
      }
  
      /**
       * Get the {@link UaVariableNode} referenced by a HasComponent reference type with a browse name
       * of {@code name}, if it exists.
       *
       * @param name the browse name of the {@link UaVariableNode} component.
       * @return the {@link UaVariableNode} component, if it exists.
       * @throws UaException if the node could not be found or an operation- or service-level error
       *                     occurs.
       */
      public UaVariableNode getVariableComponent(String name) throws UaException {
          try {
              return getVariableComponentAsync(name).get();
          } catch (InterruptedException | ExecutionException e) {
              throw UaException.extract(e)
                  .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
          }
      }
  
      /**
       * Get the {@link UaVariableNode} referenced by a HasComponent reference type with a browse name
       * of {@code name}, if it exists.
       *
       * @param namespaceUri the namespace URI the browse name belongs to.
       * @param name         the browse name of the {@link UaVariableNode} component.
       * @return the {@link UaVariableNode} component, if it exists.
       * @throws UaException if the node could not be found or an operation- or service-level error
       *                     occurs.
       */
      public UaVariableNode getVariableComponent(String namespaceUri, String name) throws UaException {
          try {
              return getVariableComponentAsync(namespaceUri, name).get();
          } catch (InterruptedException | ExecutionException e) {
              throw UaException.extract(e)
                  .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
          }
      }
  
      /**
       * Get the {@link UaVariableNode} referenced by a HasComponent reference type with a browse name
       * of {@code name}, if it exists.
       *
       * @param browseName the browse name of the {@link UaVariableNode} component.
       * @return the {@link UaVariableNode} component, if it exists.
       * @throws UaException if the node could not be found or an operation- or service-level error
       *                     occurs.
       */
      public UaVariableNode getVariableComponent(QualifiedName browseName) throws UaException {
          try {
              return getVariableComponentAsync(browseName).get();
          } catch (InterruptedException | ExecutionException e) {
              throw UaException.extract(e)
                  .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
          }
      }
  
      public CompletableFuture<? extends UaVariableNode> getVariableComponentAsync(String name) {
          return getVariableComponentAsync(new QualifiedName(getNodeId().getNamespaceIndex(), name));
      }
  
      public CompletableFuture<? extends UaVariableNode> getVariableComponentAsync(String namespaceUri, String name) {
          UShort namespaceIndex = client.getNamespaceTable().getIndex(namespaceUri);
  
          if (namespaceIndex != null) {
              return getVariableComponentAsync(new QualifiedName(namespaceIndex, name));
          } else {
              return failedUaFuture(StatusCodes.Bad_NotFound);
          }
      }
  
      public CompletableFuture<? extends UaVariableNode> getVariableComponentAsync(QualifiedName browseName) {
          return getComponentAsync(browseName, NodeClass.Variable).thenApply(UaVariableNode.class::cast);
      }
  
      public UaObjectTypeNode getTypeDefinition() throws UaException {
          try {
              return getTypeDefinitionAsync().get();
          } catch (InterruptedException | ExecutionException e) {
              throw UaException.extract(e)
                  .orElse(new UaException(StatusCodes.Bad_UnexpectedError, e));
          }
      }
  
      public CompletableFuture<? extends UaObjectTypeNode> getTypeDefinitionAsync() {
          UInteger nodeClassMask = uint(NodeClass.ObjectType.getValue());
          UInteger resultMask = uint(BrowseResultMask.All.getValue());
  
          CompletableFuture<BrowseResult> future = client.browse(
              new BrowseDescription(
                  getNodeId(),
                  BrowseDirection.Forward,
                  Identifiers.HasTypeDefinition,
                  false,
                  nodeClassMask,
                  resultMask
              )
          );
  
          return future.thenCompose(result -> {
              List<ReferenceDescription> references = l(result.getReferences());
  
              Optional<CompletableFuture<UaObjectTypeNode>> node = references.stream()
                  .flatMap(r -> {
                      Optional<CompletableFuture<UaObjectTypeNode>> opt = r.getNodeId()
                          .toNodeId(client.getNamespaceTable())
                          .map(
                              id ->
                                  client.getAddressSpace().getNodeAsync(id)
                                      .thenApply(n -> (UaObjectTypeNode) n)
                          );
  
                      return opt2stream(opt);
                  })
                  .findFirst();
  
              return node.orElse(FutureUtils.failedUaFuture(StatusCodes.Bad_NotFound));
          });
      }
  
      /**
       * Get the value of the {@link ObjectNodeProperties#NodeVersion} Property, if it exists.
       *
       * @return the value of the NodeVersion Property, if it exists.
       * @see ObjectNodeProperties
       */
      public CompletableFuture<String> getNodeVersion() {
          return getProperty(ObjectNodeProperties.NodeVersion);
      }
  
      /**
       * Get the value of the {@link ObjectNodeProperties#Icon} Property, if it exists.
       *
       * @return the value of the Icon Property, if it exists.
       * @see ObjectNodeProperties
       */
      public CompletableFuture<ByteString> getIcon() {
          return getProperty(ObjectNodeProperties.Icon);
      }
  
      /**
       * Get the value of the {@link ObjectNodeProperties#NamingRule} Property, if it exists.
       *
       * @return the value of the NamingRule Property, if it exists.
       * @see ObjectNodeProperties
       */
      public CompletableFuture<NamingRuleType> getNamingRuleAsync() {
          return getProperty(ObjectNodeProperties.NamingRule);
      }
  
      /**
       * Set the value of the {@link ObjectNodeProperties#NodeVersion} Property, if it exists.
       *
       * @param nodeVersion the value to set.
       * @return a {@link CompletableFuture} that completes with the {@link StatusCode} of the write operation.
       * @see ObjectNodeProperties
       */
      public CompletableFuture<StatusCode> setNodeVersion(String nodeVersion) {
          return setProperty(ObjectNodeProperties.NodeVersion, nodeVersion);
      }
  
      /**
       * Set the value of the {@link ObjectNodeProperties#Icon} Property, if it exists.
       *
       * @param icon the value to set.
       * @return a {@link CompletableFuture} that completes with the {@link StatusCode} of the write operation.
       * @see ObjectNodeProperties
       */
      public CompletableFuture<StatusCode> setIcon(ByteString icon) {
          return setProperty(ObjectNodeProperties.Icon, icon);
      }
  
      /**
       * Set the value of the {@link ObjectNodeProperties#NamingRule} Property, if it exists.
       *
       * @param namingRule the value to set.
       * @return a {@link CompletableFuture} that completes with the {@link StatusCode} of the write operation.
       * @see ObjectNodeProperties
       */
      public CompletableFuture<StatusCode> setNamingRuleAsync(NamingRuleType namingRule) {
          return setProperty(ObjectNodeProperties.NamingRule, namingRule);
      }
  
      @Override
      protected DataValue getAttributeValue(AttributeId attributeId) {
          switch (attributeId) {
              case EventNotifier:
                  return DataValue.valueOnly(new Variant(getEventNotifier()));
              default:
                  return super.getAttributeValue(attributeId);
          }
      }
  
      @Override
      protected void setAttributeValue(AttributeId attributeId, DataValue value) {
          switch (attributeId) {
              case EventNotifier: {
                  setEventNotifier((UByte) value.getValue().getValue());
                  break;
              }
              default: {
                  super.setAttributeValue(attributeId, value);
              }
          }
      }
  
  }
  