classdef (Sealed) AliasFileManager < handle
    %AliasFileManager Create and edit alias definition files
    % Use an AliasFileManager object to create and manage a set of alias 
    % definitions for classes in a given folder.  Aliases allow you to rename 
    % classes without introducing incompatibilities.  MATLAB will recognize 
    % the new (current) name of the class and all of the old names (aliases).  
    % In addition, you can load an old MAT-file containing instances of the 
    % class that were created before the class was renamed.  MATLAB will   
    % load the MAT-file using the new class name.  You can also load new
    % MAT-files in older versions of MATLAB before the class was renamed as
    % long as you include all of the old names in your alias definition for
    % the class.
    %
    % Use a single AliasFileManager object to manage all of the alias
    % definitions for a given folder.  
    %
    % fileMgr = matlab.alias.AliasFileManager creates an AliasFileManager
    % object with no alias definitions.
    %
    % fileMgr = matlab.alias.AliasFileManager(location) creates an
    % AliasFileManager object containing the alias definitions for classes
    % in the specified location.  The location must be specified as a string 
    % scalar.  It must be a full or relative path to the parent folder of  
    % the resources folder containing the alias definition file.  This is a  
    % convenient way to view the aliases defined for classes in the specified
    % location.
    %
    % Aliases should be defined and managed using a function (preferred) or
    % script.  Once you have written your function or script to capture your 
    % alias definitions, run the code.  This allows the alias definitions to  
    % take effect.  When updating alias definitions, re-run the code so that  
    % your changes take effect.  
    % 
    % When sharing code with others that includes classes that have been
    % renamed using aliasing, include the alias definition file alongside 
    % the code you share so that others can continue to use the old name.  
    % You can also share your function or script and have it run as part of
    % an installation or setup step in order to create a new alias
    % definition file.
    %
    % %Example 1: - Create a function to rename two classes and save the 
    % %alias definition file in the resources folder of the current folder,
    % %then run the function:
    %
    % function myAliasFunction
    %     fileMgr = matlab.alias.AliasFileManager;
    %     addAlias(fileMgr, NewName = "NewCls", OldNames = "OldCls");
    %     addAlias(fileMgr, NewName = "OtherNewCls", OldNames = "OtherOldCls");
    %     writeAliasFile(fileMgr);
    % end
    %
    % %Example 2: - Update the function to rename NewCls to NewerCls, then
    % %run the function for the updates to take effect:
    %
    % function myAliasFunction
    %     fileMgr = matlab.alias.AliasFileManager;
    %     addAlias(fileMgr, NewName = "NewerCls", OldNames = ["NewCls", "OldCls"]);
    %     addAlias(fileMgr, NewName = "OtherNewCls", OldNames = "OtherOldCls");
    %     writeAliasFile(fileMgr);
    % end
    %
    % AliasFileManager methods:
    %     addAlias -       Add an alias defining one or more old names for a class
    %     writeAliasFile - Save alias definitions to an alias definition file
    %
    % AliasFileManager properties:
    %     Aliases - List of AliasDefinition objects for this file manager
    %
    % See also matlab.alias.AliasDefinition, class, classdef
    
    % Copyright 2020-2021 The MathWorks, Inc.

    properties (Transient)
        %Aliases Alias definitions for renamed classes in a given folder
        % The Aliases property contains a list of the alias definitions for 
        % the classes in a given folder that have been renamed.  Each alias 
        % definition maps one or more old names to the new name of a class.  
        % The new  name must correspond to the actual name of a class in 
        % that folder.  None of the old names can be in use as a class or 
        % function in the folder.
        Aliases (1,:) matlab.alias.AliasDefinition
    end

    methods
        function obj = AliasFileManager(location)
            arguments
                location string {mustBeScalarOrEmpty, mustBeFolder} = string.empty
            end
            if ~isempty(location) 
                [~, values] = fileattrib(location);
                % error if no read permissions to the location.
                if ~values.UserRead
                    error(message('MATLAB:aliasFileManager:NoFilePermission', location));
                end
                aliasFile = fullfile(location, 'resources','alias.json');
                aliasFolder = dir(location).folder;
                if ~isfile(aliasFile)
                    error(message('MATLAB:aliasFileManager:NoExistingAliasFile', aliasFolder));
                end

                [~, fileValues] = fileattrib(aliasFile);
                if ~fileValues.UserRead
                    error(message('MATLAB:aliasFileManager:NoFilePermission', aliasFolder));
                end
                
                aliasesStruct = jsondecode(fileread(aliasFile));
                obj = addAliasFromStruct(obj, aliasesStruct);
            end
        end
        
        function addAlias(obj, names)
            %addAlias Add an alias defining one or more old names for a class
            % addAlias(fileMgr, NewName = newname, OldNames = oldnames) adds 
            % one or more aliases for the class specified by NewName.  
            % NewName must be a string scalar.  OldNames must be a string 
            % array of one or more old names. 
            % 
            % Use a function or script to create your alias definitions using 
            % addAlias.  Create one function to rename all of the classes in 
            % a given folder.  The new class name must match a class definition 
            % in that folder. None of the old names can be in use as classes 
            % or functions in the folder.
            %
            % When renaming a class multiple times, update the existing call 
            % to addAlias in your function or script.  The most recent
            % alias name must be first in the list of old names.  It is
            % expected that aliases are introduced over times as classes
            % are renamed.  MATLAB will issue a warning if more than one
            % old name is added for a class at the same time, but will
            % save the definition when writing the alias definition file.
            %
            % See also AliasFileManager, writeAliasFile, matlab.alias.AliasDefinition

            arguments
                obj (1,1)
                names.NewName (1,1) string
                names.OldNames (1,:) string {mustBeNonempty}
            end

            if ~isfield(names, "NewName")
                me = MException(message("MATLAB:aliasFileManager:MissingName", "OldNames"));
                throw(me);
            end
            if ~isfield(names, "OldNames")
                me = MException(message("MATLAB:aliasFileManager:MissingName", "OldNames"));
                throw(me);
            end
            newName = names.NewName;
            oldNames = names.OldNames;           
            
            matlab.alias.AliasFileManager.validateNewName(newName, oldNames);
            matlab.alias.AliasFileManager.validateOldNames(oldNames);
            
            % existingIndex stores the index of alias which should be
            % overwritten or updated by the input arguments.
            existingIndex = 0;
            for i = 1 : numel(obj.Aliases)
                existingAlias = obj.Aliases(i);
                
                % New Name cannot be existing old name
                if any(existingAlias.OldNames == newName)
                    error(message('MATLAB:aliasFileManager:NewNameIsExistingAlias', newName, existingAlias.NewName));
                end
                
                
                % Overwrite the ith alias by input arguments when the input
                % argument and an existing alias has the same new name. The 
                % existing alias will be overwritten by the input arguments.
                if matlab.alias.AliasFileManager.doesOverWriteAlias(existingAlias, newName)
                    existingIndex = i;
                    break;
                end
                
                existingNames = [existingAlias.NewName, existingAlias.OldNames];
                
                % An example of multiple renaming a class: An existing
                % alias renames a class from "foo" to "bar", the user calls
                % addAlias(NewName="newBar", "OldNames", ["bar", "foo"]) to
                % rename the class from "bar" to "newBar".
                multipleRenaming = matlab.alias.AliasFileManager.isMultipleRenaming(existingNames, oldNames);
                
                % No more checking if this is the first time aliasing.
                if ~multipleRenaming
                    continue;
                end
                
                % Do error checking for multiple renaming
                matlab.alias.AliasFileManager.validateMultipleRenaming(existingNames, oldNames, newName);
                
                % Update the ith alias for multiple renaming
                existingIndex = i;
                break;
            end
            
            % Update existing alias or add a new alias in the property
            % Aliases.
            if ~isstring(newName)
                newName = convertCharsToStrings(newName);
            end
            if ~isstring(oldNames)
                oldNames = convertCharsToStrings(oldNames);
            end
            addToAliases(obj, existingIndex, newName, oldNames, false);
        end
        
        function writeAliasFile(obj, location)
            %writeAliasFile Save alias definitions to an alias definition file
            % writeAliasFile(fileMgr) creates an alias definition file
            % containing the alias definitions in the fileMgr object.  The 
            % alias definition file is written to the resources folder of 
            % the current folder.  fileMgr must be a matlab.alias.AliasFileManager 
            % object.  A resources folder is created if it does not already
            % exist.
            %
            % writeAliasFile(fileMgr, location) creates an alias definition
            % file in the resources folder of the specified location.  The
            % location must be a string scalar.  It can be specified as a
            % full or relative path to the parent folder of the resources
            % folder where the alias definitions will be stored.  If an
            % alias definition file already exists at the specified location, 
            % it will be overwritten by a new file.
            %
            % When writing out an alias definition file, MATLAB will
            % validate that each new name matches the name of a class
            % definition in the folder where the alias definition file is
            % being written.  It also confirms than none of the specified
            % old names are used as class or function names in the folder.
            %
            % See also matlab.alias.AliasFileManager, addAlias

            arguments
                obj (1, 1) matlab.alias.AliasFileManager
                location string {mustBeScalarOrEmpty, mustBeFolder} = pwd
            end
            if exist(location, 'dir') && ~isempty(dir(location))
                location = dir(location).folder;
            end
            resourcesDir = fullfile(location, 'resources');
            
            % Check alias restrictions on all aliases and new names
            for l = 1 : numel(obj.Aliases)
                newName = obj.Aliases(l).NewName;
                oldNames = obj.Aliases(l).OldNames;
                
                %% Error if the old name file is in the folder
                for i = 1: numel(oldNames)
                    if matlab.alias.AliasFileManager.doesFileExist(location, oldNames(i))
                        error(message('MATLAB:alias:OldNameExists', oldNames(i), newName, location));
                    end
                end
                
                %% Error if the new name file is not in folder.
                if ~matlab.alias.AliasFileManager.doesFileExist(location, newName)
                    error(message('MATLAB:aliasFileManager:MissingNewClassInAliasFolder', newName, location));
                end
                
            end
            
            if ~isdir(resourcesDir)
                try
                    mkdir(resourcesDir);
                catch
                    error(message('MATLAB:aliasFileManager:CreateAliasFileFailed', location));
                end
            end
            
            fileID = fopen(fullfile(resourcesDir,'alias.json'),'w+');
            
            % Error if cannot create alias file successfully
            if fileID == -1
                error(message('MATLAB:aliasFileManager:CreateAliasFileFailed', location));
            end
            c = onCleanup(@()fclose(fileID));
            
            fprintf(fileID, jsonencode(obj, 'PrettyPrint', true));
        end
    end
    methods (Access = private)
        % Assign values from an aliases struct to property Aliases.
        function obj = addAliasFromStruct(obj, aliasesStruct)
            for i = 1:numel(aliasesStruct.Aliases)
                aliasDef = aliasesStruct.Aliases(i);
                newName = string(aliasDef.NewName);
                oldNames = string(aliasDef.OldNames)';
                warnOnOldName = aliasDef.WarnOnOldName;
                obj.Aliases = [obj.Aliases, matlab.alias.AliasDefinition(newName, oldNames, warnOnOldName)];
            end
        end
        function addToAliases(obj, existingIndex, newName, oldNames, warnOnOldName)
            % Overwrite/update existing alias
            if existingIndex > 0
                obj.Aliases(existingIndex).NewName = newName;
                obj.Aliases(existingIndex).OldNames = oldNames;
                obj.Aliases(existingIndex).WarnOnOldName = warnOnOldName;
            else
                % Warn if multiple aliases are defined for the new name
                % from the first time renaming the class.
                if isstring(oldNames) && numel(oldNames) > 1
                    warning(message('MATLAB:aliasFileManager:MultipleAliasesFirstTimeRenaming', newName));
                end
                
                obj.Aliases = [obj.Aliases, matlab.alias.AliasDefinition(newName, oldNames, warnOnOldName)];
            end
        end
    end
    methods (Static, Hidden)
        % Check if a name is a built-in class.
        function isBuiltinClassFlag = IsBuiltinClass(name)
            arguments
                name (1,1) string
            end
            isBuiltinClassFlag = false;
            if exist(name, "class") == 8 && any(strfind(which(name), "built-in"), 'all')
                isBuiltinClassFlag = true;
            end
        end
        
        % Check if a nested name is defined in the location. If the
        % nestedName is a package name, such as "a.b", return true if
        % "location/+a/b.m" exists
        function out = doesFileExist(location, nestedName)
            nameList = split(nestedName,'.');
            fileLocation = location;
            function outFlag = fileExistsInLocation (location, name)
                % Only check file extensions including ".m", ".p" and
                % ".mlx" because those are possible class definition file
                % extensions. Other extension such as ".slx" are not
                % considered.
                function outRes = fileCheck(location, name, extension)
                    outRes = false;
                    fileNameWithExt = fullfile(location, strcat(name, extension));
                    % isfile checks if a file including the path exists or not.
                    % On Mac and Windows system, isfile does not differentiate file names
                    % with lowercase and uppercase characters. Further check is applied
                    % on Mac and Windows system to make sure that the name as specified
                    % in the alias definition is an exact, case-sensitive match for the name on disk
                    if isfile(fileNameWithExt)
                        if ismac || ispc 
                            % Get a list of the file names with the extension from the location.
                            % Check each file name from the list if one file has the exact,
                            % case-sensitive match with the desired name.
                            fileList = dir(fullfile(location, strcat("*", extension)));
                            for i = 1 : numel(fileList)
                                [~, fileNameFromLocation, ~] = fileparts(fileList(i).name);
                                if strcmp(fileNameFromLocation, name)
                                    outRes = true;
                                    break;
                                end
                            end
                        else
                            % isfile can differentiate file names with lowercase and uppercase characters
                            % on Linux.
                            outRes = true;
                        end
                    end
                end
                outFlag = fileCheck(location, name, ".m") || fileCheck(location, name, ".p") || ...
                    fileCheck(location, name, ".mlx");
            end
            for i = 1 : numel(nameList)-1
                fileLocation = fullfile(fileLocation, strcat('+', nameList(i)));
            end
            
            out = fileExistsInLocation(fileLocation, nameList(end));

            if ~out
                % Check if a file exist in @-folder
                fileLocation = fullfile(fileLocation, strcat('@', nameList(end)));
                out = fileExistsInLocation(fileLocation, nameList(end));
            end            
        end
        
        function multipleRenamingFlag = isMultipleRenaming(existingNames, oldNames)
            % Check if the class is renamed multiple times. i.e. One of
            % oldNames is in existingNames
            multipleRenamingFlag = false;
            
            if any(ismember(existingNames, oldNames), 'all')
                multipleRenamingFlag = true;
            end
        end
        
        function isValid = validateName(name)
            % Error if name is not valid
            isValid = true;
            if any(strfind(name, '.'), 'all')
                if ~all(cellfun(@isvarname, split(name, '.')),'all')
                    isValid = false;
                end
            else
                if ~isvarname(name)
                    isValid = false;
                end
            end
        end
        function validateNewName(newName, oldNames)
            % Error if newName is not a valid identifier
            if  ~matlab.alias.AliasFileManager.validateName(newName)
                error(message('MATLAB:aliasFileManager:NameNotValid', newName));
            end

            % Error if new name is builtin class name
            if any(arrayfun(@matlab.alias.AliasFileManager.IsBuiltinClass, newName), 'all')
                for i = 1: numel(oldNames)
                    error(message('MATLAB:aliasFileManager:NewNameIsBuiltinClass', oldNames(i), newName));
                end
            end

            % Error if renaming to the same name
            if any(find(oldNames == newName), 'all')
                error(message('MATLAB:aliasFileManager:AliasingSameName', newName));
            end
        end
        function validateOldNames(oldNames)
            % Validate old name(s)
            if isstring(oldNames)
                for i = 1 : numel(oldNames)
                    if ~matlab.alias.AliasFileManager.validateName(oldNames(i))
                        error(message('MATLAB:aliasFileManager:NameNotValid', oldNames(i)));
                    end 
                end
            else
                if ~matlab.alias.AliasFileManager.validateName(oldNames)
                    error(message('MATLAB:aliasFileManager:NameNotValid', oldNames));
                end
            end

            % Error if any old name is builtin class name
            if any(arrayfun(@matlab.alias.AliasFileManager.IsBuiltinClass, oldNames), 'all')
                error(message('MATLAB:aliasFileManager:AliasIsBuiltinClass'));
            end
        end
        function overWriteExistingAlias = doesOverWriteAlias(existingAlias, newName)
            overWriteExistingAlias= false;
            % Warn if new name is defined multiple times
            if newName == existingAlias.NewName
                warning(message('MATLAB:aliasFileManager:NewNameDefinedMultipleTimes', existingAlias.NewName));
                % Overwrite the existing alias with the input arguments
                overWriteExistingAlias = true;
            end
        end
        
        function validateMultipleRenaming(existingNames, oldNames, newName)
            % existingNames contains the names from an existing alias
            % definition as follows:  [existingNewName, existingOldNames]
            % oldNames is the set of old names in the alias definition
            % being processed by this call to addAlias.  We expect the two 
            % to be equal.  If they are now, something has gone wrong.

            if ~isequal(existingNames, oldNames)

                % When renaming a class multiple times, the most recent name
                % should be the first in the list of old names.
                if oldNames(1) ~= existingNames(1)
                    error(message('MATLAB:aliasFileManager:RecentNameFirst'));
                end

                % Check if the set of oldNames is consistent with expectations.
                % There are four scenarios tested here:
                % 1. The set of oldNames contains the same names as
                % existingNames, but in the wrong order.
                % 2. The set of oldNames contains the expected number of names, 
                % but the actual names are different names than in
                % existingNames.
                % 3. The set of oldNames contains more names than expected.
                % It should be the same size as the existingNames.  If it
                % contains more name, more than one alias has been added
                % for this alias definition.
                % 4. The set of oldNames contains fewer names than
                % expected.  This suggests an alias has been dropped from
                % the list.
           
                if numel(existingNames) == numel(oldNames)
                    difference = setdiff(existingNames, oldNames);
                    if isempty(difference)
                        warning(message('MATLAB:aliasFileManager:OldNamesReordered',newName));
                    else
                        warning(message('MATLAB:aliasFileManager:OldNamesChanged',newName));
                    end
                elseif numel(existingNames) > numel(oldNames)
                    droppedNames = setdiff(existingNames, oldNames);
                    nameString = buildClassNameListForMessage(droppedNames);
                    warning(message('MATLAB:aliasFileManager:OldNamesDropped', nameString, newName));
                
                else %number of old names is greater
                    addedNames = setdiff(oldNames, existingNames);
                    if isempty(addedNames)
                        warning(message('MATLAB:aliasFileManager:SameOldName', newName));
                    else
                        nameString = buildClassNameListForMessage(addedNames);
                        warning(message('MATLAB:aliasFileManager:OldNamesAdded', nameString, newName));
                    end
                end
            end  %~isequal(existingNames, oldNames)
        end      %function validateMultipleRenaming

    end %[static methods]
end  %[classdef]

function names = buildClassNameListForMessage(nameArray)
    names = char(39) + nameArray + char(39);
    names = strjoin(names, ", ");
end
