function [meshNodes, paths] = helperBLEMeshSimulation(meshNodes, totalNodes, ...
    meshNetworkGraph, simulationTime, sourceDestinationPairs, ttl, ...
    friendLowPowerPairs, receiveWindow, pollTimeout, relayNodeIDs, ...
    enableVisualization, enableAnimation)
%helperBLEMeshSimulation Helper for simulating a Bluetooth mesh network
%
%   [MESHNODES, PATHS] = helperBLEMeshSimulation(MESHNODES, TOTALNODES,
%   MESHNETWORKGRAPH, SIMULATIONTIME, SOURCEDESTINATIONPAIRS, TTL,
%   FRIENDLOWPOWERPAIRS, RECEIVEWINDOW, POLLTIMEOUT, RELAYNODEIDS,
%   ENABLEVISUALIZATION, ENABLEANIMATION) runs the simulation and returns
%   the nodes with updated statistics such as time spent in various states
%   (Sleep, Listen) and number of mesh messages transmitted and received at
%   each layer of Bluetooth mesh stack.
%
%   MESHNODES is a vector of objects of type helperBLEMeshNode.
%
%   TOTALNODES is a positive integer representing the number of nodes in
%   the mesh network.
%
%   MESHNETWORKGRAPH is an object of type helperBLEMeshVisualizeNetwork.
%
%   SIMULATIONTIME is an integer value in milliseconds.
%
%   SOURCEDESTINATIONPAIRS is an n-by-2 array contains source and
%   destination node identifiers.
%
%   TTL is an integer value in the range [0-127], represents the
%   time-to-live value to be used in message publication between configured
%   source and destination pairs.
%
%   FRIENDLOWPOWERPAIRS is an n-by-2 array contains Friend and Low Power
%   node identifiers.
%
%   RECEIVEDELAY is a vector of integer values in milliseconds, represents
%   the receive delay for configured Friend and Low Power node pairs.
%
%   RECEIVEWINDOW is a vector of integer values in milliseconds, represents
%   the receive window for configured Friend and Low Power node pairs.
%
%   POLLTIMEOUT is a vector of integer values in seconds, represents the
%   poll timeout for configured Friend and Low Power node pairs.
%
%   RELAYNODEIDS is a vector of integer values containing node identifiers
%   in which the relay feature is enabled.
%
%   ENABLEVISUALIZATION is a boolean flag to enable/disable the
%   visualization.
%
%   ENABLEANIMATION is a boolean flag to enable/disable animation in the
%   visualization.
%
%   PATHS is a cell-array containing paths between the configured source &
%   destination node pairs. If the destination node is a Low Power node,
%   path for that pair can't be derived.

%   Copyright 2019 The MathWorks, Inc.

% Configuration
nodeStates = zeros(1, totalNodes);
paths = cell(1, totalNodes);
pathIndex = 1;

% Unique source & destination node pairs
sourceDestinationPairs = uniqueMeshNodePairs(sourceDestinationPairs);

% Source-destination pairs for identifying the path
if ~isempty(friendLowPowerPairs)
    srcDstPairs = pairsForPath(sourceDestinationPairs, friendLowPowerPairs);
    % Unique source & destination node pairs after filtering from the Low
    % Power node pairs
    srcDstPairs = uniqueMeshNodePairs(srcDstPairs);
else
    srcDstPairs = sourceDestinationPairs;
end

% All the timing parameters specified in the Bluetooth mesh stack are
% multiple of 1 millisecond. Timing parameters (advertising interval and
% scan interval) specified at the Link Layer are multiple of 0.625
% milliseconds. In order to achieve both the timing parameters, advertising
% interval and scan interval are configured as multiple of 5 milliseconds.
% Therefore, considered the step time as 0.5 milliseconds to achieve a
% trade-off between the simulation time and accuracy.
stepTime = 0.5; % in milliseconds
stepTimeUs = stepTime * 1e3;

