function [filesFromExistingSharedCode, newLocallyGeneratedFiles, ...
         constParamsUpdatedLocally] ...
    = mergeExistingSharedCode...
    (lExistingSharedCode, lSharedUtilsFolder, filesToMerge, varargin)
%MERGEEXISTINGSHAREDCODE update _sharedutils folder based on existing code area
%
% Given an existing shared code area and a _sharedutils folder located under
% slprj, this function updates the content of the _sharedutils folder to make
% use of the existing shared code. This update includes performing any necessary
% file copies, performing any specified diagnostic checks and updating Shared
% Code Manager to ensure it remains in sync with the content of the _sharedutils
% folder.


% Copyright 2016-2023 The MathWorks, Inc.

p = inputParser;
p.addParameter('ModelName', '');
parse(p, varargin{:});

% Check if source files in shared utilities folder must be replaced by existing
% shared code
lResolvedFiles = coder.internal.xrel.ResolvedFiles(lSharedUtilsFolder);

% Identify existing shared code (additionally checks that the existing shared
% code area is valid, e.g. no duplicates)
[librarySourceFiles, libraryFolders, jsonFilesFullPath] = ...
    coder.internal.xrel.getExistingSharedCode...
    (lExistingSharedCode, 'FilesToInclude', filesToMerge);

% Identify shared parameter files. These are files that might grow as
% additional models add more content. The usual assumption that once a
% shared utility file has been created it is never modified does not apply
% for these files.
if isempty(filesToMerge)
    % Merging all files
    [sharedParamFiles, sharedParamFileMap] = i_getSharedParamFiles(lSharedUtilsFolder);
else
    % Only merge specified files (rtwtypes.h), i.e. no need to process
    % shared parameter files
    sharedParamFiles = {};
    sharedParamFileMap = [];
end
    

% If filesToMerge is non-empty then operations on the ResolvedFiles object must
% be restricted to these specified files only
if ~isempty(filesToMerge)
    lResolvedFiles.setFilesToInclude(filesToMerge);
end


% Update any files that need to be copied from the existing shared code area
[updatedSharedParamFiles, updatedSharedParamFolders] = i_updateFilesFromExistingSharedCode...
    (lResolvedFiles, librarySourceFiles, libraryFolders, lSharedUtilsFolder, ...
    jsonFilesFullPath, sharedParamFiles);

% Special handling for const_params.c or const_params.cpp. The process for handling this file is a)
% obtain the existing shared code version, b) determine if the existing shared code version
% implements all the definitions required by the local build, c) if all definitions are implemented:
% finished, d) if additional definitions are required then (subject to UseOnlyExistingSharedCode
% diagnostic setting), append them to a copy of the existing file.
if isempty(p.Results.ModelName)
    % If no ModelName is passed, directly use the version of const_params
    % from the ExistingSharedCode location.
    constParamsUpdatedLocally = {};
else
    constParamsUpdatedLocally = i_updateConstParams...
        (librarySourceFiles, libraryFolders, lSharedUtilsFolder, p.Results.ModelName);
end

% Update resolve status for any locally generated files
newLocallyGeneratedFiles = i_updateFilesLocallyGenerated(lResolvedFiles);

% Identify locally generated files and files from existing shared code
[srcFiles, lResolveStatusUpdated] = getResolveStatus(lResolvedFiles);
existingIdx = ...
    lResolveStatusUpdated==coder.internal.xrel.FileResolveStatus.Repository;
filesFromExistingSharedCode = srcFiles(existingIdx);

% For shared parameter files copied from existing shared code area, check
% that they provide definitions as required by data serialized in
% filemap.mat.
i_checkUpdatedSharedParamFiles...
    (updatedSharedParamFiles, updatedSharedParamFolders, ...
    sharedParamFileMap, lSharedUtilsFolder);



function [sharedParamFilesToCopy, sharedParamFoldersToCopy] ...
    = i_updateFilesFromExistingSharedCode...
    (lResolvedFiles, librarySourceFiles, libraryFolders, ...
    lSharedUtilsFolder, jsonFilesFullPath, sharedParamFiles)


