import os
from collections import OrderedDict

import holoviews as hv
import hvplot.pandas  # noqa
import hvplot.xarray  # noqa
import numpy as np
import panel as pn
import spatialpandas as spd
import tifffile as tiff
import xarray as xr
from bokeh.models import HoverTool
from natsort import natsorted

from stereo.stereo_config import stereo_conf
from stereo.core.stereo_exp_data import StereoExpData
from stereo.preprocess.qc import cal_total_counts, cal_n_genes_by_counts, cal_pct_counts_mt


class PlotCells:
    def __init__(
            self,
            data,
            color_by='total_counts',
            color_key=None,
            # cluster_res_key='cluster',
            bgcolor='#2F2F4F',
            palette=None,
            width=None,
            height=None,
            fg_alpha=0.5,
            base_image=None,
            base_im_to_gray=False,
            use_raw=True
    ):
        self.data: StereoExpData = data
        if color_by != 'cluster':
            self.palette = 'stereo' if palette is None else palette
            assert isinstance(self.palette, str), f'The palette must be a name of palette when color_by is {color_by}'
            self.cluster_res = None
            self.cluster_id = []
            # if color_by == 'gene':
            #     if not np.any(np.isin(self.data.genes.gene_name, color_key)):
            #         raise ValueError(f'The gene {color_key} is not found.')
        else:
            if palette is None:
                self.palette = 'stereo_30'
            elif isinstance(palette, str):
                self.palette = palette
            else:
                self.palette = 'custom'
            if color_key in self.data.tl.result:
                res = self.data.tl.result[color_key]
                self.cluster_res = np.array(res['group'])
                self.cluster_id = natsorted(np.unique(self.cluster_res).tolist())
                n = len(self.cluster_id)
                if isinstance(palette, (list, np.ndarray)):
                    stereo_conf.palette_custom = list(palette)
                elif isinstance(palette, dict):
                    stereo_conf.palette_custom = [palette[k] for k in self.cluster_id if k in palette]
                cmap = stereo_conf.get_colors(self.palette, n)
                self.cluster_color_map = OrderedDict({k: v for k, v in zip(self.cluster_id, cmap)})
            else:
                self.cluster_res = None
                self.cluster_id = []

        self.last_cm_key_continuous = None
        self.last_cm_key_discrete = None
        self.color_by_input = color_by
        self.color_key = color_key
        self.bgcolor = bgcolor
        self.width, self.height = self._set_width_and_height(width, height)
        self.fg_alpha = fg_alpha
        self.base_image = base_image
        self.base_image_points = None

        if self.fg_alpha < 0:
            self.fg_alpha = 0.3
        elif self.fg_alpha > 1:
            self.fg_alpha = 1

        if self.base_image is None:
            self.fg_alpha = 1

        self.hover_fg_alpha = self.fg_alpha / 2
        self.figure_polygons = None
        self.figure_points = None
        self.figure_colorbar_legend = None
        self.colorbar_or_legend = None
        self.base_im_to_gray = base_im_to_gray
        self.use_raw = use_raw and self.data.raw is not None
        self.rangexy_stream = None
        self.x_range = None
        self.y_range = None
        self.firefox_path = None
        self.driver_path = None

    def _set_width_and_height(self, width, height):
        if width is None or height is None:
            if width is None and height is not None:
                width = height
            elif width is not None and height is None:
                height = width
            else:
                width, height = 500, 500
            return width, height
        return width, height

    def _get_base_image_boundary(self, image_data: np.ndarray):
        min_x, max_x, min_y, max_y = -1, -1, -1, -1
        if len(image_data.shape) == 3:
            image_data = image_data.sum(axis=2)
        col_sum = image_data.sum(axis=0)
        nonzero_idx = np.nonzero(col_sum)[0]
        if nonzero_idx.size > 0:
            min_x, max_x = np.min(nonzero_idx), np.max(nonzero_idx)

        row_sum = image_data.sum(axis=1)
        nonzero_idx = np.nonzero(row_sum)[0]
        if nonzero_idx.size > 0:
            min_y, max_y = np.min(nonzero_idx), np.max(nonzero_idx)

        return min_x, max_x, min_y, max_y

    def _create_base_image_xarray(self):
        assert os.path.exists(self.base_image), f'{self.base_image} is not exists!'

        image_xarray = None
        with tiff.TiffFile(self.base_image) as tif:
            image_data = tif.asarray()
            if len(image_data.shape) == 3 and self.base_im_to_gray:
                from cv2 import cvtColor, COLOR_BGR2GRAY
                image_data = cvtColor(image_data[:, :, [2, 1, 0]], COLOR_BGR2GRAY)
            if len(image_data.shape) == 3 and image_data.dtype == np.uint16:
                from matplotlib.colors import Normalize
                if tif.shaped_metadata is not None:
                    metadata = tif.shaped_metadata[0]
                    if 'value_range' in metadata:
                        vmin, vmax = metadata['value_range']
                    else:
                        vmin, vmax = np.min(image_data), np.max(image_data)
                else:
                    vmin, vmax = np.min(image_data), np.max(image_data)
                image_data = Normalize(vmin, vmax)(image_data).data
            min_x, max_x, min_y, max_y = self._get_base_image_boundary(image_data)
            if min_x == -1 or max_x == -1 or min_y == -1 or max_y == -1:
                raise Exception("the base image is empty.")
            max_x += 1
            max_y += 1
            min_x = max(0, min_x - 100)
            max_x = min(max_x + 100, image_data.shape[1])
            min_y = max(0, min_y - 100)
            max_y = min(max_y + 100, image_data.shape[0])
            image_data = image_data[min_y:max_y, min_x:max_x]
            if len(image_data.shape) == 2:
                image_xarray = xr.DataArray(data=image_data, coords=[range(min_y, max_y), range(min_x, max_x)], dims=['y', 'x'])
            elif len(image_data.shape) == 3:
                bg_pixel = np.array([0, 0, 0], dtype=image_data.dtype)
                if image_data.dtype == np.uint8:
                    bg_value = 255
                else:
                    bg_value = 1.0
                bg_mask = np.where(image_data == bg_pixel, bg_value, 0)
                bg_mask = bg_mask.astype(image_data.dtype)
                image_data += bg_mask
                image_xarray = xr.DataArray(data=image_data, coords=[range(min_y, max_y), range(min_x, max_x), range(0, image_data.shape[2])], dims=['y', 'x', 'channel'])
        return image_xarray

    def _create_polygons(self, color_by):
        polygons = []
        color = []
        position = []
        exp_matrix = self.data.get_exp_matrix(use_raw=self.use_raw, cell_list=self.data.cell_names, gene_list=self.data.gene_names)
        gene_names = self.data.gene_names if self.data.genes.real_gene_name is None else self.data.genes.real_gene_name
        total_counts = cal_total_counts(exp_matrix)
        n_genes_by_counts = cal_n_genes_by_counts(exp_matrix)
        pct_counts_mt = cal_pct_counts_mt(exp_matrix, gene_names)
        counts_in_selected_gene = []
        if color_by == 'gene':
            a = self.color_key.find('(')
            if a != -1:
                color_key = self.color_key[:a]
            else:
                color_key = self.color_key
            in_bool = np.isin(self.data.genes.gene_name, color_key)
            if not np.any(in_bool) and self.data.genes.real_gene_name is not None:
                in_bool = np.isin(self.data.genes.real_gene_name, color_key)
        for i, cell_border in enumerate(self.data.cells.cell_border):
            cell_border = cell_border[cell_border[:, 0] < 32767] + self.data.position[i]
            cell_border = cell_border.reshape((-1,)).tolist()
            polygons.append([cell_border])
            if color_by == 'total_counts':
                color.append(total_counts[i])
            elif color_by == 'n_genes_by_counts':
                color.append(n_genes_by_counts[i])
            elif color_by == 'gene':
                color.append(exp_matrix[i, in_bool].sum())
                counts_in_selected_gene.append(color[-1])
            elif color_by == 'cluster':
                color.append(self.cluster_res[i] if self.cluster_res is not None else total_counts[i])
            else:
                color.append(total_counts[i])
            position.append(str(tuple(self.data.position[i].astype(np.uint32))))

        polygons = spd.geometry.PolygonArray(polygons)
        polygons_detail = spd.GeoDataFrame({
            'polygons': polygons,
            'color': color,
            'position': position,
            'total_counts': total_counts,
            'pct_counts_mt': pct_counts_mt,
            'n_genes_by_counts': n_genes_by_counts,
            'cluster_id': np.zeros_like(self.data.cell_names) if self.cluster_res is None else self.cluster_res
        })
        if len(counts_in_selected_gene) > 0:
            polygons_detail['counts_in_selected_gene'] = counts_in_selected_gene

        tooltips = [
            ('Position', '@position'),
            ('Total Counts', '@total_counts'),
            ('Pct Counts Mt', '@pct_counts_mt'),
            ('nGenes By Counts', '@n_genes_by_counts'),
            ('Cluster Id', '@cluster_id')]
        if len(counts_in_selected_gene) > 0:
            tooltips.append(('Counts In Selected Gene', '@counts_in_selected_gene'))
        hover_tool = HoverTool(tooltips=tooltips)

        vdims = polygons_detail.columns.tolist()
        vdims.remove('polygons')

        return polygons_detail, hover_tool, vdims

    def _create_widgets(self):
        # self.color_map_key_continuous = pn.widgets.Select(
        #     value='stereo', options=list(stereo_conf.linear_colormaps.keys()), name='color theme', width=200
        # )
        # self.color_map_key_discrete = pn.widgets.Select(
        #     value='stereo_30', options=list(stereo_conf.colormaps.keys()), name='color theme', width=200
        # )
        self.color_map_key_continuous = pn.widgets.Select(
            value=self.palette, options=sorted(list(stereo_conf.linear_colormaps.keys())), name='color theme', width=200
        )
        self.color_map_key_discrete = pn.widgets.Select(
            value=self.palette, options=sorted(list(set(stereo_conf.colormaps.keys()) | {self.palette})), name='color theme', width=200
        )
        if self.cluster_res is None:
            self.color_map_key_discrete.visible = False
        else:
            self.color_map_key_continuous.visible = False
        self.reverse_colormap = pn.widgets.Checkbox(
            name='reverse colormap', value=False, disabled=False if self.cluster_res is None else True
        )
        self.size_input = pn.widgets.IntInput(name='size', value=500, start=300, end=1000, step=10, width=200)
        color_by_key = []
        if self.cluster_res is not None:
            default_cluster_id = self.cluster_id[0]
            default_cluster_color = self.cluster_color_map[default_cluster_id]
            self.cluster = pn.widgets.Select(
                value=default_cluster_id, options=self.cluster_id, name='cluster', width=100
            )
            self.cluster_colorpicker = pn.widgets.ColorPicker(
                name='cluster color', value=default_cluster_color, width=70
            )
            color_by_key.append('cluster')
        else:
            self.cluster = pn.widgets.Select(name='cluster', width=100, disabled=True, visible=False)
            self.cluster_colorpicker = pn.widgets.ColorPicker(
                name='cluster color', width=70, disabled=True, visible=False
            )
        color_by_key.extend(['total_counts', 'n_genes_by_counts', 'gene'])
        self.color_by = pn.widgets.Select(name='color by', options=color_by_key, value=self.color_by_input, width=200)
        if self.color_by_input == 'gene':
            gene_names_selector_value = self.color_key
        else:
            i = np.argmax(self.data.genes.n_counts)
            gene_names_selector_value = self.data.genes.gene_name[i]
        gene_idx_sorted = np.argsort(self.data.genes.n_counts * -1)
        gene_names_sorted = self.data.genes.gene_name[gene_idx_sorted].tolist()
        if self.data.genes.real_gene_name is not None:
            real_gene_name_sorted = self.data.genes.real_gene_name[gene_idx_sorted].tolist()
            gene_names_sorted = [f'{gene}({real_gene})' for gene, real_gene in zip(gene_names_sorted, real_gene_name_sorted)]
        self.gene_names = pn.widgets.Select(name='genes', value=gene_names_selector_value, 
                                            options=gene_names_sorted, size=10, width=200)
        
        self.save_title = pn.widgets.StaticText(name='', value='<b>Save Plot</b>', width=200)
        self.save_file_name = pn.widgets.TextInput(name='file name(.png, .svg or .pdf)', width=200)
        self.save_file_width = pn.widgets.IntInput(name='width', value=self.width, width=95)
        self.save_file_hight = pn.widgets.IntInput(name='height', value=self.height, width=95)
        self.save_button = pn.widgets.Button(name='save', button_type="primary", width=100)
        self.save_only_in_view = pn.widgets.Checkbox(name='only in view', value=False, width=100)
        self.with_base_image = pn.widgets.Checkbox(name='with base image', value=False, width=100)
        self.save_button.on_click(self._save_button_callback)
        self.save_message = pn.widgets.StaticText(name='', value='', width=400)
    
    def _set_firefox_and_driver_path(self):
        from sys import executable
        from os import environ
        import platform

        os_type = platform.system().lower()
        executable_dir = os.path.dirname(executable)
        environ_path = environ['PATH'].split(os.pathsep)
        if executable_dir not in environ_path:
            environ_path = [executable_dir] + environ_path
        if os_type == 'windows':
            bin_paths = [
                executable_dir,
                os.path.join(executable_dir, 'Scripts'),
                os.path.join(executable_dir, 'Library', 'bin'),
                os.path.join(executable_dir, 'Library', 'mingw-w64', 'bin'),
                os.path.join(executable_dir, 'Library', 'usr', 'bin'),
                os.path.join(executable_dir, 'bin')
            ]
            for bin_path in bin_paths:
                if bin_path not in environ_path:
                    environ_path = [bin_path] + environ_path
                a_path = os.path.join(bin_path, 'firefox.exe')
                if os.path.exists(a_path) and self.firefox_path is None:
                    self.firefox_path = a_path
                a_path = os.path.join(bin_path, 'geckodriver.exe')
                if os.path.exists(a_path) and self.driver_path is None:
                    self.driver_path = a_path
        elif os_type == 'linux':
            if self.firefox_path is None:
                self.firefox_path = os.path.join(executable_dir, 'firefox')
            if self.driver_path is None:
                self.driver_path = os.path.join(executable_dir, 'geckodriver')
        else:
            raise ValueError(f'The operating system {os_type} is not supported.')
        environ['PATH'] = os.pathsep.join(environ_path)

    def save_plot(
        self,
        save_file_name: str,
        save_width: int = None,
        save_height: int = None,
        save_only_in_view: bool = False,
        with_base_image: bool = False
    ):
        """
        Save the plot to a PNG, SVG or PDF file depending on the extension of the file name.

        :param save_file_name: the name of the file to save the plot.
        :param save_width: the width of the saved plot, defaults to be the same as the plot.
        :param save_height: the height of the saved plot, defaults to be the same as the plot.
        :param save_only_in_view: only save the plot in the view, defaults to False.
        :param with_base_image: whether to save the plot with the base image, defaults to False.
                                Currently, the dpi of the saved base image may not be high.

        """
        self._set_firefox_and_driver_path()

        from selenium import webdriver
        from selenium.webdriver import FirefoxOptions, FirefoxService
        from bokeh.io import export_png, export_svg
        from bokeh.io.export import get_svg
        from bokeh.layouts import row, Row
        from cairosvg import svg2pdf

        if save_file_name == '':
            raise ValueError('Please input the file name.')
        
        if not save_file_name.lower().endswith('.png') and \
            not save_file_name.lower().endswith('.svg') and \
            not save_file_name.lower().endswith('.pdf'):
            raise ValueError('Only PNG, SVG and PDF files are supported.')

        save_width = self.width if save_width is None else save_width
        save_height = self.height if save_height is None else save_height
        if with_base_image:
            figure_points = self._create_base_image_figure()
        else:
            figure_points = None
        with_base_image = with_base_image and figure_points is not None

        file_name_prefix, file_name_suffix = os.path.splitext(save_file_name)
        save_file_name = f"{file_name_prefix}_cells_plotting{file_name_suffix}"

        current_x_range = self.x_range
        current_y_range = self.y_range
        try:
            if save_only_in_view:
                if self.x_range is not None and self.y_range is not None:
                    figure_polygons = self.figure_polygons[self.x_range[0]:self.x_range[1], self.y_range[0]:self.y_range[1]]
                    if with_base_image:
                        figure_points = figure_points.select(x=self.x_range, y=self.y_range)
                else:
                    figure_polygons = self.figure_polygons
            else:
                figure_polygons = self.figure_polygons

            if not with_base_image:
                output_render = hv.render(figure_polygons, backend='bokeh')
                for renderer in output_render.renderers:
                    renderer.glyph.fill_alpha = 1
            else:
                output_render = hv.render(figure_points, backend='bokeh')
                polygons_render = hv.render(figure_polygons, backend='bokeh')
                output_render.renderers.extend(polygons_render.renderers)
            output_render.output_backend = 'svg'
            output_render.toolbar_location = None
            output_render.border_fill_color = None
            output_render.outline_line_color = 'gray'
            output_render.xaxis.visible = False
            output_render.yaxis.visible = False
            output_render.width = save_width
            output_render.height = save_height
            
            if save_height == self.height:
                figure_colorbar_legend = self.figure_colorbar_legend
            else:
                figure_colorbar_legend = self._create_colorbar_or_legend(
                    self.colorbar_or_legend,
                    self.figure_polygons.opts['cmap'],
                    self.figure_polygons.data,
                    save_height
                )

            if isinstance(figure_colorbar_legend, Row):
                for f in figure_colorbar_legend.children:
                    f.output_backend = 'svg'
                    f.height = save_height if save_height > 500 else 500
            else:
                figure_colorbar_legend.output_backend = 'svg'
                figure_colorbar_legend.height = save_height if save_height > 500 else 500

            to_save_instance = row(output_render, figure_colorbar_legend)

            opts = FirefoxOptions()
            opts.add_argument("--headless")
            opts.binary_location = self.firefox_path
            service = FirefoxService(executable_path=self.driver_path)
            with webdriver.Firefox(options=opts, service=service) as driver:
                if save_file_name.lower().endswith('png'):
                    export_png(to_save_instance, filename=save_file_name, webdriver=driver, timeout=86400)
                elif save_file_name.lower().endswith('svg'):
                    export_svg(to_save_instance, filename=save_file_name, webdriver=driver, timeout=86400)
                elif save_file_name.lower().endswith('pdf'):
                    svg = get_svg(to_save_instance, driver=driver, timeout=86400)[0]
                    svg2pdf(bytestring=svg, write_to=save_file_name)
        except Exception as e:
            raise e
        finally:
            self.rangexy_stream.event(x_range=current_x_range, y_range=current_y_range)
            if figure_colorbar_legend is self.figure_colorbar_legend:
                if isinstance(figure_colorbar_legend, Row):
                    for f in figure_colorbar_legend.children:
                        f.height = self.height if self.height > 500 else 500
                else:
                    figure_colorbar_legend.height = self.height if self.height > 500 else 500
        return save_file_name
            

    def _save_button_callback(self, _):
        """
        apt-get install libgtk-3-dev libasound2-dev
        conda install -c conda-forge selenium firefox geckodriver cairosvg
        """
        self.save_button.loading = True
        self.save_message.value = ''
        try:
            save_file_name = self.save_plot(
                self.save_file_name.value,
                self.save_file_width.value,
                self.save_file_hight.value,
                save_only_in_view=self.save_only_in_view.value, 
                with_base_image=self.with_base_image.value,
            )
            self.save_message.value = f'<font color="red"><b>The plot has been saved to {save_file_name}.</b></font>'
        except ValueError as e:
            self.save_message.value = f'<font color="red"><b>{str(e)}</b></font>'
        except Exception as e:
            raise e
        finally:
            self.save_button.loading = False


    def _create_colorbar_or_legend(self, type, cmap, plot_data=None, figure_height=None):

        from bokeh.plotting import figure as bokeh_figure
        from bokeh.models import (
            Legend, LegendItem, ColorBar, 
            EqHistColorMapper, BinnedTicker,
            ColumnDataSource
        )
        from bokeh.layouts import row

        figure_height = self.height if figure_height is None else figure_height
        figure_height = 500 if figure_height < 500 else figure_height
        if type == 'colorbar':
            figures = 1
        else:
            legend_items_in_col = int(figure_height / 25)
            legend_counts = len(cmap.keys())
            legend_cols, legend_left = divmod(legend_counts, legend_items_in_col)
            if legend_left > 0:
                legend_cols += 1
            figures = legend_cols

        figure_list = []
        for i in range(figures):
            f = bokeh_figure(width=80, height=figure_height, toolbar_location=None, x_axis_type=None, y_axis_type=None)
            f.outline_line_color = None
            f.xgrid.grid_line_color = None
            f.ygrid.grid_line_color = None
            figure_list.append(f)

        if type == 'colorbar':
            min_value = min(plot_data['color'])
            max_value = max(plot_data['color'])
            # ticks_num = 100
            # ticks_interval = (max_value - min_value) / (ticks_num - 1)
            # ticks = [min(min_value + i * ticks_interval, max_value) for i in range(ticks_num)]
            color_mapper = EqHistColorMapper(palette=cmap, low=min_value, high=max_value)

            data_source = ColumnDataSource(data={
                'x': [0] * len(plot_data['color']),
                'y': [0] * len(plot_data['color']),
                'color': plot_data['color']
            })
            figure_list[0].circle(x='x', y='y', color={'field': 'color', 'transform': color_mapper}, size=0, source=data_source)
            ticker = BinnedTicker(mapper=color_mapper, num_major_ticks=8)
            color_bar = ColorBar(color_mapper=color_mapper, location='center_left' if self.height >= 500 else 'top_left',
                                    orientation='vertical', height=int(figure_height // 1.5), width=int(figure_height / 500 * 20),
                                    major_tick_line_color='black', major_label_text_font_size=f'{int(figure_height / 500 * 11)}px',
                                    major_tick_in=int(figure_height / 500 * 5), major_tick_line_width=int(figure_height / 500 * 1),
                                    ticker=ticker)
            figure_list[0].width = int(figure_height / 500 * 150)
            figure_list[0].add_layout(color_bar, 'center')
            fig = figure_list[0]
        elif type == 'legend':
            legend_labels = list(cmap.keys())
            labels_len = [len(label) for label in legend_labels]
            max_label_len = max(labels_len)
            figure_width = 13 * max_label_len
            if figure_width < 80:
                figure_width = 80
            figure_width = int(figure_height / 500 * figure_width)
            for i in range(legend_cols):
                # legend = Legend(location='top_left', orientation='vertical', border_line_color=None,
                #                 label_text_font_size=f'{int(figure_height / 500 * 13)}px',
                #                 label_height=int(figure_height / 500 * 20), label_width=int(figure_height / 500 * 20))
                legend = Legend(location='top_left', orientation='vertical', border_line_color=None)
                labels = legend_labels[i * legend_items_in_col: (i + 1) * legend_items_in_col]
                for label in labels:
                    color = cmap[label]
                    legend.items.append(LegendItem(label=label, renderers=[figure_list[i].circle(x=[0], y=[0], color=color, size=0)]))
                figure_list[i].add_layout(legend, 'left')
                figure_list[i].width = figure_width
            if len(figure_list) == 1:
                fig = figure_list[0]
            else:
                fig = row(*figure_list, sizing_mode='fixed')
        
        self.colorbar_or_legend = type

        return fig
    
    def _create_base_image_figure(self):
        figure = None
        if self.base_image is not None:
            if self.base_image_points is None:
                self.base_image_points = self._create_base_image_xarray()
            if len(self.base_image_points.shape) == 2:
                figure = self.base_image_points.hvplot(
                    cmap='gray', cnorm='eq_hist', hover=False, colorbar=False,
                    datashade=True, aggregator='mean', dynspread=True
                    # rasterize=True, aggregator='mean', dynspread=True
                ).opts(
                    bgcolor=self.bgcolor,
                    width=self.width,
                    height=self.height,
                    xaxis=None,
                    yaxis=None,
                    invert_yaxis=True
                )
            else:
                figure = self.base_image_points.hvplot.rgb(
                    x='x', y='y', bands='channel', hover=False,
                    datashade=True, aggregator='mean', dynspread=True
                    # rasterize=True, aggregator='mean', dynspread=True
                ).opts(
                    bgcolor=self.bgcolor,
                    width=self.width,
                    height=self.height,
                    xaxis='bare',
                    yaxis='bare',
                    invert_yaxis=True
                )
        return figure
    
    def __rangexy_callback(self, x_range, y_range):
        self.x_range = x_range
        self.y_range = y_range

    def show(self):
        assert self.data.cells.cell_border is not None

        pn.param.ParamMethod.loading_indicator = True
        pn.extension()
        hv.extension('bokeh')

        self._create_widgets()

        @pn.depends(self.color_map_key_continuous, self.color_map_key_discrete, self.color_by, self.reverse_colormap,
                    self.cluster_colorpicker, self.gene_names)
        def _create_figure(cm_key_continuous_value, cm_key_discrete_value, color_by_value, reverse_colormap_value,
                           cluster_colorpicker_value, gene_names_value):
            if color_by_value == 'gene':
                self.color_key = gene_names_value
                self.gene_names.visible = True
                # self.gene_names.disabled = False
            else:
                self.gene_names.visible = False
                # self.gene_names.disabled = True
            
            polygons_detail, hover_tool, vdims = self._create_polygons(color_by_value)

            if self.cluster_res is None or color_by_value != 'cluster':
                self.cluster.visible = False
                self.cluster_colorpicker.visible = False
                self.reverse_colormap.disabled = False
                if self.color_map_key_discrete.visible is True:
                    cm_key_value = 'stereo' if self.last_cm_key_continuous is None else self.last_cm_key_continuous
                    # cm_key_value = self.palette if self.last_cm_key_continuous is None else self.last_cm_key_continuous
                    self.color_map_key_continuous.visible = True
                    self.color_map_key_continuous.value = cm_key_value
                    self.color_map_key_discrete.visible = False
                else:
                    cm_key_value = cm_key_continuous_value

                cmap = stereo_conf.linear_colors(cm_key_value, reverse=reverse_colormap_value)
                self.last_cm_key_continuous = cm_key_value
            else:
                self.cluster.visible = True
                self.cluster_colorpicker.visible = True
                self.reverse_colormap.disabled = True
                if self.color_map_key_continuous.visible is True:
                    # cm_key_value = 'stereo_30' if self.last_cm_key_discrete is None else self.last_cm_key_discrete
                    cm_key_value = self.palette if self.last_cm_key_discrete is None else self.last_cm_key_discrete
                    self.color_map_key_continuous.visible = False
                    self.color_map_key_discrete.visible = True
                    self.color_map_key_discrete.value = cm_key_value
                else:
                    cm_key_value = cm_key_discrete_value
                
                if cm_key_value != self.last_cm_key_discrete:
                    n = len(self.cluster_id)
                    colors = stereo_conf.get_colors(cm_key_value, n)
                    self.cluster_color_map = OrderedDict({k: v for k, v in zip(self.cluster_id, colors)})
                    default_cluster_id = self.cluster_id[0]
                    default_cluster_color = self.cluster_color_map[default_cluster_id]
                    self.cluster.value = default_cluster_id
                    self.cluster_colorpicker.value = default_cluster_color
                else:
                    self.cluster_color_map[self.cluster.value] = cluster_colorpicker_value
                # cmap = list(self.cluster_color_map.values())
                cmap = self.cluster_color_map
                self.last_cm_key_discrete = cm_key_value

            if self.cluster_res is None or color_by_value != 'cluster':
                self.figure_colorbar_legend = self._create_colorbar_or_legend('colorbar', cmap, polygons_detail)
            else:
                self.figure_colorbar_legend = self._create_colorbar_or_legend('legend', cmap)

            self.figure_polygons = polygons_detail.hvplot.polygons(
                'polygons', hover_cols=vdims
            ).opts(
                bgcolor=self.bgcolor,
                color='color',
                cnorm='eq_hist',
                cmap=cmap,
                width=self.width,
                height=self.height,
                xaxis=None,
                yaxis=None,
                invert_yaxis=True,
                line_width=1,
                line_alpha=0,
                hover_line_alpha=1,
                fill_alpha=self.fg_alpha,
                hover_fill_alpha=self.hover_fg_alpha,
                active_tools=['wheel_zoom'],
                tools=[hover_tool],
                show_legend=False,
                colorbar=False
            )

            if self.base_image is not None:
                if self.figure_points is None:
                    self.figure_points = self._create_base_image_figure()
                figure = self.figure_points * self.figure_polygons
            else:
                figure = self.figure_polygons
            
            self.rangexy_stream = hv.streams.RangeXY(source=self.figure_polygons)
            self.rangexy_stream.add_subscriber(self.__rangexy_callback)

            return pn.Row(figure, self.figure_colorbar_legend)

        return pn.Row(
            pn.Column(_create_figure),
            pn.Column(
                self.color_map_key_continuous,
                self.color_map_key_discrete,
                self.color_by,
                self.reverse_colormap,
                pn.Row(self.cluster, self.cluster_colorpicker),
                self.gene_names,
                '',
                self.save_title,
                self.save_file_name,
                pn.Row(self.save_file_width, self.save_file_hight),
                self.save_only_in_view,
                # self.with_base_image,
                self.save_button,
                self.save_message,
            )
        )