% Advertising interval used in the Bluetooth mesh network. It is the
% minimum value specified in the Bluetooth Core Specification, v5.1, Vol 6,
% PART B, Section 4.4.2.2. All the timing parameters specified in the
% Bluetooth mesh stack are multiple of 1 millisecond. Timing parameters
% (advertising interval and scan interval) specified at the Link Layer are
% multiple of 0.625 milliseconds. In order to achieve both the timing
% parameters, advertising interval and scan interval are configured as
% multiple of 5 milliseconds.
advertisingInterval = 20; % in milliseconds

% Delay added to synchronize the Link Layer and the transport layer when
% the friend feature is enabled. The minimum time taken for a message
% originated at the transport layer to reach the network layer is (10 +
% randi(0, 10)) milliseconds (Refer Bluetooth Mesh Profile v1.0, Section
% 4.2.19). Therefore, considered the delay as 20 milliseconds.
delay = 20; % in milliseconds

% Receive delay is the time between the Low Power node sending a request
% and listening for a response. Refer Bluetooth Mesh Profile v1.0, Section
% 3.6.6.1. Request sent by the Low Power node originates at the transport
% layer and then transmitted by the Link Layer by passing through the
% network layer. Therefore, considering the receive delay as
% "advertisingInterval + delay".
receiveDelay = advertisingInterval + delay;

% Get group-addresses for relevant nodes
groupAddresses = getGroupAddresses(sourceDestinationPairs);

