classdef LogicAnalyzerMessageHandler < matlabshared.scopes.WebMessageHandler
    %LogicAnalyzerMessageHandler   Define the LogicAnalyzerMessageHandler.
    
    %   Copyright 2016 The MathWorks, Inc.
    
    properties (Dependent)
        Cursors;
        DisplayChannels;
    end
    
    properties
        radix;
        height;
        color;
        format;
        fontSize;
        spacing;
        backgroundColor;
        TimeDisplayOffset;
        TimeSpan;
        TerminateComplete = false;
    end
    
    properties (SetAccess = protected)
        OpenComplete = false;
    end
    
    properties (SetAccess = protected, Hidden)
        pCursors;
        pDisplayChannels;
        NextCursorInstance = 2;
        NextChannelInstance = 2;
        DisplayChannelTags = {'W1'};
        CursorTags = {'C1'};
    end
    
    methods
        function reset(this)
            this.ClientSettingsStale = true;
            this.callMethod('reset', []);
            
            % Do not wait more than 10 seconds for a response.
            for indx = 1:100
                if ~this.ClientSettingsStale
                    return;
                end
                pause(.1);
            end
        end
        
        function initialize(this)
            this.callMethod('initialize', []);
        end
        
        function S = toStruct(this)
            S = toStruct@matlabshared.scopes.WebMessageHandler(this);
            S.radix    = this.radix;
            S.height   = this.height;
            S.color    = this.color;
            S.format   = this.format;
            S.fontSize = this.fontSize;
            S.spacing  = this.spacing;
            S.backgroundColor = this.backgroundColor;
            S.TimeDisplayOffset = this.TimeDisplayOffset;
            S.TimeSpan = this.TimeSpan;
            if isempty(S.GraphicalSettings) && isempty(this.DisplayChannelTags)
                S.GraphicalSettings = struct('timeAxis', struct('pixelsPerSecond', 49.9), ...
                    'panels', struct('startingTrace', 0, 'yOffset', 0, 'height', 100, 'traces', []), ...
                    'global', struct(), ...
                    'cursors', struct('startingCursor', 0, 'times', 0, 'lockedCursors', [], 'activeCursor', 0));
                S.GraphicalSettingsStale = false;
            elseif S.GraphicalSettingsStale
                if isempty(S.GraphicalSettings)
                    S.GraphicalSettings = struct('timeAxis', struct('pixelsPerSecond', 49.9), ...
                        'panels', struct('startingTrace', 0, 'yOffset', 0, 'height', 100, 'traces', []), ...
                        'global', struct(), ...
                        'cursors', struct('startingCursor', 0, 'times', 0, 'lockedCursors', [], 'activeCursor', 0));
                    S.GraphicalSettingsStale = false;
                end
                S.GraphicalSettings.panels.traces = getDisplayChannelsGraphicalSettings(this);
                S.GraphicalSettings.cursors = getCursorsGraphicalSettings(this);
                % We can remove 'addCursor' from the call method cache
                % because the settings should now be in the graphical
                % settings.
                idx = 1;
                while idx <= numel(S.CallMethodCache)
                    if strcmp(S.CallMethodCache{idx}.methodName, 'addCursor')
                        S.CallMethodCache(idx) = [];
                    else
                        idx = idx + 1;
                    end
                end
            end
        end
        
        function fromStruct(this, S)
            fromStruct@matlabshared.scopes.WebMessageHandler(this, S);
            this.radix    = S.radix;
            this.height   = S.height;
            this.color    = S.color;
            this.format   = S.format;
            this.fontSize = S.fontSize;
            this.spacing  = S.spacing;
            this.backgroundColor = S.backgroundColor;
            this.TimeDisplayOffset = S.TimeDisplayOffset;
            this.TimeSpan = S.TimeSpan;
            cursorTags = cell(1, numel(S.GraphicalSettings.cursors.times));
            for indx = 1:numel(S.GraphicalSettings.cursors.times)
                cursorTags{indx} = sprintf('C%d', indx);
            end
            this.CursorTags = cursorTags;
        end
        
        function cursors = get.Cursors(this)
            cursors = this.pCursors;
            if isempty(cursors)
                if isempty(this.GraphicalSettings)
                    % Use the default cursors
                    cursors = {dsp.LogicAnalyzerCursor('Tag', 'C1')};
                else
                    gs = this.GraphicalSettings.cursors;
                    if ~isfield(gs, 'instanceNumbers')
                        gs.instanceNumbers = 1:numel(gs.times);
                        gs.nextInstance    = numel(gs.times)+1;
                    end
                    cursors = cell(1, numel(gs.times));
                    for indx = 1:numel(gs.times)
                        cursors{indx} = dsp.LogicAnalyzerCursor.fromGraphicalSettings(gs, indx);
                    end
                end
                this.pCursors = cursors;
            end
        end
        
        function set.Cursors(this, cursors)
            this.pCursors = cursors;
            % format the graphical settings
            
            gs = getCursorsGraphicalSettings(this, cursors);
            this.callMethod('updateAllCursors', gs);
        end
        
        function gs = getCursorsGraphicalSettings(this, cursors)
            if nargin < 2
                cursors = this.pCursors;
                if isempty(cursors)
                    cursors = this.Cursors;
                end
            end
            gs.startingCursor = 0;
            gs.times = {};
            gs.lockedCursors = {};
            gs.colors = {};
            for indx = 1:numel(cursors)
                gs.times{indx} = cursors{indx}.Location;
                if cursors{indx}.Locked
                    gs.lockedCursors = [gs.lockedCursors indx-1]; % Use 0 based indexing for JavaScript side
                end
                if ischar(cursors{indx}.Color)
                    gs.colors{indx} = uiservices.stringColorToHexColor(cursors{indx}.Color);
                else
                    gs.colors{indx} = uiservices.rgbToHexColor(cursors{indx}.Color);
                end
            end
        end
        
        function channels = get.DisplayChannels(this)
            channels = this.pDisplayChannels;
            if isempty(channels)
                if isempty(this.GraphicalSettings)
                    % Use the defaults
                    tags = this.DisplayChannelTags;
                    for indx = 1:numel(tags)
                        channels{indx} = dsp.LogicAnalyzerWave('InputChannel', indx, 'Tag', tags{indx});
                    end
                elseif isempty(this.GraphicalSettings.panels.traces)
                    channels = {};
                else
                    % Build the channels from the graphical settings
                    gs = this.GraphicalSettings.panels.traces;
                    if ~iscell(gs)
                        gsCell = cell(1, numel(gs));
                        for indx = 1:numel(gs)
                            gsCell{indx} = gs(indx);
                        end
                        gs = gsCell;
                    end
                    for indx = 1:numel(gs)
                        switch gs{indx}.type
                            case 'wave'
                                c = 'dsp.LogicAnalyzerWave';
                            case 'divider'
                                c = 'dsp.LogicAnalyzerDivider';
                            case 'group'
                                c = 'dsp.LogicAnalyzerGroup';
                        end
                        
                        channels{indx} = feval([c '.fromGraphicalSettings'], gs{indx}, this.InputIds);
                    end
                end
                this.pDisplayChannels = channels;
            end
        end
        
        function set.DisplayChannels(this, channels)
            this.pDisplayChannels = channels;
            
            gs = getDisplayChannelsGraphicalSettings(this, channels);
            
            this.callMethod('updateAllDisplayChannels', {gs});
        end
        
        function gs = getDisplayChannelsGraphicalSettings(this, channels)
            if nargin < 2
                channels = this.pDisplayChannels;
                if isempty(channels)
                    channels = this.DisplayChannels;
                end
            end
                
            gs = cell(1, numel(channels));
            numInputs = this.NumInputs;
            
            % Increment the NumInputs to the max of the passed InputChannel
            % properties, but do not decrease it here.
            for indx = 1:numel(channels)
                if isprop(channels{indx}, 'InputChannel')
                    numInputs = max(numInputs, channels{indx}.InputChannel);
                end
            end
            this.NumInputs = numInputs;
            inputIds = this.InputIds;
            for indx = 1:numel(channels)
                gs{indx} = channels{indx}.toGraphicalSettings(inputIds);
            end
        end
        
        function tags = getCursorTags(this)
            if this.IsUILaunched && ~this.GraphicalSettingsStale
                cursors = this.Cursors;
                tags = cell(1, numel(cursors));
                for ii = 1:numel(cursors)
                    tags{ii} = cursors{ii}.getTag;
                end
                this.CursorTags = tags;
            else
                tags = this.CursorTags;
            end
        end
        
        function deleteDisplayChannel(this, tag)
            
            if iscell(tag)
                for indx = 1:numel(tag)
                    this.deleteDisplayChannel(tag{indx});
                end
                return;
            end
            isTag = true;
            if this.IsUILaunched
                tags = this.getDisplayChannelTags;
            else
                tags = this.DisplayChannelTags;
            end
            if isnumeric(tag)
                isTag = false;
                idx = tag;
            else
                idx = find(strcmp(tags, tag));
            end
            if isempty(idx) || idx > numel(tags)
                if isTag
                    error(message('dspshared:LogicAnalyzer:invalidDisplayChannelTag'));
                else
                    error(message('dspshared:LogicAnalyzer:invalidDisplayChannel'));
                end
            end
            tags(idx) = [];
            this.DisplayChannelTags = tags;
            if isTag
                params.tag = tag;
            else
                params.index = idx;
            end
            this.GraphicalSettingsStale = true;
            this.pDisplayChannels = [];
            this.callMethod('deleteDisplayChannel', params);
        end
        
        function displayChannelTags = getDisplayChannelTags(this)
            if this.IsUILaunched && ~this.GraphicalSettingsStale
                displayChannels = this.DisplayChannels;
                displayChannelTags = {};
                for ii = 1:numel(displayChannels)
                    displayChannelTags = [displayChannelTags displayChannels{ii}.getAllTags]; %#ok<AGROW>
                end
                this.DisplayChannelTags = displayChannelTags;
            else
                displayChannelTags = this.DisplayChannelTags;
            end
        end
        
        function value = getGlobalSetting(this, propName, defaultValue)
            value = [];
            if ~this.GraphicalSettingsStale
                gs = this.GraphicalSettings;
                if isfield(gs, 'global') && isfield(gs.global, propName)
                    value = gs.global.(propName);
                end
            end
            if isempty(value)
                value = this.(propName);
            end
            if isempty(value)
                value = defaultValue;
            end
        end
        
        function info = getCursorInfo(this, varargin)
            
            if nargin == 2 && (ischar(varargin{1}) || iscellstr(varargin{1}) || isstring(varargin{1}))
                tag = varargin{1};
            elseif numel(varargin) ~= 2 || ~strcmp(varargin{1}, 'CursorTag')
                % object and pv pair
                localError('getCursorInfoInputs');
            else
                tag = varargin{2};
            end
            
            % get the structs for the tags
            info = structByTag(this.Cursors, tag, 'noCursorFound');
        end
        
        function info = getDisplayChannelInfo(this, varargin)
            
            if nargin == 2 && (ischar(varargin{1}) || iscellstr(varargin{1}) || isstring(varargin{1}))
                dispTag = varargin{1};
            elseif numel(varargin) ~= 2 || ~strcmp(varargin{1}, 'DisplayChannelTag')
                % object and pv pair
                localError('getDisplayChannelInfoInputs');
            else
                dispTag = varargin{2};
            end
            
            % get the structs for the tags
            info = structByTag(this.DisplayChannels, dispTag, 'noDisplayChannelFound');
        end
        
        function moveDisplayChannel(this, tag, varargin)
            targetIdx = find(strcmpi(varargin, 'DisplayChannel'));
            if isempty(targetIdx)
                target = varargin{end};
                varargin(end) = [];
            else
                target = varargin{targetIdx+1};
                varargin(targetIdx:targetIdx+1) = [];
            end
            if numel(varargin) > 0
                varargin = [{tag} varargin];
                tagIdx = find(strcmpi(varargin(1:2:end), 'DisplayChannelTag'));
                if isempty(tagIdx) || ~isscalar(tagIdx) || ...
                        ~ischar(varargin{(2*tagIdx)})
                    localError('invalidDisplayChannelTag');
                end

                tag = varargin{2*tagIdx};
            end
            
            args.tag = tag;
            args.target = target;
            this.callMethod('moveDisplayChannel', args);
        end
        
        function modifyDisplayChannel(this, tag, varargin)
            
            if mod(nargin, 2) == 1 % first value is cursor tag
                varargin = [{tag} varargin];
                tagIdx = find(strcmp(varargin(1:2:end), 'DisplayChannelTag'));
                if isempty(tagIdx)
                    tagIdx = find(strcmp(varargin(1:2:end), 'DisplayChannel'));
                    tags = this.getDisplayChannelTags;
                    if numel(tagIdx) ~= 1 || ...
                            ~isnumeric(varargin{2*tagIdx}) || ...
                            varargin{2*tagIdx} > numel(tags)
                        localError('invalidDisplayChannel');
                    end
                    tag = tags{varargin{2*tagIdx}};
                elseif ~isscalar(tagIdx) || ...
                        ~ischar(varargin{(2*tagIdx)})
                    localError('invalidDisplayChannelTag');
                else
                    tag = varargin{2*tagIdx};
                end
                varargin(2*tagIdx-1:2*tagIdx) = [];
            elseif isnumeric(tag)
                tags = this.getDisplayChannelTags;
                if tag > numel(tags)
                    localError('invalidDisplayChannel');
                end
                tag = tags{tag};
            end
            
            if strncmpi(tag, 'W', 1)
                h = dsp.LogicAnalyzerWave(varargin{:});
                gs = h.toGraphicalSettings(this.InputIds, varargin{1:2:end});
            else
                h = dsp.LogicAnalyzerDivider(varargin{:});
                gs = h.toGraphicalSettings(varargin{1:2:end});
            end
            gs.tag = tag;
            this.callMethod('modifyDisplayChannel', gs);
        end

        function setGlobalSetting(this, propName, value)
            this.GraphicalSettingsStale = true;
            this.(propName) = value;
            this.callMethod('updateGlobalProperty', struct('property', propName, 'value', value));
        end
        
        function updateGraphicalSettings(this, params)
            updateGraphicalSettings@matlabshared.scopes.WebMessageHandler(this, params);
            
            % Empty out the cursors and displaychannels to force them to be
            % regenerated.
            this.pCursors = [];
            this.pDisplayChannels = [];
        end
        
        function requestSerializedSettings(this, varargin)
            if ~isempty(this.CallMethodCache)
                this.CallMethodCache = {};
            end
            this.IsUILaunched = true;
        end
        
        function dividerTag = addDivider(this, varargin)
            idx = find(strcmp(varargin(1:2:end), 'DisplayChannel'));
            placement = [];
            if ~isempty(idx)
                placement = varargin{2*idx};
                varargin(2*idx-1:2*idx) = [];
                validateDisplayChannel(placement);
            end
            divider = dsp.LogicAnalyzerDivider(varargin{:});
            gs = divider.toGraphicalSettings;
            if ~isempty(placement)
                gs.placement = placement;
            end
            instanceNumber = this.getNextChannelInstanceNumber;
            dividerTag = sprintf('D%d', instanceNumber);
            if isempty(placement)
                this.DisplayChannelTags{end+1} = dividerTag;
            else
                if placement > numel(this.DisplayChannelTags)+1
                    placement = numel(this.DisplayChannelTags)+1;
                end
                this.DisplayChannelTags = [this.DisplayChannelTags(1:placement) ...
                    {dividerTag} ...
                    this.DisplayChannelTags(placement+1:end)];
            end
            if this.OpenComplete
                this.GraphicalSettingsStale = true;
            end
            this.callMethod('addDivider', gs);
            this.incrementChannelInstanceNumber;
            waitForGraphicalSettings(this);
        end
        
        function waveTag = addWave(this, varargin)
            
            idx = find(strcmp(varargin(1:2:end), 'DisplayChannel'));
            placement = [];
            if ~isempty(idx)
                placement = varargin{2*idx};
                varargin(2*idx-1:2*idx) = [];
                validateDisplayChannel(placement);
            end
            
            wave = dsp.LogicAnalyzerWave(varargin{:});
            
            validateInputChannel(wave, this.NumInputs);
            
            gs = wave.toGraphicalSettings(this.InputIds);
            if ~isempty(placement)
                gs.placement = placement;
            end
            
            instanceNumber = this.getNextChannelInstanceNumber;
            waveTag = sprintf('W%d', instanceNumber);
            if isempty(placement)
                this.DisplayChannelTags{end+1} = waveTag;
            else
                if placement > numel(this.DisplayChannelTags)+1
                    placement = numel(this.DisplayChannelTags)+1;
                end
                this.DisplayChannelTags = [this.DisplayChannelTags(1:placement) ...
                    {waveTag} ...
                    this.DisplayChannelTags(placement+1:end)];
            end
            if this.OpenComplete
                this.GraphicalSettingsStale = true;
            end
            this.callMethod('addWave', gs);
            this.incrementChannelInstanceNumber;
            waitForGraphicalSettings(this)
        end
        
        function waitForGraphicalSettings(this)
            if ~this.OpenComplete
                return
            end
            for indx = 1:100
                if ~this.GraphicalSettingsStale
                    return;
                end
                pause(0.1);
            end
        end
        
        function deleteCursor(this, tag, varargin)
            
            % input: 'CursorTag', tag OR tag
            if nargin == 3 && strcmpi(tag, 'CursorTag')
                tag = varargin{1};
            end
            
            if iscell(tag)
                for indx = 1:numel(tag)
                    this.deleteCursor(tag{indx});
                end
                return;
            end
            
            % Clean up the Cursors prop
            c = this.pCursors;
            allTags = getCursorTags(this);
            index = strcmp(allTags, tag);
            if ~any(index)
                return;
            end
            if numel(allTags) == 1
                warning(message('Spcuilib:logicanalyzer:cannotDeleteLastCursor'));
                return;
            end
            if ~isempty(c)
                
                c(index) = [];
                
                this.pCursors = c;
            end
            
            if ~this.IsUILaunched || this.GraphicalSettingsStale
                % Clean up the cursor tags
                ct = this.CursorTags;
                ct(strcmp(ct, tag)) = [];
                this.CursorTags = ct;
            end
            % Send the deletion to JS
            this.GraphicalSettingsStale = true;
            instanceNumber = str2double(tag(2:end));
            this.callMethod('deleteCursor', instanceNumber);
            waitForGraphicalSettings(this);
            if this.OpenComplete
                if any(this.GraphicalSettings.cursors.instanceNumbers == instanceNumber)
                    this.GraphicalSettingsStale = true;
                    
                    waitForGraphicalSettings(this);
                end
            end
        end
                
        function cursorTag = addCursor(this, varargin)
            
            cursor = dsp.LogicAnalyzerCursor(varargin{:});
            
            gs = cursor.toGraphicalSettings;
            
            instanceNumber = this.getNextCursorInstanceNumber;
            cursorTag = sprintf('C%d', instanceNumber);
            if this.OpenComplete
                this.GraphicalSettingsStale = true;
            end
            this.callMethod('addCursor', gs);
            this.incrementCursorInstanceNumber;
            this.CursorTags{end+1} = cursorTag;
            waitForGraphicalSettings(this);
            if this.OpenComplete
                if ~any(this.GraphicalSettings.cursors.instanceNumbers == instanceNumber)
                    this.GraphicalSettingsStale = true;
                    
                    waitForGraphicalSettings(this);
                end
            else
                oldCursors = this.Cursors;
                oldCursors{end+1} = cursor.setTag(cursorTag);
                this.pCursors = oldCursors;
            end 
        end
        
        function modifyCursor(this, tag, varargin)
            
            % first value can be cursor tag or the cursor tag can be
            % specified as a pv pair value
            if mod(nargin, 2) == 1 % first value is cursor tag
                varargin = [{tag} varargin];
                tagIdx = find(strcmp(varargin(1:2:end), 'CursorTag'));
                if isempty(tagIdx) || ~isscalar(tagIdx) || ...
                        ~ischar(varargin{(2*tagIdx)})
                    localError('invalidDeleteCursorSpec');
                end

                tag = varargin{2*tagIdx};
                varargin(2*tagIdx-1:2*tagIdx) = [];
            end
            
            % find all the CursorTag properties passed in
            cursors = this.pCursors;
            if isempty(cursors)
                cursors = this.Cursors;
            end
            cursor  = [];
            for ii = 1:numel(cursors)
                if strcmp(cursors{ii}.getTag, tag)
                    % mark flag and note index
                    cursor = cursors{ii};
                    break;
                end
            end
            
            if isempty(cursor)
                cursor = dsp.LogicAnalyzerCursor(varargin{:});
                this.pCursors = [cursors {cursor}];
            else
                for propI = 1:2:numel(varargin)
                    cursor.(varargin{propI}) = varargin{propI+1};
                end
                this.pCursors{ii} = cursor;
            end
            if this.OpenComplete
                this.GraphicalSettingsStale = true;
            end        
            gs = cursor.toGraphicalSettings(varargin{1:2:end});
            gs.cursorTag = tag;
            
            this.callMethod('modifyCursor', gs);
        end
        
        function instanceNumber = getNextCursorInstanceNumber(this)
            gs = this.GraphicalSettings;
            if isempty(gs)
                instanceNumber = this.NextCursorInstance;
            else
                instanceNumber = gs.cursors.nextInstance;
            end
        end
        
        function incrementCursorInstanceNumber(this)
            gs = this.GraphicalSettings;
            if isempty(gs)
                this.NextCursorInstance = this.NextCursorInstance + 1;
            else
                gs.cursors.nextInstance = gs.cursors.nextInstance + 1;
            end
            this.GraphicalSettings = gs;
        end
        
        function instanceNumber = getNextChannelInstanceNumber(this)
            gs = this.GraphicalSettings;
            if isempty(gs)
                instanceNumber = this.NextChannelInstance;
            else
                instanceNumber = gs.panels.nextInstance;
            end
        end
        
        function incrementChannelInstanceNumber(this, increment)
            gs = this.GraphicalSettings;
            if nargin < 2
                increment = 1;
            end
            if isempty(gs)
                this.NextChannelInstance = this.NextChannelInstance + increment;
            else
                gs.panels.nextInstance = gs.panels.nextInstance + increment;
            end
            this.GraphicalSettings = gs;
        end
        
        function channel = getPubSubChannel(~)
            channel = 'logicanalyzer';
        end
        
        function url = getUrl(~)
            url = 'toolbox/shared/spcuilib/logicanalyzer/web/logicanalyzer/logicanalyzer-systemobject';
        end
        
        function iconFile = getIconFile(~)
            if ispc
                iconFile = fullfile(matlabroot, 'toolbox','shared','spcuilib','logicanalyzer','resources','logicanalyzer','la_visualize_16.ico');
            else
                iconFile = fullfile(matlabroot, 'toolbox','shared','spcuilib','logicanalyzer','resources','logicanalyzer','la_visualize_16.png');
            end
        end
        
        function release(this, time)
            this.callMethod({}, 'onSimTerminate', time);
        end
        
        function openComplete(this, ~)
            this.OpenComplete = true;
        end
        
        function terminateComplete(this, ~)
            this.TerminateComplete = true;
        end
    end
    methods (Access = protected)
        function onNumInputsChanged(this, newInputs, oldInputs)
            if newInputs > oldInputs
                next = this.getNextChannelInstanceNumber;
                tags = textscan(sprintf('W%d ', next:next+newInputs-oldInputs-1), '%s ');
                this.DisplayChannelTags = [this.DisplayChannelTags tags{1}(:)'];
                this.incrementChannelInstanceNumber(newInputs-oldInputs);
            end
        end
        function methods = getPruneableMethods(~)
            methods = {'setStartTime', 'setTimeSpan'};
        end
    end
end

function localError(ID, varargin)
error(message(['dspshared:LogicAnalyzer:' ID], varargin{:}));
end % localError

function info = structByTag(objects, tags, errorName)

foundTag = false; % not found tag yet
tags = string(tags);
found = cell(1, numel(tags));
for jj = 1:numel(tags)
    for ii = 1:numel(objects)
        found{jj} = findByTag(objects{ii}, tags(jj));
        if ~isempty(found{jj})
            foundTag = true;
            break;
        end
    end % for: run though all the display channels
    if ~foundTag % tag not found
        localError(errorName, char(tags(jj)));
    end
end

% form a structure with the information
info = cell(1, numel(tags));
s = warning('off', 'MATLAB:structOnObject');
for ii = 1:numel(tags)
    info{ii} = struct(found{ii});
end
warning(s);

if numel(tags) == 1 % cell input
    info = info{1};
end

end

function validateInputChannel(wave, numInputs)


if (wave.InputChannel > numInputs) || (wave.InputChannel <= 0)
    error(message('dspshared:LogicAnalyzer:invalidInputChannel'));
end

end

function validateDisplayChannel(placement)

if ~isscalar(placement) || ~isnumeric(placement)
    error(message('dspshared:LogicAnalyzer:invalidDisplayChannel'));
end

end



% [EOF]
