classdef DesignStore < handle
    % DESIGNSTORE captures the relationships between user driven events in
    % FixedPointTool workflows
    %
    % DesignStore will live under the FPTDataset and will contain a
    % tree where the root represents the original state of the model
    %
    % The graph consists of Nodes representing states of a model or
    % workflow at a given time and an Edge representing the dependency
    % between those two states
    %
    % Nodes:
    %   Design - the state of a model before and after all workflow steps
    %   Action - workflow step taken
    % Edges:
    %   DesignEdge - directed connection between two Designs
    %   WorkflowEdge - directed connection between any node and an Action
    
    % Copyright 2018-2019 The MathWorks, Inc.
    
    properties(Constant, Hidden)
        NodeVariableNames = {'Name', 'NodeObject'}; % Header for NodeTable
        EdgeVariableNames = {'EndNodes', 'EdgeObject'}; % Header for EdgeTable
    end
    
    properties(Hidden, GetAccess = public, SetAccess = private)
        Graph (1,1) digraph;
        NumberNodes = 0;
    end
    
    methods
        
        function this = DesignStore()
            edgeTable = table(string.empty(0,2), fxptds.WorkflowTopology.element.Edge.empty, 'VariableNames',this.EdgeVariableNames);
            nodeTable = table(string.empty(0,1), fxptds.WorkflowTopology.element.Node.empty, 'VariableNames',this.NodeVariableNames);
            this.Graph = digraph(edgeTable, nodeTable);
        end
        
    end
    
    methods(Hidden)
        
        function add(this, nodeObject)
            % add node to the graph
            t = cell2table({nodeObject.Name, nodeObject}, 'VariableNames', this.NodeVariableNames);
            this.Graph = addnode(this.Graph, t);
            this.NumberNodes = this.NumberNodes + 1;
        end
        
        function connect(this, sourceName, targetName)
            % connect takes the source node name (char) and target node
            % name (char) and creates an edge object with a unique ID then
            % adds the edge to the edge table
            
            % sourceNode -> targetNode
            sourceNode = this.getNodeObject(sourceName);
            targetNode = this.getNodeObject(targetName);
            % create an edge object to connect
            % EdgeObjects currently do not make use of their DesignStoreID
            % so newID is given a dummy value from the DesignStore. If an
            % edge was to make use of the ID, a new generateID method
            % should be written specific to Edges
            newID = '';
            edgeObject = fxptds.WorkflowTopology.element.ElementFactory.createEdge(sourceNode, targetNode, newID);
            % add edge to table
            t = cell2table({{sourceNode.Name, targetNode.Name}, edgeObject}, 'VariableNames',this.EdgeVariableNames);
            this.Graph = addedge(this.Graph, t);
            
        end
        
        function rename(this, originalName, newName)
            % renames the node by updating the Name column in node table
            % and in the NodeObject
            
            % find the node index
            nodeIdx = findnode(this.Graph, originalName);
            
            % get the node object and overwrite the name there as well
            nodeObject = this.getNodeObject(originalName);
            nodeObject.overwriteName(newName);
            
            % overwrite the node name in the table based on the Node object
            % this is to ensure consistency between the name in the table
            % adn in the object
            this.updateName(nodeIdx, newName);
        end
        
        function invalidate(this, nodeName)
            % invalidate the node and its name
            
            % find the node object
            node = this.getNodeObject(nodeName);
            invalidate(node);
            
            % after a node is invalidated, its Name changes to a unique
            % value, capture the new Name and re-write that new Name to the
            % Node table
            newName = node.Name;
            this.updateName(nodeName, newName);
        end        
        
        function b = doesEdgeExist(this, sourceDesign, targetDesign)
            % returns logical whether the two designs are connected by an
            % edge
            b = false;
            if this.doesNodeExist(sourceDesign) && this.doesNodeExist(targetDesign)
                b = findedge(this.Graph, sourceDesign, targetDesign) > 0;
            end
        end
        
        function b = doesNodeExist(this, node)
            % node can be an object or a char
            % returns logical whether the element exists as a node in the
            % graph.
            b = findnode(this.Graph, node) > 0;
        end
        
        function parentMembers = getParents(this, childName)
            % returns the name of the node's parent
            parentMembers = "";
            if this.doesNodeExist(childName)
                parents = predecessors(this.Graph, childName);
                parentMembers = string(parents);
            end
            
        end
        
        function branchMembers = getWorkflowBranch(this, sourceNodeName)
            % returns the names of the nodes connected to sourceNodeName,
            % or empty string if no path exists
            branchMembers = "";
            if this.doesNodeExist(sourceNodeName)
                branchMembers = bfsearch(this.Graph, sourceNodeName);
                branchMembers = string(branchMembers);
            end
        end
        
        function pathMembers = getWorkflowPath(this, sourceNodeName, targetNodeName)
            % returns the names of the nodes of the shortest path from the
            % sourceNodeName to targetNodeName, or empty string if no path
            % exists
            pathMembers = "";
            if this.doesNodeExist(sourceNodeName) && this.doesNodeExist(targetNodeName)
                pathMembers = shortestpath(this.Graph, sourceNodeName, targetNodeName);
                pathMembers = string(pathMembers);
            end
        end
        
        function newID = generateID(this)
            % increment the number of nodes in the Design store by one to
            % createa a unique integer that when encoded as a string, will
            % not clash with the TopModel name or any FPTRunID
            % concatenated with an underscore solves this
            % ex. 12 -> '_12'; 345 - > '_345'
            newID = ['_', num2str(this.NumberNodes + 1)];
        end
        
        function nodeObject = getNodeObject(this, nodeNames)
            % if nodeNames may be a cell array of names, nodeObject will be
            % an array of nodes
            nodeIdx = findnode(this.Graph, nodeNames);
            nodeObject = this.Graph.Nodes(nodeIdx,:).NodeObject;
        end
        
    end    
    
    
    % Private and/or for Test only APIs
    % Methods that return a node object, a table of objects or can make
    % changes to the table should be Private methods only
    methods(Access = ?WorkflowTopologyTestCase)
        
        function updateName(this, oldName, newName)
            % changes the Name column in the node table
            nodeIdx = findnode(this.Graph, oldName);
            this.Graph.Nodes(nodeIdx,:).Name = {newName};
        end
        
        function nodeTable = getNodes(this)
            % returns the Graph's Node table
            nodeTable = this.Graph.Nodes;
        end
        
        function edgeTable = getEdges(this)
            % returns the Graph's Edge table
            edgeTable = this.Graph.Edges;
        end
        
    end
    
end
