% RedisCache Manage persistent data stored on a Redis server.
% Use mps.cache.connect to create instances of RedisCache in deployed
% applications.

% Copyright 2018 The MathWorks, Inc.
classdef RedisCache < mps.cache.DataCache
    
    properties (GetAccess = public, Hidden)
        Host
        Port
    end
    
    properties (Access = private)
        serviceConnection
    end
    
    methods
        
        function obj = RedisCache(name,conn,options)
        %RedisCache Constructor. Call mps.cache.connect instead.
        
            % Forward inputs to superclass constructor
            obj = obj@mps.cache.DataCache(name,conn,options);

            % Save connection handle
            obj.serviceConnection = conn;

            % Extract host and port from input connection. 
            obj.Host = obj.serviceConnection.Host;
            obj.Port = obj.serviceConnection.Port;
            
            if isempty(obj.serviceConnection.ServiceHandle)
                connectToService(obj);
            end
        end

        function v = getp(obj, name)
        %GETP Get the value of a named property.
        %
        %   Example:
        %   Get the value of the 'Host' property.
        %
        %       hostname = getp(c, 'Host')
        %       hostname =
        %           'localhost'
        %   
            narginchk(1,2);
            
            % 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
        
        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
        
            % Cell array of strings permitted as input. Flatten.
            if iscell(varargin{1})
                key = varargin{1};
            else
                key = varargin;
            end
            sz = zeros(1,numel(key));
            % Redis Lua script -- if key exists, determine size of value,
            % otherwise return -1.
            script = ['if redis.call("exists",KEYS[1]) == 1 and redis.call("hexists",KEYS[1],ARGV[1]) == 1\n' ...
                      'then\n' ...
                      '    return string.len(' ...
                      '        redis.call("hget",KEYS[1],ARGV[1]))\n' ...
                      'else\n' ...
                      '    return -1\n' ...
                      'end'];
            script = sprintf(script);   % Convert \n to actual CR
            for k=1:numel(key)
                cn = get(obj.serviceConnection.ServiceHandle);
                sz(k) = doRedisCmd(cn, "eval", script, ...
                     '1', obj.Name, key{k});
            end
            toNaN = sz == -1;
            sz(toNaN) = NaN;
        end
        
        function count = countRemoteKeys(obj)
        %COUNTREMOTEKEYS How many keys are stored on the Redis server?
        % RedisCache stores keys remotely 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
        
            cn = get(obj.serviceConnection.ServiceHandle);
            count = doRedisCmd(cn, "hlen", obj.Name);
        end
        

    end
        
    methods (Access = protected)
        
        function [tf,c] = validateOrReconnect(obj, redisConnection)
            c = [];
            if validateRedisConnection(redisConnection)
                tf = true;   % Connection good
                c = redisConnection;
            else
                if isvalid(obj)
                    try
                        c = connectToRedis(obj.Host, obj.Port, ...
                                obj.serviceConnection.Key);
                    catch me
                        error(message('MPS:MATLAB:Cache:InactiveService', ...
                                obj.serviceConnection.ProviderName, ...
                                obj.serviceConnection.ConnectionName));

                    end
                end
                tf = false;  % Bad connection, replace with new one.
            end
        end

        function c = connectToService(obj)
            c = connectToRedis(obj.Host, obj.Port, obj.serviceConnection.Key);
            rh = mps.cache.ResourceHandle(c, @disconnectFromRedis, ...
                         message('MPS:MATLAB:Cache:NoServiceConnection', ...
                                   obj.serviceConnection.ConnectionName, ...
                                   obj.serviceConnection.ProviderName), ...
                               @(oh)validateOrReconnect(obj, oh));
            obj.serviceConnection.ServiceHandle = rh;
        end

        function list = methodList(obj)
            persistent mlist
            if isempty(mlist)
                mlist = methods(obj);
            end
            list = mlist;
        end        
        
       function [k, ck] = serviceKeys(obj)
            k = {};
            ck = containers.Map;
            cn = get(obj.serviceConnection.ServiceHandle);
            k = doRedisCmd(cn, "hkeys", obj.Name);
                
            % If there's a Redis hash named #<cache name>, some of
            % the keys in k are hashes of composite keys. We must 
            % replace them with the equivalent MATLAB structures. 
            %
            % Create a containers.Map:
            %     <hash value> -> <composite key>
            % Which is (considering data types):
            %     <8-byte string> -> <serialized MATLAB structure>
            %
            % Note: we want to return the MATLAB structure, not the
            % hash of the structure. The hash is an internal
            % implementation detail, persistence provider specific. 
            hashName = sprintf('#%s', obj.Name);
            h = doRedisCmd(cn, "exists", hashName);
            ki = false(1,numel(k));
            if h ~= 0
                % The hash #<cache name> exists. Get the keys from it,
                % then read the values of those keys.
                hk = doRedisCmd(cn, "hkeys", hashName);
                [hv{1:numel(hk)}] = readFromRedis(cn, ...
                                                  hashName, hk{:});
                % Return value is a 1xN struct array. Build the map.
                for n = 1:numel(hk)
                    ck(hk{n}) = hv{n};
                end
            end            
        end        
        
        function propgrp = getPropertyGroups(obj)
            if ~isscalar(obj)
                propgrp = getPropertyGroups@mps.cache.DataCache(obj);
            else

                fields = hiddenPublicProperties(obj);
                for k=1:numel(fields)
                    pList.(fields{k}) = obj.(fields{k});
                end
                
                propgrp = matlab.mixin.util.PropertyGroup(pList);
            end
        end
        
        
        % readFromService and writeToService accept two kinds of keys:
        % simple and composite. Simple keys are MATLAB strings of any
        % reasonable length (Redis' key length is outrageously high: 2^31
        % bytes). Composite keys are MATLAB structures.
        %
        % Redis keys must be strings. To accomodate composite keys, hash
        % the each composite key to a short string, using, e.g. SHA256 on
        % the serialized representation of the composite key.
        %
        % Users won't want the hashed composite key when they retrieve the
        % keys from cache -- they'll expect the MATLAB structure they
        % supplied. So we create a Redis hash that maps hashed keys to
        % their MATLAB structure values. 
        %
        % For a given cache named 'FleetAnalytics', the Redis hash storing
        % the composite keys used by FleetAnalytics is named
        % '#FleetAnalytics'. This is a safe choice because user cache names
        % must be valid MATLAB variable names, and '#' cannot appear in a
        % MATLAB variable name.
        
        
        function varargout = readFromService(obj, varargin)
            varargout = cell(1,nargin-1);
            k = simplifyCompositeKeys(varargin);
            cn = get(obj.serviceConnection.ServiceHandle);
            [varargout{:}] = readFromRedis(cn, ...
                obj.Name, k{:});
        end
        
        function recordCompositeKeys(obj, simple, composite)
            % Create name value pairs by interleaving simple and composite.
            % Make sure they are row vectors.
            if size(simple,1) > 1
                simple = simple';
            end
            
            if size(composite,1) > 1
                composite = composite';
            end
            
            interleave = { simple(:) ; composite{:} };
            interleave = interleave(1:end);
            
            % Write the simple key -> composite key map to Redis. Name the
            % map #<cache name>. The write will create the hash map if it
            % doesn't exist (thank you, Redis).
            hashName = sprintf('#%s', obj.Name);
            cn = get(obj.serviceConnection.ServiceHandle);
            writeToRedis(cn, hashName, interleave{:});
        end
        
        function writeToService(obj, varargin)
            
            % 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 Redis cache.
            kIdx = 1:2:numel(varargin)-1;
            k = varargin(kIdx);
            
            [k, ki] = simplifyCompositeKeys(k);
            
            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
            cn = get(obj.serviceConnection.ServiceHandle);
            writeToRedis(cn, 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);
            cn = get(obj.serviceConnection.ServiceHandle);
            removed = doRedisCmd(cn, "hdel", obj.Name, k{:});
            
            % Remove any composite keys from the composite key hash
            if any(ki)
                hashName = sprintf('#%s', obj.Name);
                h = doRedisCmd(cn, "exists", hashName); 
                if h ~= 0
                    doRedisCmd(cn, "hdel", hashName, k{ki});
                end
            end
        end
    end
end