for nodeIdx = 1:totalNodes
    % Relay feature enabled/disabled
    if any(nodeIdx == relayNodeIDs)
        meshNodes(nodeIdx).NetworkLayer.Relay = true;
        nodeStates(nodeIdx) = 2;
    else
        meshNodes(nodeIdx).NetworkLayer.Relay = false;
        nodeStates(nodeIdx) = 1;
    end
    % Friend feature enabled
    if ~isempty(friendLowPowerPairs) && any(nodeIdx == friendLowPowerPairs(:, 1))
        nodeStates(nodeIdx) = 3;
    end
    % Low power feature enabled
    if ~isempty(friendLowPowerPairs) && any(nodeIdx == friendLowPowerPairs(:, 2))
        nodeStates(nodeIdx) = 4;
    end
    
    % Configure mesh stack at each node
    nodeAddress = dec2hex(meshNodes(nodeIdx).Identifier, 4);
    nodeGroupAddress = getNodeGroupAddress(nodeIdx, groupAddresses);
    
    % Configure advertising bearer over Link Layer
    meshNodes(nodeIdx).LLGAPBearer.ScanInterval = 8;
    % Advertising interval is an integer multiple of 5 milliseconds
    meshNodes(nodeIdx).LLGAPBearer.AdvertisingInterval = advertisingInterval/5;
    
    % Configure network layer
    meshNodes(nodeIdx).NetworkLayer.ElementAddresses = nodeAddress;
    meshNodes(nodeIdx).NetworkLayer.SubscriptionAddresses = nodeGroupAddress;

    % Configure transport layer
    meshNodes(nodeIdx).TransportLayer.ElementAddress = nodeAddress;
    meshNodes(nodeIdx).TransportLayer.SubscriptionAddresses = nodeGroupAddress;
    
    if ~isempty(friendLowPowerPairs) && any(nodeIdx == friendLowPowerPairs(:, 1))
        % Configure transport layer (Friend node)
        meshNodes(nodeIdx).TransportLayer.Feature = 'Friend';
        % Index corresponding to the Friend and Low Power node pairs
        friendIdx = friendLowPowerPairs(:,1) == nodeIdx;
        
        % Configure Low Power node addresses at network layer
        lpAddress = dec2hex(friendLowPowerPairs(friendIdx, 2), 4);
        lpGroupAddress = getNodeGroupAddress(friendLowPowerPairs(friendIdx, 2), groupAddresses);
        if iscell(lpGroupAddress)
            meshNodes(nodeIdx).NetworkLayer.LowPowerNodeAddresses = [lpGroupAddress {lpAddress}];
        else
            meshNodes(nodeIdx).NetworkLayer.LowPowerNodeAddresses = lpAddress;
        end
        
        % Configure timing parameters
        meshNodes(nodeIdx).TransportLayer.FriendAddress = lpAddress;
        meshNodes(nodeIdx).TransportLayer.Friend.FriendTimer.ReceiveDelay = receiveDelay;
        meshNodes(nodeIdx).TransportLayer.Friend.FriendTimer.ReceiveWindow = receiveWindow(friendIdx);
        meshNodes(nodeIdx).TransportLayer.Friend.FriendTimer.PollTimeout = pollTimeout(friendIdx);
        meshNodes(nodeIdx).TransportLayer.Friend.FriendTimer.StepTime = stepTime;
        meshNodes(nodeIdx).TransportLayer.Friend.FriendTimer.LinkLayerDelay = receiveDelay + delay;
        initialize(meshNodes(nodeIdx).TransportLayer.Friend.FriendTimer);
        
        % Enable relay feature at Friend node
        meshNodes(nodeIdx).NetworkLayer.Relay = true;
        
        % Update Link Layer configuration when friend feature is enabled
        meshNodes(nodeIdx).LLGAPBearer.ScanInterval = floor((receiveDelay+receiveWindow(friendIdx))/5);
    end
    
    if ~isempty(friendLowPowerPairs) && any(nodeIdx == friendLowPowerPairs(:, 2))
        % Configure transport layer (Low Power node)
        meshNodes(nodeIdx).TransportLayer.Feature = 'Low power';
        % Index corresponding to the Friend and Low Power node pairs
        lowPowerIdx = friendLowPowerPairs(:,2) == nodeIdx;
        
        % Configure timing parameters
        meshNodes(nodeIdx).TransportLayer.FriendAddress = dec2hex(friendLowPowerPairs(lowPowerIdx, 1), 4);
        meshNodes(nodeIdx).TransportLayer.LowPower.FriendTimer.ReceiveDelay = receiveDelay;
        meshNodes(nodeIdx).TransportLayer.LowPower.FriendTimer.ReceiveWindow = receiveWindow(lowPowerIdx);
        meshNodes(nodeIdx).TransportLayer.LowPower.FriendTimer.PollTimeout = pollTimeout(lowPowerIdx);
        meshNodes(nodeIdx).TransportLayer.LowPower.FriendTimer.StepTime = stepTime;
        initialize(meshNodes(nodeIdx).TransportLayer.LowPower.FriendTimer);
        
        % Disable relay feature at Low Power node
        meshNodes(nodeIdx).NetworkLayer.Relay = false;
        
        % Update Link Layer configuration when friend feature is enabled
        meshNodes(nodeIdx).LLGAPBearer.ScanInterval = floor(receiveWindow(lowPowerIdx)/5);
    end
    
    % Configure access layer
    meshNodes(nodeIdx).AccessLayer.ElementAddress = nodeAddress;
    meshNodes(nodeIdx).AccessLayer.SubscriptionAddresses = nodeGroupAddress;
    
    % Initialize advertising bearer
    init(meshNodes(nodeIdx).LLGAPBearer);
end

% Update node's state in the visualization
meshNetworkGraph.NodeState = nodeStates;

% Unique source nodes
uniqueSources = unique(sourceDestinationPairs(:, 1));

% Visualization supports maximum 3 sources only
if numel(uniqueSources) > 3
    if enableAnimation
        fprintf(['Visualization supports maximum 3 unique sources only. ' ...
            'Disabling the visualization.\n']);
    end
    enableAnimation = false;
end

% Visualization doesn't support Low Power node as source node
for idx = 1:numel(uniqueSources)
    if ~isempty(friendLowPowerPairs) && any(uniqueSources(idx) == friendLowPowerPairs(:, 2))
        if enableAnimation
            fprintf(['Visualization does not support Low Power node as source node. ' ...
                'Disabling the visualization.\n']);
        end
        enableAnimation = false;
        break;
    end
end

