# absolute imports
import numpy as np
import cv2
from vispy.scene import SceneCanvas, visuals, AxisWidget, Label
from vispy.visuals.transforms import linear
from PySide6.QtWidgets import QInputDialog
from PySide6.QtGui import QUndoCommand

# relative imports
from .drawable_objects import (
    EditEllipseVisual,
    EditRectVisual,
    ControlPoints,
    EditLineVisual,
    LineControlPoints,
    EditPolygonVisual,
)

class VispyCanvas(SceneCanvas):
    """Canvas for displaying the vispy instance"""

    CANVAS_SHAPE = (800, 600)
    main_ui = None
    main_window = None
    scale_bar_params = (None, True, None, None) # seed point, relative, threshold, direction
    text_color = "black"
    current_move = None
    origin = np.zeros(2)

    def __init__(self, main_window):

        self.data_handler = main_window.data_handler
        self.main_window = main_window

        SceneCanvas.__init__(
            self, size=self.CANVAS_SHAPE, keys=dict(delete=self.delete_object_w_undo)
        )
        self.unfreeze()
        # get background color of qt session
        self.update_background_color()

        if self.main_window.is_dark_mode():
            self.text_color = "white"

        self.grid = self.central_widget.add_grid(margin=0)

        self.view = self.grid.add_view(row=1, col=1, bgcolor="black")

        self.image = visuals.Image(
            data=None,
            texture_format="auto",
            interpolation=self.main_window.settings.value("graphics/image_rendering"),
            cmap="viridis",
            parent=self.view.scene,
        )

        # title
        self.title_label = Label(
            "Microscope-Image", color=self.text_color, font_size=12
        )
        self.title_label.height_max = 40
        self.grid.add_widget(self.title_label, row=0, col=0, col_span=3)

        # y axis
        self.yaxis = AxisWidget(
            orientation="left",
            axis_font_size=12,
            axis_label_margin=0,
            tick_label_margin=15,
            text_color=self.text_color,
        )
        self.yaxis.width_max = 80
        self.grid.add_widget(self.yaxis, row=1, col=0)

        # x axis
        self.xaxis = AxisWidget(
            orientation="bottom",
            axis_font_size=12,
            axis_label_margin=0,
            tick_label_margin=15,
            text_color=self.text_color,
        )
        self.xaxis.height_max = 50
        self.grid.add_widget(self.xaxis, row=2, col=1)

        self.update_axis_color()

        # padding right
        right_padding = self.grid.add_widget(row=1, col=2, row_span=2)
        right_padding.width_max = 5

        self.load_image_label = Label(
            "Click here to select\nan image", color="white", font_size=16
        )
        self.grid.add_widget(self.load_image_label, row=1, col=1)

        # camera
        self.view.camera.set_range(x=(0, 1), y=(0, 1), margin=0)

        self.start_state = True
        self.update_image()

        # for manipulating shapes
        self.selected_object = None
        self.selected_point = None

        self.freeze()

    def update_image(self):
        """Update the image in the vispy canvas and also the file path shown above the image"""

        # if there is no image loaded, do nothing
        if self.data_handler.file_path is not None:
            try:
                # opencv reads images in BGR format,
                # so we need to convert it to RGB
                self.data_handler.logger.debug(
                    f"update data_handler.file_path = {self.data_handler.file_path}"
                )
                BGR_img = cv2.imdecode(
                    np.frombuffer(self.data_handler.img_byte_stream, dtype=np.uint8),
                    cv2.IMREAD_UNCHANGED,
                )
                self.data_handler.img_data = cv2.cvtColor(BGR_img, cv2.COLOR_BGR2RGB)

                self.draw_image()

                self.view.camera = "panzoom"
                self.center_image()
                self.view.camera.aspect = 1
                self.view.camera.flip = (0, 1, 0)

                self.xaxis.link_view(self.view)
                self.yaxis.link_view(self.view)

                self.title_label.text = self.data_handler.file_path.name

            except Exception as error:
                # handle the exception
                if self.main_window is not None:
                    self.main_window.raise_error(f"Image could not be loaded: {error}")
                    return

            self.start_state = False
            self.remove_load_text()

    def draw_image(self, img_data=None):
        if img_data is None:
            img_data = self.data_handler.img_data
        self.data_handler.logger.debug("set vispy image data")
        self.image.set_data(img_data)
        self.image.interpolation = self.main_window.settings.value(
            "graphics/image_rendering"
        )

    def center_image(self):
        try:
            if self.data_handler.img_data is not None:
                self.view.camera.set_range(
                    x=(0-self.origin[0], self.data_handler.img_data.shape[1]-self.origin[0]),
                    y=(0-self.origin[1], self.data_handler.img_data.shape[0]-self.origin[1]),
                    margin=0,
                )
        except Exception as error:
            self.main_window.raise_error(f"Image could not be centered: {error}")

    def add_load_text(self):
        # load image label
        self.load_image_label.visible = True

    def remove_load_text(self):
        # self.load_image_label.text = ""
        self.load_image_label.visible = False

    def on_mouse_release(self, event):
        # transform so that coordinates start at 0 in self.view window
        tr = self.scene.node_transform(self.view)
        # only activate when over self.view by looking
        # if coordinates < 0 or > size of self.view
        if not (
            (tr.map(event.pos)[:2] > self.view.size).any()
            or (tr.map(event.pos)[:2] < (0, 0)).any()
        ):

            if self.start_state:
                # when no image is selected open file opening
                #  sequence by clicking
                self.main_ui.select_file()
            elif event.button == 1:
                match self.main_ui.tools_buttons.checkedButton().text(): 
                    case "Line" | "Ellipse" | "Rectangle" | "Angle" | "Multi-Line" | "Polygon" | "Edit":
                        
                        # multi-line is not finished yet
                        if isinstance(self.selected_object, LineControlPoints):
                            if self.selected_object.continue_adding_points:
                                tr = self.scene.node_transform(self.selected_object)
                                pos = tr.map(event.pos)
                                self.add_point_w_undo(self.selected_object, pos[:2])
                            elif self.current_move.check_movement():
                                self.main_window.undo_stack.push(self.current_move)
                                
                                
                        # redo move object command to save current location
                        elif self.current_move is not None:
                            # if object was moved, put it on undo stack
                            if self.current_move.check_movement():
                                self.main_window.undo_stack.push(self.current_move)

        # if click is above box
        elif (
            tr.map(event.pos)[1] < 0 # above box
            and event.pos[1] > 0 # but still in vispy canvas
            and tr.map(event.pos)[0] > 0 # right of start of scene
            and tr.map(event.pos)[0] < self.view.size[0] # left of end of scene
        ):
            if not self.data_handler.file_path:
                self.main_ui.select_file()
            elif self.data_handler.file_path.name != "clipboard":
                self.data_handler.open_file_location(self.data_handler.file_path)
                        

    def on_mouse_press(self, event):
        # transform so that coordinates start at 0 in self.view window
        tr = self.scene.node_transform(self.view)
        # only activate when over self.view by looking
        #  if coordinates < 0 or > size of self.view
        if not (
            (tr.map(event.pos)[:2] > self.view.size).any()
            or (tr.map(event.pos)[:2] < (0, 0)).any()
        ):

            # if in start state, dont do anything as we are looking
            # at mouse_released for that
            if self.start_state:
                return

            # main use of program
            if event.button == 3:  # mouse wheel button for changing fov
                # enable panning
                self.view.camera._viewbox.events.mouse_move.connect(
                    self.view.camera.viewbox_mouse_event
                )

            else:
                match self.main_ui.tools_buttons.checkedButton().text():

                    case "Move":
                        # enable panning
                        self.view.camera._viewbox.events.mouse_move.connect(
                            self.view.camera.viewbox_mouse_event
                        )

                        # QApplication.setOverrideCursor(QCursor(Qt.CursorShape.SizeAllCursor))

                        # unselect object
                        self.unselect()

                    case "Set Origin":
                        # disable panning
                        self.view.camera._viewbox.events.mouse_move.disconnect(
                            self.view.camera.viewbox_mouse_event
                        )

                        tr = self.scene.node_transform(self.view.scene)
                        pos = tr.map(event.pos)[:2]

                        # unselect object
                        self.unselect()

                        self.set_origin_w_undo(pos+self.origin)

                    case "Select":
                        # disable panning
                        self.view.camera._viewbox.events.mouse_move.disconnect(
                            self.view.camera.viewbox_mouse_event
                        )

                        tr = self.scene.node_transform(self.view.scene)
                        pos = tr.map(event.pos)
                        self.view.interactive = False
                        selected = self.visual_at(event.pos)
                        self.view.interactive = True

                        if event.button == 1:

                            if selected is not None:
                                # unselect object to create room of new one
                                self.unselect()

                                self.selected_object = selected.parent

                                # update transform to selected object
                                tr = self.scene.node_transform(self.selected_object)
                                pos = tr.map(event.pos)

                                self.selected_object.select(True, obj=selected)
                                self.selected_object.start_move(pos)
                        
                                # update ui to display properties of selected object
                                self.selection_update()

                    case "Line" | "Ellipse" | "Rectangle" | "Angle" | "Multi-Line" | "Polygon" | "Edit":
                        # disable panning
                        self.view.camera._viewbox.events.mouse_move.disconnect(
                            self.view.camera.viewbox_mouse_event
                        )

                        tr = self.scene.node_transform(self.view.scene)
                        pos = tr.map(event.pos)
                        self.view.interactive = False
                        selected = self.visual_at(event.pos)
                        self.view.interactive = True
                        # unselect object to create room of new one
                        self.unselect()
                                

                        if event.button == 1:
                            # QApplication.setOverrideCursor(QCursor(Qt.CursorShape.SizeAllCursor))

                            # edit object
                            if selected is not None:
                                self.selected_object = selected.parent
                                # update transform to selected object
                                tr = self.scene.node_transform(self.selected_object)
                                pos = tr.map(event.pos)

                                self.selected_object.select(True, obj=selected)
                                self.selected_object.start_move(pos)
                                # self.move_object_w_undo(self.selected_object)
                                
                                if not isinstance(self.selected_object, LineControlPoints):    
                                    self.current_move = MoveObjectCommand(self, self.selected_object)
                                    self.data_handler.logger.debug("start moving object")
                                
                                elif not self.selected_object.continue_adding_points:
                                    self.current_move = MoveObjectCommand(self, self.selected_object)
                                    self.data_handler.logger.debug("start moving object")

                                # update ui to display properties of selected object
                                self.selection_update()
                            else:
                                self.current_move = None
                            
                            # create new object:
                            if self.selected_object is None:

                                match self.main_ui.tools_buttons.checkedButton().text():
                                    case "Line":
                                        new_object = EditLineVisual(
                                            parent=self.view.scene,
                                            settings=self.main_window.settings,
                                            num_points=2,
                                        )
                                    case "Ellipse":
                                        new_object = EditEllipseVisual(
                                            parent=self.view.scene,
                                            settings=self.main_window.settings,
                                        )
                                    case "Rectangle":
                                        new_object = EditRectVisual(
                                            parent=self.view.scene,
                                            settings=self.main_window.settings,
                                        )
                                    case "angle":
                                        new_object = EditLineVisual(
                                            settings=self.main_window.settings,
                                            parent=self.view.scene,
                                            num_points=3,
                                        )
                                    case "Multi-Line":
                                        new_object = EditLineVisual(
                                            settings=self.main_window.settings,
                                            parent=self.view.scene,
                                            num_points=0,
                                        )
                                    case "Polygon":
                                        new_object = EditPolygonVisual(
                                            settings=self.main_window.settings,
                                            parent=self.view.scene)
                                    case "Edit":
                                        return
                                # dont show object before it is added to drawing_data
                                new_object.select(False)
                                if self.check_creation_allowed(new_object):

                                    self.create_new_object_w_undo(
                                        new_object, pos=pos, selected=True
                                    )

                                    # update ui where data is shown
                                    self.selection_update(object=new_object)
                                    # if linecontrolpoint to get automatic line creation without draggin
                                    if isinstance(self.selected_object, LineControlPoints):
                                        last_cp = self.selected_object.control_points[-1]
                                        self.selected_object.select(True, last_cp)
                                else:
                                    new_object.delete()

                                

                        # delete object
                        if event.button == 2:  # right button deletes object
                            # not self.selected_object because we want to
                            # delete it on hover too
                            if selected is not None:
                                self.delete_object_w_undo(object=selected.parent)

                                # Needs change
                                # self.main_ui.update_save_window()

                    case "Identify Scaling":
                        # disable panning
                        self.view.camera._viewbox.events.mouse_move.disconnect(
                            self.view.camera.viewbox_mouse_event
                        )

                        if event.button == 1:

                            # transform to get image pixel coordinates
                            tr_image = self.scene.node_transform(self.image)
                            mouse_image_coords = tr_image.map(event.pos)[:2]
                            # mouse_click_coordinates
                            m_i_x, m_i_y = np.floor(mouse_image_coords).astype(int)

                            # get width of scaling bar and show it in image
                            self.find_scale_bar_width_w_undo(
                                (m_i_x, m_i_y), 
                                relative=False, 
                                threshold=self.main_ui.get_threshold(),
                                direction=self.main_ui.scaling_direction_dd.currentText()
                            )

                        # right click to delete scaling identification
                        if event.button == 2:
                            self.draw_image(self.data_handler.img_data)
                            self.main_ui.pixel_edit.setText(None)

    def create_new_object(
        self, new_object, pos=None, selected=False, structure_name=None
    ):

        self.data_handler.logger.info("Creating new Object")

        if pos is not None:
            new_object.set_center(pos[0:2])

        new_object.select(selected)
        if selected:
            new_object.select_creation_controlpoint()
            self.selected_object = new_object.control_points

        # save object in data_handler.drawing_data
        if structure_name is None:
            structure_name = self.main_ui.structure_dd.currentText()

        structure_name = self.data_handler.save_object(structure_name, new_object)

        if self.main_ui.structure_dd.currentText() == "":
            self.main_ui.structure_dd.setCurrentText(structure_name)

        self.main_ui.update_object_list()

        return structure_name

    def create_new_object_w_undo(
        self, new_object, pos=None, selected=False, structure_name=None
    ):
        command = CreateObjectCommand(self, new_object, pos, selected, structure_name)
        self.main_window.undo_stack.push(command)

    def update_object_colors(self):
        for name in self.data_handler.drawing_data.keys():
            for obj in self.data_handler.drawing_data[name]:
                color = self.main_window.settings.value(
                    "graphics/object_color"
                ).getRgb()
                color = tuple([value / 255 for value in color])
                border_color = self.main_window.settings.value(
                    "graphics/object_border_color"
                ).getRgb()
                border_color = tuple([value / 255 for value in border_color])

                if isinstance(obj, (EditLineVisual)):
                    obj.line_color = border_color
                    obj.update_from_controlpoints()
                else:
                    obj.update_colors(color=color, border_color=border_color)
        self.scene.update()

    def update_background_color(self, color=None):
        if color is None:
            color = self.main_window.get_bg_color()
        # if self.main_window.is_dark_mode():
        #     color = "black"
        # else:
        #     color = (240/255, 240/255, 240/255, 1)

        # Convert QColor to RGBA tuple with values in range 0-1
        self.bgcolor = (
            color.red() / 255,
            color.green() / 255,
            color.blue() / 255,
            color.alpha() / 255,
        )
        self.update()

    def update_axis_color(self):

        if self.main_window.is_dark_mode():
            color = "white"
        else:
            color = "black"
        # Convert QColor to RGBA tuple with values in range 0-1
        # self.title_label.color = color
        self.xaxis.axis.text_color = color
        # self.xaxis.axis.axis_color = color
        self.xaxis.axis.tick_color = color
        self.yaxis.axis.text_color = color
        # self.yaxis.axis.axis_color = color
        self.yaxis.axis.tick_color = color

        # title color
        self.title_label._text_visual.color = color

    def update_colors(self):
        self.update_background_color()
        self.update_axis_color()
        self.update_object_colors()

    def check_creation_allowed(self, new_object, structure_name=None):
        # check if object is allowed to be created
        if structure_name is None:
            structure_name = self.main_ui.structure_dd.currentText()

        if structure_name in self.data_handler.drawing_data.keys():
            structure_type = type(self.data_handler.drawing_data[structure_name][0])
        else:
            return True

        if isinstance(new_object, (structure_type)):
            return True
        else:
            # raise prompt to change structure name
            text, ok = QInputDialog.getText(
                self.main_ui,
                "Warning",
                f"This structure is of a different type: {structure_type}.\n"
                "Please change the structure name to create this object.",
                text=self.data_handler.generate_output_name(),
            )
            if ok:
                # After the user closes the QInputDialog,
                # you can get the text from the QLineEdit
                self.main_ui.structure_dd.setCurrentText(text)
            return False

    def find_scale_bar_width(self, seed_points, relative=True, threshold=None, direction=None):
        # get width of scaling bar by floodFilling an area of similar pixels.
        # The start point needs to be given

        if self.start_state:
            return
        if threshold is None:
            threshold = self.main_ui.DEFAULT_THRESHOLD
        if direction is None:
            direction = 'horizontal'
        
        self.scale_bar_params = (seed_points, relative, threshold, direction)
        self.main_ui.set_threshold(threshold)
        self.main_ui.scaling_direction_dd.setCurrentText(direction)

        if seed_points is None:
            self.draw_image()
            scale_px = ""
        else:
            # create copy of image which can be modified
            img_data_modified = self.data_handler.img_data.copy()
            if relative:
                seed_point_x = round(seed_points[0] * img_data_modified.shape[1])
                seed_point_y = round(seed_points[1] * img_data_modified.shape[0])
            else:
                seed_point_x, seed_point_y = seed_points
            self.data_handler.logger.debug(
                f"seed_point_x = {seed_point_x}, seed_point_y = {seed_point_y}"
            )

            cv2.floodFill(
                img_data_modified,
                None,
                (seed_point_x, seed_point_y),
                newVal=self.main_window.settings.value(
                    "graphics/scale_bar_color"
                ).getRgb(),
                loDiff=[threshold] * 3,
                upDiff=[threshold] * 3,
            )

            self.draw_image(img_data=img_data_modified)

            non_zero_indices = np.nonzero(
                np.sum(self.data_handler.img_data - img_data_modified, axis=2)
            )

            # plus one to account for the start pixel
            if direction == "horizontal":
                scale_px = np.max(non_zero_indices[1]) - np.min(non_zero_indices[1]) + 1
            else:
                scale_px = np.max(non_zero_indices[0]) - np.min(non_zero_indices[0]) + 1

        self.main_ui.pixel_edit.setText(str(scale_px))
        self.scene.update()

    def find_scale_bar_width_w_undo(self, seed_point_percentage, relative=True, threshold=None, direction=None):
        command = FindScalingBarWidthCommand(
            self, seed_point_percentage, relative, threshold, direction
        )
        self.main_window.undo_stack.push(command)

    def on_key_press(self, event):

        if self.start_state:
            return

        if isinstance(self.selected_object, LineControlPoints):
            if event.key.name == "Escape":
                if self.selected_object.continue_adding_points:
                    # self.selected_object.remove_point()
                    self.remove_point_w_undo()
                    self.selected_object.continue_adding_points = False
                else:
                    self.remove_point_w_undo()
            if event.key.name == "A":
                if (self.selected_object.num_points == 0 
                or len(self.selected_object.coords) < self.selected_object.num_points):

                    self.selected_object.continue_adding_points = True
                    
                    # get mouse position in image coordinates
                    index = self.selected_object.get_selected_index()
                    pos = self.selected_object.coords[index]
                    # undo gets called, when the released event is called
                    self.add_point_w_undo(self.selected_object, pos)


    def on_mouse_move(self, event):
        
        # transform so that coordinates start at 0 in self.view window
        tr = self.scene.node_transform(self.view)
        # only activate when over self.view by looking
        # if coordinates < 0 or > size of self.view
        if not (
            (tr.map(event.pos)[:2] > self.view.size).any()
            or (tr.map(event.pos)[:2] < (0, 0)).any()
        ):

            if self.start_state:
                return
            match event.button:
                case 1:
                    # Check if the left mouse button is being dragged
                    if not event.is_dragging:
                        return

                    # scale objects while dragging
                    if self.selected_object is not None:
                        modifiers = [key.name for key in event.modifiers]
                        tr = self.scene.node_transform(self.selected_object)
                        pos = tr.map(event.pos)

                        if self.main_ui.tools_buttons.checkedButton().text() in (
                            "Line", "Ellipse", "Rectangle", "Angle", "Edit", "Polygon", "Multi-Line"
                        ):

                            if "Shift" in modifiers and not isinstance(
                                self.selected_object,
                                (LineControlPoints, EditLineVisual),
                            ):

                                # calculate angle of current position
                                x, y = pos[0:2] - self.selected_object.center
                                angle = np.arctan2(-y, x)

                                # Check if "Control" is pressed for snapping
                                if "Control" in modifiers:
                                    # Calculate the nearest multiple of 45° (np.pi / 4 radians)
                                    nearest_multiple = round(angle / (np.pi / 4)) * (np.pi / 4)
                                    # Snap to the nearest multiple if the angle is close to it
                                    if abs(angle - nearest_multiple) < np.deg2rad(10):
                                        angle = nearest_multiple + self.selected_object.drag_reference_angle

                                self.selected_object.rotate(angle)

                            else:

                                self.selected_object.move(pos[0:2], modifiers=modifiers)

                            # update ui to display properties of selected object
                            self.selection_update()
                            self.main_window.right_ui.update_intensity_plot()

                case 3:
                    if event.is_dragging:
                        tr = self.view.node_transform(self.view.scene)
                        # Calculate the change in position of the mouse
                        dpos = tr.map(event.last_event.pos)[:2] - tr.map(event.pos)[:2]
                        # Pan the camera based on the mouse movement
                        self.view.camera.pan(dpos)
                
                case _:
                    # if it is a line that is still being drawn
                    if isinstance(self.selected_object, LineControlPoints):

                        if self.selected_object.continue_adding_points:
                            modifiers = [key.name for key in event.modifiers]
                            tr = self.scene.node_transform(self.selected_object)
                            pos = tr.map(event.pos)

                            self.selected_object.move(pos[:2], modifiers=modifiers)
                            self.selection_update()
                            self.main_window.right_ui.update_intensity_plot()
                        
    def hide_arrows(self):
        for name in self.data_handler.drawing_data.keys():
            for obj in self.data_handler.drawing_data[name]:
                if isinstance(obj, EditRectVisual):
                    obj.hide_arrow()
        self.scene.update()
        
    def show_arrows(self):
        for name in self.data_handler.drawing_data.keys():
            for obj in self.data_handler.drawing_data[name]:
                if isinstance(obj, EditRectVisual):
                    obj.show_arrow()
        self.scene.update()

    def move_object_w_undo(self, object):
        command = MoveObjectCommand(self, object)
        self.main_window.undo_stack.push(command)

    def remove_point_w_undo(self):
        # we dont want to undo the last 2 points
        if len(self.selected_object.coords) < 3:
            self.selected_object.remove_point()
        else:
            command = RemovePointCommand(self, self.selected_object)
            self.main_window.undo_stack.push(command)

    def add_point_w_undo(self, object, point):
        # we dont want to remember creating the first 2 (3 for angle) points 
        # as this is basically the creation of the object
        if (len(self.selected_object.coords) < 2 
        or (self.selected_object.num_points != 0 
            and len(self.selected_object.coords) < self.selected_object.num_points+1)):
            self.selected_object.add_point(point)
        else:
            command = AddPointCommand(self, object, point)
            self.main_window.undo_stack.push(command)

    def selection_update(self, object=None):

        if object is None:
            object = self.get_selected_object()
        if self.selected_object is None:
            self.main_ui.clear_object_table()
            return

        self.main_window.right_ui.update_intensity_plot()

        found_object = self.data_handler.find_object(object)
        if found_object:
            structure, index = found_object
        else:
            return
        self.main_ui.add_to_structure_dd(structure)
        self.main_ui.update_object_list()
        self.main_ui.object_list.item(index).setSelected(True)

        self.main_ui.update_object_table(object)

    def update_object_property_w_undo(self, obj, prop, value, scaling_factor=None, old_value=None):
        # only push onto undo stack if property is modifiable
        if prop in obj.get_modifiable_properties():
            command = UpdateObjectCommand(self, obj, prop, value, scaling_factor, old_value)
            self.main_window.undo_stack.push(command)
        # otherwise just redraw old value
        else:
            self.selection_update()

    def update_object_property(self, obj, prop, value, scaling_factor=None):
        if obj is None:
            obj = self.get_selected_object()
        # update object property
        changed = obj.update_property(prop, value, scaling_factor)
        # update ui to display properties of selected object
        self.selection_update()

        return changed


    def get_selected_object(self):
        """always returns the whole object instance,
        even if controlpoints are selected
        """
        return self.get_full_object(self.selected_object)
    
    def get_full_object(self, obj):
        if isinstance(obj, (ControlPoints, LineControlPoints)):
            return obj.parent
        return obj

    def delete_object(self, object=None):
        # delete selected object if no other is given
        if object is None:
            object = self.selected_object
        # delete object
        if object is not None:
            self.data_handler.delete_object(object)

            if object.selected:
                self.unselect()

            self.main_ui.update_object_list()
            object.delete()

        self.main_window.right_ui.update_intensity_plot()

    def delete_object_w_undo(self, object=None):
        if object is not None or self.selected_object is not None:
            command = DeleteObjectCommand(self.data_handler, object)
            self.main_window.undo_stack.push(command)

    def set_origin(self, point:np.ndarray, move_objects=True):
        """Set the origin of the image to a given point"""
        self.data_handler.logger.debug(f"set origin to {point}")

        if move_objects:
            self.move_all_objects(-(point-self.origin))
        self.origin = point 
        self.image.transform = linear.STTransform(translate=-self.origin)
        self.center_image()

    def move_all_objects(self, vector:np.ndarray):
        """Move all objects by a relative vector"""
        self.data_handler.logger.debug("move all objects")
        for structure in self.data_handler.drawing_data.keys():
            for obj in self.data_handler.drawing_data[structure]:
                # show object
                obj.set_center(obj.center + vector)
                obj.update_from_controlpoints()
        self.scene.update()        

    def set_origin_w_undo(self, pos):
        command = SetOriginCommand(self, pos)
        self.main_window.undo_stack.push(command)

    def select(self, obj):
        self.unselect()
        self.selected_object = obj
        self.selected_object.select(True, obj=obj)
        self.selected_object.set_visibility(True)
        self.selection_update()

    def unselect(self):
        if self.selected_object is not None:
            self.selected_object.select(False)
            self.selected_object = None
            self.main_ui.clear_object_table()

    def show_all_objects(self):
        self.data_handler.logger.info("show all objects")
        for structure in self.data_handler.drawing_data.keys():
            for obj in self.data_handler.drawing_data[structure]:
                # show object
                obj.set_visibility(True)
        self.scene.update()

    def show_all_objects_w_undo(self):
        command = ShowObjectCommand(self)
        self.main_window.undo_stack.push(command)

    def hide_all_objects(self):
        self.data_handler.logger.info("hide all objects")
        for structure in self.data_handler.drawing_data.keys():
            for obj in self.data_handler.drawing_data[structure]:
                # hide object
                obj.set_visibility(False)
        self.scene.update()

    def hide_all_objects_w_undo(self):
        command = HideObjectCommand(self)
        self.main_window.undo_stack.push(command)


