function [isMissingSource, missingSources] = missingSourcesFromException(E)
% missingSourcesFromException Dig through exception causes for things which
% look like comprehensible exceptions which might tell us about a missing
% source.

% Copyright 2015-2020 The MathWorks, Inc.

% Dig through E to find all exceptions
queue = {E};
results = {E};

while ~isempty(queue)
    % pop off the queue
    examining = queue{1}; queue = queue(2:end);

    if isa(examining, 'ParallelException')
        % push remotecause onto results
        results = [results; examining.remotecause]; %#ok<AGROW> unknown size result.
        % push remotecause onto queue
        queue = [queue; examining.remotecause]; %#ok<AGROW> unknown size result.
    else
        % push cause onto results
        results = [results; examining.cause]; %#ok<AGROW> unknown size result.
        % push cause onto queue
        queue = [queue; examining.cause]; %#ok<AGROW> unknown size result.
    end        
end

% Ignore ParallelException from 'results' since these have no interesting fishes
% in their holes. In other words, there are no interesting arguments to
% ParallelExceptions since the underlying "real" exception is always added as a
% "cause", and the ParallelException itself may well have been constructed
% without the normal arguments (i.e. it might be constructed using something
% equivalent to ParallelException('orig:id', '%s', 'Original complete message')).
ignore  = cellfun(@(e) isa(e, 'ParallelException'), results);
results = results(~ignore);

% Which error IDs are known to correspond to missing sources
knownMissingSourceIDs = { 'MATLAB:UndefinedFunction', ...
                    'parallel:lang:spmd:SourceCodeNotAvailable', ...
                    'parallel:lang:spmd:SourceCodeNotAvailableOnLab', ...
                    'parallel:lang:spmd:UndefinedFunctionOnWorker', ...
                    'parallel:lang:parfor:SourceCodeNotAvailable', ...
                    'MATLAB:parallel:future:CouldNotInterpretFunction' };

% For each of those, which of the Exception's arguments correspond
% to names of missing sources - as a column to ensure 'whichArgumentToUse'
% ends up as a column to match 'results'.
knownMissingSourceArguments = { 1; 1; []; 1; []; [] };

% Get the actual message ID from each observed error
observedIDs = cellfun(@(e) e.identifier, results, 'UniformOutput', false);

% Figure out which (if any) known-missing-source that corresponds to
[isObservedIDMissingSource, whichMissingSourceID] = ismember(observedIDs, knownMissingSourceIDs);
isMissingSource         = any(isObservedIDMissingSource);
missingSourceExceptions = results(isObservedIDMissingSource);
whichArgumentToUse      = knownMissingSourceArguments(...
    whichMissingSourceID(isObservedIDMissingSource));

% For each Exception that might give us missing source information,
% get the fish out of the hole.
missingSources = cellfun(@iExtractMissingSources, missingSourceExceptions, whichArgumentToUse, ...
                         'UniformOutput', false);
% Flatten and uniquify
missingSources = unique([missingSources{:}]);
end

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Given an exception and indices of arguments which might name missing
% files, return those missing files as a cell array if possible.
function missingSources = iExtractMissingSources(E, whichArguments)
if ~isempty(whichArguments) && max(whichArguments) <= numel(E.arguments)
    missingSources = E.arguments(whichArguments);
else
    missingSources = {};
end
end