if enableVisualization
    % Assign Friend & Low Power node pairs and source & destination node
    % pairs to visualization object
    if ~isempty(friendLowPowerPairs)
        meshNetworkGraph.FriendPairs = friendLowPowerPairs;
    end
    if ~isempty(sourceDestinationPairs) && enableAnimation
        % Update source & destination node pairs as groups for
        % visualization
        groupSrcDsts = zeros(numel(uniqueSources), totalNodes);
        for gIdx = 1:numel(uniqueSources)
            groupSrcDsts(gIdx, 1) = uniqueSources(gIdx);
            dstGroup = sourceDestinationPairs((...
                sourceDestinationPairs(:, 1) == uniqueSources(gIdx)), 2);
            groupSrcDsts(gIdx, 2:numel(dstGroup)+1) = dstGroup;
        end
        meshNetworkGraph.SrcDstPairs = groupSrcDsts;
    end
    % Update simulation time in the visualization
    meshNetworkGraph.SimulationTime = simulationTime;
    
    % Visualize the network
    meshNetworkGraph.createNetwork();
    % Initialize the progress bar with zero percentage (0%)
    updateProgressBar(meshNetworkGraph, 0);
end

% Validate Friend & Low Power node pairs
validateFNLPPairs(friendLowPowerPairs, meshNetworkGraph.Positions, ...
            meshNetworkGraph.VicinityRange);

% Configure application data at each unique source node
for srcIdx = 1:numel(uniqueSources)
    srcNode = uniqueSources(srcIdx);
    dstNodes = sourceDestinationPairs(sourceDestinationPairs(:, 1) == srcNode, 2);
    
    % Destination node is a Low Power node or destination address is a
    % group address
    if (~isempty(friendLowPowerPairs) && hasLPNode(dstNodes, friendLowPowerPairs(:, 2))) ...
            || numel(dstNodes) > 1
        % Message type at model layer to be transmitted - unacknowledged
        messageType = 'Generic onOff set unacknowledged';
    else
        % Message type at model layer to be transmitted - acknowledged
        messageType = 'Generic onOff set';
    end
    
    % Model PDU ('Generic onOff set') from mesh generic model
    [messageType, genericPDU] = helperBLEMeshAppGenericPDU(messageType, 1, 1, 10, 2, 5);
    
    % Configure publish address at access layer
    if (numel(dstNodes) == 1)
        meshNodes(srcNode).AccessLayer.PublishAddress = dec2hex(dstNodes, 4);
        meshNodes(srcNode).AccessLayer.TTL = ttl(srcIdx);
    else
        groupAddress = getCurrentGroupAddress(dstNodes, groupAddresses);
        meshNodes(srcNode).AccessLayer.PublishAddress = groupAddress;
        meshNodes(srcNode).AccessLayer.TTL = max(ttl(sourceDestinationPairs(:, 1) == srcNode));
    end
    
    % Push model PDU into access layer input queue
    meshNodes(srcNode).AccessLayer.pushModelMessage(messageType, genericPDU);
end

% Network messages originated at each node in the mesh network
meshNetworkChannel = cell(totalNodes, 5);
for chIdx = 1:totalNodes
    % Mesh nodes
    meshNetworkChannel{chIdx, 1} = meshNodes(chIdx);
    % Previous channel indexes
    meshNetworkChannel{chIdx, 2} = -1;
    % Previous mesh message
    meshNetworkChannel{chIdx, 3} = zeros(1, 0);
    % Current channel indexes
    meshNetworkChannel{chIdx, 4} = -1;
    % Current mesh message
    meshNetworkChannel{chIdx, 5} = zeros(1, 0);
end

% Buffer to store all the message exchanges between mesh nodes within the
% network
maxTuples = totalNodes*totalNodes;
totalTuples = cell(maxTuples, 3);
tupleIndex = 1;

