classdef ExtensionSet < extmgr.AbstractSet
    %ExtensionSet   Define the ExtensionSet class.
    
    %   Copyright 2012 The MathWorks, Inc.
    
    properties (SetAccess = private)
        
        % Handle to associated RegisterDb registration database
        RegistrationSet;
        
        % Handle to associated ConfigDb configuration database
        ConfigurationSet;
        
        % Handle to instance of parent application
        Application;
        
        % Optional handle to a MessageLog repository
        % If present, it is used for reporting messages
        % If not present, messages are ignored
        MessageLog;
    end
    
    methods
        function this = ExtensionSet(hRegisterDb, hConfigDb, hApplication, hMessageLog)
            %ExtensionSet Constructor for Extensions Instance Database.
            %   ExtensionSet(hRegisterDb,hConfigDb,hAppInst) constructs a
            %   new extension instance database, passing the extension
            %   registry database, configuration database, and
            %   application-instance handles. All arguments are required.
            
            mlock;
            
            % Instantiate and record handles to databases
            this.RegistrationSet  = hRegisterDb;
            this.ConfigurationSet = hConfigDb;
            
            % Record handle to the application instance
            % This is NOT an instance of the extension object.
            % This is a handle to the parent application - say, a scope instance.
            % It is specific to the client-app.  This instance object is used as an
            % argument to the constructor for an extension instance object.
            this.Application = hApplication;
            
            if nargin > 3
                this.MessageLog = hMessageLog;
            end
        end
        
        function add(this, hRegister, hConfig, varargin)
            %ADD Add an instance-specific copy of an extension that is
            %    to remain persistent for the lifecycle of a scope instance,
            %    until explicitly removed (disabled).
            %
            %    add(this,hRegister,hConfig) creates and adds an extension instance
            %    corresponding to registration hRegister, using property values as
            %    specified by configuration hConfig.
            %
            %    If omitted, the existing configuration object (hConfig) is
            %    taken from the configuration database (ConfigDb).
            %
            %    hRegister may be obtained from RegisterDb using:
            %       hRegister = findRegister(hRegisterDb,extType,extName)
            %
            %    ADD calls enableExtension(hExtension) method after connecting,
            %    which in turns calls an empty enable() overload on Extension
            %    extension instance.  Extension developers can overload
            %    the enable() method in their extension for initialization actions.
            
            % Create and store persistent instance of extension
            
            % Find the corresponding configuration in the database,
            % if one was not passed in
            if nargin<3
                hConfig = findConfiguration(this.ConfigurationSet, ...
                    hRegister.Type, hRegister.Name);
                
                if isempty(hConfig)
                    % Assertion - no good reason for it to be missing
                    error(message('Spcuilib:extmgr:ConfigNotFound', ...
                        [hRegister.Type ':' hRegister.Name]));
                end
            end
            
            % Check if configuration is enabled
            if ~hConfig.Enable
                % Need to enable extension, and all that goes with it
                % (Registration, property merging, etc, via listener)
                %
                % We get here only on initial power-up, when no source
                % has been added and no configuration has been loaded
                % (i.e., all configs are disabled).
                
                % Enable the configuration
                % triggers recursive call to add
                hConfig.Enable = true;
            end
            
            % NOTE 1: We do not check for adding duplicate entries.
            %         processAll() removes any existing instances first, for example,
            %         before calling add() in a loop.
            %
            % NOTE 2: Sources could go in as duplicates - when a new source is
            %         attempted, the old source is left enabled - until the new
            %         one is confirmed to be working.  So we NEED to allow
            %         duplicate entries - at least, for a short duration.
            %
            %         Consider a "checkForDupExtension" overload, for which Sources
            %         perform one type of check, while Tools (etc) perform a
            %         different (default?) check
            
            % Instantiate extension class, using application-instance object
            % Record the resulting extension-instance object
            %
            % Note that this will check out a license if the class resides
            % in a license-protected directory.  It may fail.
            %
            
            try
                
                if ~hConfig.Enable
                    error(message('Spcuilib:extmgr:DisabledConfig', ...
                        [hRegister.Type ':' hRegister.Name]));
                end
                
                % Verify that all required extensions are added before we add this one.
                % The majority of the time 'Depends' is empty.
                for indx = 1:length(hRegister.Depends)
                    
                    if isempty(getExtension(this, hRegister.Depends{indx}))
                        error(message('Spcuilib:extmgr:MissingDependentExtension', ...
                            [hRegister.Type ':' hRegister.Name], hRegister.Depends{indx}));
                    end
                end
                
                % Merge the properties from the register to the configuration.
                mergePropertySet(hRegister, hConfig);
                
                % Create instance
                % Could fail, e.g., by invalid user-supplied code
                % Pass application instance in case there are special tasks
                % the extension needs to do (allocate listeners, etc) during
                % instantiation (i.e., prior to an optional enable() method call)
                hExtension = feval(hRegister.Class, this.Application, hRegister, hConfig);
                
                % Add new plug-in (Extension-derived) instance to database
                % Must put new object FIRST in arg list
                %  - wrong way to do this: connect(this,hExtension,'down')
                this.Children = [this.Children hExtension];
                
                % We want to "batch up" the installGUI calls, so each instance does not
                % incrementally update the GUI until all enabled instances are added.
                % enableExtension() takes an optional 2nd arg to suppress UI updates,
                % that is passed in varargin.  When this method is called 1 at a time,
                % we just install the GUI, but when this is called by
                % extmgr.Driver.processAll we suppress the rendering.
                enableExtension(hExtension, varargin{:});
                
            catch e
                % Failure to instantiate extension
                %
                % Issue an error to the message log, and
                % disable the extension configuration
                
                % Let user know of the extension instantiation failure
                local_failExtensionMsg(this,hRegister,e);
                
                % EXTENSION CONSTRAINT ISSUE:
                %     Suppose this (failed) extension has a type-constraint such as
                %     "EnableOne" or "EnableAll". We should not disable this extension,
                %     because it will violate the constraint. Perhaps we should "fail
                %     out" here, and shut down the scope. For simplicity, we shut down
                %     the extension and violate the constraint, launch a failure
                %     message to the log and hopefully rely on dependency analysis to
                %     prevent "bad things" from happening.  The scope will not likely
                %     function as desired, but the scope should be "stable", and the
                %     user should be aware as to what has happened.
                
                notifyOfConstraintViolationIfDisabled( this, ...
                    hRegister, hConfig, this.MessageLog);
                
                % Disable the extension
                hConfig.Enable = false;
            end
        end
        
        function hExtension = getExtension(this, theType,theName)
            %getExtension Return handle to named extension instance.
            %  getExtension(hExtensionDb,'ExtType','ExtName') returns handle to named
            %  extension instance from database.  If not found, empty is returned.
            %
            %  getExtension(hExtensionDb,hRegister) specifies theType and theName via the
            %  extension registration hRegister.
            
            if ~ischar(theType) && ~(isstring(theType) && isscalar(theType))
                theName = theType.Name;
                theType = theType.Type;
            elseif nargin == 2
                [theType, theName] = strtok(theType, ':');
                if ~isempty(theName)
                    theName(1) = [];
                end
            end
            
            allExtensions = this.Children;
            if isempty(allExtensions)
                hExtension = [];
                return;
            end
            allRegisters  = [allExtensions.Registration];
                        
            allTypes = {allRegisters.Type};
            foundIndices = strcmp(allTypes, theType);
            if ~isempty(theName)
                allNames = {allRegisters.Name};
                foundIndices = foundIndices & strcmp(allNames, theName);
            end
            
            hExtension = allExtensions(foundIndices);
        end
    
        function remove(this,hRegister)
            %REMOVE Remove instance-specific copy from extension list.
            %  REMOVE(hExtensionDb,hRegister) removes one child from instance list.
            %  No error occurs if instance does not exist.
            %
            %  REMOVE(hExtensionDb) removes all children from instance list.
            %
            %     Calls disableExtension() method before disconnecting,
            %     which in turns calls an empty disable() overload on Extension
            %     extension instance.  Extension developers can overload
            %     the disable() for termination actions.
            
            % The extension might never have been enabled
            %
            % Upon exit of this function, the instance of hRegister goes
            % out of scope and triggers destructor
            
            if nargin<2
                hChildren = this.Children;
                for hChild = hChildren
                    disableExtension(hChild);
                end
                remove@extmgr.AbstractSet(this);
            else
                % Remove first Extension object with matching Register record
                % (i.e., has matching Type/Name in global Register)
                hExtension = getExtension(this, hRegister);
                if ~isempty(hExtension)
                    disableExtension(hExtension);
                    remove@extmgr.AbstractSet(this, hExtension);
                end
            end
        end
        
        function renderWidgets(this)
            hExtensions = this.Children;
            for hExtension = hExtensions
                renderWidgets(hExtension);
            end
        end
        
        function renderMenus(this)
            hExtensions = this.Children;
            for hExtension = hExtensions
                renderMenus(hExtension);
            end
        end
        
        function renderToolbars(this)
            hExtensions = this.Children;
            for hExtension = hExtensions
                renderToolbars(hExtension);
            end
        end
    end
    
    methods (Access = protected, Static)
        function childClass = getChildClass()
            childClass = 'extmgr.Extension';
        end
    end
