program Open62541Test;

{$APPTYPE CONSOLE}

uses
{$IFDEF FPC}
//  System,
{$ELSE}
  System.SysUtils,
{$ENDIF}
  open62541 in 'open62541.pas'; // Make sure the path points to your translated unit


procedure TestOpen62541;
var
  Server: PUA_Server;
begin
  Writeln('=== OPC UA Library Linkage Test ===');
  Writeln('Attempting to initialize raw server allocation module...');

  try
    // 1. Allocate a blank server profile structure
    Server := _UA_Server_new(); // Calls your newly prefixed function format

    if Assigned(Server) then
    begin
      Writeln('Success! libopen62541.dll loaded and returned a valid memory pointer.');

      // 2. Safely free the allocated server memory back to the DLL
      _UA_Server_delete(Server);
      Writeln('Success! Server structure successfully disposed of.');
      Writeln('====================================');
      Writeln('Status: Verification Passed Natively.');
    end
    else
    begin
      Writeln('Error: DLL linked, but failed to allocate memory space.');
    end;

  except
    on E: Exception do
    begin
      Writeln('Fatal Crash: ', E.ClassName, ' - ', E.Message);
      Writeln('This usually means a mismatch in Calling Conventions or 32/64-bit target layers.');
    end;
  end;
end;


// ==============================================================================
// LIVE DATA SOURCE READ CALLBACK FUNCTION
// Executes dynamically every time a client requests the ADC value
// ==============================================================================
function TaskOpcUaServer_ReadAdc(
  server: Pointer;
  const sessionId: PUA_NodeId; sessionIdContext: Pointer;
  const nodeId: PUA_NodeId; nodeContext: Pointer;
  includeSourceTimeStamp: UA_Boolean;
  const range: Pointer;
  dataValue: PUA_DataValue): UA_StatusCode; cdecl;
var
  RandomValue: UA_Double;
begin
  if not Assigned(dataValue) or not Assigned(_UA_TYPES) then
  begin
    Result := UA_STATUSCODE_BADINTERNALERROR;
    Exit;
  end;

  // Generate a simulated random hardware value (0.0 to 100.0)
  RandomValue := Random * 100.0;

  _UA_Variant_setScalarCopy(@dataValue^.value, @RandomValue, @_UA_TYPES[UA_TYPES_DOUBLE]);

  // Mark the tracking indicators as containing valid data
  dataValue^.hasValue := Cardinal(True);

  Result := UA_STATUSCODE_GOOD;
end;


// ==============================================================================
// INJECT AND BIND THE DATA SOURCE VARIABLE NODE
// Configures the node, fixes type checks, and registers it to the server tree
// ==============================================================================
procedure TaskOpcUaServer_AddAdcDataSource(Server: Pointer);
var
  LAdcDataSource       : UA_DataSource;
  LAttr                : UA_VariableAttributes;
  NodeId               : UA_NodeId;
  ParentNodeId         : UA_NodeId;
  ParentReferenceNodeId: UA_NodeId;
  BrowseName           : UA_QualifiedName;
  TypeDefinition       : UA_NodeId;