% Special handling for shared parameter header files: check if the content
% of each shared parameter file exactly matches the corresponding file in
% the existing shared code area. If not, copy from existing shared code.
% This is because a file might have been updated locally and the copy from
% the existing shared code area must be used instead of a locally generated
% file.
[~, sharedParamIdx] = intersect(librarySourceFiles, sharedParamFiles);
[sharedParamFilesToCopy, sharedParamFoldersToCopy] = i_getSharedParamFilesToCopy...
    (librarySourceFiles(sharedParamIdx), ...
    libraryFolders(sharedParamIdx), ...
    lSharedUtilsFolder);
[librarySourceFiles, ia] = setdiff(librarySourceFiles, sharedParamFilesToCopy);
libraryFolders = libraryFolders(ia);


% Special handling for const_params 
constParamsIdx = contains(librarySourceFiles, [coder.internal.SharedUtilities.ConstParamsName '.']);

% If const_params must be copied, the return values will be non-empty
[constParamsFile, constParamsFolder] = i_getConstParamsToCopy...
    (librarySourceFiles(constParamsIdx), ...
    libraryFolders(constParamsIdx), ...
    lSharedUtilsFolder);
librarySourceFiles = librarySourceFiles(~constParamsIdx);
libraryFolders = libraryFolders(~constParamsIdx);

% Identify files to copy from the existing shared code area
[srcFilesLocal, lResolveStatus] = getResolveStatus(lResolvedFiles);
alreadyUpToDateIdx = ...
    lResolveStatus~=coder.internal.xrel.FileResolveStatus.Unknown;
alreadyUpToDateSourceFiles = srcFilesLocal(alreadyUpToDateIdx);
[~, filesToCopyIdx] = setdiff(librarySourceFiles, alreadyUpToDateSourceFiles);

files = [librarySourceFiles(filesToCopyIdx) constParamsFile sharedParamFilesToCopy];
folders = [libraryFolders(filesToCopyIdx) constParamsFolder sharedParamFoldersToCopy];

% Shared function source files may have an associated JSON file in the same
% folder and with the same suffix, containing checksum information
sharedFunctionJSONFiles = repmat({''}, size(files));
jsonFilesNoSuffix = regexprep(jsonFilesFullPath, '\.json$', '', 'once');

% Files copied to the _sharedutils folder must be writable
for i=1:length(files)
    srcFileFullPath = fullfile(folders{i}, files{i});
    destFile = fullfile(lSharedUtilsFolder, files{i});
    copyfile(srcFileFullPath, destFile);
    fileattrib(destFile, '+w')
    
    % Identify the corresponding .json file, if any
    [p, f] = fileparts(srcFileFullPath);
    srcFileNoSuffix = fullfile(p, f);
    jsonFileIdx = strcmp(srcFileNoSuffix, jsonFilesNoSuffix);
    if sum(jsonFileIdx==1)
        sharedFunctionJSONFiles(i) = jsonFilesFullPath(jsonFileIdx);
    end
end

% Set resolve status for files copied from the repository
updatedResolveStatus = ...
    repmat(coder.internal.xrel.FileResolveStatus.Repository, size(files));
lDefaultCode = repmat({''}, size(files));
setResolveStatus(lResolvedFiles, files, updatedResolveStatus, lDefaultCode, ...
    sharedFunctionJSONFiles);


% Identify const_params.c or .cpp
function constParamsIdx = i_getConstParamsIdx(files)
constParamsIdx = contains(files, [coder.internal.SharedUtilities.ConstParamsName '.']);


function newLocalSrcFiles = i_updateFilesLocallyGenerated(lResolvedFiles)

% Update resolve status for new local-only files
[srcFilesLocal, lResolveStatus] = getResolveStatus(lResolvedFiles);
resolveStatusUnknownIdx = (lResolveStatus=='Unknown');
newLocalSrcFiles = srcFilesLocal(resolveStatusUnknownIdx);
if ~isempty(newLocalSrcFiles)
    updatedResolveStatus = ...
        repmat(coder.internal.xrel.FileResolveStatus.Local, ...
        size(newLocalSrcFiles));
    lDefaultCode = repmat({''}, size(newLocalSrcFiles));
    sharedFunctionJSONFiles = repmat({''}, size(newLocalSrcFiles));
    setResolveStatus(lResolvedFiles, newLocalSrcFiles, ...
        updatedResolveStatus, lDefaultCode, sharedFunctionJSONFiles);
