function [uploadedFiles, errors] = uploadToCloud(varargin)
% uploadToCloud Upload files to a your cloud storage area. 
%
%  [UPLOADEDFILES, ERRORS] = uploadToCloud(CLUSTER, LOCALFILES, REMOTEFILES)
%  uploads files from the local machine to your cloud storage area, storing
%  them in the locations specified in REMOTEFILES. Returns a list of files 
%  successfully uploaded and an array of error structures indicating any 
%  failures.
%
%  CLUSTER (optional) - A cluster object representing the MathWorks Hosted 
%    Cloud cluster that is used to perform the upload. If this is not
%    provided, the default cluster will be used. 
%
%  LOCALFILES - Character vector or cell array of character vectors
%    containing the list of files or folders to upload. Wildcards are not
%    permitted. 
%
%  REMOTEFILES - Character vector or cell array of character vectors
%    containing the destination locations for the respective files and
%    folders specified in LOCALFILES.  
%
%  If LOCALFILES and REMOTEFILES are cell arrays, they must be the same
%  length.
%
%  UPLOADEDFILES is a cell array containing the elements of REMOTEFILES 
%  that were successfully uploaded. 
%
%  ERRORS is a structure array containing items that describe errors that
%    occurred while uploading. Each element has the following fields: 
%   - localfile   The absolute path of the file specified for upload.
%   - remotefile  The destination path specified.
%   - message     A description of the error that occurred. 
%   - exception   The exception that caused the error (if any). 
%
% In scripts and functions that are executed on remote MathWorks Hosted
% Cloud machines, the getCloudFolder function can be used to find the root
% of the cloud storage area in order to access your files. 
%
%
% Example: Upload specific files
%
% Assume that the working folder contains the files data1.dat and
% data2.dat. Upload these to the cloud storage area folder called 
% "resources". 
%
% localfiles = {'data1.dat', 'data2.dat'};
% remotefiles = {'resources/data1.dat', 'resources/data2.dat'};
% parallel.internal.cloud.uploadToCloud(localfiles, remotefiles)
%
%
% Example: Upload a whole folder
%
% Assume that the local folder "resources" exists in the current working
% folder. Copy the whole folder to the cloud storage area. 
%
% parallel.internal.cloud.uploadToCloud('resources', 'resources'); 
%
%
% Example: Upload a specific file specifying a particular cluster
%
% Assume that the file data.dat exists in the current working folder, and
% that the MathWorks Hosted Cloud cluster profile is called 'mhccluster'. 
% Copy data.dat to a folder called resources in the cloud storage area.
% 
% cluster = parcluster('mhccluster');
% parallel.internal.cloud.uploadToCloud(cluster, 'data.dat', 'resources/data.dat');
%
%
% Example: Check that files were uploaded successfully
%
% Assume that the working folder contains the files data1.dat and
% data2.dat. Upload these to the cloud storage area, and generate a list of
% any files that could not be uploaded, and display any errors that
% occurred. 
%
% localfiles = {'data1.dat', 'data2.dat'};
% remotefiles = {'data1.dat', 'data2.dat'};
% [uploadedfiles, errors] = parallel.internal.cloud.uploadToCloud(localfiles, remotefiles);
%
% % Empty elements in uploadedfiles correspond to local files that were not
% % uploaded.
% failedfiles = {localfiles{cellfun(@isempty, uploadedfiles)}};
%
% % Display any errors
% for ii=1:numel(errors)
%     errors(ii)
% end
%
% See also parallel.internal.cloud.getCloudFolder,
%          parallel.internal.cloud.addCloudFolderToPath
%

%   Copyright 2014-2016 The MathWorks, Inc.

    narginchk(2, 3)
    
    if feature('isdmlworker')
        error(...
            'parallel:cloud:files:InteractiveOnly', ...
            'This function can only be executed on interactive MATLAB sessions.');
    end    

    if (nargin == 2)
        % The optional cluster parameter was not specified, so the user
        % must be providing two lists.  
        cluster = parcluster;
        localFiles = varargin{1};
        remoteFiles = varargin{2};
    else
        % All three parameters were provided.
        cluster = varargin{1};
        localFiles = varargin{2};
        remoteFiles = varargin{3};
    end
    
    if ~isa(cluster, 'parallel.Cluster')
        error(...
            'parallel:cloud:files:BadClusterParameter', ...
            'The CLUSTER parameter must be a parallel.Cluster object.');
    end
    
    if ischar(localFiles)
        localFiles = {localFiles};        
    end
    
    if ischar(remoteFiles)
        remoteFiles = {remoteFiles};
    end
    
    if ~iscell(localFiles) || any(~cellfun(@ischar, localFiles)) || ~isvector(localFiles)
        error(... 
            'parallel:cloud:files:LocalFilesMustBeStringOrCell', ...
            'The LOCALFILES parameter must be a string or a 1D cell array of strings. ');
    end
    
    if ~iscell(remoteFiles) || any(~cellfun(@ischar, remoteFiles)) || ~isvector(remoteFiles)
        error(...
            'parallel:cloud:files:RemoteFilesMustBeStringOrCell', ...
            'The REMOTEFILES parameter must be a string or a 1D cell array of strings. ');
    end
    
    if numel(localFiles) ~= numel(remoteFiles)
        error(...
            'parallel:cloud:files:ArgumentsMustBeSameLength', ...
            'LOCALFILES and REMOTEFILES must contain the same number of items. ');
    end
    
    try      
        % So that we can look up which file is which on the worker, we
        % pre-convert each local file to its absolute filename, then pass these
        % absolute paths along to the storeInCloud function. 
        absoluteLocalFiles = cellfun(...
            @parallel.internal.apishared.FilenameUtils.getAbsolutePath, ...
            localFiles, ...
            'UniformOutput', false);

        batchJob = batch(...
            cluster, ...
            @parallel.internal.cloud.storeInCloud, ...
            2, ...
            {{absoluteLocalFiles, remoteFiles}}, ...
            'AttachedFiles', absoluteLocalFiles);

        jobCleanup = onCleanup(@() delete(batchJob)); 
        wait(batchJob);
        batchVariables = fetchOutputs(batchJob);
        uploadedFiles = batchVariables{1};
        errors = batchVariables{2};
    catch e
        ex = MException(...
            'parallel:cloud:files:UploadFailed', ...
            'Failed to upload files to the cloud storage location. ');
        ex = ex.addCause(e);
        throw(ex); 
    end
end