begin
  if not Assigned(Server) or not Assigned(_UA_TYPES) then Exit;

  // INITIALIZE THE DATA SOURCE CALLBACK HANDLES
  FillChar(LAdcDataSource, SizeOf(LAdcDataSource), 0);
  LAdcDataSource.read := @TaskOpcUaServer_ReadAdc;
  LAdcDataSource.write := nil; // Read-only node parameter

  // DEFINE THE ATTRIBUTES WITH EXPLICIT SCALAR PROPERTIES
  _UA_init(@LAttr, @_UA_TYPES[UA_TYPES_VARIABLEATTRIBUTES]);
  try
    LAttr.displayName := _UA_LOCALIZEDTEXT_ALLOC('en-US', 'Simulated Raw ADC');
    LAttr.description := _UA_LOCALIZEDTEXT_ALLOC('en-US', 'Analog input');
    LAttr.accessLevel := UA_ACCESSLEVELMASK_READ;

    // Assign the explicit Double type definition ID
    LAttr.dataType    := _UA_TYPES[UA_TYPES_DOUBLE].typeId;
    // Force standard Scalar validation properties
    LAttr.valueRank   := -1; // -1 explicitly stands for a singular scalar node primitive

    // MAP THE STRUCTURAL TREE LOCATION IDENTIFIERS
    NodeId                := _UA_NODEID_STRING(1, 'ADC_Channel_0');
    ParentNodeId          := _UA_NODEID_NUMERIC(0, UA_NS0ID_OBJECTSFOLDER);
    ParentReferenceNodeId := _UA_NODEID_NUMERIC(0, UA_NS0ID_HASCOMPONENT);
    BrowseName            := _UA_QUALIFIEDNAME(1, 'ADC_Channel_0');
    TypeDefinition        := _UA_NODEID_NUMERIC(0, UA_NS0ID_BASEDATAVARIABLETYPE);

    // 5. EXECUTE THE INJECTION VIA THE STABLE EXPORT
    _UA_Server_addCallbackValueSourceVariableNode(
      Server,
      NodeId,
      ParentNodeId,
      ParentReferenceNodeId,
      BrowseName,
      TypeDefinition,
      LAttr,
      LAdcDataSource,
      nil,
      nil
    );

    Writeln('Live DataSource Node registered successfully via CallbackValueSource API.');
  finally
    _UA_clear(@LAttr, @_UA_TYPES[UA_TYPES_NODEID]);
  end;
end;


procedure RunOpcUaServer;
var
  LServer        : PUA_Server;
  LConfig        : PUA_ServerConfig;
  LUrlString     : UA_String;
  LString        : UA_String;
  LDateTimeStruct: UA_DateTimeStruct;
  LValue         : UA_Variant;
  LLocalizedText : UA_LocalizedText;
