class ImageView(QWidget):
    position_changed = Signal([int, int, int], [int, int])
    component_clicked = Signal(int)
    text_info_change = Signal(str)
    hide_signal = Signal(bool)
    view_changed = Signal()
    image_added = Signal()

    def __init__(
        self,
        settings: BaseSettings,
        channel_property: ChannelProperty,
        name: str,
        parent: Optional[QWidget] = None,
        ndisplay=2,
    ):
        super().__init__(parent=parent)

        self.settings = settings
        self.channel_property = channel_property
        self.name = name
        self.image_info: Dict[str, ImageInfo] = {}
        self.current_image = ""
        self._current_order = "xy"
        self.components = None
        self.worker_list = []

        self.viewer = Viewer(ndisplay=ndisplay)
        self.viewer.theme = self.settings.theme_name
        self.viewer_widget = NapariQtViewer(self.viewer)
        self.image_state = ImageShowState(settings, name)
        self.channel_control = ColorComboBoxGroup(settings,
                                                  name,
                                                  channel_property,
                                                  height=30)
        self.ndim_btn = QtNDisplayButton(self.viewer)
        self.reset_view_button = QtViewerPushButton(self.viewer, "home",
                                                    "Reset view",
                                                    self._reset_view)
        self.roll_dim_button = QtViewerPushButton(self.viewer, "roll",
                                                  "Roll dimension",
                                                  self._rotate_dim)
        self.roll_dim_button.setContextMenuPolicy(Qt.CustomContextMenu)
        self.roll_dim_button.customContextMenuRequested.connect(
            self._dim_order_menu)
        self.mask_chk = QCheckBox()
        self.mask_label = QLabel("Mask:")

        self.btn_layout = QHBoxLayout()
        self.btn_layout.addWidget(self.reset_view_button)
        self.btn_layout.addWidget(self.ndim_btn)
        self.btn_layout.addWidget(self.roll_dim_button)
        self.btn_layout.addWidget(self.channel_control, 1)
        self.btn_layout.addWidget(self.mask_label)
        self.btn_layout.addWidget(self.mask_chk)
        self.btn_layout2 = QHBoxLayout()
        layout = QVBoxLayout()
        layout.addLayout(self.btn_layout)
        layout.addLayout(self.btn_layout2)
        layout.addWidget(self.viewer_widget)

        self.setLayout(layout)

        self.channel_control.change_channel.connect(self.change_visibility)
        self.viewer.events.status.connect(self.print_info)

        settings.mask_changed.connect(self.set_mask)
        settings.mask_representation_changed.connect(
            self.update_mask_parameters)
        settings.roi_changed.connect(self.set_roi)
        settings.roi_clean.connect(self.set_roi)
        settings.image_changed.connect(self.set_image)
        settings.image_spacing_changed.connect(self.update_spacing_info)
        # settings.labels_changed.connect(self.paint_layer)
        self.old_scene: BaseCamera = self.viewer_widget.view.scene

        self.image_state.coloring_changed.connect(self.update_roi_coloring)
        self.image_state.roi_presented_changed.connect(
            self.update_roi_representation)
        self.image_state.borders_changed.connect(
            self.update_roi_representation)
        self.mask_chk.stateChanged.connect(self.change_mask_visibility)
        self.viewer_widget.view.scene.transform.changed.connect(
            self._view_changed, position="last")
        try:
            self.viewer.dims.events.current_step.connect(self._view_changed,
                                                         position="last")
        except AttributeError:
            self.viewer.dims.events.axis.connect(self._view_changed,
                                                 position="last")
        self.viewer.dims.events.ndisplay.connect(self._view_changed,
                                                 position="last")
        if hasattr(self.viewer.dims.events, "ndisplay"):
            self.viewer.dims.events.ndisplay.connect(self._view_changed,
                                                     position="last")
            self.viewer.dims.events.ndisplay.connect(self.camera_change,
                                                     position="last")
        else:
            self.viewer.dims.events.camera.connect(self._view_changed,
                                                   position="last")
            self.viewer.dims.events.camera.connect(self.camera_change,
                                                   position="last")
        self.viewer.events.reset_view.connect(self._view_changed,
                                              position="last")

    def _dim_order_menu(self, point: QPoint):
        menu = QMenu()
        for key in ORDER_DICT:
            action = menu.addAction(key)
            action.triggered.connect(partial(self._set_new_order, key))
            if key == self._current_order:
                font = action.font()
                font.setBold(True)
                action.setFont(font)

        menu.exec_(self.roll_dim_button.mapToGlobal(point))

    def _set_new_order(self, text: str):
        self._current_order = text
        self.viewer.dims.order = ORDER_DICT[text]
        self.update_roi_representation()

    def _reset_view(self):
        self._set_new_order("xy")
        self.viewer.dims.order = ORDER_DICT[self._current_order]
        self.viewer.reset_view()

    def _rotate_dim(self):
        self._set_new_order(NEXT_ORDER[self._current_order])

    def camera_change(self, _args):
        self.old_scene.transform.changed.disconnect(self._view_changed)
        self.old_scene: BaseCamera = self.viewer_widget.view.camera
        self.old_scene.transform.changed.connect(self._view_changed,
                                                 position="last")

    def _view_changed(self, _args):
        self.view_changed.emit()

    def get_state(self):
        return {
            "ndisplay": self.viewer.dims.ndisplay,
            "point": self.viewer.dims.point,
            "camera": self.viewer_widget.view.camera.get_state(),
        }

    def set_state(self, dkt):
        if "ndisplay" in dkt and self.viewer.dims.ndisplay != dkt["ndisplay"]:
            self.viewer.dims.ndisplay = dkt["ndisplay"]
            return
        if "point" in dkt:
            for i, val in enumerate(dkt["point"]):
                self.viewer.dims.set_point(i, val)
        if "camera" in dkt:
            try:
                self.viewer_widget.view.camera.set_state(dkt["camera"])
            except KeyError:
                pass

    def change_mask_visibility(self):
        for image_info in self.image_info.values():
            if image_info.mask is not None:
                image_info.mask.visible = self.mask_chk.isChecked()

    def update_spacing_info(self, image: Optional[Image] = None) -> None:
        """
        Update spacing of image if not provide, then use image pointed by settings.

        :param Optional[Image] image: image which spacing should be updated.
        :return: None
        """
        if image is None:
            image = self.settings.image

        if image.file_path not in self.image_info:
            raise ValueError("Image not registered")

        image_info = self.image_info[image.file_path]

        for layer in image_info.layers:
            layer.scale = image.normalized_scaling()

        if image_info.roi is not None:
            image_info.roi.scale = image.normalized_scaling()

        if image_info.mask is not None:
            image_info.mask.scale = image.normalized_scaling()

    def print_info(self, value):
        if not self.viewer.active_layer:
            return
        cords = np.array(
            [int(x) for x in self.viewer.active_layer.coordinates])
        bright_array = []
        components = []
        for image_info in self.image_info.values():
            if not image_info.coords_in(cords):
                continue
            moved_coords = image_info.translated_coords(cords)
            for layer in image_info.layers:
                if layer.visible:
                    bright_array.append(layer.data[tuple(moved_coords)])
            if image_info.roi_info.roi is not None and image_info.roi is not None:
                val = image_info.roi_info.roi[tuple(moved_coords)]
                if val:
                    components.append(val)

        if not bright_array and not components:
            self.text_info_change.emit("")
            return
        text = f"{cords}: "
        if bright_array:
            if len(bright_array) == 1:
                text += str(bright_array[0])
            else:
                text += str(bright_array)
        self.components = components
        if components:
            if len(components) == 1:
                text += f" component: {components[0]}"
            else:
                text += f" components: {components}"
        self.text_info_change.emit(text)

    def get_control_view(self) -> ImageShowState:
        return self.image_state

    @staticmethod
    def convert_to_vispy_colormap(colormap: ColorMap):
        return Colormap(ColorArray(create_color_map(colormap) / 255))

    def mask_opacity(self) -> float:
        """Get mask opacity"""
        return self.settings.get_from_profile("mask_presentation_opacity", 1)

    def mask_color(self) -> Colormap:
        """Get mask marking color"""
        color = Color(
            np.divide(
                self.settings.get_from_profile("mask_presentation_color",
                                               [255, 255, 255]), 255))
        return Colormap(ColorArray(["black", color.rgba]))

    def get_image(self, image: Optional[Image]) -> Image:
        if image is not None:
            return image
        if self.current_image not in self.image_info:
            return self.settings.image
        return self.image_info[self.current_image].image

    def set_roi(self,
                roi_info: Optional[ROIInfo] = None,
                image: Optional[Image] = None) -> None:
        image = self.get_image(image)
        if roi_info is None:
            roi_info = self.settings.roi_info
        image_info = self.image_info[image.file_path]
        if image_info.roi is not None:
            self.viewer.layers.unselect_all()
            image_info.roi.selected = True
            self.viewer.layers.remove_selected()
            image_info.roi = None

        if roi_info.roi is None:
            return

        image_info.roi_info = roi_info
        image_info.roi_count = max(
            roi_info.bound_info) if roi_info.bound_info else 0
        self.add_roi_layer(image_info)
        image_info.roi.colormap = self.get_roi_view_parameters(image_info)
        image_info.roi.opacity = self.image_state.opacity

    def get_roi_view_parameters(self, image_info: ImageInfo) -> Colormap:
        colors = self.settings.label_colors / 255
        if self.image_state.show_label == LabelEnum.Not_show or image_info.roi_count == 0 or colors.size == 0:
            colors = np.array([[0, 0, 0, 0], [0, 0, 0, 0]])
        else:
            repeat = int(np.ceil(image_info.roi_count / colors.shape[0]))
            colors = np.concatenate([colors] * repeat)
            colors = np.concatenate(
                [colors,
                 np.ones(colors.shape[0]).reshape(colors.shape[0], 1)],
                axis=1)
            colors = np.concatenate([[[0, 0, 0, 0]],
                                     colors[:image_info.roi_count]])
            if self.image_state.show_label == LabelEnum.Show_selected:
                try:
                    colors *= self.settings.components_mask().reshape(
                        (colors.shape[0], 1))
                except ValueError:
                    pass
        control_points = [0] + list(
            np.linspace(1 / (2 * colors.shape[0]),
                        1,
                        endpoint=True,
                        num=colors.shape[0]))
        return Colormap(colors, controls=control_points, interpolation="zero")

    def update_roi_coloring(self):
        for image_info in self.image_info.values():
            if image_info.roi is None:
                continue
            image_info.roi.colormap = self.get_roi_view_parameters(image_info)
            image_info.roi.opacity = self.image_state.opacity

    def remove_all_roi(self):
        self.viewer.layers.unselect_all()
        for image_info in self.image_info.values():
            if image_info.roi is None:
                continue
            image_info.roi.selected = True
            image_info.roi = None

        self.viewer.layers.remove_selected()

    def add_roi_layer(self, image_info: ImageInfo):
        if image_info.roi_info.roi is None:
            return
        try:
            max_num = max(1, image_info.roi_count)
        except ValueError:
            max_num = 1
        roi = image_info.roi_info.alternative.get(
            self.image_state.roi_presented, image_info.roi_info.roi)
        if self.image_state.only_borders:

            data = calculate_borders(
                roi.transpose(ORDER_DICT[self._current_order]),
                self.image_state.borders_thick // 2,
                self.viewer.dims.ndisplay == 2,
            ).transpose(np.argsort(ORDER_DICT[self._current_order]))
            image_info.roi = self.viewer.add_image(
                data,
                scale=image_info.image.normalized_scaling(),
                contrast_limits=[0, max_num],
            )
        else:
            image_info.roi = self.viewer.add_image(
                roi,
                scale=image_info.image.normalized_scaling(),
                contrast_limits=[0, max_num],
                name="ROI",
                blending="translucent",
            )
        image_info.roi._interpolation[3] = Interpolation3D.NEAREST

    def update_roi_representation(self):
        self.remove_all_roi()

        for image_info in self.image_info.values():
            self.add_roi_layer(image_info)

        self.update_roi_coloring()

    def set_mask(self,
                 mask: Optional[np.ndarray] = None,
                 image: Optional[Image] = None) -> None:
        image = self.get_image(image)
        if image.file_path not in self.image_info:
            raise ValueError("Image not added to viewer")
        if mask is None:
            mask = image.mask

        image_info = self.image_info[image.file_path]
        if image_info.mask is not None:
            self.viewer.layers.unselect_all()
            image_info.mask.selected = True
            self.viewer.layers.remove_selected()
            image_info.mask = None

        if mask is None:
            return

        mask_marker = mask == 0

        layer = self.viewer.add_image(mask_marker,
                                      scale=image.normalized_scaling(),
                                      blending="additive")
        layer.colormap = self.mask_color()
        layer.opacity = self.mask_opacity()
        layer.visible = self.mask_chk.isChecked()
        image_info.mask = layer

    def update_mask_parameters(self):
        opacity = self.mask_opacity()
        colormap = self.mask_color()
        for image_info in self.image_info.values():
            if image_info.mask is not None:
                image_info.mask.opacity = opacity
                image_info.mask.colormap = colormap

    def set_image(self, image: Optional[Image] = None):
        self.image_info = {}
        self.add_image(image, True)

    def has_image(self, image: Image):
        return image.file_path in self.image_info

    @staticmethod
    def calculate_filter(
            array: np.ndarray,
            parameters: Tuple[NoiseFilterType, float]) -> Optional[np.ndarray]:
        if parameters[0] == NoiseFilterType.No or parameters[1] == 0:
            return array
        if parameters[0] == NoiseFilterType.Gauss:
            return gaussian(array, parameters[1])
        return median(array, int(parameters[1]))

    def _remove_worker(self, sender):
        for worker in self.worker_list:
            signals = "_signals" if hasattr(worker, "_signals") else "signals"
            if sender is getattr(worker, signals):
                self.worker_list.remove(worker)
                break
        else:
            print("[_remove_worker]", sender)

    def _add_layer_util(self, index, layer, filters):
        self.viewer.add_layer(layer)

        def set_data(val):
            self._remove_worker(self.sender())
            data_, layer_ = val
            if data_ is None:
                return
            if layer_ not in self.viewer.layers:
                return
            layer_.data = data_

        @thread_worker(connect={"returned": set_data})
        def calc_filter(j, layer_):
            if filters[j][0] == NoiseFilterType.No or filters[j][1] == 0:
                return None, layer_
            return self.calculate_filter(layer_.data,
                                         parameters=filters[j]), layer_

        worker = calc_filter(index, layer)
        self.worker_list.append(worker)

    def _add_image(self, image_data: Tuple[ImageInfo, bool]):
        self._remove_worker(self.sender())

        image_info, replace = image_data
        image = image_info.image
        if replace:
            self.viewer.layers.select_all()
            self.viewer.layers.remove_selected()

        filters = self.channel_control.get_filter()
        for i, layer in enumerate(image_info.layers):
            self._add_layer_util(i, layer, filters)

        self.image_info[image.file_path].filter_info = filters
        self.image_info[image.file_path].layers = image_info.layers
        self.current_image = image.file_path
        self.viewer.reset_view()
        if self.viewer.layers:
            self.viewer.layers[-1].selected = True

        for i, axis in enumerate(image.axis_order):
            if axis == "C":
                continue
            self.viewer.dims.set_point(
                i, image.shape[i] * image.normalized_scaling()[i] // 2)
        if self.image_info[image.file_path].roi is not None:
            self.set_roi()
        if image_info.image.mask is not None:
            self.set_mask()
        self.image_added.emit()

    def add_image(self, image: Optional[Image], replace=False):
        if image is None:
            image = self.settings.image

        if not image.channels:
            raise ValueError("Need non empty image")

        if image.file_path in self.image_info:
            raise ValueError("Image already added")

        self.image_info[image.file_path] = ImageInfo(image, [])

        channels = image.channels
        if self.image_info and not replace:
            channels = max(
                channels,
                *[x.image.channels for x in self.image_info.values()])

        self.channel_control.set_channels(channels)
        visibility = self.channel_control.channel_visibility
        limits = self.channel_control.get_limits()
        ranges = image.get_ranges()
        limits = [
            ranges[i] if x is None else x
            for i, x in zip(range(image.channels), limits)
        ]
        gamma = self.channel_control.get_gamma()
        colormaps = [
            self.convert_to_vispy_colormap(
                self.channel_control.selected_colormaps[i])
            for i in range(image.channels)
        ]
        parameters = ImageParameters(limits, visibility, gamma, colormaps,
                                     image.normalized_scaling(),
                                     len(self.viewer.layers))

        self._prepare_layers(image, parameters, replace)

        return image

    def _prepare_layers(self, image, parameters, replace):
        worker = prepare_layers(image, parameters, replace)
        worker.returned.connect(self._add_image)
        self.worker_list.append(worker)
        worker.start()

    def images_bounds(self) -> Tuple[List[int], List[int]]:
        ranges = []
        for image_info in self.image_info.values():
            if not image_info.layers:
                continue
            ranges = [(min(a, b), max(c, d), min(e, f)) for (a, c, e), (
                b, d,
                f) in itertools.zip_longest(image_info.layers[0].dims.range,
                                            ranges,
                                            fillvalue=(np.inf, -np.inf,
                                                       np.inf))]

        visible = [ranges[i] for i in self.viewer.dims.displayed]
        min_shape, max_shape, _ = zip(*visible)
        size = np.subtract(max_shape, min_shape)
        return size, min_shape

    @staticmethod
    def _shift_layer(layer: Layer, translate_2d):
        translate = [0] * layer.ndim
        translate[-2:] = translate_2d
        layer.translate_grid = translate

    def grid_view(self):
        """Present multiple images in grid view"""
        n_row = np.ceil(np.sqrt(len(self.image_info))).astype(int)
        n_row = max(1, n_row)
        scene_size, _ = self.images_bounds()
        for image_info, pos in zip(self.image_info.values(),
                                   itertools.product(range(n_row), repeat=2)):
            translate_2d = np.multiply(scene_size[-2:], pos)
            for layer in image_info.layers:
                self._shift_layer(layer, translate_2d)

            if image_info.mask is not None:
                self._shift_layer(image_info.mask, translate_2d)

            if image_info.roi is not None:
                self._shift_layer(image_info.roi, translate_2d)
        self.viewer.reset_view()

    def change_visibility(self, name: str, index: int):
        for image_info in self.image_info.values():
            if len(image_info.layers) > index:
                image_info.layers[
                    index].visible = self.channel_control.channel_visibility[
                        index]
                if self.channel_control.channel_visibility[index]:
                    image_info.layers[
                        index].colormap = self.convert_to_vispy_colormap(
                            self.channel_control.selected_colormaps[index])
                    limits = self.channel_control.get_limits()[index]
                    limits = image_info.image.get_ranges(
                    )[index] if limits is None else limits
                    image_info.layers[index].contrast_limits = limits
                    image_info.layers[
                        index].gamma = self.channel_control.get_gamma()[index]
                    filter_type = self.channel_control.get_filter()[index]
                    if filter_type != image_info.filter_info[index]:
                        image_info.layers[index].data = self.calculate_filter(
                            image_info.image.get_channel(index), filter_type)
                        image_info.filter_info[index] = filter_type

    def reset_image_size(self):
        self.viewer.reset_view()

    def set_theme(self, theme: str):
        self.viewer.theme = theme

    def closeEvent(self, event):
        for worker in self.worker_list:
            worker.quit()
        super().closeEvent(event)

    def get_tool_tip_text(self) -> str:
        image = self.settings.image
        image_info = self.image_info[image.file_path]
        text_list = []
        for el in self.components:
            text_list.append(
                _print_dict(image_info.roi_info.annotations.get(el, {})))
        return " ".join(text_list)

    def event(self, event: QEvent):
        if event.type() == QEvent.ToolTip and self.components:
            text = self.get_tool_tip_text()
            if text:
                QToolTip.showText(event.globalPos(), text)
        return super().event(event)
class ImageView(QWidget):
    position_changed = Signal([int, int, int], [int, int])
    component_clicked = Signal(int)
    text_info_change = Signal(str)
    hide_signal = Signal(bool)
    view_changed = Signal()
    image_added = Signal()

    def __init__(
        self,
        settings: BaseSettings,
        channel_property: ChannelProperty,
        name: str,
        parent: Optional[QWidget] = None,
        ndisplay=2,
    ):
        super().__init__(parent=parent)

        self.settings = settings
        self.channel_property = channel_property
        self.name = name
        self.image_info: Dict[str, ImageInfo] = {}
        self.current_image = ""
        self._current_order = "xy"
        self.components = None
        self.worker_list = []
        self.points_layer = None
        self.roi_alternative_selection = "ROI"
        self._search_type = SearchType.Highlight
        self._last_component = 1

        self.viewer = Viewer(ndisplay=ndisplay)
        self.viewer.theme = self.settings.theme_name
        self.viewer_widget = NapariQtViewer(self.viewer)
        if hasattr(self.viewer_widget.canvas, "background_color_override"):
            self.viewer_widget.canvas.background_color_override = "black"
        self.channel_control = ColorComboBoxGroup(settings,
                                                  name,
                                                  channel_property,
                                                  height=30)
        self.ndim_btn = QtNDisplayButton(self.viewer)
        self.reset_view_button = QtViewerPushButton(self.viewer, "home",
                                                    "Reset view",
                                                    self._reset_view)
        self.points_view_button = QtViewerPushButton(
            self.viewer, "new_points", "Show points",
            self.toggle_points_visibility)
        self.points_view_button.setVisible(False)
        self.search_roi_btn = SearchROIButton(self.settings)
        self.search_roi_btn.clicked.connect(self._search_component)
        self.search_roi_btn.setDisabled(True)
        self.roll_dim_button = QtViewerPushButton(self.viewer, "roll",
                                                  "Roll dimension",
                                                  self._rotate_dim)
        self.roll_dim_button.setContextMenuPolicy(Qt.CustomContextMenu)
        self.roll_dim_button.customContextMenuRequested.connect(
            self._dim_order_menu)
        self.mask_chk = QCheckBox()
        self.mask_chk.setVisible(False)
        self.mask_label = QLabel("Mask:")
        self.mask_label.setVisible(False)

        self.btn_layout = QHBoxLayout()
        self.btn_layout.addWidget(self.reset_view_button)
        self.btn_layout.addWidget(self.ndim_btn)
        self.btn_layout.addWidget(self.roll_dim_button)
        self.btn_layout.addWidget(self.points_view_button)
        self.btn_layout.addWidget(self.search_roi_btn)
        self.btn_layout.addWidget(self.channel_control, 1)
        self.btn_layout.addWidget(self.mask_label)
        self.btn_layout.addWidget(self.mask_chk)
        self.btn_layout2 = QHBoxLayout()
        layout = QVBoxLayout()
        layout.addLayout(self.btn_layout)
        layout.addLayout(self.btn_layout2)
        layout.addWidget(self.viewer_widget)

        self.setLayout(layout)

        self.channel_control.change_channel.connect(self.change_visibility)
        self.viewer.events.status.connect(self.print_info)

        settings.mask_changed.connect(self.set_mask)
        settings.roi_changed.connect(self.set_roi)
        settings.roi_clean.connect(self.set_roi)
        settings.image_changed.connect(self.set_image)
        settings.image_spacing_changed.connect(self.update_spacing_info)
        settings.points_changed.connect(self.update_points)
        settings.connect_to_profile(RENDERING_MODE_NAME, self.update_rendering)
        settings.labels_changed.connect(self.update_roi_coloring)
        settings.connect_to_profile(f"{name}.image_state.opacity",
                                    self.update_roi_coloring)
        settings.connect_to_profile(f"{name}.image_state.only_border",
                                    self.update_roi_border)
        settings.connect_to_profile(f"{name}.image_state.border_thick",
                                    self.update_roi_border)
        settings.connect_to_profile(f"{name}.image_state.show_label",
                                    self.update_roi_labeling)
        settings.connect_to_profile("mask_presentation_opacity",
                                    self.update_mask_parameters)
        settings.connect_to_profile("mask_presentation_color",
                                    self.update_mask_parameters)
        # settings.labels_changed.connect(self.paint_layer)
        self.old_scene: BaseCamera = self.viewer_widget.view.scene

        self.mask_chk.stateChanged.connect(self.change_mask_visibility)
        self.viewer_widget.view.scene.transform.changed.connect(
            self._view_changed, position="last")
        self.viewer.dims.events.current_step.connect(self._view_changed,
                                                     position="last")
        self.viewer.dims.events.ndisplay.connect(self._view_changed,
                                                 position="last")
        self.viewer.dims.events.ndisplay.connect(self._view_changed,
                                                 position="last")
        self.viewer.dims.events.ndisplay.connect(self.camera_change,
                                                 position="last")
        self.viewer.events.reset_view.connect(self._view_changed,
                                              position="last")

    def toggle_points_visibility(self):
        if self.points_layer is not None:
            self.points_layer.visible = not self.points_layer.visible

    def _dim_order_menu(self, point: QPoint):
        menu = QMenu()
        for key in ORDER_DICT:
            action = menu.addAction(key)
            action.triggered.connect(partial(self._set_new_order, key))
            if key == self._current_order:
                font = action.font()
                font.setBold(True)
                action.setFont(font)

        menu.exec_(self.roll_dim_button.mapToGlobal(point))

    def _set_new_order(self, text: str):
        self._current_order = text
        self.viewer.dims.order = ORDER_DICT[text]
        # self.update_roi_representation()

    def _reset_view(self):
        self._set_new_order("xy")
        self.viewer.dims.order = ORDER_DICT[self._current_order]
        self.viewer.reset_view()

    def _rotate_dim(self):
        self._set_new_order(NEXT_ORDER[self._current_order])

    def camera_change(self, _args):
        self.old_scene.transform.changed.disconnect(self._view_changed)
        self.old_scene: BaseCamera = self.viewer_widget.view.camera
        self.old_scene.transform.changed.connect(self._view_changed,
                                                 position="last")

    def _view_changed(self, _args):
        self.view_changed.emit()

    def get_state(self):
        return {
            "ndisplay": self.viewer.dims.ndisplay,
            "point": self.viewer.dims.point,
            "camera": self.viewer_widget.view.camera.get_state(),
        }

    def set_state(self, dkt):
        if "ndisplay" in dkt and self.viewer.dims.ndisplay != dkt["ndisplay"]:
            self.viewer.dims.ndisplay = dkt["ndisplay"]
            return
        if "point" in dkt:
            for i, val in enumerate(dkt["point"]):
                self.viewer.dims.set_point(i, val)
        if "camera" in dkt:
            with suppress(KeyError):
                self.viewer_widget.view.camera.set_state(dkt["camera"])

    def change_mask_visibility(self):
        for image_info in self.image_info.values():
            if image_info.mask is not None:
                image_info.mask.visible = self.mask_chk.isChecked()

    def update_spacing_info(self, image: Optional[Image] = None) -> None:
        """
        Update spacing of image if not provide, then use image pointed by settings.

        :param Optional[Image] image: image which spacing should be updated.
        :return: None
        """
        if image is None:
            image = self.settings.image

        if image.file_path not in self.image_info:
            raise ValueError("Image not registered")

        image_info = self.image_info[image.file_path]

        for layer in image_info.layers:
            layer.scale = image.normalized_scaling()

        if image_info.roi is not None:
            image_info.roi.scale = image.normalized_scaling()

        if image_info.mask is not None:
            image_info.mask.scale = image.normalized_scaling()

    def _active_layer(self):
        if hasattr(self.viewer.layers, "selection"):
            return self.viewer.layers.selection.active
        return self.viewer.active_layer

    def _coordinates(self):
        active_layer = self._active_layer()
        if active_layer is None:
            return
        if (hasattr(self.viewer, "cursor")
                and hasattr(self.viewer.cursor, "position")
                and hasattr(active_layer, "world_to_data")):
            return [
                int(x) for x in active_layer.world_to_data(
                    self.viewer.cursor.position)
            ]
        return [int(x) for x in active_layer.coordinates]

    def print_info(self, event=None):
        cords = self._coordinates()
        if cords is None:
            return
        bright_array = []
        components = []
        for image_info in self.image_info.values():
            if not image_info.coords_in(cords):
                continue
            moved_coords = image_info.translated_coords(cords)
            for layer in image_info.layers:
                if layer.visible:
                    bright_array.append(layer.data[tuple(moved_coords)])
            if image_info.roi_info.roi is not None and image_info.roi is not None:
                val = image_info.roi_info.roi[tuple(moved_coords)]
                if val:
                    components.append(val)

        if not bright_array and not components:
            self.text_info_change.emit("")
            return
        text = f"{cords}: "
        if bright_array:
            text += str(bright_array[0]) if len(bright_array) == 1 else str(
                bright_array)
        self.components = components
        if components:
            if len(components) == 1:
                text += f" component: {components[0]}"
            else:
                text += f" components: {components}"
        self.text_info_change.emit(text)

    def mask_opacity(self) -> float:
        """Get mask opacity"""
        return self.settings.get_from_profile("mask_presentation_opacity", 1)

    def mask_color(self) -> ColorInfo:
        """Get mask marking color"""
        color = Color(
            np.divide(
                self.settings.get_from_profile("mask_presentation_color",
                                               [255, 255, 255]), 255))
        return {0: (0, 0, 0, 0), 1: color.rgba}

    def get_image(self, image: Optional[Image]) -> Image:
        if image is not None:
            return image
        if self.current_image not in self.image_info:
            return self.settings.image
        return self.image_info[self.current_image].image

    def update_points(self):
        if self.settings.points is not None:
            self.points_view_button.setVisible(True)
            if self.points_layer is None or self.points_layer not in self.viewer.layers:
                self.points_layer = Points(
                    self.settings.points,
                    scale=self.settings.image.normalized_scaling())
                self.viewer.add_layer(self.points_layer)
            else:
                self.points_layer.data = self.settings.points
                self.points_layer.scale = self.settings.image.normalized_scaling(
                )
        elif self.points_layer is not None and self.points_layer in self.viewer.layers:
            self.points_view_button.setVisible(False)
            self.points_layer.data = np.empty((0, 4))

    def set_roi(self,
                roi_info: Optional[ROIInfo] = None,
                image: Optional[Image] = None) -> None:
        image = self.get_image(image)
        if roi_info is None:
            roi_info = self.settings.roi_info
        image_info = self.image_info[image.file_path]
        if image_info.roi is None and roi_info.roi is not None:
            image_info.roi_info = roi_info
            self.add_roi_layer(image_info)
            self.search_roi_btn.setDisabled(False)
        elif image_info.roi is None:
            return
        elif roi_info.roi is None:
            image_info.roi.visible = False
            self.search_roi_btn.setDisabled(True)
            return
        else:
            image_info.roi_info = roi_info
            image_info.roi.data = roi_info.alternative.get(
                self.roi_alternative_selection, roi_info.roi)
            image_info.roi.visible = True
            self.search_roi_btn.setDisabled(False)

        image_info.roi_count = max(
            roi_info.bound_info) if roi_info.bound_info else 0

        # image_info.roi.color = self.get_roi_view_parameters(image_info)
        self.set_roi_colormap(image_info)
        image_info.roi.opacity = self.settings.get_from_profile(
            f"{self.name}.image_state.opacity", 1.0)
        image_info.roi.refresh()

    def get_roi_view_parameters(self, image_info: ImageInfo) -> ColorInfo:
        colors = self.settings.label_colors / 255
        if (self.settings.get_from_profile(
                f"{self.name}.image_state.show_label",
                LabelEnum.Show_results) == LabelEnum.Not_show
                or image_info.roi_count == 0 or colors.size == 0):
            return {x: [0, 0, 0, 0] for x in range(image_info.roi_count + 1)}

        res = {
            x: colors[(x - 1) % colors.shape[0]]
            for x in range(1, image_info.roi_count + 1)
        }
        res[0] = [0, 0, 0, 0]
        return res

    def set_roi_colormap(self, image_info) -> None:
        if _napari_ge_4_13:
            image_info.roi.color = self.get_roi_view_parameters(image_info)
            return
        colors = self.settings.label_colors / 255
        if (self.settings.get_from_profile(
                f"{self.name}.image_state.show_label",
                LabelEnum.Show_results) == LabelEnum.Not_show
                or image_info.roi_count == 0 or colors.size == 0):
            image_info.roi.colormap = Colormap([[0, 0, 0, 0], [0, 0, 0, 0]])

        res = [
            list(colors[(x - 1) % colors.shape[0]]) + [1]
            for x in range(image_info.roi_count + 1)
        ]
        res[0] = [0, 0, 0, 0]
        if len(res) < 2:
            res += [[0, 0, 0, 0]] * (2 - len(res))

        image_info.roi.colormap = Colormap(
            colors=res, interpolation=ColormapInterpolationMode.ZERO)
        max_val = image_info.roi_count + 1
        image_info.roi._all_vals = np.array(  # pylint: disable=W0212
            [0] + [(x + 1) / (max_val + 1) for x in range(1, max_val)])

    def update_roi_coloring(self):
        for image_info in self.image_info.values():
            if image_info.roi is None:
                continue
            # image_info.roi.color = self.get_roi_view_parameters(image_info)
            self.set_roi_colormap(image_info)
            image_info.roi.opacity = self.settings.get_from_profile(
                f"{self.name}.image_state.opacity", 1.0)

    def remove_all_roi(self):
        for image_info in self.image_info.values():
            if image_info.roi is None:
                continue
            image_info.roi.visible = False

    def update_roi_border(self) -> None:
        for image_info in self.image_info.values():
            if image_info.roi is None:
                continue
            roi = image_info.roi_info.alternative.get(
                self.roi_alternative_selection, image_info.roi_info.roi)
            border_thick = self.settings.get_from_profile(
                f"{self.name}.image_state.border_thick", 1)
            only_border = self.settings.get_from_profile(
                f"{self.name}.image_state.only_border", True)
            alternative = image_info.roi.metadata.get(
                "alternative", self.roi_alternative_selection)
            if alternative != self.roi_alternative_selection:
                image_info.roi.data = roi
            image_info.roi.contour = border_thick if only_border else 0
            image_info.roi.metadata[
                "alternative"] = self.roi_alternative_selection

    @ensure_main_thread
    def update_rendering(self):
        rendering = self.settings.get_from_profile(RENDERING_MODE_NAME,
                                                   RENDERING_LIST[0])
        for image_info in self.image_info.values():
            if image_info.roi is not None and hasattr(image_info.roi,
                                                      "rendering"):
                image_info.roi.rendering = rendering

    @ensure_main_thread
    def update_roi_labeling(self):
        for image_info in self.image_info.values():
            if image_info.roi is not None:
                # image_info.roi.color = self.get_roi_view_parameters(image_info)
                self.set_roi_colormap(image_info)

    def add_roi_layer(self, image_info: ImageInfo):
        if image_info.roi_info.roi is None:
            return
        roi = image_info.roi_info.alternative.get(
            self.roi_alternative_selection, image_info.roi_info.roi)
        border_thick = self.settings.get_from_profile(
            f"{self.name}.image_state.border_thick", 1)
        kwargs = {
            "scale": image_info.image.normalized_scaling(),
            "name": "ROI",
            "blending": "translucent",
            "metadata": {
                "alternative": self.roi_alternative_selection
            },
        }
        if napari_rendering:
            kwargs["rendering"] = self.settings.get_from_profile(
                RENDERING_MODE_NAME, RENDERING_LIST[0])

        only_border = self.settings.get_from_profile(
            f"{self.name}.image_state.only_border", True)
        image_info.roi = self.viewer.add_labels(roi, **kwargs)
        image_info.roi.contour = border_thick if only_border else 0

    def set_mask(self,
                 mask: Optional[np.ndarray] = None,
                 image: Optional[Image] = None) -> None:
        image = self.get_image(image)
        if image.file_path not in self.image_info:
            raise ValueError("Image not added to viewer")
        if mask is None:
            mask = image.mask

        image_info = self.image_info[image.file_path]
        if mask is None:
            if image_info.mask is not None:
                image_info.mask.visible = False
                image_info.mask.metadata["valid"] = False
                self._toggle_mask_chk_visibility()
            return
        mask_marker = mask == 0
        if image_info.mask is None:
            image_info.mask = self.viewer.add_labels(
                mask_marker,
                scale=image.normalized_scaling(),
                blending="translucent",
                name="Mask")
        else:
            image_info.mask.data = mask_marker
        image_info.mask.metadata["valid"] = True
        image_info.mask.color = self.mask_color()
        image_info.mask.opacity = self.mask_opacity()
        image_info.mask.visible = self.mask_chk.isChecked()
        self._toggle_mask_chk_visibility()

    def _toggle_mask_chk_visibility(self):
        visibility = any(image_info.mask is not None
                         and image_info.mask.metadata.get("valid", False)
                         for image_info in self.image_info.values())
        self.mask_chk.setVisible(visibility)
        self.mask_label.setVisible(visibility)

    def update_mask_parameters(self):
        opacity = self.mask_opacity()
        colormap = self.mask_color()
        for image_info in self.image_info.values():
            if image_info.mask is not None:
                image_info.mask.opacity = opacity
                image_info.mask.color = colormap

    def set_image(self, image: Optional[Image] = None):
        self.image_info = {}
        self.add_image(image, True)

    def has_image(self, image: Image):
        return image.file_path in self.image_info

    @staticmethod
    def calculate_filter(
            array: np.ndarray,
            parameters: Tuple[NoiseFilterType, float]) -> Optional[np.ndarray]:
        if parameters[0] == NoiseFilterType.No or parameters[1] == 0:
            return array
        if parameters[0] == NoiseFilterType.Gauss:
            return gaussian(array, parameters[1])
        if parameters[0] == NoiseFilterType.Bilateral:
            return bilateral(array, parameters[1])
        return median(array, int(parameters[1]))

    def _remove_worker(self, sender):
        for worker in self.worker_list:
            if sender is worker.signals:
                self.worker_list.remove(worker)
                break
        else:
            print("[_remove_worker]", sender)

    def _add_layer_util(self, index, layer, filters):
        self.viewer.add_layer(layer)

        def set_data(val):
            self._remove_worker(self.sender())
            data_, layer_ = val
            if data_ is None:
                return
            if layer_ not in self.viewer.layers:
                return
            layer_.data = data_

        @thread_worker(connect={"returned": set_data})
        def calc_filter(j, layer_):
            if filters[j][0] == NoiseFilterType.No or filters[j][1] == 0:
                return None, layer_
            return self.calculate_filter(layer_.data,
                                         parameters=filters[j]), layer_

        worker = calc_filter(index, layer)
        self.worker_list.append(worker)

    def _add_image(self, image_data: Tuple[ImageInfo, bool]):
        self._remove_worker(self.sender())

        image_info, replace = image_data
        image = image_info.image
        if replace:
            self.viewer.layers.select_all()
            self.viewer.layers.remove_selected()
            QApplication.instance().processEvents()

        filters = self.channel_control.get_filter()
        for i, layer in enumerate(image_info.layers):
            try:
                self._add_layer_util(i, layer, filters)
            except AssertionError:
                layer.colormap = "gray"
                self._add_layer_util(i, layer, filters)

        self.image_info[image.file_path].filter_info = filters
        self.image_info[image.file_path].layers = image_info.layers
        self.current_image = image.file_path
        self.viewer.reset_view()
        if self.viewer.layers:
            if hasattr(self.viewer.layers, "selection"):
                self.viewer.layers.selection.clear()
                self.viewer.layers.selection.add(self.viewer.layers[-1])
            else:
                self.viewer.layers[-1].selected = True

        for i, axis in enumerate(image.axis_order):
            if axis == "C":
                continue
            self.viewer.dims.set_point(
                i, image.shape[i] * image.normalized_scaling()[i] // 2)
        if self.image_info[image.file_path].roi is not None:
            self.set_roi()
        if image_info.image.mask is not None:
            self.set_mask()
        self._toggle_mask_chk_visibility()
        self.image_added.emit()

    def add_image(self, image: Optional[Image], replace=False):
        if image is None:
            image = self.settings.image

        if not image.channels:
            raise ValueError("Need non empty image")

        if image.file_path in self.image_info:
            raise ValueError("Image already added")

        self.image_info[image.file_path] = ImageInfo(image, [])

        channels = image.channels
        if self.image_info and not replace:
            channels = max(
                channels,
                *(x.image.channels for x in self.image_info.values()))

        self.channel_control.set_channels(channels)
        visibility = self.channel_control.channel_visibility
        limits = self.channel_control.get_limits()
        ranges = image.get_ranges()
        limits = [
            ranges[i] if x is None else x
            for i, x in zip(range(image.channels), limits)
        ]
        gamma = self.channel_control.get_gamma()
        colormaps = [
            self.channel_control.selected_colormaps[i]
            for i in range(image.channels)
        ]
        parameters = ImageParameters(limits, visibility, gamma, colormaps,
                                     image.normalized_scaling(),
                                     len(self.viewer.layers))

        self._prepare_layers(image, parameters, replace)

        return image

    def _prepare_layers(self, image, parameters, replace):
        worker = prepare_layers(image, parameters, replace)
        worker.returned.connect(self._add_image)
        self.worker_list.append(worker)
        worker.start()

    def images_bounds(self) -> Tuple[List[int], List[int]]:
        ranges = []
        for image_info in self.image_info.values():
            if not image_info.layers:
                continue
            ranges = [(min(a, b), max(c, d), min(e, f)) for (a, c, e), (
                b, d,
                f) in itertools.zip_longest(image_info.layers[0].dims.range,
                                            ranges,
                                            fillvalue=(np.inf, -np.inf,
                                                       np.inf))]

        visible = [ranges[i] for i in self.viewer.dims.displayed]
        min_shape, max_shape, _ = zip(*visible)
        size = np.subtract(max_shape, min_shape)
        return size, min_shape

    @staticmethod
    def _shift_layer(layer: Layer, translate_2d):
        translate = [0] * layer.ndim
        translate[-2:] = translate_2d
        layer.translate = translate

    def grid_view(self):
        """Present multiple images in grid view"""
        n_row = np.ceil(np.sqrt(len(self.image_info))).astype(int)
        n_row = max(1, n_row)
        scene_size, _ = self.images_bounds()
        for image_info, pos in zip(self.image_info.values(),
                                   itertools.product(range(n_row), repeat=2)):
            translate_2d = np.multiply(scene_size[-2:], pos)
            for layer in image_info.layers:
                self._shift_layer(layer, translate_2d)

            if image_info.mask is not None:
                self._shift_layer(image_info.mask, translate_2d)

            if image_info.roi is not None:
                self._shift_layer(image_info.roi, translate_2d)
        self.viewer.reset_view()

    def change_visibility(self, name: str, index: int):
        for image_info in self.image_info.values():
            if len(image_info.layers) > index:
                image_info.layers[
                    index].visible = self.channel_control.channel_visibility[
                        index]
                if self.channel_control.channel_visibility[index]:
                    image_info.layers[
                        index].colormap = self.channel_control.selected_colormaps[
                            index]
                    limits = self.channel_control.get_limits()[index]
                    limits = image_info.image.get_ranges(
                    )[index] if limits is None else limits
                    image_info.layers[index].contrast_limits = limits
                    image_info.layers[
                        index].gamma = self.channel_control.get_gamma()[index]
                    filter_type = self.channel_control.get_filter()[index]
                    if filter_type != image_info.filter_info[index]:
                        image_info.layers[index].data = self.calculate_filter(
                            image_info.image.get_channel(index), filter_type)
                        image_info.filter_info[index] = filter_type

    def reset_image_size(self):
        self.viewer.reset_view()

    def set_theme(self, theme: str):
        self.viewer.theme = theme

    def closeEvent(self, event):
        for worker in self.worker_list:
            worker.quit()
        self.viewer.layers.clear()
        self.viewer_widget.close()
        super().closeEvent(event)

    def get_tool_tip_text(self) -> str:
        image = self.settings.image
        image_info = self.image_info[image.file_path]
        text_list = []
        for el in self.components:
            data = image_info.roi_info.annotations.get(el, {})
            if data:
                try:
                    text_list.append(_print_dict(data))
                except ValueError:  # pragma: no cover
                    logging.warning(
                        "Wrong value provided as layer annotation.")
        return " ".join(text_list)

    def event(self, event: QEvent):
        if event.type() == QEvent.ToolTip and self.components:
            text = self.get_tool_tip_text()
            if text:
                QToolTip.showText(event.globalPos(), text, self)
        return super().event(event)

    def _search_component(self):
        max_components = max(
            max(image_info.roi_info.bound_info)
            for image_info in self.image_info.values())
        if self.viewer.dims.ndisplay == 3:
            self._search_type = SearchType.Highlight

        dial = SearchComponentModal(self, self._search_type,
                                    self._last_component, max_components)
        if self.viewer.dims.ndisplay == 3:
            dial.zoom_to.setDisabled(True)
        dial.show_right_of_mouse()

    def component_unmark(self, _num):
        self.viewer.layers.selection.clear()
        for el in self.image_info.values():
            if el.highlight is None:
                continue
            if "timer" in el.highlight.metadata:
                el.highlight.metadata["timer"].stop()
            el.highlight.visible = False

    def _mark_layer(self, num: int, flash: bool, image_info: ImageInfo):
        bound_info = image_info.roi_info.bound_info.get(num, None)
        if bound_info is None:
            return
        # TODO think about marking on bright background
        slices = bound_info.get_slices(1)
        slices[image_info.image.stack_pos] = slice(None)
        component_mark = image_info.roi_info.roi[tuple(slices)] == num
        if self.viewer.dims.ndisplay == 3:
            component_mark = binary_dilation(component_mark)
        shift_base = bound_info.lower - 1
        shift_base[0] += 1  # remove shift on time axis
        translate = image_info.roi.translate + shift_base * image_info.roi.scale
        translate[image_info.image.stack_pos] = 0
        if image_info.highlight is None:
            image_info.highlight = self.viewer.add_labels(
                component_mark,
                scale=image_info.roi.scale,
                blending="translucent",
                color={
                    0: (0, 0, 0, 0),
                    1: "white"
                },
                opacity=0.7,
            )
        else:
            image_info.highlight.data = component_mark
        image_info.highlight.translate = translate
        image_info.highlight.visible = True
        if flash:
            layer = image_info.highlight

            def flash_fun(layer_=layer):
                opacity = layer_.opacity + 0.1
                if opacity > 1:
                    opacity = 0.1
                layer_.opacity = opacity

            timer = QTimer()
            timer.setInterval(100)
            timer.timeout.connect(flash_fun)
            timer.start()
            layer.metadata["timer"] = timer

    def component_mark(self, num: int, flash: bool = False):
        self.component_unmark(num)
        self._search_type = SearchType.Highlight
        self._last_component = num

        bounding_box = self._bounding_box(num)
        if bounding_box is None:
            return

        for image_info in self.image_info.values():
            self._mark_layer(num, flash, image_info)

        lower_bound, upper_bound = bounding_box
        self._update_point(lower_bound, upper_bound)

        if self.viewer.dims.ndisplay == 2:
            l_bound = lower_bound[-2:][::-1]
            u_bound = upper_bound[-2:][::-1]
            rect = Rect(self.viewer_widget.view.camera.get_state()["rect"])
            if rect.contains(*l_bound) and rect.contains(*u_bound):
                return
            size = u_bound - l_bound
            rect.size = tuple(np.max([rect.size, size * 1.2], axis=0))
            pos = rect.pos
            if rect.left > l_bound[0]:
                pos = l_bound[0], pos[1]
            if rect.right < u_bound[0]:
                pos = pos[0] + u_bound[0] - rect.right, pos[1]
            if rect.bottom > l_bound[1]:
                pos = pos[0], l_bound[1]
            if rect.top < u_bound[1]:
                pos = pos[0], pos[1] + (u_bound[1] - rect.top)
            rect.pos = pos
            self.viewer_widget.view.camera.set_state({"rect": rect})

    def component_zoom(self, num):
        self.component_unmark(num)
        self._search_type = SearchType.Zoom_in
        self._last_component = num

        bounding_box = self._bounding_box(num)
        if bounding_box is None:
            return

        lower_bound, upper_bound = bounding_box
        diff = upper_bound - lower_bound
        frame = diff * 0.2
        if self.viewer.dims.ndisplay == 2:
            rect = Rect(pos=(lower_bound - frame)[-2:][::-1],
                        size=(diff + 2 * frame)[-2:][::-1])
            self.set_state({"camera": {"rect": rect}})
        self._update_point(lower_bound, upper_bound)

    def _update_point(self, lower_bound, upper_bound):
        point = (lower_bound + upper_bound) / 2
        current_point = self.viewer.dims.point
        for i in range(self.viewer.dims.ndim - self.viewer.dims.ndisplay):
            if not (lower_bound[i] <= current_point[i] <= upper_bound[i]):
                self.viewer.dims.set_point(i, point[i])

    @staticmethod
    def _data_to_world(layer: Layer, cords):
        return layer._transforms[1:3].simplified(cords)  # pylint: disable=W0212

    def _bounding_box(self, num) -> Optional[Tuple[np.ndarray, np.ndarray]]:
        lower_bound_list = []
        upper_bound_list = []
        for image_info in self.image_info.values():
            bound_info = image_info.roi_info.bound_info.get(num, None)
            if bound_info is None:
                continue
            lower_bound_list.append(
                self._data_to_world(image_info.roi, bound_info.lower))
            upper_bound_list.append(
                self._data_to_world(image_info.roi, bound_info.upper))

        if not lower_bound_list:
            return

        lower_bound = np.min(lower_bound_list, axis=0)
        upper_bound = np.min(upper_bound_list, axis=0)
        return lower_bound, upper_bound