end

% -------------------------------------------------------------------------
function details = coreDetailMsg(hRegister,title)
% Construct common detail message content

details = sprintf([ title ':<br>' ...
    '<ul>' ...
    '<li>Type: %s' ...
    '<li>Name: %s' ...
    '<li>Class: %s' ...
    '<li>Description: %s' ...
    '<li>File: %s' ...
    '</ul>'], ...
    hRegister.Type, hRegister.Name, hRegister.Class, ...
    hRegister.Description, hRegister.File);

end

% -------------------------------------------------------------------------
function local_failExtensionMsg(this,hRegister,e)
% Add message to log, if a message log is available

hMessageLog = this.MessageLog;
summary = sprintf('%s failed to instantiate',hRegister.FullName);
if isempty(hMessageLog)
    warning('Spcuilib:extmgr:ExtensionInstanceFailed', '%s\n\n%s', ...
        summary, uiservices.cleanErrorMessage(e));
else
    details = coreDetailMsg(hRegister, ...
        'Extension class failed to instantiate');
    details = [details ...
        '<b>Error message:</b><br>' uiservices.cleanErrorMessage(e) '<br><br>' ...
        '<b>Disabling extension.</b><br>'];
    
    hMessageLog.add('Fail','Extension',summary,details);
end
end

% -------------------------------------------------------------------------
function notifyOfConstraintViolationIfDisabled(this, hRegister,hConfig,hMessageLog)

% Get extension type database
hRegisterDb     = this.RegistrationSet;
hRegisterTypeDb = hRegisterDb.TypeDescriptionSet;
hConstraint     = getConstraint(hRegisterTypeDb, hConfig.Type);

if hConstraint.willViolateIfDisabled(this.ConfigurationSet, hConfig)
    % Let user know of the constraint violation that will ensue
    
    summary = sprintf('Disabling "%s" violates constraint', hRegister.FullName);
    
    if isempty(hMessageLog)
        warning('Spcuilib:extmgr:ConstraintViolation', summary); %#ok<SPWRN>
    else
        details = coreDetailMsg(hRegister, ...
            'Disabling failed extension violates constraint');
        hMessageLog.add('Fail','Extension',summary,details);
    end
end
end

% [EOF]