class DeleteObjectCommand(QUndoCommand):
    def __init__(self, data_handler, object=None):
        super().__init__()
        self.data_handler = data_handler
        self.main_window = self.data_handler.main_window
        self.vispy_canvas = self.main_window.vispy_canvas
        if object is None:
            object = self.vispy_canvas.selected_object

        object = self.vispy_canvas.get_full_object(object)

        self.object = object
        self.structure, self.index = data_handler.find_object(self.object)

    def undo(self):
        # Restore the old state
        self.data_handler.logger.info("Undoing delete object")
        if self.structure in self.data_handler.drawing_data.keys():
            self.data_handler.drawing_data[self.structure].insert(
                self.index, self.object
            )
        else:
            self.data_handler.drawing_data[self.structure] = [self.object]
        self.main_window.main_ui.update_structure_dd()
        self.object.parent = self.vispy_canvas.view.scene

        self.data_handler.logger.info("Restored object")

    def redo(self):
        # Delete object
        self.vispy_canvas.delete_object(self.object)
        self.data_handler.logger.info("Deleted object")


class MoveObjectCommand(QUndoCommand):
    def __init__(self, vispy_instance, object):
        super().__init__()
        # make a copy of the object to be able to restore the old state
        self.vispy_instance = vispy_instance

        # if object is a control point, get the whole object
        self.object = vispy_instance.get_full_object(object)

        # for redoing
        self.redoing = False
        if isinstance(self.object, (EditLineVisual, EditPolygonVisual)):
            self.coords = None
            self.old_coords = object.coords.copy()
        elif isinstance(self.object, (EditEllipseVisual, EditRectVisual)):
            
            self.center = None
            self.height = None
            self.width = None
            self.angle = None
            self.old_height = self.object.height
            self.old_width = self.object.width
            self.old_center = self.object.center
            self.old_angle = self.object.angle
        # for undoing

    def undo(self):
        self.vispy_instance.data_handler.logger.info("Undoing move object")

        # save state before undoing move
        if isinstance(self.object, (EditLineVisual, EditPolygonVisual)):
            self.object.coords = self.old_coords
        elif isinstance(self.object, (EditEllipseVisual, EditRectVisual)):

            self.object.set_center(self.old_center)
            self.object.width = self.old_width
            self.object.height = self.old_height
            self.object.angle = self.old_angle

        # restore old state

        self.object.update_from_controlpoints()
        self.vispy_instance.selection_update()

    def redo(self):
        # Move the object
        self.vispy_instance.data_handler.logger.info("Redoing move object")

        # in the first creation we don't need to set it as it is already set
        # but we need to save the state
        if not self.redoing:
            if isinstance(self.object, (EditLineVisual, EditPolygonVisual)):
                self.coords = self.object.coords.copy()
            elif isinstance(self.object, (EditEllipseVisual, EditRectVisual)):
                self.center = self.object.center.copy()
                self.height = self.object.height
                self.width = self.object.width
                self.angle = self.object.angle
            self.redoing = True
        # move object on all redo operations except the first one
        else:
            if isinstance(self.object, (EditLineVisual, EditPolygonVisual)):
                self.object.coords = self.coords
            elif isinstance(self.object, (EditEllipseVisual, EditRectVisual)):
                self.object.set_center(self.center)
                self.object.width = self.width
                self.object.height = self.height
                self.object.angle = self.angle

        self.object.update_from_controlpoints()
        self.vispy_instance.selection_update()
        
    def check_movement(self):
        if isinstance(self.object, (EditLineVisual, EditPolygonVisual)):
            if len(self.old_coords) == len(self.object.coords):
                return (self.old_coords != self.object.coords).any()
            return True
        elif isinstance(self.object, (EditEllipseVisual, EditRectVisual)):
            return (
                self.old_width != self.object.width
                or self.old_height != self.object.height
                or (self.old_center != self.object.center).any()
                or self.old_angle != self.object.angle
            )


