classdef RoIs < matlab.apps.AppBase

    % Properties that correspond to app components
    properties (Access = public)
        RootFigure              matlab.ui.Figure
        type_select             matlab.ui.control.DropDown
        roi_list                matlab.ui.control.Table
        undo_button             matlab.ui.control.Button
        redo_button             matlab.ui.control.Button
        delete_button           matlab.ui.control.Button
        paint_button            matlab.ui.control.StateButton
        select_button           matlab.ui.control.StateButton
        brush_size_input        matlab.ui.control.NumericEditField
        set_parent              matlab.ui.control.Button
        paint_button_2          matlab.ui.control.StateButton
        subtract_toggle         matlab.ui.control.StateButton
        channel_select          matlab.ui.control.DropDown
        BrushEditFieldLabel     matlab.ui.control.Label
        ChannelDropDownLabel    matlab.ui.control.Label
        TypeDropDownLabel       matlab.ui.control.Label
        save_button             matlab.ui.control.Button
        unit_label              matlab.ui.control.Label
        show_labels             matlab.ui.control.CheckBox
        show_relelations        matlab.ui.control.CheckBox
        split_button            matlab.ui.control.Button
        remove_parent           matlab.ui.control.Button
        ParentLabel             matlab.ui.control.Label
        BrushoffsetSliderLabel  matlab.ui.control.Label
        brush_offset            matlab.ui.control.Slider
        show_rois               matlab.ui.control.CheckBox
        last_button             matlab.ui.control.Button
        poly_button             matlab.ui.control.StateButton
    end

    
    properties (Access = private)
        manager % Description
        application % Description
        
        % event listeners that needs to be refreshed when the view changes:
        data_change_listener % Description
        memento_change_listener % Description
    end
    
    methods (Access = private)
        
        function handle_data_changed(app)
            [~, ~, m_read, m_flagged] = app.manager.data.shorts();
            [~, ~, ~, v_flagged] = app.manager.current_view.data.shorts();
            
            if m_flagged("roitool_redraw_controls") || v_flagged("roitool_redraw_controls")
                % toggle mode buttons:
                selected = m_read("roiedit_selected");
                mode = m_read('mode');
                mode_name = mode.name;
                
                % change interactivity of buttons:
                app.paint_button.Value = mode_name == "ROI:PAINT";
                app.poly_button.Value = mode_name == "ROI:POLYGON";
                
                app.select_button.Value = mode_name == "ROI:SELECT";
                app.delete_button.Enable = mode_name == "ROI:SELECT" & ~isempty(selected);
                app.split_button.Enable = mode_name == "ROI:SELECT" & ~isempty(selected);
                app.set_parent.Enable = mode_name == "ROI:SELECT" & length(selected) > 1;
                app.remove_parent.Enable = mode_name == "ROI:SELECT" & ~isempty(selected);
                %app.save_button.Enable = m_obso("roi_table", "roitool_table_changed", false);
                app.show_labels.Value = m_read("roi_show_labels");
                app.show_relelations.Value = m_read("roi_show_relations");
                app.show_rois.Value = m_read("roi_show_rois");
                
                app.subtract_toggle.Enable = mode_name == "ROI:PAINT";
                app.subtract_toggle.Value = m_read("roipaint_operation") == "subtract";
                
                % set brush size using unit convertion function:
                brush_px = m_read("roipaint_brush_size_px");
                px_to_dunit = m_read("pix_to_dunit");
                app.brush_size_input.Value = round(px_to_dunit(brush_px), 2);
               
                % has roi type changed?
                app.type_select.Value = m_read("roiedit_roi_type");
            end
            
            % do we need to update the roi list?
            % we need this on channel change and roi table updates:
            if m_flagged("roitool_redraw_roilist") || v_flagged("roitool_redraw_roilist")
                %if m_obso("roi_table", "roitool_roilist") || v_obso("channel", "roitool_roilist_subset")
                app.update_roilist();
                app.save_button.Enable = true;
            end
                
        end
        
        
        function update_roilist(app)
            [~, ~, m_read] = app.manager.data.shorts();
            [~, ~, v_read, ~] = app.manager.current_view.data.shorts();
            
            % update channel selection:
            chan = v_read("channel");
            chan_names = m_read("channel_names");
            if chan <= length(chan_names)
                app.channel_select.Value = chan_names(chan);
            else
                return
            end
            
            % update and filter roi table:
            roi_table = m_read("roi_table");
            chan = v_read("channel");
            
            name = roi_table.short_name;
            type = roi_table.type;
            channel = roi_table.channel;
            tags = roi_table.tags;
            id = roi_table.roi_id;
            
            tab = table(name, type, channel, tags, id);
            tab_chan = tab(tab.channel == chan,:);
            
            app.roi_list.Data = tab_chan;
        end
        
        
        function handle_memento_availability_changed(app)
            memo = app.manager.memento;
            app.undo_button.Enable = memo.has_undo;
            app.redo_button.Enable = memo.has_redo;
        end
        
        
        function handle_view_changed(app, ~, ev)
            % clear old event listeners:
            delete(app.data_change_listener);
            delete(app.memento_change_listener);
            
            % the new items could be empty - in whcih case we simply hide
            % outselves:
            if isempty(ev.viewmanager) || isempty(ev.to) || ~ev.viewmanager.has_mode("ROI:SELECT")
                app.RootFigure.Visible = false;
                return
            else
                app.RootFigure.Visible = true;
                
                % this steams focus, so we must focus top view again:
                ev.viewmanager.focus_top_view();
            end
            
            
            
            % we have a new manager, and need to re-register our events:
            app.manager = ev.viewmanager;            
            disp("RoI tool: new manager set")
            [~, ~, m_read] = app.manager.data.shorts();
            
            app.data_change_listener = addlistener(app.manager.data, ...
               'on_data_changed', @(~, ~) app.handle_data_changed());
           
            app.memento_change_listener = addlistener(app.manager.memento, ...
               'on_availability_changed', @(~, ~) app.handle_memento_availability_changed());

            m_ctrl_flag_on = ["mode", "roiedit_selected", "roi_table","roi_show_rois", "roi_show_labels", "roi_show_relations", "roipaint_operation", "roipaint_brush_size_px", "roiedit_roi_type"];
          %  v_ctrl_flag_on = ["channel"];
            app.manager.data.assign_changeflag(m_ctrl_flag_on, "roitool_redraw_controls");
            app.manager.views.data.assign_changeflag("channel", "roitool_redraw_roilist");
            app.manager.data.assign_changeflag("roi_table", "roitool_redraw_roilist");
            
            % set channel name list:
            chan_names = m_read("channel_names");
            app.channel_select.Items = chan_names;
            app.channel_select.ItemsData = chan_names;
            
            app.update_roilist();
            %app.manager.data.stamp("roitool_table_changed");
            
            % populate the roi type list:
            roi_types = m_read("roiedit_roi_types_available");
            app.type_select.Items = roi_types.type;
            app.type_select.ItemsData = roi_types.type;
            
            % display unit for brush size:
            app.unit_label.Text = app.manager.data.read("dunit");
        end
    end
    

    % Callbacks that handle component events
    methods (Access = private)

        % Code that executes after component creation
        function startupFcn(app, application)
            addlistener(application, 'on_view_changed',...
                @app.handle_view_changed);
        end

        % Value changed function: paint_button
        function handle_paint_button_click(app, event)
            if event.Value
                app.manager.set_mode("ROI:PAINT");
            else
                app.manager.set_mode("IDLE");
            end
        end

        % Value changed function: select_button
        function handle_select_button_click(app, event)
            if event.Value
                app.manager.set_mode("ROI:SELECT");
            else
                app.manager.set_mode("IDLE");
            end
        end

        % Cell selection callback: roi_list
        function handle_roi_list_select(app, event)
            [~, m_write, m_read] = app.manager.data.shorts();
            [~, ~, v_read] = app.manager.current_view.data.shorts();
            
            roi_table = m_read("roi_table");
            chan = v_read("channel");
            roi_table = roi_table(roi_table.channel == chan,:);
            
            idxs = event.Indices(:,1);
            uuids = roi_table.roi_id(idxs)';
            
            m_write("roiedit_selected", uuids);
            app.manager.set_mode("ROI:SELECT");
        end

        % Button pushed function: delete_button
        function handle_delete_button(app, event)
            roiman.modes.RoISelect.delete_selection(app.manager);
        end

        % Value changed function: type_select
        function handle_type_select(app, event)
            [~, ~, m_read] = app.manager.data.shorts();
            
            value = app.type_select.Value;
            
            mode = m_read("mode");
            app.manager.data.write("roiedit_roi_type", value);
            
            % if we are in roi select mode, we change the type of selected
            % rois as well:
            if mode.name == "ROI:SELECT"
                roiman.modes.RoISelect.change_selection_type(app.manager, value);
            end
                
        end

        % Button pushed function: save_button
        function handle_save_button(app, event)
            [~, ~, read, ~] = app.manager.data.shorts();
            
            roi_table = read("roi_table");
            ts = read("tseries");
            ts.save_var('roi_table');
            %stamp("roitool_table_changed");
            app.save_button.Enable = false;
        end

        % Close request function: RootFigure
        function handle_close(app, event)
            app.RootFigure.Visible = false;
        end

        % Value changed function: brush_size_input
        function handle_brush_size_set(app, event)
            dunit_to_pix = app.manager.data.read("dunit_to_pix");
            
            size_unit = app.brush_size_input.Value;
            size_px = round(dunit_to_pix(size_unit));
            if size_px < 1
                size_px = 1;
            end
            
            roiman.modes.RoIPaint.set_brush(app.manager, size_px, "set");
        end

        % Value changed function: channel_select
        function handle_channel_select(app, event)
            [~, ~, m_read] = app.manager.data.shorts();
            [~, v_write, ~] = app.manager.current_view.data.shorts();
            
            chan_name = app.channel_select.Value;
            chan_names = m_read("channel_names");
            chan = find(chan_names == chan_name, 1);
            
            v_write("channel", chan);
            app.update_roilist()
          
            
            % FIXME: we need to force an update here because we're the source of
            % the change.. 🤔
            %app.update();
        end

        % Key release function: RootFigure
        function handle_key_up(app, event)
            app.manager.handle_keyboard("up", event);
        end

        % Button pushed function: split_button
        function handle_split(app, event)
            roiman.modes.RoISelect.split_selected(app.manager);
        end

        % Value changed function: subtract_toggle
        function handle_subtract(app, event)
            [~, m_write, ~] = app.manager.data.shorts();
            value = app.subtract_toggle.Value;
            if value
                m_write("roipaint_operation", "subtract");
                m_write("message", "Brush: subtract");
            else
                m_write("roipaint_operation", "add");
                m_write("message", "Brush: add");
            end
        end

        % Button pushed function: set_parent
        function handle_set_parent(app, event)
            roiman.modes.RoISelect.set_parent_from_selected(app.manager);
        end

        % Button pushed function: remove_parent
        function handle_remove_parent(app, event)
            roiman.modes.RoISelect.clear_parents_from_selected(app.manager);
        end

        % Value changed function: show_labels
        function handle_show_labels(app, event)
            [~, m_write, ~] = app.manager.data.shorts();
            m_write("roi_show_labels", app.show_labels.Value);
        end

        % Value changed function: show_relelations
        function handle_show_relations(app, event)
            [~, m_write, ~] = app.manager.data.shorts();
            m_write("roi_show_relations", app.show_relelations.Value);
        end

        % Button pushed function: last_button
        function handle_last_button(app, event)
            roiman.modes.RoISelect.select_last(app.manager);
        end

        % Button pushed function: undo_button
        function handle_undo(app, event)
            app.manager.memento.undo();
        end

        % Button pushed function: redo_button
        function handle_redo(app, event)
            app.manager.memento.redo();
        end

        % Value changed function: poly_button
        function handle_poly_button_click(app, event)
            if event.Value
                app.manager.set_mode("ROI:POLYGON");
            else
                app.manager.set_mode("IDLE");
            end
        end

        % Value changed function: show_rois
        function handle_show_rois(app, event)
            [~, m_write, ~] = app.manager.data.shorts();
            m_write("roi_show_rois", app.show_rois.Value);                  
        end
    end

    % Component initialization
    methods (Access = private)

        % Create UIFigure and components
        function createComponents(app)

            % Create RootFigure and hide until all components are created
            app.RootFigure = uifigure('Visible', 'off');
            app.RootFigure.Position = [100 100 467 591];
            app.RootFigure.Name = 'RoI Editor';
            app.RootFigure.CloseRequestFcn = createCallbackFcn(app, @handle_close, true);
            app.RootFigure.KeyReleaseFcn = createCallbackFcn(app, @handle_key_up, true);

            % Create type_select
            app.type_select = uidropdown(app.RootFigure);
            app.type_select.Items = {'<None>'};
            app.type_select.ValueChangedFcn = createCallbackFcn(app, @handle_type_select, true);
            app.type_select.Position = [272 560 61 22];
            app.type_select.Value = '<None>';

            % Create roi_list
            app.roi_list = uitable(app.RootFigure);
            app.roi_list.ColumnName = {'Name'; 'Type'; 'Chan.'; 'Tags'; 'ID'};
            app.roi_list.ColumnWidth = {'auto', 60, 60, 'auto', 50};
            app.roi_list.RowName = {};
            app.roi_list.ColumnSortable = [true true true true false];
            app.roi_list.ColumnEditable = [false false false true false];
            app.roi_list.CellSelectionCallback = createCallbackFcn(app, @handle_roi_list_select, true);
            app.roi_list.FontSize = 10;
            app.roi_list.Position = [10 9 449 442];

            % Create undo_button
            app.undo_button = uibutton(app.RootFigure, 'push');
            app.undo_button.ButtonPushedFcn = createCallbackFcn(app, @handle_undo, true);
            app.undo_button.Enable = 'off';
            app.undo_button.Position = [119 494 29 23];
            app.undo_button.Text = '↶';

            % Create redo_button
            app.redo_button = uibutton(app.RootFigure, 'push');
            app.redo_button.ButtonPushedFcn = createCallbackFcn(app, @handle_redo, true);
            app.redo_button.Enable = 'off';
            app.redo_button.Position = [148 494 28 23];
            app.redo_button.Text = '↷';

            % Create delete_button
            app.delete_button = uibutton(app.RootFigure, 'push');
            app.delete_button.ButtonPushedFcn = createCallbackFcn(app, @handle_delete_button, true);
            app.delete_button.Position = [182 493 100 24];
            app.delete_button.Text = '♻ Delete';

            % Create paint_button
            app.paint_button = uibutton(app.RootFigure, 'state');
            app.paint_button.ValueChangedFcn = createCallbackFcn(app, @handle_paint_button_click, true);
            app.paint_button.Text = '🎨 Paint';
            app.paint_button.Position = [10 559 100 24];

            % Create select_button
            app.select_button = uibutton(app.RootFigure, 'state');
            app.select_button.ValueChangedFcn = createCallbackFcn(app, @handle_select_button_click, true);
            app.select_button.Text = '📌 Select';
            app.select_button.Position = [10 493 100 24];

            % Create brush_size_input
            app.brush_size_input = uieditfield(app.RootFigure, 'numeric');
            app.brush_size_input.ValueChangedFcn = createCallbackFcn(app, @handle_brush_size_set, true);
            app.brush_size_input.Position = [170 560 38 22];

            % Create set_parent
            app.set_parent = uibutton(app.RootFigure, 'push');
            app.set_parent.ButtonPushedFcn = createCallbackFcn(app, @handle_set_parent, true);
            app.set_parent.Position = [171 526 42 24];
            app.set_parent.Text = 'Set';

            % Create paint_button_2
            app.paint_button_2 = uibutton(app.RootFigure, 'state');
            app.paint_button_2.Text = 'Fill';
            app.paint_button_2.Position = [357 527 39 22];

            % Create subtract_toggle
            app.subtract_toggle = uibutton(app.RootFigure, 'state');
            app.subtract_toggle.ValueChangedFcn = createCallbackFcn(app, @handle_subtract, true);
            app.subtract_toggle.Text = 'Subtract';
            app.subtract_toggle.Position = [403 527 54 22];

            % Create channel_select
            app.channel_select = uidropdown(app.RootFigure);
            app.channel_select.Items = {'Ch1', 'Ch2'};
            app.channel_select.ValueChangedFcn = createCallbackFcn(app, @handle_channel_select, true);
            app.channel_select.Position = [397 560 60 22];
            app.channel_select.Value = 'Ch1';

            % Create BrushEditFieldLabel
            app.BrushEditFieldLabel = uilabel(app.RootFigure);
            app.BrushEditFieldLabel.HorizontalAlignment = 'right';
            app.BrushEditFieldLabel.Position = [119 560 46 22];
            app.BrushEditFieldLabel.Text = 'Radius:';

            % Create ChannelDropDownLabel
            app.ChannelDropDownLabel = uilabel(app.RootFigure);
            app.ChannelDropDownLabel.HorizontalAlignment = 'right';
            app.ChannelDropDownLabel.Position = [337 560 53 22];
            app.ChannelDropDownLabel.Text = 'Channel:';

            % Create TypeDropDownLabel
            app.TypeDropDownLabel = uilabel(app.RootFigure);
            app.TypeDropDownLabel.HorizontalAlignment = 'right';
            app.TypeDropDownLabel.Position = [234 560 35 22];
            app.TypeDropDownLabel.Text = 'Type:';

            % Create save_button
            app.save_button = uibutton(app.RootFigure, 'push');
            app.save_button.ButtonPushedFcn = createCallbackFcn(app, @handle_save_button, true);
            app.save_button.FontWeight = 'bold';
            app.save_button.Position = [10 461 100 22];
            app.save_button.Text = 'Save';

            % Create unit_label
            app.unit_label = uilabel(app.RootFigure);
            app.unit_label.Position = [212 560 26 22];
            app.unit_label.Text = 'unit';

            % Create show_labels
            app.show_labels = uicheckbox(app.RootFigure);
            app.show_labels.ValueChangedFcn = createCallbackFcn(app, @handle_show_labels, true);
            app.show_labels.Text = 'Labels';
            app.show_labels.Position = [250 460 57 22];

            % Create show_relelations
            app.show_relelations = uicheckbox(app.RootFigure);
            app.show_relelations.ValueChangedFcn = createCallbackFcn(app, @handle_show_relations, true);
            app.show_relelations.Text = 'Relations';
            app.show_relelations.Position = [174 460 72 22];

            % Create split_button
            app.split_button = uibutton(app.RootFigure, 'push');
            app.split_button.ButtonPushedFcn = createCallbackFcn(app, @handle_split, true);
            app.split_button.Position = [357 495 40 22];
            app.split_button.Text = 'Split';

            % Create remove_parent
            app.remove_parent = uibutton(app.RootFigure, 'push');
            app.remove_parent.ButtonPushedFcn = createCallbackFcn(app, @handle_remove_parent, true);
            app.remove_parent.Position = [218 526 64 24];
            app.remove_parent.Text = 'Remove';

            % Create ParentLabel
            app.ParentLabel = uilabel(app.RootFigure);
            app.ParentLabel.Position = [125 527 44 22];
            app.ParentLabel.Text = 'Parent:';

            % Create BrushoffsetSliderLabel
            app.BrushoffsetSliderLabel = uilabel(app.RootFigure);
            app.BrushoffsetSliderLabel.HorizontalAlignment = 'right';
            app.BrushoffsetSliderLabel.Enable = 'off';
            app.BrushoffsetSliderLabel.Position = [318 460 73 22];
            app.BrushoffsetSliderLabel.Text = 'Brush offset:';

            % Create brush_offset
            app.brush_offset = uislider(app.RootFigure);
            app.brush_offset.Limits = [-20 20];
            app.brush_offset.MajorTicks = [];
            app.brush_offset.MinorTicks = [];
            app.brush_offset.Enable = 'off';
            app.brush_offset.Position = [400 470 51 3];

            % Create show_rois
            app.show_rois = uicheckbox(app.RootFigure);
            app.show_rois.ValueChangedFcn = createCallbackFcn(app, @handle_show_rois, true);
            app.show_rois.Text = 'RoIs';
            app.show_rois.Position = [121 460 47 22];
            app.show_rois.Value = true;

            % Create last_button
            app.last_button = uibutton(app.RootFigure, 'push');
            app.last_button.ButtonPushedFcn = createCallbackFcn(app, @handle_last_button, true);
            app.last_button.Position = [403 495 54 22];
            app.last_button.Text = 'Last';

            % Create poly_button
            app.poly_button = uibutton(app.RootFigure, 'state');
            app.poly_button.ValueChangedFcn = createCallbackFcn(app, @handle_poly_button_click, true);
            app.poly_button.Text = '🖋 Polygon';
            app.poly_button.Position = [10 524 100 26];

            % Show the figure after all components are created
            app.RootFigure.Visible = 'on';
        end
    end

    % App creation and deletion
    methods (Access = public)

        % Construct app
        function app = RoIs(varargin)

            % Create UIFigure and components
            createComponents(app)

            % Register the app with App Designer
            registerApp(app, app.RootFigure)

            % Execute the startup function
            runStartupFcn(app, @(app)startupFcn(app, varargin{:}))

            if nargout == 0
                clear app
            end
        end

        % Code that executes before app deletion
        function delete(app)

            % Delete UIFigure when app is deleted
            delete(app.RootFigure)
        end
    end
end