end

% Special handling for const_params.c or const_params.cpp
function constParamsUpdatedLocally = i_updateConstParams...
    (librarySourceFiles, libraryFolders, lSharedUtilsFolder, model)

% Identify const_params.c or .cpp
constParamsIdx = i_getConstParamsIdx(librarySourceFiles);

constParamsUpdatedLocally = {};

if ~any(constParamsIdx)
    return;
end
constParamsFile = librarySourceFiles{constParamsIdx};
constParamsFileLocal = fullfile(lSharedUtilsFolder, constParamsFile);
constParamsFileExisting = fullfile(libraryFolders{constParamsIdx}, constParamsFile);
[includes, paramNames] = coder.internal.xrel.parseConstParams(constParamsFileLocal);

% Append additional parameters that were not already included in the
% existing shared code copy of const_params
hasSharedConstants = false; % Actual value determined inside call to emitSharedConstants
appendToFile = true;

[~, constPFileBase, constPFileExt] = fileparts(constParamsFileLocal);

rtwprivate('emitSharedConstants', model, lSharedUtilsFolder, ...
    fullfile(lSharedUtilsFolder, 'shared_file.dmr'), ...
    [constPFileBase constPFileExt], '', hasSharedConstants, appendToFile, ...
    'ExistingIncludes', includes, ...
    'ExistingParameters', paramNames);

lUseOnlyExistingSharedCode = get_param(model, 'UseOnlyExistingSharedCode');

if ~strcmpi(lUseOnlyExistingSharedCode, 'none')
    % Need diagnostic checks to report if any code has been generated locally

    fileContentLocal = fileread(constParamsFileLocal);
    fileContentExisting = fileread(constParamsFileExisting);
    
    if ~strcmp(fileContentLocal, fileContentExisting)
        % To support diagnostic settings, need to report if any file has been
        % locally modified (i.e. not just a direct copy of content from the
        % existing shared code area).
        constParamsUpdatedLocally = {constParamsFile};
        
        if strcmpi(lUseOnlyExistingSharedCode, 'error')
            % An error is going to be thrown because the diagnostic setting
            % prohibits use of any locally generated shared utility code.
            % Before throwing the error, replace the locally modified source
            % file with the existing shared code copy.
            fid = fopen(constParamsFileLocal, 'w');
            fwrite(fid, fileContentExisting);
            fclose(fid);
        end        
    end
end


% Return specifications of shared parameter header files that must be
% copied to the local build folder
function [sharedParamFilesToCopy, sharedParamFoldersToCopy] = i_getSharedParamFilesToCopy...
    (files, folders, lSharedUtilsFolder)

sharedParamFilesToCopy = {};
sharedParamFoldersToCopy = {};

if isempty(files)
    return;
end

% Skip const_params.c or .cpp because it is handled separately
constParamsIdx = i_getConstParamsIdx(files);
files = files(~constParamsIdx);

needCopyIdx = false(size(files));

for i=1:length(needCopyIdx)
    
    src = fullfile(folders{i}, files{i});
    dst = fullfile(lSharedUtilsFolder, files{i});
    if isfile(dst)
        srcContent = fileread(src);
        dstContent = fileread(dst);
        % If destination file is up to date, it must match the existing shared
        % code copy of the same file. Otherwise, need to copy existing shared
        % code copy of const_params.
        needCopyIdx(i) = ~strcmp(srcContent, dstContent);
    else
        needCopyIdx(i) = true;
    end
end

sharedParamFilesToCopy = files(needCopyIdx);
sharedParamFoldersToCopy = folders(needCopyIdx);


% Return specifications of the const_params file, if and only if it must be copied to the local
% build folder
function [constParamsFileToCopy, constParamsFolderToCopy] = i_getConstParamsToCopy...
    (constParamsFile, constParamsFolder, lSharedUtilsFolder)

