code: /*
   * 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.binaryschema;
  
  import java.math.BigInteger;
  import java.util.HashMap;
  import java.util.LinkedHashMap;
  import java.util.Map;
  import java.util.UUID;
  import java.util.function.BiConsumer;
  import java.util.function.Function;
  
  import com.google.common.collect.ImmutableMap;
  import com.google.common.collect.Iterators;
  import com.google.common.collect.PeekingIterator;
  import org.eclipse.milo.opcua.stack.core.BuiltinDataType;
  import org.eclipse.milo.opcua.stack.core.StatusCodes;
  import org.eclipse.milo.opcua.stack.core.UaSerializationException;
  import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamDecoder;
  import org.eclipse.milo.opcua.stack.core.serialization.OpcUaBinaryStreamEncoder;
  import org.eclipse.milo.opcua.stack.core.serialization.SerializationContext;
  import org.eclipse.milo.opcua.stack.core.serialization.codecs.OpcUaBinaryDataTypeCodec;
  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.DateTime;
  import org.eclipse.milo.opcua.stack.core.types.builtin.DiagnosticInfo;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
  import org.eclipse.milo.opcua.stack.core.types.builtin.ExtensionObject;
  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.XmlElement;
  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.ULong;
  import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UShort;
  import org.eclipse.milo.opcua.stack.core.util.Namespaces;
  import org.jetbrains.annotations.Nullable;
  import org.opcfoundation.opcua.binaryschema.FieldType;
  import org.opcfoundation.opcua.binaryschema.StructuredType;
  import org.opcfoundation.opcua.binaryschema.SwitchOperand;
  
  public abstract class AbstractCodec<StructureT, MemberT> implements OpcUaBinaryDataTypeCodec<StructureT> {
  
      private static final ImmutableMap<String, Function<OpcUaBinaryStreamDecoder, Object>> READERS;
      private static final ImmutableMap<String, BiConsumer<OpcUaBinaryStreamEncoder, Object>> WRITERS;
      private static final ImmutableMap<String, Class<?>> TYPE_CLASS_MAP;
  
      static {
          READERS = ImmutableMap.<String, Function<OpcUaBinaryStreamDecoder, Object>>builder()
              .put("Boolean", OpcUaBinaryStreamDecoder::readBoolean)
              .put("SByte", OpcUaBinaryStreamDecoder::readSByte)
              .put("Int16", OpcUaBinaryStreamDecoder::readInt16)
              .put("Int32", OpcUaBinaryStreamDecoder::readInt32)
              .put("Int64", OpcUaBinaryStreamDecoder::readInt64)
              .put("Byte", OpcUaBinaryStreamDecoder::readByte)
              .put("UInt16", OpcUaBinaryStreamDecoder::readUInt16)
              .put("UInt32", OpcUaBinaryStreamDecoder::readUInt32)
              .put("UInt64", OpcUaBinaryStreamDecoder::readUInt64)
              .put("Float", OpcUaBinaryStreamDecoder::readFloat)
              .put("Double", OpcUaBinaryStreamDecoder::readDouble)
              .put("String", OpcUaBinaryStreamDecoder::readUtf8CharArray)
              .put("DateTime", OpcUaBinaryStreamDecoder::readDateTime)
              .put("Guid", OpcUaBinaryStreamDecoder::readGuid)
              .put("ByteString", OpcUaBinaryStreamDecoder::readByteString)
              .put("XmlElement", OpcUaBinaryStreamDecoder::readXmlElement)
              .put("NodeId", OpcUaBinaryStreamDecoder::readNodeId)
              .put("ExpandedNodeId", OpcUaBinaryStreamDecoder::readExpandedNodeId)
              .put("StatusCode", OpcUaBinaryStreamDecoder::readStatusCode)
              .put("QualifiedName", OpcUaBinaryStreamDecoder::readQualifiedName)
              .put("LocalizedText", OpcUaBinaryStreamDecoder::readLocalizedText)
              .put("ExtensionObject", OpcUaBinaryStreamDecoder::readExtensionObject)
              .put("DataValue", OpcUaBinaryStreamDecoder::readDataValue)
              .put("Variant", OpcUaBinaryStreamDecoder::readVariant)
              .put("DiagnosticInfo", OpcUaBinaryStreamDecoder::readDiagnosticInfo)
  
              .put("Bit", OpcUaBinaryStreamDecoder::readBit)
              .put("Char", OpcUaBinaryStreamDecoder::readCharacter)
              .put("CharArray", OpcUaBinaryStreamDecoder::readUtf8CharArray)
              .put("WideChar", OpcUaBinaryStreamDecoder::readWideChar)
              .put("WideCharArray", OpcUaBinaryStreamDecoder::readUtf16CharArray)
              .put("WideString", OpcUaBinaryStreamDecoder::readUtf16CharArray)
  
              .build();
  
          WRITERS = ImmutableMap.<String, BiConsumer<OpcUaBinaryStreamEncoder, Object>>builder()
              .put("Boolean", (w, v) -> w.writeBoolean((Boolean) v))
              .put("SByte", (w, v) -> w.writeSByte((Byte) v))
              .put("Int16", (w, v) -> w.writeInt16((Short) v))
              .put("Int32", (w, v) -> w.writeInt32((Integer) v))
              .put("Int64", (w, v) -> w.writeInt64((Long) v))
              .put("Byte", (w, v) -> w.writeByte((UByte) v))
              .put("UInt16", (w, v) -> w.writeUInt16((UShort) v))
              .put("UInt32", (w, v) -> w.writeUInt32((UInteger) v))
              .put("UInt64", (w, v) -> w.writeUInt64((ULong) v))
              .put("Float", (w, v) -> w.writeFloat((Float) v))
              .put("Double", (w, v) -> w.writeDouble((Double) v))
              .put("String", (w, v) -> w.writeUtf8CharArray((String) v))
              .put("DateTime", (w, v) -> w.writeDateTime((DateTime) v))
              .put("Guid", (w, v) -> w.writeGuid((UUID) v))
              .put("ByteString", (w, v) -> w.writeByteString((ByteString) v))
              .put("XmlElement", (w, v) -> w.writeXmlElement((XmlElement) v))
              .put("NodeId", (w, v) -> w.writeNodeId((NodeId) v))
              .put("ExpandedNodeId", (w, v) -> w.writeExpandedNodeId((ExpandedNodeId) v))
              .put("StatusCode", (w, v) -> w.writeStatusCode((StatusCode) v))
              .put("QualifiedName", (w, v) -> w.writeQualifiedName((QualifiedName) v))
              .put("LocalizedText", (w, v) -> w.writeLocalizedText((LocalizedText) v))
              .put("ExtensionObject", (w, v) -> w.writeExtensionObject((ExtensionObject) v))
              .put("DataValue", (w, v) -> w.writeDataValue((DataValue) v))
              .put("Variant", (w, v) -> w.writeVariant((Variant) v))
              .put("DiagnosticInfo", (w, v) -> w.writeDiagnosticInfo((DiagnosticInfo) v))
  
              .put("Bit", (w, v) -> w.writeBit((Integer) v))
              .put("Char", (w, v) -> w.writeCharacter((Character) v))
              .put("CharArray", (w, v) -> w.writeUtf8CharArray((String) v))
              .put("WideChar", (w, v) -> w.writeWideChar((Character) v))
              .put("WideCharArray", (w, v) -> w.writeUtf16CharArray((String) v))
              .put("WideString", (w, v) -> w.writeUtf16CharArray((String) v))
  
              .build();
  
          TYPE_CLASS_MAP = ImmutableMap.<String, Class<?>>builder()
              .put("Boolean", BuiltinDataType.Boolean.getBackingClass())
              .put("SByte", BuiltinDataType.SByte.getBackingClass())
              .put("Int16", BuiltinDataType.Int16.getBackingClass())
              .put("Int32", BuiltinDataType.Int32.getBackingClass())
              .put("Int64", BuiltinDataType.Int64.getBackingClass())
              .put("Byte", BuiltinDataType.Byte.getBackingClass())
              .put("UInt16", BuiltinDataType.UInt16.getBackingClass())
              .put("UInt32", BuiltinDataType.UInt32.getBackingClass())
              .put("UInt64", BuiltinDataType.UInt64.getBackingClass())
              .put("Float", BuiltinDataType.Float.getBackingClass())
              .put("Double", BuiltinDataType.Double.getBackingClass())
              .put("String", BuiltinDataType.String.getBackingClass())
              .put("DateTime", BuiltinDataType.DateTime.getBackingClass())
              .put("Guid", BuiltinDataType.Guid.getBackingClass())
              .put("ByteString", BuiltinDataType.ByteString.getBackingClass())
              .put("XmlElement", BuiltinDataType.XmlElement.getBackingClass())
              .put("NodeId", BuiltinDataType.NodeId.getBackingClass())
              .put("ExpandedNodeId", BuiltinDataType.ExpandedNodeId.getBackingClass())
              .put("StatusCode", BuiltinDataType.StatusCode.getBackingClass())
              .put("QualifiedName", BuiltinDataType.QualifiedName.getBackingClass())
              .put("LocalizedText", BuiltinDataType.LocalizedText.getBackingClass())
              .put("ExtensionObject", BuiltinDataType.ExtensionObject.getBackingClass())
              .put("DataValue", BuiltinDataType.DataValue.getBackingClass())
              .put("Variant", BuiltinDataType.Variant.getBackingClass())
              .put("DiagnosticInfo", BuiltinDataType.DiagnosticInfo.getBackingClass())
  
              .put("Bit", Integer.class)
              .put("Char", Character.class)
              .put("CharArray", String.class)
              .put("WideChar", Character.class)
              .put("WideCharArray", String.class)
              .put("WideString", String.class)
              .build();
      }
  
      private final Map<String, FieldType> fields = new HashMap<>();
      private final Map<String, FieldType> lengthFields = new HashMap<>();
  
      private final StructuredType structuredType;
  
      protected AbstractCodec(StructuredType structuredType) {
          this.structuredType = structuredType;
  
          structuredType.getField().forEach(
              field ->
                  fields.put(field.getName(), field)
          );
  
          structuredType.getField().forEach(field -> {
              String lengthField = field.getLengthField();
  
              if (lengthField != null) {
                  lengthFields.put(lengthField, fields.get(lengthField));
              }
          });
      }
  
      @Override
      public StructureT decode(
          SerializationContext context,
          OpcUaBinaryStreamDecoder decoder
      ) throws UaSerializationException {
  
          LinkedHashMap<String, MemberT> members = new LinkedHashMap<>();
  
          PeekingIterator<FieldType> fieldIterator = Iterators
              .peekingIterator(structuredType.getField().iterator());
  
          while (fieldIterator.hasNext()) {
              FieldType field = fieldIterator.next();
              String fieldName = field.getName();
              String typeName = field.getTypeName().getLocalPart();
              String typeNamespace = field.getTypeName().getNamespaceURI();
  
              if (fieldIsAbsent(field, members)) {
                  continue;
              }
  
              boolean typeNamespaceIsUa =
                  Namespaces.OPC_UA.equals(typeNamespace) ||
                      Namespaces.OPC_UA_BSD.equals(typeNamespace);
  
              if (fieldIsScalar(field)) {
                  if (typeNamespaceIsUa && READERS.containsKey(typeName)) {
                      Object value = READERS.get(typeName).apply(decoder);
  
                      members.put(fieldName, opcUaToMemberTypeScalar(fieldName, value, typeName));
                  } else {
                      Object value = context.decode(typeNamespace, typeName, decoder);
  
                      members.put(fieldName, opcUaToMemberTypeScalar(fieldName, value, typeName));
                  }
              } else {
                  if (field.isIsLengthInBytes()) {
                      throw new UaSerializationException(
                          StatusCodes.Bad_DecodingError,
                          "IsLengthInBytes=true not supported");
                  }
  
                  int length = fieldLength(field, members);
  
                  if ("Bit".equals(typeName) && typeNamespaceIsUa) {
                      BigInteger bitAccumulation = BigInteger.valueOf(0L);
  
                      for (int i = 0; i < length; i++) {
                          BigInteger bitValue = BigInteger.valueOf(decoder.readBit());
  
                          bitAccumulation = bitAccumulation.or(bitValue.shiftLeft(i));
                      }
  
                      members.put(fieldName, opcUaToMemberTypeArray(fieldName, bitAccumulation.intValue(), typeName));
                  } else {
                      Object[] values = null;
  
                      if (length >= 0) {
                          values = new Object[length];
  
                          if (typeNamespaceIsUa && READERS.containsKey(typeName)) {
                              for (int i = 0; i < length; i++) {
                                  Object value = READERS.get(typeName).apply(decoder);
  
                                  values[i] = value;
                              }
                          } else {
                              for (int i = 0; i < length; i++) {
                                  Object value = context.decode(typeNamespace, typeName, decoder);
  
                                  values[i] = value;
                              }
                          }
                      }
  
                      members.put(fieldName, opcUaToMemberTypeArray(fieldName, values, typeName));
                  }
              }
          }
  
          for (String lengthField : lengthFields.keySet()) {
              members.remove(lengthField);
          }
  
          return createStructure(structuredType.getName(), members);
      }
  
      @Override
      public void encode(
          SerializationContext context,
          OpcUaBinaryStreamEncoder encoder,
          StructureT structure
      ) throws UaSerializationException {
  
          LinkedHashMap<String, MemberT> members = new LinkedHashMap<>(getMembers(structure));
  
          PeekingIterator<FieldType> fieldIterator = Iterators
              .peekingIterator(structuredType.getField().iterator());
  
          while (fieldIterator.hasNext()) {
              FieldType field = fieldIterator.next();
  
              if (fieldIsAbsent(field, members)) {
                  continue;
              }
  
              if (lengthFields.containsKey(field.getName())) {
                  // let arrays encode their own length field so the
                  // structure representation isn't required to include it
                  continue;
              }
  
              encodeField(context, encoder, members, field);
          }
      }
  
      private void encodeField(
          SerializationContext context,
          OpcUaBinaryStreamEncoder encoder,
          LinkedHashMap<String, MemberT> members,
          FieldType field
      ) {
  
          String typeName = field.getTypeName().getLocalPart();
          String typeNamespace = field.getTypeName().getNamespaceURI();
  
          MemberT member = members.get(field.getName());
  
          boolean typeNamespaceIsUa =
              Namespaces.OPC_UA.equals(typeNamespace) ||
                  Namespaces.OPC_UA_BSD.equals(typeNamespace);
  
          if (fieldIsScalar(field)) {
              Object scalarValue = memberTypeToOpcUaScalar(member, typeName);
  
              if (typeNamespaceIsUa && WRITERS.containsKey(typeName)) {
                  WRITERS.get(typeName).accept(encoder, scalarValue);
              } else {
                  context.encode(typeNamespace, typeName, scalarValue, encoder);
              }
          } else {
              if (field.isIsLengthInBytes()) {
                  throw new UaSerializationException(
                      StatusCodes.Bad_EncodingError,
                      "IsLengthInBytes=true not supported");
              }
  
              if ("Bit".equals(typeName) && typeNamespaceIsUa) {
                  int length = fieldLength(field, members);
  
                  Number number = (Number) memberTypeToOpcUaArray(member, typeName);
                  BigInteger bi = BigInteger.valueOf(number.longValue());
  
                  for (int i = 0; i < length; i++) {
                      encoder.writeBit(bi.shiftRight(i).and(BigInteger.ONE).intValue());
                  }
              } else {
                  Object[] valueArray = (Object[]) memberTypeToOpcUaArray(member, typeName);
  
                  FieldType lengthField = fields.get(field.getLengthField());
  
                  if (lengthField != null) {
                      int length = valueArray.length;
  
                      members.put(lengthField.getName(), opcUaToMemberTypeScalar(
                          lengthField.getName(),
                          length,
                          lengthField.getTypeName().getLocalPart()
                      ));
  
                      encodeField(context, encoder, members, lengthField);
                  }
  
                  if (valueArray != null) {
                      if (typeNamespaceIsUa && WRITERS.containsKey(typeName)) {
                          for (Object value : valueArray) {
                              WRITERS.get(typeName).accept(encoder, value);
                          }
                      } else {
                          for (Object value : valueArray) {
                              context.encode(typeNamespace, typeName, value, encoder);
                          }
                      }
                  }
              }
          }
      }
  
      protected abstract StructureT createStructure(String name, LinkedHashMap<String, MemberT> members);
  
      protected abstract Map<String, MemberT> getMembers(StructureT value);
  
      /**
       * Convert an OPC UA scalar value into a member of type {@link MemberT}.
       *
       * @param name     then name of the member.
       * @param value    the value of the member.
       * @param typeName the name of the OPC UA DataType.
       * @return a member of type {@link MemberT}.
       */
      protected abstract MemberT opcUaToMemberTypeScalar(String name, @Nullable Object value, String typeName);
  
      /**
       * Convert an OPC UA array value into a member of type {@link MemberT}.
       *
       * @param name     the name of the member.
       * @param values   the values of the member array.
       * @param typeName the name of the OPC UA DataType.
       * @return member of type {@link MemberT}.
       */
      protected abstract MemberT opcUaToMemberTypeArray(String name, @Nullable Object values, String typeName);
  
      protected abstract Object memberTypeToOpcUaScalar(MemberT member, String typeName);
  
      protected abstract Object memberTypeToOpcUaArray(MemberT member, String typeName);
  
      private int fieldLength(FieldType field, LinkedHashMap<String, MemberT> members) {
          int length = 1;
  
          if (field.getLength() != null) {
              length = field.getLength().intValue();
          } else if (field.getLengthField() != null) {
              MemberT lengthMember = members.get(field.getLengthField());
  
              if (lengthMember != null) {
                  String lengthTypeName = structuredType.getField().stream()
                      .filter(f -> f.getName().equals(field.getLengthField()))
                      .findFirst()
                      .map(f -> f.getTypeName().getLocalPart())
                      .orElse("Int32");
  
                  length = ((Number) memberTypeToOpcUaScalar(lengthMember, lengthTypeName)).intValue();
              }
          }
  
          return length;
      }
  
      private boolean fieldIsAbsent(FieldType field, Map<String, MemberT> members) {
          if (field.getSwitchField() == null) {
              return false;
          } else {
              MemberT controlField = members.get(field.getSwitchField());
  
              String controlTypeName = structuredType.getField().stream()
                  .filter(f -> f.getName().equals(field.getSwitchField()))
                  .findFirst()
                  .map(f -> f.getTypeName().getLocalPart())
                  .orElse("Int32");
  
              long controlValue = ((Number) memberTypeToOpcUaScalar(controlField, controlTypeName)).longValue();
  
              long switchValue = field.getSwitchValue() != null ?
                  field.getSwitchValue() : 1L;
  
              SwitchOperand switchOperand = field.getSwitchOperand() != null ?
                  field.getSwitchOperand() : SwitchOperand.EQUALS;
  
              return !compareToSwitchValue(controlValue, switchOperand, switchValue);
          }
      }
  
      private static boolean compareToSwitchValue(long controlValue, SwitchOperand switchOperand, long switchValue) {
          switch (switchOperand) {
              case EQUALS:
                  return controlValue == switchValue;
              case NOT_EQUAL:
                  return controlValue != switchValue;
              case GREATER_THAN:
                  return controlValue > switchValue;
              case GREATER_THAN_OR_EQUAL:
                  return controlValue >= switchValue;
              case LESS_THAN:
                  return controlValue < switchValue;
              case LESS_THAN_OR_EQUAL:
                  return controlValue <= switchValue;
              default:
                  throw new UaSerializationException(
                      StatusCodes.Bad_InternalError,
                      "unknown SwitchOperand: " + switchOperand);
          }
      }
  
      private static boolean fieldIsScalar(FieldType field) {
          return field.getLengthField() == null && field.getLength() == null;
      }
  
  }
  