% DataCache Access persistent data in MPS deployed application.
% 
%  A persistent user data cache stores MATLAB values in a persistent
%  location. Each value is identified by a key, which must be specified to 
%  store or retrieve the value. The lifetime of the stored data depends 
%  on the configuration of the persistence service that manages the cache. 
%
%  Keys may be simple or composite. Simple keys are scalar strings or
%  character vectors. Composite keys are MATLAB structures. String or
%  character vector keys must be valid MATLAB identifiers.
%
%  Archives deployed to a MATLAB Production Server instance use DataCache
%  objects to preserve data between stateless requests. MATLAB code uses
%  DataCache objects to access persistent data. A DataCache object
%  represents a connection to a named cache managed by a persistence
%  service. 
%
%  Create DataCache objects with mps.cache.connect, which requires an
%  active persistence service. MATLAB Production Server administrators
%  typically activate persistence services for deployed archives. For
%  testing before deployment, manage local persistence services using
%  mps.cache.control.
%
%  Examples: 
%
%  Connect to a cache named 'DataStorage17' in the persistence service
%  identified by the connection named 'WorkData':
%
%      c = mps.cache.connect('DataStorage17', 'Connection', 'WorkData');
%
%  Store a numeric matrix in the cache c under the key 'temperature':
%
%      c.temperature = [ 350, 107, 28 ];
%
%  Store multiple values in the cache c:
%
%      numbers = [ 3 5 7 9 ];
%      names = {'three', 'five', 'seven', 'nine'};
%      put(c, 'numbers', numbers, 'names', names);
%
%  Retrieve the value stored at key 'names' from the cache:
%
%      n = c.names
%      n = 
%        {'three'}    {'five'}    {'seven'}    {'nine'}  
%
%  Retrieve multiple vales from cache c:
%
%      [t, num] = get(c,'temperature','numbers')
%      t =
%         350   107    28
%      num =
%         3     5     7     9
%
%  Create and start a local persistence service named 'WorkData' using the
%  'MatlabTest' persistence provider:
%
%      ctrl = mps.cache.control('WorkData','MatlabTest');
%      start(ctrl);
%
%  See Also connect, control, mps.sync.mutex