constParamsFileToCopy = {};
constParamsFolderToCopy = {};

if isempty(constParamsFile)
    return;
end

assert(iscell(constParamsFile) && length(constParamsFile)==1, ...
    'There must be a single const_params file')
constParamsFile = constParamsFile{1};
constParamsFolder = constParamsFolder{1};
    
srcConstParams = fullfile(constParamsFolder, constParamsFile);
dstConstParams = fullfile(lSharedUtilsFolder, constParamsFile);
if isfile(dstConstParams)
    srcContent = fileread(srcConstParams);
    dstContent = fileread(dstConstParams);
    % If destination file is up to date, it must match the existing
    % shared code copy of const_params (and it's allowed to have
    % additional content appended). Otherwise, need to copy existing
    % shared code copy of const_params.
    needCopy = ~strncmp(srcContent, dstContent, length(srcContent));
else
    needCopy = true;
end
if needCopy
    constParamsFileToCopy = {constParamsFile};
    constParamsFolderToCopy = {constParamsFolder};
end
        
    

% Identify shared parameter files
function [sharedParamFiles, fileMap] = i_getSharedParamFiles(lSharedUtilsFolder)
fileMapFile = fullfile(lSharedUtilsFolder, 'filemap.mat');
if isfile(fileMapFile)
    fileMap = load(fileMapFile);
    fileMap = fileMap.fileMap;
    sharedParamFiles = cell(1, fileMap.length);
    values = fileMap.values;
    for i=1:length(values)
        sharedParamFiles{i} = values{i}.file;
    end
    sharedParamFiles = unique(sharedParamFiles, 'stable');
else
    sharedParamFiles = {};
    fileMap = [];
end

% Convert the required content to a regular expression
% sensitivity to white space
function definitionsExpr = i_getRequiredContentExpr(definitions)

% Convert the text to match into an regular expression
definitionsExpr = regexptranslate('escape', definitions);

% Don't be particular about white space, i.e. space, space+tab or tab must
% all be equivalent.
definitionsExpr = regexprep(definitionsExpr, '\s+', '\\s+');

% Semi-colon can optionally be preceded by white space
definitionsExpr = regexprep(definitionsExpr, '\s*;', '\\s*;');


function i_checkUpdatedSharedParamFiles...
    (updatedSharedParamFiles, updatedSharedParamFolders, fileMap, lSharedUtilsFolder)

if isempty(fileMap)
    return
end


sharedParamFiles = cell(1, fileMap.length);
definitions = cell(1, fileMap.length);
values = fileMap.values;
for i=1:length(values)
    sharedParamFiles{i} = values{i}.file;
    definitions{i} = values{i}.definition;
end

% Strip out comments (e.g. containing traceability data)
commentExpr = '\/\*.*?\*\/';
definitions = regexprep(definitions, commentExpr, '');

% Remove leading and trailing white space
definitions = strtrim(definitions);


for i=1:length(updatedSharedParamFiles)
    
    updatedFile = updatedSharedParamFiles{i};
    matchIdx = strcmp(sharedParamFiles, updatedFile);
    requiredDefinitions = definitions(matchIdx);
    
    sharedParamFile = fullfile(lSharedUtilsFolder, updatedFile);
    content = fileread(sharedParamFile);
    
    % Convert the required content to a regular expression that removes
    % sensitivity to white space
    requiredContentExprs = i_getRequiredContentExpr(requiredDefinitions);
    
    % Use regexp to check for required content
    result = regexp(content, requiredContentExprs, 'once');
    foundContent = ~cellfun(@isempty, result);
    
    if ~all(foundContent)
        missingDefinitions = string(requiredDefinitions(~foundContent));
        missingDefinitions = """" + missingDefinitions + """";
        missingDefinitions = join(missingDefinitions, ", ");
        fileInExistingSharedCodeArea = ...
            fullfile(updatedSharedParamFolders{i}, updatedFile);
        error(message('coder_xrel:crossRelease:MissingSharedParamDef', ...
            sharedParamFile, missingDefinitions, ...
            fileInExistingSharedCodeArea, lSharedUtilsFolder));
    end
end