class HideObjectCommand(QUndoCommand):
    def __init__(self, vispy_canvas):
        super().__init__()
        self.vispy_canvas = vispy_canvas

    def undo(self):
        # Show the object
        self.vispy_canvas.show_all_objects()

    def redo(self):
        # Hide the object
        self.vispy_canvas.hide_all_objects()


class ShowObjectCommand(QUndoCommand):
    def __init__(self, vispy_canvas):
        super().__init__()
        self.vispy_canvas = vispy_canvas

    def undo(self):
        # Show the object
        self.vispy_canvas.hide_all_objects()

    def redo(self):
        # Hide the object
        self.vispy_canvas.show_all_objects()


class FindScalingBarWidthCommand(QUndoCommand):
    def __init__(self, vispy_canvas, seed_points, relative, threshold, direction):
        super().__init__()
        self.vispy_canvas:VispyCanvas = vispy_canvas

        self.seed_points = seed_points
        self.relative = relative
        self.threshold = threshold
        self.direction = direction

        self.old_scale_bar_params = self.vispy_canvas.scale_bar_params

    def undo(self):
        # Restore the old state
        self.vispy_canvas.find_scale_bar_width(*self.old_scale_bar_params)

    def redo(self):
        # Find the scaling bar width
        self.vispy_canvas.find_scale_bar_width(
            self.seed_points, self.relative, self.threshold, self.direction
        )