% Run simulation
for timerTicks = 1:stepTime:simulationTime
    % Buffer to store message transmissions as tuples (srcNode, dstNode)
    messageTuples = zeros(totalNodes*2, 3);
    
    % Index for message transmissions
    messageTransmissionsIdx = 1;
    
    pathPairIDs = cell(totalNodes, 3);
    
    pathPairIndex = 1;
    
    destinationFlag = false;
    
    % Update each node state
    for nodeIndex = 1:totalNodes
        curNode = meshNodes(nodeIndex);
        
        % Get indexes of the current node and indexes of messages within
        % the vicinity of the 'curNode'
        [curIdx, vicinityNodeIdxs] = helperBLEMeshNetworkChannelMessage(...
            curNode, meshNetworkChannel, meshNetworkGraph.Positions, ...
            meshNetworkGraph.VicinityRange);
        
        % No messages are transmitted by the vicinity nodes
        if isempty(vicinityNodeIdxs)
            % Pass empty message to 'curNode'
            [meshNetworkChannel{curIdx, 4}, meshNetworkChannel{curIdx, 5}] = curNode.run(stepTimeUs, zeros(1, 0));
        else
            % Fetch the vicinity node indexes transmitting in the same
            % channel in which the current node is listening
            transmitVicinityNodeIdxs = zeros(1, totalNodes);
            transmitVicinityIdx = 1;
            for idx = 1:numel(vicinityNodeIdxs)
                if (meshNetworkChannel{vicinityNodeIdxs(idx), 2} == curNode.LLGAPBearer.pChannelIndex)
                    transmitVicinityNodeIdxs(transmitVicinityIdx) = vicinityNodeIdxs(idx);
                    transmitVicinityIdx = transmitVicinityIdx + 1;
                end
            end
            transmitVicinityNodeIdxs = transmitVicinityNodeIdxs(1:transmitVicinityIdx-1);
            
            % Only one vicinity node is transmitting to the current node in
            % the same channel
            if numel(transmitVicinityNodeIdxs) == 1
                % Message from the vicinity node
                meshMessageRx = meshNetworkChannel{transmitVicinityNodeIdxs(1), 3};
                
            % Nothing is transmitted by any of the vicinity nodes to the
            % current node in the same channel
            elseif numel(transmitVicinityNodeIdxs) == 0
                % Empty message
                meshMessageRx = zeros(1, 0);
            % Multiple nodes in the vicinity are transmitting to the
            % current node in the same channel
            else
                % Corrupt the message as more than one node is transmitting
                % in the same channel at the same time
                meshMessageRx = meshNetworkChannel{transmitVicinityNodeIdxs(1), 3};
                meshMessageRx(end-1:end) = ~meshMessageRx(end-1:end);
                
                % Message identifier for identifying the corrupted mesh
                % message. Here, -1 is used because it is not a valid
                % identifier. Standard specifies that identifiers are
                % represented in octets. So, only positive integers are
                % valid.
                msgID = -1;
                
                for cor = 1:numel(transmitVicinityNodeIdxs)
                    % Update corrupted message tuples for the visualization
                    messageTuples(messageTransmissionsIdx, :) = [...
                        meshNetworkChannel{transmitVicinityNodeIdxs(cor), 1}.Identifier ...
                        curNode.Identifier msgID];
                    
                    % Update index
                    messageTransmissionsIdx = messageTransmissionsIdx + 1;
                end
            end
            
            % Pass the transmitted message from the vicinity node to 'curNode'
            [meshNetworkChannel{curIdx, 4}, meshNetworkChannel{curIdx, 5}] = curNode.run(stepTimeUs, meshMessageRx);
            
            if numel(transmitVicinityNodeIdxs) == 1
                % Get current message identifier for visualization
                octets = curNode.LLGAPBearer.pMessageIdentifier';
                % Calculate unique integer from the message identifier at
                % the network layer
                msgID = 0;
                for k = 1 : 5
                    msgID = msgID + (octets(end-(k-1))*(10^(k-1)));
                end
                % Update message tuples for the visualization
                messageTuples(messageTransmissionsIdx, :) = [...
                    meshNetworkChannel{transmitVicinityNodeIdxs(1), 1}.Identifier ...
                    curNode.Identifier msgID];
                
                % Update index
                messageTransmissionsIdx = messageTransmissionsIdx + 1;
            end
            
            % Message processed at 'curNode'
            if curNode.NetworkLayer.pMessageDestinedToNode || ...
                    curNode.NetworkLayer.pMessageRelayedFromNode
                % Update the buffer with recent message exchanges between
                % nodes within the mesh network
                totalTuples(tupleIndex, :) = {...
                    messageTuples(messageTransmissionsIdx-1, 1) ...
                    messageTuples(messageTransmissionsIdx-1, 2) ...
                    curNode.LLGAPBearer.pMessageIdentifier};
                
                if tupleIndex > maxTuples
                    tupleIndex = 1;
                else
                    tupleIndex = tupleIndex + 1;
                end
            end
            
            % Set flag if the destination reached
            if curNode.NetworkLayer.pMessageDestinedToNode
                srcNode = hex2dec(curNode.NetworkLayer.pDecodedSRC);
                dstNode = curNode.Identifier;
                % Source destination pair matched and path is not yet
                % identified
                if isSourceDestinationPair(srcDstPairs, [srcNode dstNode])
                    destinationFlag = true;
                    
                    % Store the source & destination node pair IDs
                    pathPairIDs{pathPairIndex, 1} = srcNode;
                    pathPairIDs{pathPairIndex, 2} = dstNode;
                    pathPairIDs{pathPairIndex, 3} = curNode.LLGAPBearer.pMessageIdentifier;
                    pathPairIndex = pathPairIndex + 1;
                end
            end
        end
        if enableVisualization && enableAnimation && ~isempty(friendLowPowerPairs) && ...
                any(curNode.Identifier == friendLowPowerPairs(:, 2))
            % Update the state of Low Power node in visualization
            lpnState = curNode.LLGAPBearer.pState;
            updateLowPowerNodeState(meshNetworkGraph, curNode.Identifier, ...
                lpnState);
        end
    end
    
    % Visualize the animation
    if messageTransmissionsIdx > 1 && enableVisualization && enableAnimation
        % Visualize message transmissions
        messageTransmissions(meshNetworkGraph, messageTuples(1:messageTransmissionsIdx-1, :));
        pause(0.1);
    end
    
    % Update the network messages
    meshNetworkChannel(:, 2) = meshNetworkChannel(:, 4);
    meshNetworkChannel(:, 3) = meshNetworkChannel(:, 5);
    meshNetworkChannel(:, 4) = cell(totalNodes, 1);
    meshNetworkChannel(:, 5) = cell(totalNodes, 1);
    
    % Highlight destination
    if destinationFlag
        % Update destination IDs
        pathPairIDs = pathPairIDs(1:pathPairIndex-1, :);
    
        % Blink destination node in the visualization
        for k = 1:pathPairIndex-1
            % Fetch source & destination node pair
            srcNode = pathPairIDs{k, 1};
            dstNode = pathPairIDs{k, 2};
            pathID = pathPairIDs{k, 3};
            
            % Filter tuples by source & destination node pair
            tupleSet = zeros(totalNodes*2, 2);
            tupleSetIndex = 1;
            for m = 1:maxTuples
                if ~isempty(totalTuples{m, 1}) && all(totalTuples{m, 3} == pathID)
                    tupleSet(tupleSetIndex, :) = [totalTuples{m, 1} totalTuples{m, 2}];
                    tupleSetIndex = tupleSetIndex + 1;
                end
            end
            
            % Path derivation from source node to destination node
            path = helperBLEMeshPath(srcNode, dstNode, meshNodes, tupleSet(1:tupleSetIndex-1, :));
            
            % Store path
            paths(pathIndex) = {path};
            pathIndex = pathIndex + 1;
            
            % Highlight path in the visualization
            if enableVisualization && enableAnimation && all(path ~= -1)
                showPath(meshNetworkGraph, path);
            end
        end
    end
    % Update progress bar
    if enableVisualization
        if (rem((timerTicks/simulationTime)*100, 1) == 0)
            updateProgressBar(meshNetworkGraph, timerTicks);
        end
    end