begin
  Writeln('=== Starting Live Branded OPC UA Server ===');

  _UA_String_format(@LUrlString, 'opc.tcp://%s:%d', PAnsiChar('127.0.0.1'), 4840);

  // Initialize a native server instance securely
  LServer := _UA_Server_new();
  if not Assigned(LServer) then Exit;

  try
    LConfig := _UA_Server_getConfig(LServer);

    if not Assigned(LConfig) then
    begin
      Writeln('=== Issue getting configuration pointer ===');
      Exit;
    end;

    // Set the server Urls (the existing one uses a non existing host name, so replace it)
    if (LConfig.serverUrlsSize > 0) then
      _UA_Array_delete(LConfig.serverUrls, LConfig.serverUrlsSize, @_UA_TYPES[UA_TYPES_STRING]);
    LConfig^.serverUrlsSize := 1;
    LConfig^.serverUrls     := _UA_Array_new(1, @_UA_TYPES[UA_TYPES_STRING]);
    if Assigned(LConfig^.serverUrls) then
      _UA_copy(@LUrlString, LConfig^.serverUrls, @_UA_TYPES[UA_TYPES_STRING]);

    // Set discovery Urls (we don't expect any being set, but just the same check it)
    // Must be done after UA_ServerConfig_setMinimalCustomBuffer
    if (LConfig^.applicationDescription.discoveryUrlsSize > 0) then
      _UA_Array_delete(LConfig^.applicationDescription.discoveryUrls, LConfig^.applicationDescription.discoveryUrlsSize, @_UA_TYPES[UA_TYPES_STRING]);
    LConfig^.applicationDescription.discoveryUrlsSize := 1;
    LConfig^.applicationDescription.discoveryUrls := _UA_Array_new(1, @_UA_TYPES[UA_TYPES_STRING]);
    if Assigned(LConfig^.applicationDescription.discoveryUrls) then
      _UA_copy(@LUrlString, LConfig^.applicationDescription.discoveryUrls, @_UA_TYPES[UA_TYPES_STRING]);

    // DisplayName, use the specialized write function (Server node)
    _UA_Init(@LValue, @_UA_TYPES[UA_TYPES_VARIANT]);
    LLocalizedText := _UA_LOCALIZEDTEXT('', 'Server URI Value');
    _UA_Variant_setScalarCopy(@LValue, @LUrlString, @_UA_TYPES[UA_TYPES_STRING]);
    _UA_Server_writeValue      (LServer, _UA_NODEID_NUMERIC(0, 25725), LValue);
    _UA_Server_writeDisplayName(LServer, _UA_NODEID_NUMERIC(0, 25725), _UA_LOCALIZEDTEXT('', 'Server URI Display name'));
    // Description, use the specialized write function (Server node)
    _UA_Server_writeDescription(LServer, _UA_NODEID_NUMERIC(0, 25725), _UA_LOCALIZEDTEXT('', 'Server URI Description'));
    // DisplayName, use the specialized write function (Server node)
    _UA_Server_writeDisplayName(LServer, _UA_NODEID_NUMERIC(0, 2253), _UA_LOCALIZEDTEXT('', 'Server DisplayName'));
    // Description, use the specialized write function (Server node)
    _UA_Server_writeDescription(LServer, _UA_NODEID_NUMERIC(0, 2253), _UA_LOCALIZEDTEXT('', 'Server Description'));
    // DisplayName, use the specialized write function (ServerArray node)
    _UA_Server_writeDisplayName(LServer, _UA_NODEID_NUMERIC(0, 2254), _UA_LOCALIZEDTEXT('', 'ServerArray DisplayName'));
    // Description, use the specialized write function (ServerArray node)
    _UA_Server_writeDescription(LServer, _UA_NODEID_NUMERIC(0, 2254), _UA_LOCALIZEDTEXT('', 'ServerArray Description'));
    // DisplayName, use the specialized write function (NamespaceArray node)
    _UA_Server_writeDisplayName(LServer, _UA_NODEID_NUMERIC(0, 2255), _UA_LOCALIZEDTEXT('', 'NamespaceArray DisplayName'));
    // Description, use the specialized write function (NamespaceArray node)
    _UA_Server_writeDescription(LServer, _UA_NODEID_NUMERIC(0, 2255), _UA_LOCALIZEDTEXT('', 'NamespaceArray Description'));
    // DisplayName, use the specialized write function (ServerStatus node)
    _UA_Server_writeDisplayName(LServer, _UA_NODEID_NUMERIC(0, 2256), _UA_LOCALIZEDTEXT('', 'ServerStatus DisplayName'));
    // Description, use the specialized write function (ServerStatus node)
    _UA_Server_writeDescription(LServer, _UA_NODEID_NUMERIC(0, 2256), _UA_LOCALIZEDTEXT('', 'ServerStatus Description'));
    // DisplayName, use the specialized write function (BuildInfo node)
    // Note: When using a real locale, like 'en', the client most likely will -not- use
    //       our provided text, but uses its internal text instead!
    _UA_Server_writeDisplayName(LServer, _UA_NODEID_NUMERIC(0, 2260), _UA_LOCALIZEDTEXT('', 'BuildInfo DisplayName'));
    // Description, use the specialized write function (BuildInfo node)
    _UA_Server_writeDescription(LServer, _UA_NODEID_NUMERIC(0, 2260), _UA_LOCALIZEDTEXT('', 'BuildInfo Description'));
    // Etcetera
              LString := _UA_STRING('urn:open62541:server:delphi');
    _UA_copy(@LString, @LConfig^.applicationDescription.applicationUri, @_UA_TYPES[UA_TYPES_STRING]);
    // Set build information
              LString := _UA_STRING('Our productUri');
    _UA_copy(@LString, @LConfig^.buildInfo.productUri      , @_UA_TYPES[UA_TYPES_STRING]);
              LString := _UA_STRING('Our manufacturerName');
    _UA_copy(@LString, @LConfig^.buildInfo.manufacturerName, @_UA_TYPES[UA_TYPES_STRING]);
              LString := _UA_STRING('Our productName');
    _UA_copy(@LString, @LConfig^.buildInfo.productName     , @_UA_TYPES[UA_TYPES_STRING]);
              LString := _UA_STRING('Our softwareVersion');
    _UA_copy(@LString, @LConfig^.buildInfo.softwareVersion , @_UA_TYPES[UA_TYPES_STRING]);
              LString := _UA_STRING('Our buildNumber');
    _UA_copy(@LString, @LConfig^.buildInfo.buildNumber     , @_UA_TYPES[UA_TYPES_STRING]);
    _UA_Clear(@LValue, @_UA_TYPES[UA_TYPES_VARIANT]);

    // And finally a date/time thing
    FillChar(LDateTimeStruct, sizeof(LDateTimeStruct), 0);
    // Set your desired parameters
    LDateTimeStruct.year  := 2026;
    LDateTimeStruct.month := 5;  // 1 to 12
    LDateTimeStruct.day   := 10; // 1 to 31
    LDateTimeStruct.hour  := 12;
    LDateTimeStruct.min   := 30;
    LDateTimeStruct.sec   := 0;
    // Convert to UA_DateTime (the 64-bit integer format)
    LConfig^.buildInfo.buildDate := _UA_DateTime_fromStruct(LDateTimeStruct);

    TaskOpcUaServer_AddAdcDataSource(LServer);
    Writeln('Server is listening actively on port 4840...');
    Writeln('Press Ctrl+C in this window to stop.');
    Writeln('------------------------------------------------------------');

    // 4. Run the stable network listener loop
    _UA_Server_runUntilInterrupt(LServer);

  finally
    _UA_Server_delete(LServer);
    Writeln('Server stopped cleanly.');
  end;