% Copyright 2018 The MathWorks, Inc.
classdef DataCache < handle & matlab.mixin.CustomDisplay

    % Visible to the public, via getp method. Can't extract via
    % indexing, because this class overrides subsref and subsasgn.
    properties (GetAccess = public, Hidden)
        Name          % Name of the cache.
        Operations    % Operations permitted on data in this cache.
        LocalKeys     % Names of variables to cache in local storage.
        Connection    % Name of the connection used to access this cache.
    end
        
    % Implementation, invisible, subject to frequent, incompatible change.
    properties (Access = protected)
        localData     % Local storage for higher performance.
        modified      % Which locally stored values have been modified?
        compositeKey  % Hashed value -> MATLAB structure.
    end        

    methods
        function obj = DataCache(name,connection,options)
        % DataCache Constructor. Abstract class. Do not call directly.
            if ~isvarname(name)
                error(message('MPS:MATLAB:Cache:InvalidCacheName', name));
            end
            
            % Check for unrecognized options. Subtract the known options
            % from the set of input options -- if there's anything left,
            % complain. Be careful -- the unknown options are not
            % guaranteed to be strings.
            unknownOptions = {};
            if nargin >= 3
                unknownOptions = ...
                        setdiff(fieldnames(options),{'LocalKeys'});        
            end
            if ~isempty(unknownOptions)
                badOption = unknownOptions{1}; 
                if ~isstring(badOption) && ~ischar(badOption)
                    if isscalar(badOption)
                        badOption = string(badOption);
                    else
                        badOption = sprintf('<%d element array of %s>', ...
                            numel(badOption), class(badOption));
                    end
                end
                error(message('MPS:MATLAB:Cache:UnrecognizedOption', ...
                    badOption));
            end
            
            obj.Operations = connection.Operations;
            obj.Connection = connection.ConnectionName;
            obj.Name = name;
            if isfield(options,'LocalKeys')
                obj.LocalKeys = options.LocalKeys;
            else
                obj.LocalKeys = cell.empty;
            end
            initLocalData(obj);
        end
        
        function k = keys(obj,location)
        %KEYS Return the names of all the keys in the cache. 
        %
        %   list = keys(C) returns a cell array of all the keys in cache 
        %   C. LIST names both local and remote keys. 
        %
        %   A key may be a scalar string or character vector or a MATLAB
        %   structure. Keys identify a value stored in a cache. Keys must 
        %   be unique within a cache but may be duplicated in different
        %   caches. String or character keys must be valid MATLAB
        %   identifiers.
        %
        %   list = keys(C, LOCATION) returns the keys stored in LOCATION,
        %   which may be mps.cache.Location.Remote for keys of values 
        %   stored in the persistence service or mps.cache.Location.Local 
        %   for keys of values temporarily cached locally.
        %
        %   Examples:
        %
        %     Store data in the cache named 'c18' and retrieve the keys.
        %
        %     c = mps.cache.connect('c18', 'Connection', 'LocalRedis');
        %     x.time = now;
        %     x.id = 23;
        %     c.(x) = {'data', 'to', 'be', 'used', 'later'};
        %     c.euler = exp(1)^(pi * i) + 1;
        %     list = keys(c)
        %     list = 
        %         {1x1 struct}
        %         {'euler'  }
        %         
        % See Also isKey, put, get, retain, Location
        
            % Get remote keys by default (nargin == 1) and if specifically
            % requested (location == mps.cache.Location.Remote).
            if nargin == 1 || location == mps.cache.Location.Remote
                % Get all the keys stored by the service. Returns a cell 
                % array of simple keys and a containers.Map of composite 
                % keys, where:
                %
                %   ck{<hashed composite key>} -> <composite key structure>. 
                % 
                % So, keys(ck) is the list of hash strings and values(ck)
                % is the list of actual composite key structures.
                [k, ck] = serviceKeys(obj);
            else
                k = {};
                ck = containers.Map('KeyType','char','ValueType','any');
            end
            
            % Check the local cache for composite keys too.
            if nargin == 1 || location == mps.cache.Location.Local
                if ~isempty(obj.compositeKey)
                    hk = keys(obj.compositeKey);
                    for n = 1:numel(hk)
                        ck(hk{n}) = obj.compositeKey(hk{n});
                    end
                end
                k = union(k, localKeys(obj));
            end
            
            % Now we have a list of unique keys. If any are composite hashes,
            % replace them with their actual non-hashed composite values.
            if isempty(ck) == false
                for n=1:numel(k)
                    if isKey(ck,k{n})
                        k{n} = ck(k{n});    %#ok -- does not change size
                    end
                end
            end
            % Result should be a column vector.
            if isrow(k)
                k = k';
            end
        end  
        
       function tf = isKey(obj, varargin)
        %ISKEY Which of the input names are keys in the cache? 
        % 
        %   tf = isKey(C, KEYS) checks to see if the elements in KEYS are
        %   keys in the cache C. The returned logical vector TF has one 
        %   entry for each key in KEYS. 
        %
        %   Each element in KEYS may be a simple or composite key. A simple
        %   key is a scalar string or character vector. A composite key is
        %   a MATLAB structure. KEYS may be a scalar, cell array or
        %   comma-separated list. String or character keys must be valid
        %   MATLAB identifiers.
        %   
        %   Example:
        %
        %     Let c be a cache with keys 'temperature', 'pressure' and
        %     'altitude'. 
        %
        %         tf = isKey(c, 'psi', 'temperature', 'height')
        %         tf =
        %             [ 0, 1, 0 ]
        %
        %   See Also keys, retain
        
            % Allow the input to be a single string array or cell array.
            if numel(varargin) == 1
                d = varargin{1};
                if iscell(d)
                	varargin = d;
                elseif isstring(d)
                    varargin = cellstr(d);
                end
            end
            
            k = keys(obj);
            tf = false(1,numel(varargin));
            for n = 1:numel(tf)
                m = 1;
                while m <= numel(k) && tf(n) == false
                    tf(n) = isequal(k{m}, varargin{n});
                    m = m + 1;
                end
            end
        end       
        
        function k = retain(obj, varargin)
        %RETAIN Set or get the list of locally cached keys.
        %
        %   RETAIN(C, KEYS) sets the list of keys cached locally by cache
        %   C to the elements of KEYS. Any currently local data is flushed
        %   to the persistence service and the list of retained keys reset
        %   to KEYS.
        %
        %   k = RETAIN(cache) returns the list of keys cached locally.
        %
        %   KEYS is a list of simple or composite keys or a logical value,
        %   TRUE or FALSE. If KEYS is a list, only those keys named in the
        %   list will be cached locally. If KEYS is the value TRUE, all
        %   data will be cached locally. If KEYS is false, no data will be
        %   cached locally. String or character keys must be valid MATLAB
        %   identifiers.
        %
        %   KEYS may name keys that do not yet exist. Use flush or purge to
        %   write locally cached data to a persistence service.
        %
        %   Data cached locally may be accessed more quickly than data
        %   stored on a remote persistence service, but at a cost: if the
        %   MATLAB session or MPS worker hosting the local cache crashes or
        %   exits, the values in the local cache may be lost. Use this
        %   feature with caution and only for data you can afford to lose.
        %   
        %   Examples:
        %  
        %      Retain data stored under the keys 'phi', 'a' and 'b':
        %
        %          retain(c,'phi','a','b');
        %          c.phi = 1.61803;
        %          c.a = 17;
        %
        %      Get the list of local keys:
        %
        %          local = keys(c,mps.cache.Location.Local);
        %          local = 
        %              {'a'  }
        %              {'b'  }
        %              {'phi'}
        %
        %      Get the list of remote keys:
        %
        %          remote = keys(c,mps.cache.Location.Remote);
        %          remote = 
        %              1x0 empty cell array       
        %
        %   See Also flush, purge, keys
        
            if nargin == 1
                k = obj.LocalKeys;
            else
                % Setting new list of local keys, so purge the local cache
                % (write all locally stored data to remote storage) and
                % reset the list of local keys.
                purge(obj);
                
                % Allow localKeys (input) to be character vector, 
                % MATLAB string vector or cell array of character vectors.
                % Convert input to cell array of character vectors.
                if numel(varargin) == 1
                    d = varargin{1};
                    if iscell(d) || islogical(d)
                        localKeys = d;
                    elseif isstring(d)
                        localKeys = cellstr(d);
                    else 
                        localKeys = varargin;
                    end
                else
                    localKeys = varargin;
                end
                
                obj.LocalKeys = localKeys;
                initLocalData(obj);
            end
        end
        
        function varargout = get(obj, varargin)
        %GET Given one or more keys, retrieve the corresponding values.
        %
        %   VALUES = get(C, KEYS) retrieves the MATLAB data stored in cache 
        %   C under the keys in KEYS. For long lists of keys, get is faster
        %   than subsref. The data in VALUES has the same order as the keys
        %   in KEYS. That is, VALUES{k} is the value stored at KEYS{k}.
        % 
        %   Each element in KEYS may be a simple or composite key. A simple
        %   key is a scalar string or character vector. A composite key is
        %   a MATLAB structure. KEYS may be a scalar, cell array or
        %   comma-separated list.  String or character keys must be valid
        %   MATLAB identifiers.
        %
        %   Examples:
        %
        %   Let c be a cache with keys 'mass_flow', 'pressure', 'velocity'
        %   and 'area'. Get the values stored under 'mass_flow' and 'area':
        %
        %       v = get(c,'mass_flow','area')
        %       v = 
        %          {[280]} {[99]}
        %
        %   Retrieve all the data in the cache:
        %
        %       v = get(c,keys(c))
        %       v = 
        %          {[99]}  {[280]}  {[1000]}  {[17]}
        %
        %   See Also put, keys, retain, subsref
        
            % Allow single cell array input. Flatten into list of variable
            % names.
            if numel(varargin) == 1 
                if iscell(varargin{1})
                    varargin = varargin{1};
                elseif isstring(varargin{1})
                    varargin = cellstr(varargin{1});
                end
            end
            
            varargout = cell(1,numel(varargin));
            
            % If we're using the local cache, look there first
            if ~isempty(obj.localData)  
                            
                % The local cache mechanism (containers.Map)
                % requires keys to be strings. 
                varargin = mps.cache.DataCache.encodeKeys(varargin);

                % Find which, if any, of the requested variables are in the
                % local cache.
                isLocal = isfield(obj.localData,varargin);
                localVars = varargin(isLocal);
                n = numel(localVars);
                localIdx = find(isLocal);
                for k=1:n
                    varargout{localIdx(k)} = obj.localData.(localVars{k});
                end
                
                % If some variables weren't cached locally, fetch the
                % values and cache them.
                if all(isLocal) == false
                    isRemote = ~isLocal;
                    remoteVars = varargin(isRemote);
                    remoteVals = cell(1,numel(remoteVars));
                    [remoteVals{:}] = readFromService(obj,remoteVars{:});
                    [varargout(isRemote)] = remoteVals;
                    for k=1:numel(remoteVars)
                        obj.localData.(remoteVars{k}) = remoteVals{k};
                    end
                end
            else
                % Read all the variables from the service, since none are
                % cached (since we have no local cache).
                [varargout{:}] = readFromService(obj,varargin{:});     
            end
            
            % If user asked for one output, and there's more than one, 
            % give them a cell array of the results.
            if nargout == 1 && numel(varargout) > 1
                varargout = { varargout };
            end
        end
        
        function put(obj, varargin)
        %PUT Write data to cache. Inputs are name-value pairs.
        %
        %   PUT(C, KEY_1, VALUE_1, KEY_2, VALUE_2, ...) stores the values
        %   VALUE_1 and VALUE_2 under the keys KEY_1 and KEY_2
        %   respectively, in cache C. put accepts as many name value pairs
        %   as MATLAB allows.
        %
        %   PUT(C, KEYS, VALUES) stores the values in VALUES under the keys
        %   in KEYS. KEYS and VALUES must be vectors of the same size.
        %
        %   Each key may be simple or composite. A simple key is a scalar 
        %   string or character vector. A composite key is a MATLAB 
        %   structure. String or character keys must be valid  MATLAB
        %   identifiers.
        %
        %   Examples:
        %
        %   Store three values in cache c using name value pairs:
        %
        %       put(c,'Vb',12,'Vbe',0.5,'Rb',500000)
        %       k = keys(c)
        %       k = 
        %          {'Rb' }
        %          {'Vb' }
        %          {'Vbe'}
        %
        %   Store three values in cache c using cell arrays:
        %
        %       keys = { 'Vb', 'Vbe', 'Rb' };
        %       values = { 12, 0.5, 500000 }
        %       put(c, keys, values);
        %       k = keys(c)
        %       k = 
        %          {'Rb' }
        %          {'Vb' }
        %          {'Vbe'}        
        %
        %   See Also get, keys, retain, subsasgn
        
            % Assume an interleaved list of keys and values, e.g.
            % { 'x', 3, 'y', 4, 'z', 5 }, because that's what varargin
            % gives us with:
            %
            %  put(c, 'x', 3, 'y', 4, 'z', 5)
            
            % Extract names and values into separate vectors.
            % FYI: llaves means keys in Spanish.
            llaves = varargin(1:2:numel(varargin)-1);
            values = varargin(2:2:numel(varargin));
            
            % Allow varargin{1} to be a cell array of names and 
            % varargin{2} to be a cell array of values. If they are,
            % flatten them into names and values. 
            %
            % The nargin == 3 case handles these variants:
            %
            %  put(c, { 'Vb', 'Vbe', 'Rb' }, { 12, 0.5, 500000 })
            %     llaves: {{ 'Vb', 'Vbe', 'Rb' }}
            %     values: {{ 12, 0.5, 500000 }}
            %
            %  put(c, [ "Vb", "Vbe", "Rb" ], { 12, 0.5, 500000 })
            %     llaves: {[ 'Vb', 'Vbe', 'Rb' ]}
            %     values: {{ 12, 0.5, 500000 }}   
            %
            %  put(c, { 'Vb' }, { 12 } )
            %     llaves: {{ 'Vb' }}
            %     values: {{ 12 }}
            %
            %  put(c, "Vb", { 12 } )
            %     llaves: { "Vb" }
            %     values: {{ 12 }}
            %
            %  put(c, 'Vb', 12)
            %     llaves: { 'Vb' }
            %     values: {{ 12 }}
            %
            %  put(c, "Vb", 12)
            %     llaves: { "Vb" }
            %     values: { 12 }
            %
            % In all cases we want a cell array of keys and a cell array of
            % values. But we also have to allow storing a cell array:
            %
            %  put(c, "V", { 2, 4, 8 })
            %     llaves: { "V" }
            %     values: {{ 2, 4, 8 }}
            %
            % So we can only flatten the values if we flattened the keys.
            % 
            if nargin == 3 
                % At this point we're looking for { { 'Vb', 'Vbe', 'Rb' } }
                % or { [ "Vb", "Vbe", "Rb" ] }, which we want to turn into
                % a cell array of character vectors:
                %
                %  { 'Vb', 'Vbe', 'Rb' }
                % 
                flattenValues = false;
                if iscell(llaves{1})
                    llaves = llaves{1};
                    flattenValues = true;
                elseif isstring(llaves{1})
                    llaves = cellstr(llaves{1});
                    flattenValues = true;
                end
                % If the key is a cell array, and values{1} is a cell array,
                % remove one layer of cell array nesting from values.
                if iscell(values{1}) && (numel(llaves) > 1 || flattenValues)
                    values = values{1};
                    if numel(values) ~= numel(llaves)
                        error(message('MPS:MATLAB:KVStore:KeyValueCountMismatch', ...
                            numel(llaves), numel(values)));
                    end
                end
            end
        
            % If not using local cache, write data to service and return.
            if isempty(obj.LocalKeys) || ...
                    ( islogical(obj.LocalKeys) && obj.LocalKeys == false)
                varargin = [ llaves; values ];
                varargin = varargin(1:end);
                writeToService(obj,varargin{:});
                return;
            end
            
            % Determine which names, if any, are to be cached locally.
            % At this point, llaves must be a cell array of strings, or
            % ismember will fail.
            if islogical(obj.LocalKeys) && obj.LocalKeys == true
                isLocal = true(1,numel(llaves));
            elseif iscell(obj.LocalKeys) && ~isempty(obj.LocalKeys)
                isLocal = ismember(llaves,obj.LocalKeys);
            end
            
            % Write locally-cached data to local cache. And remember that
            % it has been modified.
            localNames = llaves(isLocal);
            if ~isempty(localNames)
                
                % The local cache mechanism (containers.Map)
                % requires keys to be strings. But composite keys may be
                % structs. Map (hash) structs to strings.
                [k,e] = mps.cache.DataCache.encodeKeys(localNames);
                
                % Write encodings to composite key map, for later lookup by
                % other functions.
                if any(e)
                    encKeys = k(e);
                    compKeys = localNames(e);
                    for n=1:numel(encKeys)
                        obj.compositeKeys(encKeys{n}) = compKeys{n};
                    end
                    % Replace composite keys with their hash representation
                    % for use in the local cache.
                    localNames(e) = encKeys;
                end
                
                % If this is the first write to the local cache, make a
                % place to remember that we've modified this variable.
                if isempty(obj.modified), obj.modified = struct(); end
                
                localValues = values(isLocal);
                for n=1:numel(localNames)
                    obj.localData.(localNames{n}) = localValues{n};
                    obj.modified.(localNames{n}) = true;
                end
            end
            
            % Write non-local data to the persistence service. If remote
            % keys are composite, defer composite key management policy to
            % persistence service interface class.
            remoteKeys = llaves(~isLocal);
            if ~isempty(remoteKeys)
                remoteValues = values(~isLocal);
                remoteData = [remoteKeys; remoteValues];
                remoteData = remoteData(1:numel(remoteKeys)*2);
                writeToService(obj,remoteData{:});
            end
            
        end
        
        function names = purge(obj)
        %PURGE Write all local data to cache, clear local data.
        %
        %   KEYS = purge(C) writes all locally cached data in cache C to 
        %   the persistence service that manages the cache's data. purge
        %   returns a cell array of the names of the keys written to the
        %   persistence service.
        %
        %   After calling purge, the list of locally cached keys will be 
        %   empty, that is K = retain(C) will return an empty cell array K.
        %
        %   Example:
        %
        %   Let C be a cache with locally cached keys 'Vb', 'Vbe' and 'Rb'. 
        %
        %       k = retain(c)
        %       k = 
        %          {'Vb' } {'Vbe'} {'Rb'}
        %
        %       k = purge(c);
        %       k = 
        %          {'Vb' }
        %          {'Vbe'}
        %          {'Rb' }
        %    
        %       k = retain(c)
        %       k = 
        %           0x0 empty cell array
        %
        %   See Also retain, keys, flush
        
            if ~isempty(obj.localData)
                names = flush(obj);
                obj.LocalKeys = cell.empty;
                initLocalData(obj);
            elseif nargout == 1
                names = {};
            end
        end
        
        function names = flush(obj)
        %FLUSH Write all local dirty data to the cache, keep local data.
        %
        %   KEYS = flush(C) writes all locally cached data in cache C to 
        %   the persistence service that manages the cache's data. flush
        %   returns a cell array of the names of the keys written to the
        %   persistence service.
        %
        %   Unlike purge, flush does not clear the list of locally cached
        %   keys.
        %
        %   Example:
        %
        %   Let C be a cache with locally cached keys 'Vb', 'Vbe' and 'Rb'. 
        %
        %       k = retain(c)
        %       k = 
        %          {'Vb' } {'Vbe'} {'Rb'}
        %
        %       k = flush(c);
        %       k = 
        %          {'Vb' }
        %          {'Vbe'}
        %          {'Rb' }
        %    
        %       k = retain(c)
        %       k = 
        %          {'Vb' } {'Vbe'} {'Rb'} 
        %
        %   See Also retain, keys, purge
        
            if ~isempty(obj.modified)
                names = fieldnames(obj.modified);
                
                % If all modified names have been removed, the structure
                % will be non-empty, but have zero fields. No data to write
                % in this case.
                if ~isempty(names)
                    values = cellfun(@(k)obj.localData.(k),names,...
                        'UniformOutput',false);
                
                    % Interleave names and values:
                    %    (name1, value1, name2, value2 ...)
                    % names and values must be 1xN row vectors. Stack them
                    % into a 2xN matrix, then extract interleaving by 
                    % walking along the columns -- down the first column, up to
                    % the top of the second, down the second, etc. 
                    %
                    % writeToService expects N name-value pairs.
                    savePairs = [names' ; values'];
                    writeToService(obj, savePairs{1:end}); 
                end
                
                % All data is clean now, so forget all the dirty data flags
                obj.modified = struct.empty;
                
            elseif nargout == 1
                names = {};
            end
        end
            
        function removed = remove(obj, varargin)
        %REMOVE Remove data from the cache (forget it forever). 
        %
        %   REMOVED = remove(C, KEYS) removes the MATLAB data stored in
        %   cache C under the keys in KEYS and returns the number of keys
        %   removed. remove is permanent. The removed data cannot be 
        %   recovered. 
        % 
        %   Each element in KEYS may be a simple or composite key. A simple
        %   key is a scalar string or character vector. A composite key is
        %   a MATLAB structure. KEYS may be a scalar, cell array or
        %   comma-separated list.  String or character keys must be valid
        %   MATLAB identifiers.
        %
        %   Example:
        %
        %   Let c be a cache with keys 'lambda', 'h', 'm' and 'v':
        %
        %       removed = remove(c,'h','v')
        %       removed =
        %           2
        %
        %   Note: remove counts locally cached and remote keys
        %   independently. 
        %
        %  See Also clear, retain, purge, flush, keys
        
            % Allow the input to be a single string array or cell array.
            if numel(varargin) == 1
                d = varargin{1};
                if iscell(d)
                	varargin = d;
                elseif isstring(d)
                    varargin = cellstr(d);
                end
            end       
        
            removed = removeFromService(obj,varargin{:});
            if ~isempty(obj.localData)
                for k=1:numel(varargin)
                    if isfield(obj.localData,varargin{k})
                        obj.localData = rmfield(obj.localData,varargin{k});
                        removed = removed + int64(1);
                        if isfield(obj.modified,varargin{k})
                            obj.modified = rmfield(obj.modified,varargin{k});
                        end
                    end
                end
            end
        end
        
        function n = clear(obj)
        %CLEAR Empty the cache. Remove all keys and values. 
        %
        %   N = clear(C) removes all keys and MATLAB data stored in
        %   cache C and returns the number of keys cleared. clear is 
        %   permanent. The cleared data cannot be recovered. 
        % 
        %   Examples:
        %
        %   Let c be a cache with keys 'lambda', 'h', 'm' and 'v':
        %
        %       n = clear(c)
        %       n =
        %           2
        %
        %   Clear cache c with four remote and two locally cached keys:
        %
        %       retain(c,'lambda','v');
        %       n = clear(c)
        %       n =
        %           6        
        %
        %   Note: clear counts locally cached and remote keys
        %   independently. 
        %
        %  See Also remove, retain, purge, flush, keys        
        
            n = 0;
            if length(obj) > 0  %#ok -- Calling subclass' length method
                n = remove(obj, keys(obj));
            end
        end
        
        function varargout = subsref(obj,expr)
        %SUBSREF Retrieve one data value stored in the cache.
        %
        %   V = C.x retrieves the MATLAB data value stored at key 'x' in
        %   cache C. For large numbers of keys, get is faster.
        %
        %   V = C.(k) retrieves the MATLAB data value stored at the key
        %   whose value is that of MATLAB variable k. Composite keys must
        %   use this form of subsref. (See 'Dynamic Field Names').
        %
        %   Examples:
        %
        %   Let c be a cache with keys 'phi', 'pi' and 'e'. Read the value
        %   stored under 'phi' from the cache:
        %
        %       phiV = c.phi
        %       phiV = 
        %          1.618033
        %
        %   Read the value stored under the key "e", using dynamic
        %   field names:
        %
        %       k = "e";
        %       eV = c.(k)
        %       eV = 
        %          2.718
        %           
        %   Note: Because DataCache uses subsref to access data stored in
        %   the cache, subsref cannot access the properties of a DataCache
        %   object. To read properties of a DataCache object, use the
        %   method getp.
        %   
        %   See Also get, subsasgn, getp
        
            switch expr(1).type
                case '.'
                    name = expr(1).subs;
                    v = get(obj, name);
                    % Allow dot-ref method calls, but note: cached variable
                    % name will shadow methods. Must support zero and
                    % multiple output functions.
                    if isempty(v) && any(strcmp(name,methodList(obj)))
                        n = nargout;
                        if n > 0
                            [varargout{1:n}] = builtin('subsref', obj, expr);
                        else
                            % Try one output. If that fails, call again with
                            % zero outputs. Bad form to use try/catch for
                            % flow of control, forced upon us because we
                            % cannot know a priori how many outputs an
                            % arbitrary MATLAB function has.
                            try
                                varargout{1} = builtin('subsref',obj,expr);
                            catch 
                                builtin('subsref', obj, expr);
                                varargout = {};
                            end
                        end
                    else
                        if length(expr) > 1
                            v = builtin('subsref', v, expr(2:end));
                        end
                        varargout{1} = v;
                    end
                otherwise
                    % Forward all other types of indexing to the builtin
                    v = builtin('subsref',obj,expr);
                    varargout{1} = v;
            end
            
        end
        
        function obj = subsasgn(obj,expr,value)
        %SUBSASGN Store a single data value in the cache.
        %
        %   C.x = V stores the value of MATLAB variable V at key 'x' in
        %   cache C. For large numbers of keys, put is faster.
        %
        %   C.(k) = V stores the value of MATLAB variable V at the key
        %   whose value is that of MATLAB variable k. Composite keys must
        %   use this form of subsasgn. (See 'Dynamic Field Names').
        %
        %   Examples:
        %
        %   Let c be a cache with keys 'phi' and 'e'. Add the value 
        %   3.14159 to the cache under the key 'pi':
        %
        %       c.pi = 3.14159;
        %
        %   Add the value 3.14159 to the cache with key 'pi' using 
        %   dynamic field names:
        %
        %       k = 'pi';
        %       c.(k) = 3.14159;
        %
        % See also: subsref, get, put.
        
            switch expr.type
                case '.'
                    name = expr(1).subs;
                    % Warn if name is identical to a public method name
                    if any(strcmp(name,methodList(obj)))
                        warning(message(...
                            'MPS:MATLAB:Cache:VariableShadowsMethod', ...
                            name));
                    end
                    
                    if length(expr) == 1
                        put(obj, name, value);
                    else
                        val = builtin('subasgn',name,expr(2:end),value);
                        put(obj, name, val); % Probably wrong.
                    end
                otherwise
                    % If creating first object in object array, assign
                    % value to obj.
                    if strcmp(expr.type,'()') && length(expr) == 1 && ...
                            isempty(obj) && isa(value, 'mps.cache.DataCache')
                        obj = value;
                    else 
                        % Forward all other types of indexing to the builtin
                        obj = builtin('subsasgn',obj,expr,value); 
                    end
            end   
        end
        
        function count = length(obj, location)
        %LENGTH How many keys stored in this cache?
        %
        %   COUNT = length(C) returns a MATLAB int64 value equal to the
        %   number of unique keys in the cache.
        %
        %   COUNT = length(C, LOCATION) returns a MATLAB int64 value equal
        %   to the number of keys stored at LOCATION, which may be:
        %
        %       mps.cache.Location.Local : count locally cached keys.
        %       mps.cache.Location.Remote: count remotely stored keys.
        %
        %   Examples:
        %
        %   Let c be a cache with keys 'Su', 'Ps' and 'R1'. Count the keys
        %   in the cache:
        %
        %       n = length(c)
        %       n = 
        %           3
        % 
        %   Cache a new key 'G' locally, then count keys again:
        %
        %       retain(c,'G');
        %       c.G = 40;
        %       n = length(c)
        %       n = 
        %           4
        %
        %   Count number of remote and local keys:
        %
        %       local = length(c, mps.cache.Location.Local)
        %       local =
        %           1
        %       remote = length(c, mps.cache.Location.Remote)
        %       remote = 
        %           3
        %
        %   Note the last result implies that the key 'G' and its value
        %   exist only in the local cache and have not yet been written to
        %   the remote cache.
        %
        %   See Also Location, retain
            count = -1;
            if nargin == 1
                if isempty(obj.LocalKeys) || ...
                    (islogical(obj.LocalKeys) && obj.LocalKeys == false)
                    location = mps.cache.Location.Remote;
                else
                    % Count the union of the local and remote keys.
                    count = numel(keys(obj));
                end
            end
            if count < 0
                switch location
                    case mps.cache.Location.Remote
                        count = countRemoteKeys(obj);
                    case mps.cache.Location.Local
                        count = numel(fieldnames(obj.localData));
                end
            end
            % Redis always returns int64. Be consistent.
            count = int64(count);  
        end
    end
    
    methods (Abstract)
        sz = bytes(obj, key)         % Bytes used by the value @ key.
        count = countRemoteKeys(obj) % How many keys stored by the service?
        p = getp(obj, name)          % Get a named property of DataCache
    end
    
    methods (Access = protected)
        
       function names = hiddenPublicProperties(obj)
        % Return the names of the hidden, public properties.
            mco = metaclass(obj);
            plist = mco.PropertyList;
            h = find([plist.Hidden]);
            p = find(strcmp({plist.GetAccess},'public'));
            idx = intersect(h,p);
            names = [];
            if ~isempty(idx)
                names = string({plist(idx).Name});
            end
        end        
        
        function header = getHeader(obj)
            if ~isscalar(obj)
                header = getHeader@matlab.mixin.CustomDisplay(obj);
            else
                cls = matlab.mixin.CustomDisplay.getClassNameForHeader(obj);
                if ~isvalid(obj)
                    header = getString(message('MPS:MATLAB:Cache:DeletedClass', cls));
                elseif isempty(obj)
                    header = '';
                else
                    header = getString(message('MPS:MATLAB:Cache:ClassWithProperties', cls));
                end
            end      
        end
        
        function footer = getFooter(obj)
            if isscalar(obj)
                footer = getString(message('MPS:MATLAB:Cache:FooterLink'));
            else
                footer = '';
            end
        end
        
        function propgrp = getPropertyGroups(obj)
            if ~isscalar(obj)
                propgrp = getPropertyGroups@matlab.mixin.CustomDisplay(obj);
            else
                names = hiddenPublicProperties(obj);
                for k = 1:numel(names)
                    pList.(names{k}) = obj.(names{k});
                end
                propgrp = matlab.mixin.util.PropertyGroup(pList);
            end
        end
        
        function initLocalData(obj)
            if ~isempty(obj.LocalKeys)
                if iscell(obj.LocalKeys)
                    % Build a cell array that interleaves the variable 
                    % names and empty arrays. Use this to initialize the
                    % struct so it doesn't have to grow dynamically.
                    vals = cell(1,numel(obj.LocalKeys));
                    args = { obj.LocalKeys{:} ; vals{:} };
                    obj.localData = struct(args{:});
                else
                    obj.localData = struct();
                end
                obj.modified = struct();
            else
                obj.localData = struct.empty;
                obj.modified = struct.empty;
            end
        end
        
        function names = locallyModified(obj)
            names = {};
            if ~isempty(obj.modified)
                names = fieldnames(obj.modified);
            end
        end
        
        function names = localKeys(obj)
            names = {};
            if ~isempty(obj.localData)
                names = fieldnames(obj.localData);
            end       
        end
    end
        
    methods(Static)
        
       function [keyStrList,encoded] = encodeKeys(keyList)
        % encodeKeys Transform composite keys to simple keys; hash them.
        % Composite keys must be MATLAB structures.

            n = numel(keyList);
            encoded = false(1,n);
            keyStrList = keyList;
            for k=1:n
                key = keyList{k};
                if isstruct(key)
                    keyStrList{k} = mps.persistence.internal.fingerprint(key);
                    encoded(k) = true;
                end
            end
        end        
        
    end
     
    methods (Abstract,Access = protected)
        % Subclasses must implement these C.R.U.D. methods -- they're
        % so common, and so important we suggest specialized
        % implementation, rather than relying on generic command forwarding
        % (as we expect to be used for the other abstract methods, for
        % which performance is not such a concern).
        
        varargout = readFromService(obj, varargin)
        writeToService(obj, varargin)
        removed = removeFromService(obj, varargin)
        
        % Return a list of keys and a map of composite keys: 
        %   <hashed composite key> -> <actual composite key>
        [kList, compositeKeys] = serviceKeys(obj)
        
        % Return a list of the class's methods
        mList = methodList(obj)

    end   
end