end

% Cell array of paths (each element represents the path between a pair of
% source & destination nodes)
paths = paths(1:pathIndex-1);

% Update path cell array in the order of srcDstPairs specified
pathInOrder = cell(1, numel(srcDstPairs)/2);
for pathIdx = 1:numel(paths)
    cmpMatch = (srcDstPairs == [paths{pathIdx}(1) paths{pathIdx}(end)]);
    index = sum(cmpMatch, 2) == 2;
    pathInOrder{index} = paths{pathIdx};
end
for pathIdx = 1:numel(pathInOrder)
    if isempty(pathInOrder{pathIdx})
        pathInOrder{pathIdx} = -1;
    end
end
paths = pathInOrder;

% Collect statistics of each node
nodeStatistics = cell(1, totalNodes);
for k = 1:totalNodes
    lpnRx = 0;
    if strcmp(meshNodes(k).State, 'On')
        if meshNodes(k).NetworkLayer.Relay
            type = 'Relay';
        else
            type = 'End';
        end
        if strcmp(meshNodes(k).TransportLayer.Feature, 'Friend')
            type = 'Friend';
            lpnRx = meshNodes(k).TransportLayer.ReceivedLPNMsgs;
        elseif strcmp(meshNodes(k).TransportLayer.Feature, 'Low power')
            type = 'Low power';
        end
        stats = {'Node ID', meshNodes(k).Identifier, ...
            'Node Type', type, ...
            'Tx', meshNodes(k).LLGAPBearer.TransmittedMsgs, ...
            'Rx', meshNodes(k).LLGAPBearer.ReceivedMsgs, ...
            'LPN Rx', lpnRx, ...
            'App Rx', meshNodes(k).AccessLayer.ReceivedMsgs, ...
            'Relayed', meshNodes(k).NetworkLayer.MsgsRelayed, ...
            'Dropped', meshNodes(k).NetworkLayer.MsgsDropped, ...
            'CRC Failed', meshNodes(k).LLGAPBearer.CRCFailedMsgs, ...
            'Tx Bytes', meshNodes(k).LLGAPBearer.BytesTransmitted, ...
            'Rx Bytes', meshNodes(k).LLGAPBearer.BytesReceived, ...
            'Sleep Time (milliseconds)', meshNodes(k).LLGAPBearer.SleepTime, ...
            'Idle Time (milliseconds)', meshNodes(k).LLGAPBearer.IdleTime, ...
            'Listen Time (milliseconds)', meshNodes(k).LLGAPBearer.ListenTime, ...
            'Transmission Time (milliseconds)', (meshNodes(k).LLGAPBearer.BytesTransmitted*8)*0.001};
        nodeStatistics{k} = stats;
    end