end;


// Recursive function!
procedure BrowseServerNodes(Client: PUA_Client; const NodeId: UA_NodeId; Depth: Integer);
type
  TUA_ReferenceDescriptionArray = array[0..$FFFF] of UA_ReferenceDescription;
  PUA_ReferenceDescriptionArray = ^TUA_ReferenceDescriptionArray;
const
  NodeClassNames: array[0..7] of string = (
    'Unspecified', 'Object', 'Variable', 'Method',
    'ObjectType', 'VariableType', 'DataType', 'ReferenceType'
  );
var
  BrowseDesc     : UA_BrowseDescription;
  BrowseResult   : UA_BrowseResult;
  I              : Integer;
  Indent         : string;
  NextNodeId     : UA_NodeId;
  RefArray       : PUA_ReferenceDescriptionArray;
  NodeClassName  : string;
  NodeDisplayName: string;
  ClassIdx       : Integer;
begin
  Indent := StringOfChar(' ', Depth * 2);

  // Initialize the browse description
  _UA_init(@BrowseDesc, @_UA_TYPES[UA_TYPES_BROWSEDESCRIPTION]);
  try
    // FIX 1: Safely deep-copy the incoming NodeId using the generic API
    _UA_copy(@NodeId, @BrowseDesc.nodeId, @_UA_TYPES[UA_TYPES_NODEID]);

    BrowseDesc.browseDirection := UA_BROWSEDIRECTION_FORWARD;
    BrowseDesc.includeSubtypes := True;
    BrowseDesc.resultMask := Ord(UA_BROWSERESULTMASK_ALL);

    // Execute the browse request
    BrowseResult := _UA_Client_browse(Client, nil, 0, @BrowseDesc);
    try
      RefArray := PUA_ReferenceDescriptionArray(BrowseResult.references);
      if BrowseResult.statusCode = UA_STATUSCODE_GOOD then
      begin
        for I := 0 to BrowseResult.referencesSize - 1 do
        begin
          ClassIdx := Integer(RefArray^[I].nodeClass);
          if (ClassIdx >= 0) and (ClassIdx <= 7) then
            NodeClassName := NodeClassNames[ClassIdx]
          else
            NodeClassName := 'Unknown';

          if (RefArray^[I].displayName.text.length > 0) and (RefArray^[I].displayName.text.data <> nil) then
            SetString(NodeDisplayName, PAnsiChar(RefArray^[I].displayName.text.data), RefArray^[I].displayName.text.length)
          else
            NodeDisplayName := '(No Name)';

          Writeln(Format('%s[%s] %s', [Indent, NodeClassName, NodeDisplayName]));

          if (ClassIdx = 1) or (ClassIdx = 2) then
          begin
            // FIX 2: Initialize and deep-copy the child node ID for memory isolation
            _UA_init(@NextNodeId, @_UA_TYPES[UA_TYPES_NODEID]);
            try
              _UA_copy(@RefArray^[I].nodeId.nodeId, @NextNodeId, @_UA_TYPES[UA_TYPES_NODEID]);

              // Recurse safely
              BrowseServerNodes(Client, NextNodeId, Depth + 1);
            finally
              _UA_clear(@NextNodeId, @_UA_TYPES[UA_TYPES_NODEID]);
            end;
          end;
        end;
      end;
    finally
      _UA_clear(@BrowseResult, @_UA_TYPES[UA_TYPES_BROWSERESULT]);
    end;
  finally
    _UA_clear(@BrowseDesc, @_UA_TYPES[UA_TYPES_BROWSEDESCRIPTION]);
  end;
