classdef Block < bioinfo.pipeline.internal.SemanticModelObject
    
    
    properties (SetAccess={?bioinfo.pipeline.Block, ?bioinfo.pipeline.internal.designer.PipelineController})
        % INPUTS Block input ports. INPUTS is a struct whose
        % fields are the names of input ports, and whose values are
        % bioinfo.pipeline.Input objects describing the input port behavior.
        % These input port names are the expected fields of the input struct
        % given to the RUN method of the block.
        %
        % See also bioinfo.pipeline.Outputs, bioinfo.pipeline.Block.run, bioinfo.pipeline.Block
        Inputs(1,1) struct {bioinfo.pipeline.Block.mustBeInputsStruct} = struct()
        
        % OUTPUTS Block output ports. OUTPUTS is a struct whose
        % fields are the names of the output ports, and whose values are
        % bioinfo.pipeline.Output objects describing the output port behavior.
        % These output port names are the expected fields of the output struct
        % from the RUN method of the block.
        %
        % See also bioinfo.pipeline.Inputs, bioinfo.pipeline.Block.run, bioinfo.pipeline.Block
        Outputs(1,1) struct {bioinfo.pipeline.Block.mustBeOutputsStruct} = struct()
    end
    
    properties
        % ErrorHandler Function handle specifying the function to call if
        % the RUN method of the block errors within a pipeline. In order
        % for a pipeline to continue after a block fails, ErrorHandler must
        % return a struct compatible with the output ports of the block.
        % The error handling function is called with the following input
        % arguments:
        %  - a structure with the fields: "identifier", "message", and
        %    "index", containing the identifier of the error
        %    that occurred, the text of the error message, and the linear
        %    index indicating which parallel RUN method failed, respectively.
        %    By default, the index will always be 1, as there is only one
        %    run call per block. For details on how block inputs can be
        %    split across different dimensions for multiple run calls, type
        %    help bioinfo.pipeline.Input.SplitDimension
        %  - The input struct passed to RUN when it failed.
        %
        % Example: 
        % 
        % p = bioinfo.pipeline.Pipeline;
        % FRB = bioinfo.pipeline.blocks.UserFunction("Info = fastaread(File)"));
        % FRB.ErrorHandler = @(errInfo, runInputs) struct('Info', struct('Header', {}, 'Sequence', {}));
        % addBlock(p, FRB);
        %
        % p.run(struct('File', 'missing.txt'));
        % results(p, FRB).Info
        % 
        % See also run, bioinfo.pipeline.Block,
        % bioinfo.pipeline.Input.SplitDimension
        ErrorHandler function_handle {mustBeScalarOrEmpty} = function_handle.empty;
    end

    properties (Access = private, Transient)
        % ActiveTransactionCleanup is either empty, or a WeakHandle to an onCleanup
        % object owned by a function that called beginModelChangeTransaction.
        % When the cleanup object is destroyed, the Block emits the
        % semantic model changes since the transaction was started. If
        % ActiveTransaction is empty or points to a destroyed cleanup
        % object, then beginModelTransaction creates a new cleanup object
        % and WeakHandle to it.
        ActiveTransactionCleanup = []
        InputsListeners(1,1) struct = struct();
    end
    
    methods (Abstract)
        % EVAL Evaluation method for block objects.
        %
        % This method is a customized evaluation method of a block, and it
        % represents a single unit of work within a pipeline.
        %
        % This method must have the following signature:
        %
        % OUTSTRUCT = eval(B, INSTRUCT) where INSTRUCT is a scalar struct
        % whose fieldnames are the names of inputs to B. For example if B
        % has an input X, then an input struct might be struct("X",
        % 1). Similarly, OUTSTRUCT is a scalar struct representing outputs
        % of the block. The fieldnames of OUTSTRUCT are names of outputs of B.
        %
        % This function signature is enforced by both run methods of the
        % pipeline and block during the execution of a pipeline, but eval
        % can be run outside a pipeline for debugging purposes. Note that
        % the eval method of a block should not modify any properties of
        % the block, so that the pipeline can skip execution of the block
        % when the inputs are the same as a previous run.
        %
        % Example:
        %
        % zerosBlock = bioinfo.pipeline.blocks.UserFunction("A = zeros(M,N)");
        % eval(zerosBlock, struct("M", 2, "N", 3))
        %
        % See also bioinfo.pipeline.Block.run
        eval(obj)
    end

    methods (Hidden)
        function tf = isStaleComparedTo(A, B)
            % isStaleComparedTo returns true if block object A should be considered
            % stale relative to block B. By default, this check is equivalent to
            % ~isequaln, but subclasses of block can alter this behavior to exclude
            % particular properties.
            tf = ~isequaln(A, B);
        end
    end
    
    methods (Sealed, Hidden)
        function tf = eq(A, B)
            %EQ Determine whether two arrays of blocks are equal.
            %
            % TF = eq(A, B)
            
            % Determines whether a set of blocks are equal. This must be
            % defined for a class that can create heterogeneous arrays.
            tf = eq@handle(A, B);
        end
        
        function tf = ne(A, B)
            %NE Determine whether two arrays of blocks are not equal.
            %
            % TF = neq(A, B)
            
            % Determines whether a set of blocks are unequal. This must
            % be defined for a class that can create heterogeneous arrays.
            tf = ne@handle(A, B);
        end
        
        function tf =  hasInputNamed(obj, name)
            %HASINPUTNAMED Check whether the block has an input named
            % NAME.
            %
            % TF = HASINPUTNAMED(OBJ, NAME) returns a logical "true" if
            % block OBJ has an input named NAME. TF is "false" otherwise.
            
            arguments
                obj(1,1) bioinfo.pipeline.Block
                name(1,1) string
            end
            
            tf = isfield(obj.Inputs, name);
            
        end
        
        function tf = hasOutputNamed(obj, name)
            %HASOUTPUTNAMED Check whether the block has an output named
            % NAME.
            %
            % TF = HASOUTPUTNAMED(OBJ, NAME) returns a logical "true" if
            % block OBJ has an output named NAME. TF is "false" otherwise.
            
            arguments
                obj(1,1) bioinfo.pipeline.Block
                name(1,1) string
            end
            
            tf = isfield(obj.Outputs, name);
        end
        
        function requiredInputs = getRequiredInputs(obj)
            % GETREQUIREDINPUTS returns the names of ports that must be
            % connected to run in a Pipeline.
            %
            % RP = GETREQUIREDINPUTS(B) returns the names of the
            % required ports for B as a string vector RP.
            %
            % An input is required if Required is set to true, and there is
            % the input does not have a set value
            
            arguments
                obj(1,1) bioinfo.pipeline.Block
            end
            
            % Convert to array of inputs
            inputNames = string(fieldnames(obj.Inputs));
            
            if isempty(inputNames)
                requiredInputs = inputNames;
                return;
            end
            
            inputs = struct2cell(obj.Inputs);
            inputs = [inputs{:}];
            
            
            unsetInputs = arrayfun(@(i) isequal(i.Value, bioinfo.pipeline.datatypes.Unset), inputs);
            requiredInputs = [inputs.Required] & unsetInputs;
            requiredInputs = inputNames(requiredInputs);
        end
        
        function requiredOutputs = getRequiredOutputs(obj)
            % GETREQUIREDOUTPUTS returns the names of ports that must be
            % present in the output struct of eval for the call to be
            % considered successful.
            %
            % RP = GETREQUIREDOUTPUTS(B) returns the names of the
            % required ports for B as a string vector RP.
            %
            % An output is required if Required is set to true.
            
            outputNames = string(fieldnames(obj.Outputs));
            requiredOutputs = cellfun(@(outp) outp.Required, struct2cell(obj.Outputs));
            
            requiredOutputs = outputNames(requiredOutputs);
        end
        
    end
    
    methods(Sealed, Access = private)
        function runInputStruct = addSetInputValues(obj, runInputStruct)
            % addSetInputValues add any preset Values of input ports to the
            % runInputStruct. See bioinfo.pipeline.Input.Value
            arguments
                obj(1,1) bioinfo.pipeline.Block
                runInputStruct(1,1) struct
            end
            
            % Loop over inputs, and update the input struct
            inputNames = string(fieldnames(obj.Inputs));
            inputPorts = struct2cell(obj.Inputs);
            
            for i = 1:numel(inputNames)
                % If the input port has a set value, and the
                % inputStruct does not include this value, add it to the
                % input struct.
                inputPort = inputPorts{i};
                inputPortName = inputNames(i);
                
                if ~isequal(inputPort.Value, bioinfo.pipeline.datatypes.Unset) && ...
                        ~isfield(runInputStruct, inputPortName)
                    
                    runInputStruct.(inputPortName) = inputPort.Value;
                end
            end
        end

        function data = semanticPortDiff(parentBlock, previousPorts, newPorts)
            % Create a SemanticModelChangedEventData of the difference
            % between two port structs
                        
            previousPortNames = string(fieldnames(previousPorts));
            previousPortObjects = arrayfun(@(ppn) previousPorts.(ppn), previousPortNames);

            newPortNames = string(fieldnames(newPorts));
            newPortObjects = arrayfun(@(npn) newPorts.(npn), newPortNames);

            % Any ports present in both lists were kept, and we want to
            % detect any renames. 
            [renamedPortObjects, iPreviousName, iNewName] = intersect(previousPortObjects, newPortObjects);

            nameChanges = [previousPortNames(iPreviousName), newPortNames(iNewName)]; 

            % Filter out ports that were kept, but maintained the existing names           
            unmodifiedPorts = nameChanges(:,1) == nameChanges(:,2);
            renamedPortObjects(unmodifiedPorts) = [];
            nameChanges(unmodifiedPorts,:) = [];

            % Remove renamed ports. Any remaining in previousPortNames were
            % deleted, while any remaining in newPortNames were added. Note
            % that there may be a port name that was both added and
            % removed, but this different from renaming in that the
            % connections to those ports should be discarded, not
            % reconnected.

            % Ports that were not renamed were removed, and new ports that
            % didn't exist were added
            previousPortObjects(iPreviousName) = [];
            previousPortNames(iPreviousName) = [];

            newPortObjects(iNewName) = [];
            newPortNames(iNewName) = [];

            % Construct Event Data
            data = bioinfo.pipeline.internal.SemanticModelChangedEventData();

            for a = 1:numel(newPortObjects)
                data.add(newPortObjects(a), parentBlock, newPortNames(a));
            end

            for d = 1:numel(previousPortObjects)
                data.remove(previousPortObjects(d), parentBlock, previousPortNames(d));
            end

            for r = 1:numel(renamedPortObjects)
                data.rename(renamedPortObjects(r), parentBlock, nameChanges(r,:));
            end
 
        end
        
        function handleInputPropertyChanged(obj, inputName, pvcEvent)
            if pvcEvent.PropertyName == "Value"
                % Create ValueSet event
                smcData = bioinfo.pipeline.internal.SemanticModelChangedEventData;
                smcData.valueSet(pvcEvent.Source, obj, inputName, pvcEvent.PreSetValue, pvcEvent.PostSetValue);
                notify(obj, 'SemanticModelChanged', smcData);
            end
        end
    end
    
    methods
        function compile(~)
            % COMPILE May be implemented by subclasses of Block to
            % perform additional actions or validation during COMPILE,
            % rather than during EVAL. For example, a block may validate
            % relationships between properties. This method is called at
            % during the parent pipeline's COMPILE, and should throw errors
            % if compilation is not successful.
        end

        function set.Inputs(obj, value)
            cleanup = obj.beginModelChangeTransaction();
            obj.Inputs = value;

            % Re-generate listeners for each input
            inputNames = fieldnames(obj.Inputs);
            obj.InputsListeners = struct();
            for in = 1:numel(inputNames)
                inputName = inputNames{in};
                obj.InputsListeners.(inputNames{in}) = listener(obj.Inputs.(inputName), 'PropertyValueChanged', ...
                                    @(src, evt) obj.handleInputPropertyChanged(inputName, evt));
            end

        end

        function set.Outputs(obj, value)
            cleanup = obj.beginModelChangeTransaction();
            obj.Outputs = value;
        end
    end
    
    methods (Sealed)
        
        function inputStruct = emptyInputs(obj, NVPArgs)
            % emptyInputs Create an input struct with field names compatible with block RUN method.
            %
            % S = emptyInputs(B) produces a scalar struct S whose
            % fieldnames are the required inputs of the block object B. The
            % value of each field is set as an empty array [].
            %
            % S = emptyInputs(B, 'IncludeOptional', TF) specfies whether to
            % include optional inputs as fields of S. An optional input
            % means that its Required property is set to false. TF is a
            % logical scalar. The default is false.
            %
            % Example:
            %  ST = bioinfo.pipeline.blocks.SeqTrim;
            %  s = ST.emptyInputs
            %  s.FASTQFiles = "SRR6008575_10k_1.fq";
            %  ST.run(s)
            %
            % See also run, Inputs
            arguments
                obj(1,1) bioinfo.pipeline.Block
                NVPArgs.IncludeOptional(1,1) logical = false
            end
            
            inputStruct = struct();
            
            inputNames = string(fieldnames(obj.Inputs));
            
            for ip = 1:numel(inputNames)
                if NVPArgs.IncludeOptional || obj.Inputs.(inputNames(ip)).Required
                    inputStruct.(inputNames(ip)) = [];
                end
            end
        end
        
        function outputStruct = run(obj, runInputStruct)
            % RUN Run block with pipeline error checking.
            %
            % OUTSTRUCT = RUN(B, INSTRUCT) runs the block B on the inputs
            % specified by INSTRUCT. INSTRUCT is a scalar struct, whose
            % fieldnames match the names of the inputs for B. OUTSTRUCT is
            % a scalar struct whose fieldnames match the names of the
            % outputs of B.
            %
            % Example:
            %
            % samSorter = bioinfo.pipeline.blocks.SamSort;
            % samSorter.run(struct('SAMFile', 'ex1.sam'))
            %
            % See also bioinfo.pipeline.Pipeline.run
            arguments
                obj(1,1) bioinfo.pipeline.Block
                runInputStruct(1,1) struct = struct();
            end
            
            runStructFieldNames = string(fieldnames(runInputStruct));
            
            % Check that all inputs are named ports
            for inp=1:numel(runStructFieldNames)
                if ~obj.hasInputNamed(runStructFieldNames(inp))
                    error(message("bioinfo:Block:InvalidInputName", runStructFieldNames(inp)));
                end
            end
            
            unsatisfiedInputs = setdiff(obj.getRequiredInputs(), runStructFieldNames);
            
            % Check that all required inputs are satisfied
            if ~isempty(unsatisfiedInputs)
                error(message("bioinfo:Block:InputNotSatisfied", strjoin(unsatisfiedInputs, ' ')));
            end
            
            % In case there are any ports with values,
            % update the input struct
            runInputStruct = obj.addSetInputValues(runInputStruct);

            % unwrap File datatypes
            for inp=1:numel(runStructFieldNames)
                if isa(runInputStruct.(runStructFieldNames(inp)),...
                        "bioinfo.pipeline.datatypes.File")
                    runInputStruct.(runStructFieldNames(inp)) = runInputStruct.(runStructFieldNames(inp)).unwrap();
                end
            end
            
            try
                outputStruct = obj.eval(runInputStruct);
            catch ME
                % Detect invalid number of outputs to eval()
                % dbstack().name is this function's name, so we are checking
                % if the error occurred here instead of within eval()
                
                errStack = ME.stack;
                dbStack = dbstack();
                
                if strcmpi(ME.identifier, "MATLAB:TooManyOutputs") && strcmpi(errStack(1).name, dbStack(1).name)
                    error(message("bioinfo:Block:InvalidEval"));
                else
                    rethrow(ME);
                end
            end
            
            try
                validateattributes(outputStruct, {'struct'}, {'scalar'});
            catch
                error(message("bioinfo:Block:EvalOutputMustBeScalarStruct"));
            end
            
            % Ensure proper format for outputs
            outputFieldNames = fieldnames(outputStruct);
            
            for outp=1:numel(outputFieldNames)
                if ~obj.hasOutputNamed(outputFieldNames{outp})
                    error(message("bioinfo:Block:InvalidOutputName", outputFieldNames{outp}));
                end
            end
            
            % Ensure all required outputs have been satisfied
            unsatisfiedOutputs = setdiff(obj.getRequiredOutputs(), outputFieldNames);
            if ~isempty(unsatisfiedOutputs)
                error(message("bioinfo:Block:OutputNotSatisfied", strjoin(unsatisfiedOutputs, ' ')));
            end
        end
    end

    methods (Sealed, Access = protected)
        function nonBlockNVPs = setBlockProperties(obj, nvpArgs)
            % SETBLOCKPROPERTIES
            
            % Determine which nvpArgs fields are Block properties, set Block properties
            blockNVPs = intersect(fields(nvpArgs), properties(obj));
            for i = 1:numel(blockNVPs)
                obj.(blockNVPs{i}) = nvpArgs.(blockNVPs{i});
            end
            
            % Remove Block properties from nvpArgs struct
            nonBlockNVPs = rmfield(nvpArgs, blockNVPs);
        end
        
        function createInput(obj, inputName, NVPArgs)
            %CREATEINPUT shorthand for creating input ports.
            arguments
                obj(1,1) bioinfo.pipeline.Block
                inputName(1,1) string {mustBeValidVariableName}
                NVPArgs.Required(1,1) logical
            end
            
            inputArgs = namedargs2cell(NVPArgs);
            obj.Inputs.(inputName) = bioinfo.pipeline.Input(inputArgs{:});
        end
        
        function createOutput(obj, outputName, NVPArgs)
            %CREATEOUTPUT shorthand for creating input ports.
            arguments
                obj(1,1) bioinfo.pipeline.Block
                outputName(1,1) string {mustBeValidVariableName}
                NVPArgs.Required(1,1) logical
            end
            
            outputArgs = namedargs2cell(NVPArgs);
            obj.Outputs.(outputName) = bioinfo.pipeline.Output(outputArgs{:});
        end
        
        function cObj = beginModelChangeTransaction(obj)
            % Bundles semantic model changes into a single event. If a
            % transaction is not active, start a new transaction, creating
            % and returning a cleanup object. When this cleanup object goes
            % out of scope, the model change event is emitted. If a
            % transaction is already active, a cleanup object must already
            % exist in an enclosing scope, so instead return a WeakHandle
            % to that cleanup Object, which is stored in
            % ActiveTransactionCleanup

            nargoutchk(1,1); % Require cleanup object to be saved by call site
            
            if isempty(obj.ActiveTransactionCleanup) || isDestroyed(obj.ActiveTransactionCleanup)
                % No transaction is active, so create a new onCleanup
                % object and save a WeakHandle reference to it.
                oldInputs = obj.Inputs;
                oldOutputs = obj.Outputs;
                cObj = onCleanup(@() emitModelChanges(obj, oldInputs, oldOutputs));
                obj.ActiveTransactionCleanup = matlab.internal.WeakHandle(cObj);
            else
                % There is an active transaction, return a WeakHandle to
                % the existing onCleanup
                cObj = obj.ActiveTransactionCleanup;
            end
        end

        function emitModelChanges(obj, oldInputs, oldOutputs)
            if event.hasListener(obj, 'SemanticModelChanged')
                inputData = obj.semanticPortDiff(oldInputs, obj.Inputs);
                outputData = obj.semanticPortDiff(oldOutputs, obj.Outputs);
                
                inputData.Delta = [inputData.Delta;outputData.Delta];
                if ~isempty(inputData.Delta)
                    notify(obj, 'SemanticModelChanged', inputData);
                end
            end
        end
    end
    
    methods (Static, Access = private)
        function mustBeUniquePorts(ports)
            if ~isempty(ports)
                portNames = [ports.Name];
                if numel(portNames) ~= numel(unique(portNames))
                    error(message("bioinfo:Block:NonUniquePortNames"));
                end
            end
        end

    end
    
    methods (Sealed, Static, Access = protected)
        function mustBeEmptyOrFile(fileName, fileExt)
            % MUSTBEEMPTYORFILE a property must be an empty string or a string matching
            % one of a list of file extensions.
            
            if ~isempty(fileName) 
                [~,~,ext] = fileparts(fileName);
                 if ~isscalar(ext) || ~any(strcmpi(ext, fileExt), "all")
                     error(message("bioinfo:Block:MustBeEmptyOrFile", strjoin(fileExt, ", ")));
                 end
            end
        end
        
        function mustBeFile(fileName, fileExt)
            % MUSTBEEMPTYORFILE
            
            [~, ~, ext] = fileparts(fileName);
            if ~any(strcmpi(ext, fileExt), "all")
                error(message("bioinfo:Block:MustBeFile", fileName, strjoin(fileExt, ", ")));
            end
        end
        
        function mustBeInputsStruct(inputStruct)
            inputValues = struct2cell(inputStruct);
            
            if ~isempty(inputValues) && any(cellfun(@(v) ~isa(v, 'bioinfo.pipeline.Input') || ~isscalar(v), inputValues))
                error(message("bioinfo:Block:InvalidInputsStruct"));
            end
            
        end
        
        function mustBeOutputsStruct(outputStruct)
            outputValues = struct2cell(outputStruct);
            
            if ~isempty(outputValues) && any(cellfun(@(v) ~isa(v, 'bioinfo.pipeline.Output') || ~isscalar(v), outputValues))
                error(message("bioinfo:Block:InvalidOutputsStruct"));
            end
            
        end
    end
    
    methods (Access = protected)
        function cpObj = copyElement(obj)
            cpObj = copyElement@bioinfo.pipeline.internal.SemanticModelObject(obj);
            
            % Deep copy Inputs & Outputs
            inputNames = fieldnames(obj.Inputs);

            cpObj.InputsListeners = struct();
            for in=1:numel(inputNames)
                cpObj.Inputs.(inputNames{in}) = copy(obj.Inputs.(inputNames{in}));
                cpObj.InputsListeners.(inputNames{in}) = listener(cpObj.Inputs.(inputNames{in}), 'PropertyValueChanged', ...
                                    @(src, evt) cpObj.handleInputPropertyChanged(inputNames{in}, evt));
            end
            
            outputNames = fieldnames(obj.Outputs);
            
            for on=1:numel(outputNames)
                cpObj.Outputs.(outputNames{on}) = copy(obj.Outputs.(outputNames{on}));
            end
        end
    end
end
% Copyright 2020-2022 The MathWorks, Inc.