end

if enableVisualization
    % Update statistics in visualization object
    meshNetworkGraph.updateNodeStatistics(nodeStatistics);
end
end


% Validate Friend & Low Power node pairs
function validateFNLPPairs(fnLPPairs, positions, vicinityRange)
% Current implementation supports Friendship between only one Friend node
% and one Low Power node.
if numel(unique(fnLPPairs)) ~= numel(fnLPPairs)
    error(['Current implementation supports "Friendship establishment" ' ...
        'between one Friend node and one Low Power node'])
else
    for pairIdx = 1:numel(fnLPPairs)/2
        vicinityNodes = helperBLEMeshVicinityNodes(fnLPPairs(pairIdx, 1), positions, ...
            vicinityRange);
        if ~any(fnLPPairs(pairIdx, 2) == vicinityNodes)
            error(['Friend node and Low Power node pair (%d, %d) ' ...
                'should be within the vicinity.\n'], fnLPPairs(pairIdx, 1), ...
                fnLPPairs(pairIdx, 2));
        end
    end
end
end

% Get group addresses for the relevant nodes
function groupAddresses = getGroupAddresses(sourceDestinationPairs)

% Unique source nodes
uniqueSourceNodes = unique(sourceDestinationPairs(:, 1));
totalElements = numel(uniqueSourceNodes);

% Initialize
groupAddresses = cell(totalElements, 2);
groupIdx = 1;