end;


function ReadProperty(AClient: PUA_Client; APropertyId: Cardinal): string;
var
  LNodeId: UA_NodeId;
  LValue : UA_Variant;
  LStatus: UA_StatusCode;
  LString: UA_String;
begin
  Result := '';
  // Generic initialization
  // This zeros out the record memory safely
  _UA_init(@LValue, @_UA_TYPES[UA_TYPES_VARIANT]);
  try
    // Create the numeric NodeId (Namespace 0, ID passed via parameter)
    LNodeId := _UA_NODEID_NUMERIC(0, APropertyId);
    // Call the DLL to read the attribute value
    LStatus := _UA_Client_readValueAttribute(AClient, LNodeId, @LValue);
    if LStatus = UA_STATUSCODE_GOOD then
    begin
      // Check if the returned data type is a standard UA_String
      if (LValue.&type = @_UA_TYPES[UA_TYPES_STRING]) and (LValue.data <> nil) then
      begin
        LString := PUA_String(LValue.data)^;

        // Safely convert the C-style open62541 string buffer to a native Delphi string
        if LString.length > 0 then
          SetString(Result, PAnsiChar(LString.data), LString.length);
      end;
    end;
  finally
    // Always clear the variant to prevent memory leaks in the C layer
    _UA_clear(@LValue, @_UA_TYPES[UA_TYPES_VARIANT]);
  end;
end;


procedure BrowseServerSpecificNodes(Client: PUA_Client);
type
  // Declare a local indexable array type that matches your architecture size
  UA_ReadValueIdArray  = array[0..$FFFF] of UA_ReadValueId;
  PUA_ReadValueIdArray = ^UA_ReadValueIdArray;
  UA_DataValueArray    = array[0..$FFFF] of UA_DataValue;
  PUA_DataValueArray   = ^UA_DataValueArray;
var
  LRequest       : UA_ReadRequest;
  LResponse      : UA_ReadResponse;
  LReadValueArray: PUA_ReadValueIdArray;
  LDataValueArray: PUA_DataValueArray;
  LName          : string;
