classdef polariPeaksTable < handle
    % Table of peak values, in a FigurePanel widget.
    
    events
        CloseRequest
    end
    
    properties (Dependent)
        Location % upper left corner, in pixels, relative to figure origin
    end
    
    properties
        ContextMenuFcn
        PolariObj
        Title = 'Peaks'
        Units = ''
        Values = []      % vector of numeric values
        Visible = true
        Width = 150      % pixel width
    end
    
    properties (Access=private)
        pLocation = [1 1]
        hFigPanel
        hListeners
    end
    
    properties (Constant,Hidden)
        MaxPeaksInTable = 10
    end
    
    methods
        function obj = polariPeaksTable(p,varargin)
            
            % PolariObj must be passed in
            obj.PolariObj = p;
            fig = p.hFigure;
            if ~ishghandle(fig)
                error('Parent must be an HG handle.');
            end
            if ~any(strcmpi(fig.Type,{'figure','uicontainer','uipanel'}))
                error('Parent must be a figure, uicontainer or uipanel.');
            end
            
            % Set param/value pairs:
            if nargin > 1
                Nv = numel(varargin);
                if rem(Nv,2) ~= 0
                    error('Incorrect number of parameter/value arguments.');
                end
                for i = 1:2:Nv
                    obj.(varargin{i}) = varargin{i+1};
                end
            end
            
            init(obj);
        end
        
        function set.Values(obj,val)
            if ~isempty(val)
                validateattributes(val,{'numeric'}, ...
                    {'vector','real'},'polariPeaksTable','Values');
            end
            obj.Values = val;
            
            % Side-effect:
            updateString(obj);
        end
        
        function set.Location(obj,val)
            validateattributes(val,{'numeric'}, ...
                {'vector','numel',2,'real','positive'}, ...
                'polariPeaksTable','Location');
            obj.pLocation = val;
            
            % Side-effect:
            fp = obj.hFigPanel;
            if ~isempty(fp) && ishghandle(fp)
                h = fp.Position(4); % panel height
                fp.Position(1:2) = val - [0 h]; % translate upper left to lower left
            end
        end
        
        function set.Title(obj,val)
            if isempty(val)
                val = '';
            else
                validateattributes(val,{'char', 'string'}, ...
                    {'vector'}, ...
                    'polariPeaksTable','Title');
            end
            obj.Title = val;
            
            % Side-effect:
            updateTitle(obj);
        end
        
        function set.Units(obj,val)
            if isempty(val)
                val = '';
            else
                validateattributes(val,{'char', 'string'}, ...
                    {'vector'}, ...
                    'polariPeaksTable','Units');
            end
            obj.Units = val;
            
            % Side-effect: re-use updateString here
            updateString(obj);
        end
        
        function val = get.Location(obj)
            % Return [x y] of upper left coord (not native lower left)
            fp = obj.hFigPanel;
            if ~isempty(fp) && ishghandle(fp)
                h = fp.Position(4); % height
                val = fp.Position(1:2) + [0 h]; % translate from lower left to upper left
                obj.pLocation = val; % update on read
            else
                val = obj.pLocation;
            end
        end
        
        function set.Width(obj,val)
            validateattributes(val,{'numeric'}, ...
                {'scalar','real','positive'}, ...
                'polariPeaksTable','Width');
            obj.Width = val;
            
            % Side-effect:
            updatePosition(obj);
        end
        
        function set.Visible(obj,val)
            validateattributes(val,{'logical','numeric'}, ...
                {'scalar','real'}, ...
                'polariPeaksTable','Visible');
            obj.Visible = logical(val);
            
            % Side-effect:
            updateVisible(obj);
        end
        
        function val = get.Visible(obj)
            
            fp = obj.hFigPanel;
            if ~isempty(fp) && ishghandle(fp)
                % fig panel exists
                val = obj.Visible;
            else
                % fig panel was closed
                val = false;
            end
        end
        
        function delete(obj)
            % Deleting polariPeaksTable object
            
            deleteAllListeners(obj);
            delete(obj.hFigPanel);
        end
    end
    
    methods (Access=private)
        function init(obj,loc)
            
            % Only proceed if parent is valid
            p = obj.PolariObj;
            hf = p.hFigure;
            if ~isempty(hf) && ishghandle(hf)
                
                % Create context menu
                hc = uicontextmenu( ...
                    'Parent',hf, ...
                    'Callback',@(h,ev)callbackContextMenu(obj,h,ev), ...
                    'HandleVisibility', 'off');
                
                % Create FigurePanel
                fp = ...
                    matlab.graphics.shape.internal.FigurePanel('Parent', hf);
                fp.DeleteFcn = @(h,ev)deleteFigPanel(obj);
                fp.UIContextMenu = hc;
                obj.hFigPanel = fp;
                
                % Updates:
                updateTitle(obj);
                updateString(obj);
                if nargin > 1
                    updatePosition(obj,loc);
                else
                    updatePosition(obj);
                end
                updateVisible(obj);
            end
            
            % Create/recreate listeners
            initListeners(obj);
            
            updatePeakMarkersList(obj); % kick it once manually
        end
        
        function initListeners(obj)
            % Listen to marker properties in polari 
            %
            % We make two substructs since we create and delete these
            % subsets at different times.
            
            deleteAllListeners(obj);
            p = obj.PolariObj;
            
            % Primary Listeners
            % -----------------
            %
            % Listen for a change to marker list, current dataset:
            lis.PeakMarkersListListener = addlistener(p, ...
                {'hPeakAngleMarkers','pCurrentDataSetIndex'}, ...
                'PostSet', @(~,~)updatePeakMarkersList(obj));
            
            % Listen for global data / data unit changes
            % Ex: normalize data flag was changed
            lis.PolariDataUnits = addlistener(p, ...
                'DataUnitsChanged',@(~,~)updatePeakMarkersList(obj));
            
            obj.hListeners = lis;
        end
        
        function callbackContextMenu(obj,h,ev)
            % Callback for table context menu.
            %
            % If ContextMenuFcn is non-empty, invokes that function handle
            % using ContextMenuFcn(hMenu,eventStruct).
            %
            % If .ContextMenu is empty, do nothing
            cmFcn = obj.ContextMenuFcn;
            if ~isempty(cmFcn)
                cmFcn(h,ev);
            end
        end
        
        function updatePeakMarkersList(obj)
            % Update peak table entries
            
            p = obj.PolariObj;
            if isempty(p)
                return % EARLY EXIT
            end
            
            dIdx = p.pCurrentDataSetIndex;
            if isempty(dIdx)
                obj.Title = 'Peaks';
                obj.Values = [];
                obj.Units = '';
            else
                obj.Title = sprintf('Peaks (Dataset %d)',dIdx);
                
                % Get all Peak markers with same dataset index.
                % Note that all Peak markers have DataSetMode='manual'.
                mAll = findPeakMarkersOnDataset(p,dIdx);
                Np = numel(mAll);
                v = zeros(1,Np);
                u_i = '';
                for i = 1:Np
                    m_i = mAll(i);
                    d = getData(m_i);
                    v(i) = d.mag.*d.scale;
                    u_i = d.units;
                end
                obj.Values = v;
                obj.Units = u_i; % just use last one, all the same
            end
        end
        
        function deleteFigPanel(obj)
            % FigurePanel closing, typically due to user hitting "X"
            % button in title bar
            
            if isvalid(obj)
                % Cache Location before closing:
                %  - the "get" operation updates hLocation property
                dummy = obj.Location; %#ok<NASGU>
                
                notify(obj,'CloseRequest');
            end
        end
        
        function deleteAllListeners(obj)
            obj.hListeners = internal.polariCommon.deleteListenerStruct( ...
                obj.hListeners);
        end
        
        function updateTitle(obj)
            
            fp = obj.hFigPanel;
            if ~isempty(fp) && isvalid(fp)
                fp.Title = obj.Title;
            end
        end
        
        function updatePosition(obj,loc)
            % Call in response to a change in:
            %   .Location
            %   .Width
            
            fp = obj.hFigPanel;
            if ~isempty(fp) && isvalid(fp)
                % Retain height for automatic setting
                % All the rest are settable
                %
                % We want .Location to be UPPER LEFT, not native LOWER
                % LEFT. This keeps the title-bar stationary/in view during
                % height changes.
                
                pos = fp.Position; % [x, lower left y, width, heigth]
                h = pos(4); % current height
                if nargin > 1
                    pos(1:2) = loc - [0 h];
                else
                    pos(1:2) = obj.Location - [0 h];
                end
                pos(3) = obj.Width;
                fp.Position = pos;
            end
        end
        
        function updateVisible(obj)
            fp = obj.hFigPanel;
            if isempty(fp) || ~ishghandle(fp)
                % Create if fig panel was closed
                init(obj,obj.pLocation);
            else
                fp.Visible = internal.LogicalToOnOff(obj.Visible);
            end
        end
        
        function updateString(obj)
            % Print values to a string and set into FigurePanel:
            %   {'1: 12.34dB',
            %    '2: 12.34dB', ...}
            
            fp = obj.hFigPanel;
            if ~isempty(fp) && isvalid(fp)
                % Determine new string
                %
                v = obj.Values;
                Nv = numel(v);
                if Nv==0
                    str = {'(no peaks)'};
                else
                    % Print vector of peak locations to a cell-string
                    %  - maximum of MaxPeaksInTable
                    %  - show "(+N more)" as last row when clipped
                    Nmax = obj.MaxPeaksInTable;
                    suppress = Nv > Nmax;
                    if suppress
                        Nx = Nv - Nmax;
                        Nv = Nmax;
                        Nstrs = Nv+1; % add one row for "(+N more)"
                    else
                        Nstrs = Nv;
                    end
                    vstr = internal.polariCommon.sprintfMaxNumTotalDigits(v(1:Nv),4,true);
                    unitStr = obj.Units;
                    str = cell(Nstrs,1);
                    for i = 1:Nv
                        str{i} = [sprintf('%d: ',i) vstr{i} ' ' unitStr];
                    end
                    if suppress
                        str{end} = sprintf(' (+%d more)',Nx);
                    end
                end
                
                repositionForSizeChange(obj,str);
            end
        end
        
        function repositionForSizeChange(obj,str_new)
            % Assumes hFigPanel is non-empty
            %
            % When changing # rows, keep all on screen if possible
            % Priority goes to top line: truncate bottom if too tall
            
            fp = obj.hFigPanel;
            loc_orig = obj.Location; % get/update current upper left location
            pos_orig = fp.Position;
            
            fp.String = str_new;
            
            % Maintain pos at top of readout
            obj.Location = loc_orig;
            
            % See if bottom extends below fig
            loc_new = obj.Location;
            pos_new = fp.Position;
            if pos_new(2) < 1
                % Bottom of readout falls below bottom of figure.
                % Bump readout location upwards:
                fig_pos = obj.PolariObj.hFigure.Position;
                fig_dy = fig_pos(4);
                del = pos_orig(2) - pos_new(2);
                loc_new(2) = loc_new(2) + del;
                if loc_new(2) > fig_dy
                    % Moving readout up will make top of readout fall
                    % beyond top of figure.  Priority is to keep top of
                    % readout visible:
                    del = loc_new(2) - fig_dy;
                    loc_new(2) = loc_new(2) - del;
                end
                obj.Location = loc_new;
            end
        end
    end
end