class CreateObjectCommand(QUndoCommand):
    def __init__(self, vispy_canvas, new_object, pos, selected, structure_name):
        super().__init__()
        self.vispy_canvas = vispy_canvas
        self.new_object = self.vispy_canvas.get_full_object(new_object)
        self.pos = pos
        self.selected = selected
        self.structure_name = structure_name
        self.parent = new_object.parent

    def undo(self):
        # Delete object
        self.vispy_canvas.data_handler.logger.info("Undoing create object")
        self.vispy_canvas.delete_object(self.new_object)

    def redo(self):

        # when it is an undo->redo operation some parameters are set
        if self.new_object and self.new_object.parent is None:
            self.selected = False
            self.new_object.parent = self.parent
            self.pos = None

        # Create object
        self.structure_name = self.vispy_canvas.create_new_object(
            self.new_object, self.pos, self.selected, self.structure_name
        )


class UpdateObjectCommand(QUndoCommand):
    def __init__(self, vispy_canvas, obj, prop, value, scaling_factor, old_value):
        super().__init__()

        self.vispy_canvas = vispy_canvas
        self.obj = obj
        self.prop = prop
        self.value = value
        self.scaling_factor = scaling_factor
        self.old_value = old_value

        if scaling_factor is None:
            self.scaling_factor = 1
    
    def undo(self):
        
        # logging
        if self.scaling_factor is None:
            scaling = 1
        else:
            scaling = self.scaling_factor
        self.vispy_canvas.data_handler.logger.debug(
            f"Undoing Change {self.prop} from {self.old_value*scaling} to {self.value}"
        )

        # update object property
        self.vispy_canvas.update_object_property(
            self.obj, self.prop, self.old_value, self.scaling_factor
        )


    def redo(self):
        # logging
        if self.scaling_factor is None:
            scaling = 1
        else:
            scaling = self.scaling_factor
        self.vispy_canvas.data_handler.logger.debug(
            f"Change {self.prop} from {self.old_value*scaling} to {self.value}"
        )

        # update object property
        self.vispy_canvas.update_object_property(
            self.obj, self.prop, self.value, self.scaling_factor
        )
        
