classdef TMFProps < handle

    % Copyright 2017-2020 The MathWorks, Inc.

    properties(Constant=true)
        CompilerCommandFileNameToken = '|>COMPILER_COMMAND_FILE_NAME<|';
    end

    properties(Access=private)
        HasCompilerCommandFileName;
        TMF_COMPILER_TOOL_CHAIN;
        TMF_DOWNLOAD_SUCCESS;
        TMF_DOWNLOAD_ERROR;
        TMF_MAKE;
        TMF_MAKECMD;
        TMF_TOOLCHAIN_NAME;
    end

    properties(SetAccess=private, GetAccess=public)
        TMF_BUILD;
        TMF_DO_NOT_USE_CHECKSUMS
        TMF_BUILD_SUCCESS;
        TMF_BUILD_ERROR;
        TMF_MAKEFILE_FILESEP;
        TMF_HOST;
        TMF_VERBOSE_BUILD_OFF_TREATMENT;
        TMF_SYS_TARGET_FILE;
        TMF_DOWNLOAD;
        TemplateMakefile;
        Toolchain;
        MexToolchainName;
        COMPILER_TOOL_CHAIN = '';
        CompilerFromTMF;
        TMF_MODEL;
        TMF_NUMST;
        TMF_TID01EQ;
        TMF_NCSTATES;
        TMF_MAT_FILE;
        TMF_INTEGER_CODE;
        TMF_ONESTEPFCN;
        TMF_TERMFCN;
        TMF_MULTITASKING;
        TMF_MULTI_INSTANCE_CODE;
        TMF_CLASSIC_INTERFACE;
        TMF_ALLOCATIONFCN;
        TMF_ENABLE_SLEXEC_SSBRIDGE;
        TMF_MODEL_HAS_DYNAMICALLY_LOADED_SFCNS;
    end
    
    properties(SetAccess=private, GetAccess=public, Hidden=true)    
        % Hidden parameter is not echoed to command line when object displayed
        TMFContent; 
        TMFChecksum;
    end

    properties(Dependent=true, GetAccess=public, SetAccess=private)
        RequiresCommandFile;
        UseCompilerCommandFile; % e.g. LCC supports linker but not compiler command file
        RequiresCopyingHeaders;
    end

    properties(GetAccess=public, SetAccess=private)
        SupportsModelReference = true;
        SkipRtNonFinite = false; % Some legacy TMFs explicitly handle rt_nonfinite
        IsSimplifiedFormat;
    end

    methods

        function val = get.UseCompilerCommandFile(this)
            tc = this.Toolchain;
            if ~isempty(tc)
                val = coder.make.useCompilerCommandFile(tc);

                if ~isequal(val, this.HasCompilerCommandFileName)

                    lRequiresCommandFile = isAttribute(tc, 'RequiresCommandFile') && ...
                        getAttribute(tc, 'RequiresCommandFile');
                    lNoCompilerCommandFile = isAttribute(tc, 'NoCompilerCommandFile') && ...
                        getAttribute(tc, 'NoCompilerCommandFile');

                    msg = message('coder_compile:CoderCompile:InconsistentNoCompilerCommandFile', ...
                                   this.TemplateMakefile, tc.Name, ...
                                   string(logical(lRequiresCommandFile)), ...
                                   string(logical(lNoCompilerCommandFile)), ...
                                   string(logical(this.HasCompilerCommandFileName)));
                    error(msg.Identifier, string(msg));
                end
            else
                % Default if no Toolchain was specified
                val = this.HasCompilerCommandFileName;
            end
        end
        function val = get.RequiresCommandFile(this)
            tc = this.Toolchain;
            if ~isempty(tc)
                val = isAttribute(tc, 'RequiresCommandFile') ...
                    && getAttribute(tc, 'RequiresCommandFile');
            else
                % Default if no Toolchain was specified
                val = false;
            end
        end
        function val = get.RequiresCopyingHeaders(this)
            tc = this.Toolchain;
            if ~isempty(tc)
                val = isAttribute(tc, 'CopyReferencedModelHeaders') ...
                    && getAttribute(tc, 'CopyReferencedModelHeaders');
            else
                % Default if no Toolchain was specified
                val = true;
            end
        end
    end

    methods(Access=private)

        function loadTMF(this, lTemplateMakefile)
            
            if isempty(fileparts(lTemplateMakefile))
                lTemplateMakefile = which(lTemplateMakefile);
            end

            % Open the template make file for reading, in text mode. Accordingly, when the makefile
            % is subsequently opened for write, it must also be opened in text mode.
            fid = fopen(lTemplateMakefile, 'rt');
            assert(fid ~= -1, ...
                   'Already checked for existence of %s therefore must be able to open it', ...
                   lTemplateMakefile);
            this.TMFContent = fread(fid, [1, inf], '*char');
            fclose(fid);
            
            % Additionally store a checksum
            this.TMFChecksum = builtin('_coder_compile_getMd5', lTemplateMakefile);
            
        end


        function lToolchain = extractToolchainAttributesFromName(~, tcName, templateMakefile, ...
                                                              lMexCompilerKey)

            % Determine the preferred Toolchain based on current mex configuration
            lToolchainName = '';
            if ~isempty(lMexCompilerKey)
                lToolchainName = ...
                    coder.make.internal.getToolchainNameFromRegistry(lMexCompilerKey);
            end

            foundMatch = false;
            if ~isempty(lToolchainName) && any(strcmp(lToolchainName, tcName))
                % One of the compatible Toolchain names matches the mex Toolchain name
                lToolchain = coder.make.internal.getToolchainInfoFromRegistry(lToolchainName);
                foundMatch = true;
            else
                % Choose the first compatible Toolchain name that we can find in the registry
                tr = coder.targetreg.internal.TargetRegistry.getWithoutDataLoad();
                toolchainsInRegistry = coder.make.internal.getToolchainInfos(tr);
                registeredNames = {toolchainsInRegistry.Name};
                matchingNames = intersect(tcName, registeredNames, 'stable');
                
                % Identify the first one that's installed
                for i = 1:length(matchingNames)
                    lToolchain = coder.make.internal.getToolchainInfoFromRegistry...
                        (matchingNames{i});
                    if coder.make.internal.isToolchainInstalled(lToolchain.Alias)
                        foundMatch = true;
                        break;
                    end
                end
                        
                if ~foundMatch && ~isempty(matchingNames)
                    % Choose the first one
                    lToolchain = coder.make.internal.getToolchainInfoFromRegistry(matchingNames{1});
                    foundMatch = true;
                end
            end

            if ~foundMatch
                % No matching Toolchain
                registeredNamesString = join(string(registeredNames), ', ');
                tcNames = join(string(tcName), ', ');
                DAStudio.error('RTW:buildProcess:NoToolchainFound', ...
                    tcNames, templateMakefile, registeredNamesString);
            end

        end
    end

    methods(Access=public)

        function make = getMAKECMD(this)

            make = this.TMF_MAKECMD;
            make = strrep(make, '|>MATLAB_ROOT<|', matlabroot);
            make = strrep(make, '|>ALT_MATLAB_ROOT<|', matlabroot);
            make = strrep(make, '|>MATLAB_BIN<|', fullfile(matlabroot, 'bin'));
            make = strrep(make, '|>ALT_MATLAB_BIN<|', fullfile(matlabroot, 'bin'));
            make = strrep(make, '|>ARCH<|', computer('arch'));

            % Legacy behaviour
            if ~this.IsSimplifiedFormat
                if isempty(make)
                    make = this.TMF_MAKE; % very old legacy token
                end
                if isunix && strcmp(make, 'make')
                    gmake = fullfile(matlabroot, 'bin', computer('arch'), 'gmake');
                    if isfile(gmake)
                    make = gmake;
                    end
                end

                if ispc
                    % If gmake is used the forward slashes may be used
                    % in the specification of the make location in the
                    % tmf file. Convert to backslash path
                    % format for invoking make from the shell.
                    make = strrep(make, '/', '\');
                end
            end

        end

        function macros = getMacrosForBuildResult(this)

            macros = struct(...
                'DOWNLOAD_SUCCESS', {this.TMF_DOWNLOAD_SUCCESS}, ...
                'DOWNLOAD_ERROR', {this.TMF_DOWNLOAD_ERROR}, ...
                'VERBOSE_BUILD_OFF_TREATMENT', {this.TMF_VERBOSE_BUILD_OFF_TREATMENT});
        end

        function lCompilerString = getCompilerString(this)

            lCompilerString = i_getCompilerString...
                (this.COMPILER_TOOL_CHAIN, this.TMF_MAKECMD, this.TemplateMakefile);

        end


        function this = TMFProps(lTemplateMakefile, lMexCompilerKey)
            
            % Special case for MSVC project build
            if contains(lTemplateMakefile, 'MSVCBuild.m')
                % Obtain properties from ert_vcx64.tmf: this ensures that we select 
                % an MSVC compiler using the same algorithm that applies for TMF builds
                loadTMF(this, fullfile(tmffolder, 'ert_vcx64.tmf'));
            else
                loadTMF(this, lTemplateMakefile);
            end

            this.TemplateMakefile = lTemplateMakefile;

            % Determine if it's a legacy TMF using a compiler command file
            this.HasCompilerCommandFileName = contains(this.TMFContent, ...
                 coder.make.internal.TMFProps.CompilerCommandFileNameToken);

            % Determine if the TMF supports Model Reference
            this.SupportsModelReference = contains(this.TMFContent, 'MODELREFS');

            % Extract TMF macros
            names = {'TOOLCHAIN_NAME', 'COMPILER_TOOL_CHAIN', 'MAKECMD', 'MAKE', ... 
                    'DOWNLOAD_SUCCESS', 'BUILD_SUCCESS', ...
                     'DOWNLOAD_ERROR', 'BUILD_ERROR', ...
                     'VERBOSE_BUILD_OFF_TREATMENT', 'BUILD', ...
                     'DO_NOT_USE_CHECKSUMS', ...
                     'MAKEFILE_FILESEP', ...
                     'SYS_TARGET_FILE', 'DOWNLOAD', 'HOST',...
                     'MODEL', 'NUMST', 'TID01EQ', 'NCSTATES', 'MAT_FILE', 'INTEGER_CODE', 'ONESTEPFCN',...
                     'TERMFCN', 'MULTITASKING', 'MULTI_INSTANCE_CODE', 'CLASSIC_INTERFACE', 'ALLOCATIONFCN',...
                     'ENABLE_SLEXEC_SSBRIDGE', 'MODEL_HAS_DYNAMICALLY_LOADED_SFCNS'
                     };               
                 
            values = coder.make.internal.getTMFMacros(this.TMFContent, names);

            % Set property value or retrieve variable for each macro
            for i=1:length(names)
                this.(['TMF_' names{i}]) = values{i};
            end

            % Process toolchain names
            toolchainValues = this.TMF_TOOLCHAIN_NAME;
            allToolchainNames = {};

            % Special case if MSVC project build template makefile is used on Unix. In this case, skip
            % Toolchain evaluation because the MSVC toolchains will not be registered. Needed for
            % compatibility reasons.
            isMSVCProjectBuildOnUnix = isunix && contains(lTemplateMakefile, 'MSVCBuild.m');
            
            if ~isMSVCProjectBuildOnUnix
                for i=1:length(toolchainValues)
                    toolchainNames = i_processToolchainNames(toolchainValues{i}, lTemplateMakefile);
                    allToolchainNames = [allToolchainNames toolchainNames]; %#ok<AGROW>
                end
            end

            if ~isempty(allToolchainNames)
                lToolchain = extractToolchainAttributesFromName...
                    (this, allToolchainNames, lTemplateMakefile, lMexCompilerKey);
                lIsSimplifiedFormat  = true;
            else
                lToolchain = [];
                lIsSimplifiedFormat  = false;
            end

            this.Toolchain = lToolchain;

            % Identify legacy TMF with explicit handling of rt_nonfinite
            if ~lIsSimplifiedFormat
                this.SkipRtNonFinite = ...
                    coder.make.internal.findRtNonfiniteInTMF(this.TMFContent) && ...
                    ~contains(this.TMFContent, 'SHARED_SRC');
            end

            this.IsSimplifiedFormat = lIsSimplifiedFormat;

            % Post-processing for macro COMPILER_TOOL_CHAIN
            lCOMPILER_TOOL_CHAIN = this.TMF_COMPILER_TOOL_CHAIN;

            lCompilerString = i_getCompilerString(lCOMPILER_TOOL_CHAIN, this.TMF_MAKECMD, lTemplateMakefile);

            % Compiler from TMF
            if ~isempty(lToolchain)
                lMexToolchainName = coder.make.internal.getMexToolchainName(lToolchain);
            else
                lMexToolchainName = '';
            end

            this.MexToolchainName = lMexToolchainName;
            
            if ~isempty(lToolchain)
                lToolchainAlias = lToolchain.Alias{1};
            else
                lToolchainAlias = '';
            end

            lCompilerFromTMF =  i_getTMFCompiler(lCompilerString, lToolchainAlias);
            this.CompilerFromTMF = lCompilerFromTMF;

            if ~isempty(lCompilerFromTMF)
                this.COMPILER_TOOL_CHAIN = lCompilerFromTMF.toolChain;
            else 
                this.COMPILER_TOOL_CHAIN = lCompilerString;
            end

        end

    end
end


function toolchainNames = i_processToolchainNames(toolchainNamesData, lTemplateMakefile)

% Add double quotes if not an array and not already double-quoted
if isempty(toolchainNamesData) || ~any(strcmp(toolchainNamesData(1), {'"', '['}))
    toolchainNamesData = sprintf('"%s"', toolchainNamesData);
end

% Wrap the Toolchain names such that we can process with jsondecode
jsonData = sprintf('{ "TOOLCHAIN" : %s }', toolchainNamesData);

try
    data = jsondecode(jsonData);
catch jsonExc
    % Turn the JSON exception into a graceful error message
    msg = DAStudio.message('RTW:buildProcess:TmfPropertiesInvalid', ...
        regexprep(jsonData, '\:', '=', 'once'), lTemplateMakefile);
    exc = MException('RTW:buildProcess:TmfPropertiesInvalid', msg);
    exc = addCause(exc, jsonExc);
    throw(exc);
end

toolchainNames = data.TOOLCHAIN;
end


function lCompilerString = i_getCompilerString(lCOMPILER_TOOL_CHAIN, lMAKECMD, lTemplateMakefile)

    lCompilerString = lCOMPILER_TOOL_CHAIN;

    if isempty(lCompilerString)
        lCompilerString = ...
            coder.make.internal.getCompilerFromMakeCmd(lMAKECMD, lTemplateMakefile);
    end
end




function comp = i_getTMFCompiler(lCompilerString, lToolchainAlias)

if ~isempty(lToolchainAlias)
    comp = coder.make.internal.getMexCompInfoFromKey(lToolchainAlias);
else
    compiler = lCompilerString;
    
    % based on the tool chain, get the compiler info.
    switch(compiler)
        case {'vcx64'}
            comp = coder.make.internal.getMexCompInfoFromKey(lToolchainAlias);
        case {'lcc'}
            comp = coder.make.internal.getMexCompInfoFromKey('LCC-X');
        case {'unix'}
            if ismac
                comp = coder.make.internal.getMexCompInfoFromKey('Apple-X');
            else
                comp = coder.make.internal.getMexCompInfoFromKey('GNU-X');
            end
        otherwise
            comp = [];
    end
end
end