for elementIdx = 1:totalElements
    % Indexes of the nodes receiving data from the same source node
    matchedIdxs = sourceDestinationPairs(:, 1) == uniqueSourceNodes(elementIdx);
    if nnz(matchedIdxs) >= 2
        % Group the nodes receiving data from the same source node
        groupAddresses{groupIdx, 1} = sourceDestinationPairs(matchedIdxs, 2);
        % Get a random group address
        groupAddresses{groupIdx, 2} = getRandomGroupAddress();
        % Update group index
        groupIdx = groupIdx + 1;
    end
end

% Return group addresses
groupAddresses = groupAddresses(1:groupIdx-1, :);
end


function groupAddress = getRandomGroupAddress()
groupBits = [1 1 randi([0 1], 1, 14)];
groupAddress = dec2hex(bi2de(groupBits, 'left-msb'), 4);
end

% Get group addresses of the given node
function nodeGroupAddress = getNodeGroupAddress(nodeID, groupAddresses)
% Initialize
numGroups = numel(groupAddresses)/2;
nodeGroupAddress = cell(numGroups);
groupIdx = 1;

% Fetch the group addresses of the given node
for idx = 1:numGroups
    groupNodes = groupAddresses{idx, 1};
    if any(groupNodes == nodeID)
        nodeGroupAddress{groupIdx} = groupAddresses{idx, 2};
        groupIdx = groupIdx + 1;
    end
end

% Return group addresses of the given node
if groupIdx == 1
    nodeGroupAddress = 'FFFF';
else
    nodeGroupAddress = nodeGroupAddress(1:groupIdx-1);
end
end

% Get group address of the given group
function groupAddress = getCurrentGroupAddress(destNodes, groupAddresses)
groupAddress = '0000';
for groupIdx = 1:numel(groupAddresses)/2
    if all(groupAddresses{groupIdx, 1} == destNodes)
        groupAddress = groupAddresses{groupIdx, 2};
    end
end
end

% Whether destination node is a Low Power nodes or not
function flag = hasLPNode(dstNodes, lpNodes)
% Initialize
flag = false;
for dstIdx = 1:numel(dstNodes)
    if any(dstNodes(dstIdx) == lpNodes)
        flag = true;
        break;
    end
end
end

% Get unique pairs from the given pairs
function uniquePairs = uniqueMeshNodePairs(inputPairs)
% Initialize
uniqueSrcDstNodes = zeros(numel(inputPairs)/2, 2);
uniqueIdx = 1;

% Fetch unique mesh node pairs (source & destination node pair)
for pairIdx = 1:(numel(inputPairs)/2)
    isUniquePair = true;
    pair = inputPairs(pairIdx, :);
    for srcIdx = 1:(numel(uniqueSrcDstNodes)/2)
        if unique(uniqueSrcDstNodes(srcIdx, :) == pair) == 1
            isUniquePair = false;
            break;
        end
    end
    if isUniquePair
        uniqueSrcDstNodes(uniqueIdx, :) = pair;
        uniqueIdx = uniqueIdx + 1;
    end
end
% Return unique pairs
uniquePairs = uniqueSrcDstNodes(1:uniqueIdx-1, :);
end

% Fetch the presence of source destination pair
function flag = isSourceDestinationPair(sdPair, sd)
% Initialize
flag = false;

% Compare source destination pair
sdComp = sdPair == sd;

% Fetch the presence of source destination pair
for sdIdx = 1:numel(sdComp)/2
    if all(sdComp(sdIdx, :))
        flag = true;
        break;
    end
end
end

% Update source destination pairs for identifying the path
function srcDstPairs = pairsForPath(sourceDestinationPairs, fnLPPairs)
srcDstPairs = sourceDestinationPairs;
% Update the destination as Friend node when the destination is a Low Power
% node
for idx = 1:numel(srcDstPairs)/2
    if any(srcDstPairs(idx, 2) == fnLPPairs(:, 2))
        srcDstPairs(idx, 2) = fnLPPairs(srcDstPairs(idx, 2) == fnLPPairs(:, 2), 1);
    end
end
end