begin
  // Read the build info
  _UA_init(@LRequest, @_UA_TYPES[UA_TYPES_READREQUEST]);
  try
    // Allocate space for two read targets (DisplayName and Description)
    LRequest.nodesToReadSize := 2;
    LRequest.nodesToRead := _UA_Array_new(2, @_UA_TYPES[UA_TYPES_READVALUEID]);
    LReadValueArray      := PUA_ReadValueIdArray(LRequest.nodesToRead);

    // Target 1: Read the DisplayName attribute
    LReadValueArray[0].nodeId      := _UA_NODEID_NUMERIC(0, 2260);
    LReadValueArray[0].attributeId := Ord(UA_ATTRIBUTEID_DISPLAYNAME);
    // Target 2: Read the Description attribute
    LReadValueArray[1].nodeId      := _UA_NODEID_NUMERIC(0, 2260);
    LReadValueArray[1].attributeId := Ord(UA_ATTRIBUTEID_DESCRIPTION);
    // Execute read request over the active client connection
    LResponse := _UA_Client_Service_read(Client, LRequest);
    try
      // Ensure the response status is good and data is present
      if (LResponse.responseHeader.serviceResult = UA_STATUSCODE_GOOD) and (LResponse.resultsSize = 2) then
      begin
        LDataValueArray := PUA_DataValueArray(LResponse.results);
        // Extract DisplayName
        if (LDataValueArray[0].hasValue <> 0) and (LDataValueArray[0].value.&type = @_UA_TYPES[UA_TYPES_LOCALIZEDTEXT]) then
        begin
          SetString(LName, PAnsiChar(PUA_LocalizedText(LDataValueArray[0].value.data)^.text.data), PUA_LocalizedText(LDataValueArray[0].value.data)^.text.length);
          WriteLn('DisplayName text: ' + LName);
          SetString(LName, PAnsiChar(PUA_LocalizedText(LDataValueArray[0].value.data)^.locale.data), PUA_LocalizedText(LDataValueArray[0].value.data)^.locale.length);
          WriteLn('DisplayName locale: ' + LName);
        end;
          // Extract Description
        if (LDataValueArray[1].hasValue <> 0) and (LDataValueArray[1].value.&type = @_UA_TYPES[UA_TYPES_LOCALIZEDTEXT]) then
        begin
          SetString(LName, PAnsiChar(PUA_LocalizedText(LDataValueArray[1].value.data)^.text.data), PUA_LocalizedText(LDataValueArray[0].value.data)^.text.length);
          WriteLn('Description text: ' + LName);
          SetString(LName, PAnsiChar(PUA_LocalizedText(LDataValueArray[1].value.data)^.locale.data), PUA_LocalizedText(LDataValueArray[0].value.data)^.locale.length);
          WriteLn('Description locale: ' + LName);
        end;
      end;
    finally
      _UA_clear(@LResponse, @_UA_TYPES[UA_TYPES_READRESPONSE])
    end;
  finally
    _UA_clear(@LRequest, @_UA_TYPES[UA_TYPES_READREQUEST]);
  end;
  //
  //
  // Standard OPC UA Numeric NodeIds for BuildInfo properties:
  WriteLn('ProductName     : ' + ReadProperty(Client, 2261));
  WriteLn('ProductUri      : ' + ReadProperty(Client, 2262));
  WriteLn('ManufacturerName: ' + ReadProperty(Client, 2263));
  WriteLn('SoftwareVersion : ' + ReadProperty(Client, 2264));
  WriteLn('BuildNumber     : ' + ReadProperty(Client, 2265));
end;


procedure RunOpcUaClient();
var
  Client    : PUA_Client;
  Status    : UA_StatusCode;
  ServerUrl : PAnsiChar;
  RootNodeId: UA_NodeId;
begin
  ServerUrl := 'opc.tcp://127.0.0.1:4840';
  Writeln('Connecting to ' + string(ServerUrl) + '...');

  // Create a new client instance
  Client := _UA_Client_new();
  if Client = nil then
  begin
    Writeln('Failed to create the client instance.');
    Exit;
  end;

  try
    // Connect to the target server
    Status := _UA_Client_connect(Client, ServerUrl);
    if Status <> UA_STATUSCODE_GOOD then
    begin
      Writeln(Format('Connection failed with status code: 0x%X', [Status]));
      Exit;
    end;
    Writeln('Connected successfully! Mapping server properties...');
    Writeln('----------------------------------------------------');

    // Define starting point (Objects Folder: Numeric ID 85)
    RootNodeId := _UA_NODEID_NUMERIC(0, 85);

    // Run the browse sequence
    BrowseServerNodes(Client, RootNodeId, 0);
    BrowseServerSpecificNodes(Client);

    // Disconnect gracefully
    _UA_Client_disconnect(Client);
    Writeln('----------------------------------------------------');
    Writeln('Disconnected.');

  finally
    // Clean memory
    _UA_Client_delete(Client);
  end;
end;


begin
 try
    // Check if the user passed the "-server" or "/server" switch
    if FindCmdLineSwitch('server', ['-', '/'], True) then
    begin
      // If the switch is present, launch the live network server
      RunOpcUaServer;
    end
    else
    // Check if the user passed the "-client" or "/client" switch
    if FindCmdLineSwitch('client', ['-', '/'], True) then
    begin
      // If the switch is present, launch the live network client
      RunOpcUaClient();
      Writeln('Press ENTER to exit...');
      Readln;
    end
    else
    begin
      // Default behavior: run the quick validation test
      TestOpen62541;

      Writeln;
      Writeln('Tip: Run this executable with the "-server" parameter to start the live server.');
      Writeln('Tip: Run this executable with the "-client" parameter to start the live client.');
      Writeln('Press ENTER to exit...');
      Readln;
    end;

  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