class RemovePointCommand(QUndoCommand):

    def __init__(self, vispy_canvas, object):
        super().__init__()
        self.object = object
        self.delete_index = object.control_points.index(object.selected_cp)
        self.old_point = None
        self.vispy_canvas = vispy_canvas
        self.continue_adding_points = object.continue_adding_points

    def undo(self):
        # Show the object
        self.vispy_canvas.data_handler.logger.debug(
            f"Undoing remove point at index {self.delete_index}"
        )
        self.object.continue_adding_points = True
        self.object.add_point(self.old_point, index=self.delete_index)
        self.object.continue_adding_points = self.continue_adding_points

    def redo(self):
        # Hide the object
        self.vispy_canvas.data_handler.logger.debug(
            f"Redoing remove point at index {self.delete_index}"
        )
        self.old_point = self.object.coords[self.delete_index]
        self.object.remove_point(self.delete_index)

class AddPointCommand(QUndoCommand):

    def __init__(self, vispy_canvas, object, point):
        super().__init__()
        self.object = object
        self.add_index = object.get_selected_index() + 1
        self.point = point
        self.vispy_canvas = vispy_canvas
        self.redoing = False

    def undo(self):
        # remove movable point on undo
        self.object.continue_adding_points = False
        self.vispy_canvas.data_handler.logger.debug(
            f"Undoing adding point at index {self.add_index}"
        )
        if self.add_index < len(self.object.coords):
            self.point = self.object.coords[self.add_index]
            self.object.remove_point(self.add_index)

    def redo(self):
        self.vispy_canvas.data_handler.logger.debug(
            f"Redoing adding point at index {self.add_index}"
        )
        
        show=False
        if self.object is self.vispy_canvas.selected_object:
            self.object.select(True)
            show = True

        # when redoing, we need to add the point at the index 
        # before the one we added because that one is the currently moving one 
        if self.redoing:
            self.object.add_point(self.point, index=self.add_index, select=False, show=show)
        else:
            self.object.add_point(self.point, index=self.add_index)
        self.redoing = True

class SetOriginCommand(QUndoCommand):
    def __init__(self, vispy_canvas, pos):
        super().__init__()
        self.vispy_canvas = vispy_canvas
        self.old_origin = self.vispy_canvas.origin
        self.new_origin = pos

    def undo(self):
        # Show the object
        self.vispy_canvas.set_origin(self.old_origin)

    def redo(self):
        # Hide the object
        self.vispy_canvas.set_origin(self.new_origin)