classdef PythonChecker
    % A class to check the python configurations of the blocks before
    % running them. Subclassed to do block-specific checks.

    properties
        PyInfoPath = fileparts(mfilename('fullpath'))
        RequiredModuleNames(1,:) string = ""
        UserVisibleBlockName(1,1) string = ""
    end

    methods
        function obj = PythonChecker(moduleNames, blockName)
            obj.RequiredModuleNames = moduleNames;
            obj.UserVisibleBlockName = blockName;
        end

        function runChecks(obj)
            % Check if python is installed and works
            checkPyenv(obj);
            checkBasicPythonCommands(obj);
            % Add our internal module to the python path
            addInternalModuleToPythonPath(obj);
            % Check modules like torch, tensorflow, sklearn, etc.
            checkExternalPythonModules(obj);
        end

        function checkPyenv(obj)
            pe = pyenv;
            if pe.Version == ""
                error(message("shared_pycoexblks:messages:PythonIsNotInstalled"));
            end
        end

        function checkExternalPythonModules(obj)
            for i = 1:numel(obj.RequiredModuleNames)
                modName = obj.RequiredModuleNames{i};
                tuple = py.PyInfo.is_package_installed(py.str(modName));
                c = cell(tuple);
                [tf, ME] = c{:};
                if ~tf
                    error(message("shared_pycoexblks:messages:PythonModuleCouldNotBeLoaded",...
                        modName, string(ME.msg)));
                end
            end
        end

        function checkBasicPythonCommands(obj)
            % Check a few basic python commands and moving data between PY
            % and MATLAB
            mlList = {1,2,"three"};
            failed = false;
            try
                % py.str, py.list and conversions back
                pyS = py.str("three");
                pyList = py.list(mlList);
                C = cell(pyList);
            catch ME
                failed = true;
            end
            if failed || ~isequal(string(pyS), "three") || ~isequal(C, {1, 2, pyS})
                error(message("shared_pycoexblks:messages:PythonCommandsFailed"));
            end
        end

        function addInternalModuleToPythonPath(obj)
            moduleName = 'PyInfo';
            P = py.sys.path;
            if count(P, obj.PyInfoPath) == 0
                insert(P, int32(0), obj.PyInfoPath);
            end
            % In addition to adding the module's directory to the path, CD
            % to the directory then load the module.
            curDir = pwd;
            C = onCleanup(@()cd(curDir));
            if ~isempty(obj.PyInfoPath) && obj.PyInfoPath~=""
                cd(obj.PyInfoPath);
            end
            try
                modObj = py.importlib.import_module(moduleName);
                py.importlib.reload(modObj);
            catch ME
                % disp(ME)
                modObj = [];
            end
            clear C   % cd back to curDir
            assert(~isempty(modObj), "Failed to import PyInfo.py");
        end
    end
end
