% MATFileCache Manage persistent data stored in MATLAB MAT-files.
% Use mps.cache.connect to create instances of MATFileCache for testing
% cache-enabled applications in a MATLAB session. MATFileCache objects
% cannot be deployed to MATLAB Production Server or with MATLAB Compiler
% or Compiler SDK.
%
% A MATFileCache is a key/value store, where keys may be simple or
% composite. A simple key is a MATLAB string. A composite key is a MATLAB
% structure. Values are arbitrary MATLAB data.
%
% See Also DataCache, control, connect

% Copyright 2018 The MathWorks, Inc.
classdef MATFileCache < mps.cache.DataCache
    
    properties (Access = private)
        serviceConnection
        compositeKeyVar = 'compositeKey'
    end
    
    properties
        Folder
    end
    
    methods
        
        function obj = MATFileCache(name, connection, options)
        %MATFILECACHE Constructor. Call mps.cache.connect instead.
        
            obj = obj@mps.cache.DataCache(name, connection, options);
            
            obj.Folder = connection.Folder;
            obj.serviceConnection = connection;
            
            if isempty(connection.ServiceHandle)
                connectToService(obj);
            end
        end
        

        function sz = bytes(obj, varargin)
        %BYTES How many bytes used by the data stored under key?
        %
        %   sz = BYTES(c, names} returns an array of byte counts for all
        %   the keys in names, which can be a character vector, a
        %   string array or a cell array of character vectors.
        %
        %   If a key doesn't exist in the cache, its size is NaN.
        %   BYTES returns the number of bytes used by the serialized data 
        %   stored in the cache. To calculate the MATLAB size of the data 
        %   stored under key, retrieve the data and call size: 
        %   size(cache.<key>).
        %
        %   Example:
        %   Create a cache and find out how much storage a 10x10 matrix
        %   of doubles requires:
        %
        %       myCache = mps.cache.connect('myCache');
        %       myCache.x = invhilb(10);
        %       sz = bytes(myCache,'x')
        %       sz = 
        %           864
            c = get(obj.serviceConnection.ServiceHandle);
            sz = bytes(c, obj.Name, varargin{:});
        end
        
        function count = countRemoteKeys(obj)
        %COUNTREMOTEKEYS How many keys are stored in MAT-files?
        %   MatlabCache stores keys in MAT-files by default. Use retain to 
        %   store keys locally.
        %
        %   Example:
        %   
        %   myCache = mps.cache.connect('myCache');
        %   myCache.today = datestr(now);
        %   remoteKeys = countRemoteKeys(myCache)
        %   remoteKeys = 
        %       1
        %
        %   See Also retain
        
            c = get(obj.serviceConnection.ServiceHandle);
            count = countRemoteKeys(c, obj.Name);
        end
        
        function v = getp(obj, name)
        %GETP Get the value of a named NATFileCache property.
        %
        %   V = getp(C, NAME) returns the value of MATFileCache property
        %   NAME in MATFileCache object C. 
        %
        %   Available properties include:
        %
        %       'Folder': Full path to the folder storing the MAT-files
        %            storing the data for this cache.
        %
        %   See Also subsref, get
            
            % Only allow access to public, hidden properties.
            accessibleProperties = hiddenPublicProperties(obj);
            if nargin == 2 && any(strcmp(accessibleProperties, name))
                v = obj.(name);
            else
                error(message('MPS:MATLAB:Cache:InvalidPropertyName', ...
                    class(obj), name));
            end
        end
        
        function save(obj)
            flush(obj);
        end
        
        function delete(obj)
            save(obj);
        end
        
    end
    
    methods (Access = protected)
        
        function mfs = connectToService(obj)
            ap = mps.persistence.internal.ActiveServices.getInstance();
            mfs = get(ap,obj.Folder);
            if isempty(mfs)
                error(message('MPS:MATLAB:Cache:InactiveService',...
                    obj.serviceConnection.ProviderName, ...
                    obj.serviceConnection.ConnectionName));
            end
            obj.serviceConnection.ServiceHandle = ...
                mps.cache.ResourceHandle(mfs, [], ...   
                    message('MPS:MATLAB:Cache:NoServiceConnection', ...
                                   obj.serviceConnection.ConnectionName, ...
                                   obj.serviceConnection.ProviderName), ...
                    @(oh)validateOrReconnect(obj, oh));

        end
        
        function [tf, c] = validateOrReconnect(obj, matfileService)
            c = [];
            if ~isempty(matfileService) && matfileService ~= 0 && ...
                    activeService(matfileService)
                tf = true;
                c = matfileService;
            else
                % Don't reconnect if MATFileCache has been deleted.
                if isvalid(obj)
                    c = connectToService(obj);
                end
                tf = false;
            end
        end
        
        function list = methodList(obj)
            persistent mlist
            if isempty(mlist)
                mlist = methods(obj);
            end
            list = mlist;
        end
        
        function [k, ck] = serviceKeys(obj)
        % serviceKeys Return the keys stored in this cache by the service.
        % [k, ck] = serviceKeys(obj) returns the simple and composite keys
        % this cache has stored in the service. The simple keys are
        % returned as a cell array of strings. The composite keys are
        % are returned as a map <hashed key string> -> <composite key>.
       
            c = get(obj.serviceConnection.ServiceHandle);
            k = keys(c, obj.Name);
            ck = [];
                
            % Read the composite key cache -- keys and values, because the
            % values are the keys we need to substitute into the returned
            % key list k.
            if hasHash(c, obj.Name, obj.compositeKeyVar)
                % Returns [values,names].
                [composite,hash] = hashGet(c, obj.Name, ...
                    obj.compositeKeyVar);
                % containers.Map does not support string keys, so convert
                % string matrix to cell array.
                if ~isempty(composite) && ~isempty(hash)
                    ck = containers.Map(cellstr(hash), composite);   
                end
            end
            k = cellstr(k);
       end
        
       function varargout = readFromService(obj, varargin)
            varargout = cell(1,nargin-1);
            k = simplifyCompositeKeys(varargin);
            c = get(obj.serviceConnection.ServiceHandle);
            [varargout{:}] = read(c, obj.Name, k{:});
       end
       
         function recordCompositeKeys(obj, hash, composite)
         % Save <hash string>, <composite key> pairs in a separate cache.
         
            % Create name value pairs in parallel MATLAB variables 
            % hashString and compositeKey. Can't simply use hash strings
            % as variable names (as Redis does) since the hash strings 
            % might not be valid MATLAB variable names (start with a 
            % number, longer than namelengthmax).
            %
            % Make them both row vectors.
         
            if size(hash,1) > 1
                hash = hash';
            end
            
            if size(composite,1) > 1
                composite = composite';
            end
            
            % Write the simple key -> composite key map to a MAT-file hash
            % variable named obj.compositeKeyVar.
            c = get(obj.serviceConnection.ServiceHandle);
            hashSet(c, obj.Name, obj.compositeKeyVar, hash, ...
                composite);
        end
        
        function writeToService(obj, varargin)
        % Input consists of (key,value) pairs. Keys may be simple or 
        % composite. A simple key is a MATLAB string or char vector.
        % A composite key is a MATLAB structure. Values are arbitrary 
        % MATLAB values.
            
            % Examine keys to determine which, if any, are composite. A
            % composite key is a MATLAB structure that must be hashed to a
            % unique string value before being stored in the cache.
            kIdx = 1:2:numel(varargin)-1;
            k = varargin(kIdx);
            
            % k: List of simplified keys
            % ki: If k(n) was composite, ki(n) is true. False otherwise.
            [k, ki] = simplifyCompositeKeys(k);
            
            % Record the composite keys, if there were any. 
            if any(ki)
                % Compute the location of the composite keys in the input
                % varargin list.
                cIdx = false(1,numel(varargin));
                cIdx(kIdx) = ki;
                
                % Keep a list of the composite keys so we can record them
                % later.
                composite = varargin(cIdx);
                
                % Write the simple keys over the composite keys. 
                varargin{cIdx} = k{ki};              
                
                % Remember the composite keys. May write data we've already
                % written (if we've seen this key before), but probably
                % cheaper to just write rather than checking for duplicates
                % (which involves a server round trip) then writing only
                % the new (unique) keys.
                recordCompositeKeys(obj, string(k(ki)), composite);
            end
            
            % Composite keys have been converted to strings. Write the data
            % to storage.
            c = get(obj.serviceConnection.ServiceHandle);
            write(c, obj.Name, varargin{:});
        end
            
        function removed = removeFromService(obj, varargin)
        % removeFromService Remove keys stored by the service. 
        % Does not remove local keys.
            removed = 0;
            if isempty(varargin), return, end
            [k,ki] = simplifyCompositeKeys(varargin);
            c = get(obj.serviceConnection.ServiceHandle);
            removed = remove(c, obj.Name, k{:});
            % Remove any composite keys from the composite key hash
            if any(ki)
                % Extract the simplified keys
                varargin = k{ki};
                rk = hashRemove(c, obj.Name, ...
                    obj.compositeKeyVar, varargin{:});
            end
        end
   end  
end
