import logging
import os
import sys
from pathlib import Path

import apollinaire as apol
import bokeh.palettes

try:
    from env import Environment  # type: ignore[import]
    import functions  # type: ignore[import]
except ImportError:
    from iechelle.gui.env import Environment
    from iechelle.gui import functions
import numpy as np
import pandas as pd
from astropy import units as u
from bokeh.models import Button
from bokeh.models import CategoricalColorMapper
from bokeh.models import CheckboxGroup
from bokeh.models import ColumnDataSource
from bokeh.models import CustomJS
from bokeh.models import DataTable
from bokeh.models import LogColorMapper
from bokeh.models import Paragraph
from bokeh.models import RangeSlider
from bokeh.models import (
    Select,
    Slider,
    TableColumn,
    TextInput,
    LinearScale,
    LogScale,
    CheckboxEditor,
    StringEditor,
    ColorPicker,
)
from bokeh.plotting import figure

from lightkurve import periodogram as lk_prd_module
from lightkurve.seismology import SeismologyQuantity
from matplotlib import pyplot as plt

from iechelle.gui.file_opener import FileOpener

log = logging.getLogger(__name__)

log = logging.getLogger(__name__)


class Interactive:
    def __init__(self, env):
        self.env = env
        # Expose the active interactive controller so Home can delegate
        # PKB loading to the same code path that already populates Table 2.
        self.env.mode_selection_controller = self

        # -----------------------------------------------------------------------
        # Reset ALL session-bound Bokeh model references at the start of each
        # new Bokeh session. Fresh models are always
        # created for each new browser session connection via the per-session
        # Environment instance.
        # -----------------------------------------------------------------------
        self.env.tb_echelle_diagram = None
        self.env.tb_other_periodogram = None
        self.env.fig_other_periodogram = None
        self.env.tb_grid_source = None
        self.env.tb_plot = None
        self.env.fig_tpfint = None
        self.env.stretch_sliderint = None
        self.env.dnu_slider = None
        self.env.inverted_slider = None
        self.env.r_button = None
        self.env.l_button = None
        self.env.rr_button = None
        self.env.ll_button = None

        self.mode_color_map = {
            "999": "grey",
            "0": "red",
            "1": "magenta",
            "2": "blue",
            "3": "green",
        }

        self.tb_constants_val = ColumnDataSource(
            data=dict(
                other_prd_cuttoff=list([500]),
                minimum_frequency=list([0]),
                maximum_frequency=list([8000]),
            )
        )

        self.mode_shape_selection = "circle"
        self.id_mycatalog = self.env.tb_source.data["id_mycatalog"][0]
        # self.palette = "hot"

        self.env.text_osc_query = TextInput(
            value="n_pg>=0", title="Select Cluster"
        )

        self.env.tb_other_periodogram, self.env.fig_other_periodogram = (
            self.initialize_dnu_periodogram()
        )
        self.env.frequency_minimum_text = TextInput(
            value=str(self.env.minimum_frequency), title="Freq min", width=60
        )
        self.env.frequency_maximum_text = TextInput(
            value=str(self.env.maximum_frequency), title="Freq max", width=60
        )
        self.env.frequency_maxdnu_text = TextInput(
            value=str(self.env.maxdnu), title="Max dnu", width=60
        )
        self.env.echelle_noise_cuttoff_text = TextInput(
            value=str(0), title="Threshold", width=60
        )

        self.env.dnu_text = TextInput(
            value=str(self.env.dnu_val), title="dnu", width=80
        )
        self.env.update_int_button = Button(
            label="Update Plot", button_type=self.env.button_type, width=200
        )
        self.env.update_int_button.on_click(self.update_value)

        self.env.get_data_button = Button(
            label="Reload Input PSD",
            button_type=self.env.button_type,
            width=150,
        )
        self.env.get_data_button.on_click(self.force_refresh)

        self.env.select_mode_menu = Select(
            title="Mode Select",
            options=["999", "0", "1", "2", "3", "4", "-1", "-2"],
            value="999",
            width=150,
        )
        self.env.mode_apply_button = Button(
            label="Assign Mode", button_type=self.env.button_type, width=150
        )
        self.env.mode_apply_button.on_click(self.click_mode_apply_button)

        self.env.move_se_1_2_button = Button(
            label="Move Tab1 -> Tab2",
            button_type=self.env.button_type,
            width=150,
        )
        self.env.move_se_1_2_button.on_click(self.click_move_se_1_2_button)
        self.env.move_se_2_1_button = Button(
            label="Move Tab2 -> Tab1",
            button_type=self.env.button_type,
            width=150,
        )
        self.env.move_se_2_1_button.on_click(self.click_move_se_2_1_button)

        self.env.delete_row_button = Button(
            label="Delete Selected Rows",
            button_type="danger",
            width=150,
        )
        self.env.delete_row_button.on_click(self.click_delete_row_button)

        self.env.save_table_2_button = Button(
            label="Save Tab2", button_type=self.env.button_type, width=150
        )
        self.env.save_table_2_button.on_click(self.save_table_2)

        self.env.load_table_2_button = Button(
            label="Load PKB from default file",
            button_type=self.env.button_type,
            width=150,
        )
        self.env.load_table_2_button.on_click(self.load_table_2)

        self.env.save_as_table_2_button = Button(
            label="Save Tab 2 as", button_type=self.env.button_type, width=150
        )
        self.env.save_as_table_2_button.on_click(self.save_as_table_2)

        self.env.save_to_default_button = Button(
            label="Save to Default File",
            button_type=self.env.button_type,
            width=150,
        )
        self.env.save_to_default_button.on_click(self.save_to_default_file)

        self.env.load_from_specific_file_table_2_button = Button(
            label="Load PKB from specific file",
            button_type=self.env.button_type,
            width=150,
        )
        self.env.load_from_specific_file_table_2_button.on_click(
            self.load_from_file
        )

        self.env.load_bkg_param_from_file_button = Button(
            label="Load BKG from specific file",
            button_type=self.env.button_type,
            width=150,
        )
        self.env.load_bkg_param_from_file_button.on_click(
            self.load_bkg_param_from_file
        )

        self.env.select_grid_menu = Select(
            title="Select Grid",
            options=["Obs", "Syn", "Sub"],
            value="Obs",
            width=80,
        )

        self.env.grid_circle_size = TextInput(
            value=str(2), title="Circle Size", width=80
        )

        self.env.select_grid_marker = Select(
            title="Grid Marker",
            options=[
                "circle",
                "circle_hollow",
                "square",
                "square_hollow",
                "triangle",
                "triangle_hollow",
                "diamond",
                "diamond_hollow",
                "cross",
                "x",
                "asterisk",
            ],
            value="circle",
            width=110,
        )

        self.env.select_color_palette = Select(
            title="Color Palette",
            options=self.env.color_palette_options,
            value=self.env.default_color_palette,
            width=100,
        )
        pal = getattr(bokeh.palettes, self.env.default_color_palette)
        if type(pal) is dict:
            self.palette = pal[9]
        else:
            self.palette = pal
        self.env.check_reverse_color_palette = CheckboxGroup(
            labels=["Reverse"], active=[1], height=10, width=10
        )

        self.env.check_make_grid = CheckboxGroup(
            labels=["Make Grid"],
            active=[0, 1],
            height=10,
            width=100,
        )

        self.env.check_show_echelle = CheckboxGroup(
            labels=["Show Echelle"],
            active=[0, 1],
            height=10,
            width=100,
        )

        self.env.check_show_horizontal_lines = CheckboxGroup(
            labels=["Show lines"],
            active=[0, 1],
            height=10,
            width=100,
        )

        self.env.check_show_inverted_lines = CheckboxGroup(
            labels=["Inverted lines"],
            active=[0, 1],
            height=10,
            width=100,
        )

        self.env.check_show_modes_grid = CheckboxGroup(
            labels=["Show Modes Only"],
            active=[1],
            height=10,
            width=120,
        )
        self.env.check_color_map_lock = CheckboxGroup(
            labels=["Lock"],
            active=[1],
            height=10,
            width=100,
        )

        self.env.check_periodogram_axis_scale = CheckboxGroup(
            labels=["Log Scale"],
            active=[],
            height=10,
            width=100,
        )

        self.env.check_periodogram_axis_scale.on_change(
            "active", self.toggle_periodogram_axis_scale
        )

        self.initialize_selection_tables()
        self.make_tb_echelle_diagram()
        self.interact_echelle()
        self.make_grid()
        self.initilize_plot_table()

        self.env.test_button = Button(
            label="Get Selection", button_type=self.env.button_type, width=150
        )
        self.env.test_button.on_click(self.get_all_selection_button)

        self.env.clear_se_grid_prd_button = Button(
            label="Clear Selection",
            button_type=self.env.button_type,
            width=150,
        )
        self.env.clear_se_grid_prd_button.on_click(self.clear_se_grid_prd)

        self.env.clear_se_table1_button = Button(
            label="Clear Table 1", button_type=self.env.button_type, width=150
        )
        self.env.clear_se_table1_button.on_click(self.clear_se_table1)

        self.env.clear_se_table2_button = Button(
            label="Clear Table 2", button_type=self.env.button_type, width=150
        )
        self.env.clear_se_table2_button.on_click(self.clear_se_table2)

        self.env.find_peaks_button = Button(
            label="Find Peak", button_type=self.env.button_type, width=150
        )
        self.env.find_peaks_button.on_click(self.find_peak_frequencies)

        self.env.calculate_synthetic_psd_button = Button(
            label="Calculate Synthetic PSD",
            button_type=self.env.button_type,
            width=150,
        )
        self.env.calculate_synthetic_psd_button.on_click(
            self.calculate_synthetic_psd
        )

        self.env.show_plot = Button(
            label="Show Plot", button_type=self.env.button_type, width=150
        )
        self.env.show_plot.on_click(self.show_plot)

        # Inverted line
        self.env.inverted_line_initial_y_text = TextInput(
            value=str(0), title="Initial y", width=50
        )
        self.env.inverted_line_length_text = TextInput(
            value=str(10), title="Length", width=50
        )
        self.env.inverted_line_scale_text = TextInput(
            value=str(-1), title="Scale", width=50
        )
        self.env.inverted_line_xvalue_text = TextInput(
            value=str(0), title="x value", width=50
        )
        self.env.inverted_line_update_button = Button(
            label="Update Slider", button_type=self.env.button_type, width=100
        )
        self.env.inverted_line_update_button.on_click(
            self.update_inverted_line
        )

        self.env.inverted_slider_max_value_text = TextInput(
            value=str(400), title="Max x Value", width=50
        )

        # self.update_selection_tables()

        self.env.tb_source.on_change("data", self.update_whole_plot)

        self.plot_vertical_lines()
        self.inverted_slider()

        self.env.select_x_axis_type = Select(
            title="PSD X-Axis Scale",
            value="linear",
            options=["linear", "log"],
            width=100,
        )
        self.env.select_y_axis_type = Select(
            title="PSD Y-Axis Scale",
            value="linear",
            options=["linear", "log"],
            width=100,
        )
        self.env.select_x_axis_type.on_change("value", self.update_axis_scale)
        self.env.select_y_axis_type.on_change("value", self.update_axis_scale)
        self.old_frequency_min = 0
        self.old_frequency_max = 0
        self.old_grid_selection = ""
        self.publish_message(text="Select Fits File")
        self.pkb_columns_selected = self.env.pkb_columns_extended

    def initilize_plot_table(self):
        """
        This function initialise plot table, later this table will further used for
        plotting different test.
        """

        self.env.tb_plot = ColumnDataSource(
            data=dict(
                name=["Grid", "l=0", "l=1", "l=2", "l=3"],
                colour=[
                    self.mode_color_map["999"],
                    self.mode_color_map["0"],
                    self.mode_color_map["1"],
                    self.mode_color_map["2"],
                    self.mode_color_map["3"],
                ],
                show=[True, True, True, True, True],
            )
        )

        columns = [
            TableColumn(
                field="show",
                title="Visible",
                editor=CheckboxEditor(),
                width=60,
            ),
            TableColumn(field="name", title="Name", width=100),
            TableColumn(
                field="colour",
                title="Colour",
                editor=StringEditor(),
                width=100,
            ),
        ]

        self.env.table_plot = DataTable(
            source=self.env.tb_plot,
            columns=columns,
            editable=True,
            sizing_mode="stretch_width",
            height=200,
            sortable=True,
            stylesheets=["""
                :host {
                    border: 1px solid #ddd !important;
                    border-radius: 2px;
                }
            """],
        )

        def on_plot_config_change(attr, old, new):
            self.show_plot()

        self.env.tb_plot.on_change("data", on_plot_config_change)
        self.env.tb_plot.selected.indices = [0]

    def update_source(self, attr, old, new):
        # ff, pp = self.read_fits_get_fp()
        # mm = ['999']*(len(pp))
        # f = (ff*u.Hz).to(self.env.frequency_unit)
        # p = pp*self.env.power_unit
        # period = lk_prd_module.Periodogram(f, p)
        # self.periodogram = period
        # old_data= ColumnDataSource(
        #     data=dict(
        #         frequency=list(self.periodogram.frequency.value),
        #         power=list(self.periodogram.power.value),
        #         Mode = mm,
        #     ))
        self.publish_message(text="Updating")
        self.trim_frequency()
        # self.env.tb_other_periodogram.data=dict(old_data.data)
        self.clear_se_table1()
        self.clear_se_table2()
        self.update_plot(0, 0, 0)
        # self.update_value()
        self.publish_message(text="Ready")

    def initialize_dnu_periodogram(self):
        """
        Function initialise the the dnu periodogram table source and also the periodogram graph
        """
        ff, pp = self.read_fits_get_fp()
        mm = ["999"] * (len(pp))

        self.env.minimum_frequency = 1  # 1
        self.env.maximum_frequency = 8000  # 8000
        self.env.maxdnu = 100
        self.env.dnu_val = 24.8

        f = (ff * u.Unit(self.env.source_target_frequency_unit)).to(
            self.env.frequency_unit
        )
        p = pp * self.env.power_unit
        if ff.any():
            period = lk_prd_module.Periodogram(f, p)

            self.periodogram = period

            tb_other_periodogram = ColumnDataSource(
                data=dict(
                    frequency=list(self.periodogram.frequency.value),
                    power=list(self.periodogram.power.value),
                    Mode=mm,
                    # cuttoff=list(np.array([0])),
                )
            )
        else:

            self.periodogram = None

            tb_other_periodogram = ColumnDataSource(
                data=dict(
                    frequency=list([]),
                    power=list([]),
                    Mode=list([]),
                    # cuttoff=list(np.array([0])),
                )
            )

        # Intialize other periodgram
        fig_other_periodogram = figure(
            sizing_mode="stretch_width",
            height=600,
            tools=[
                "box_zoom",
                "wheel_zoom",
                "lasso_select",
                "tap",
                "reset",
                "save",
            ],
            title="PSD",
            tooltips=self.env.TOOLTIPS,
        )

        color_map = self.mode_color_map
        color_mapper = CategoricalColorMapper(
            factors=list(color_map.keys()), palette=list(color_map.values())
        )

        # fig_other_periodogram.circle("frequency", "power",
        #                              source=tb_other_periodogram,
        #                              alpha=0.7,
        #                              color = {'field': 'Mode',
        #                                         'transform': color_mapper},
        #                              **self.env.selection,name='Grid'
        #                              )

        fig_other_periodogram.scatter(
            "frequency",
            "power",
            source=tb_other_periodogram,
            alpha=0.7,
            color={"field": "Mode", "transform": color_mapper},
            size=5,
            name="Grid",
        )

        fig_other_periodogram.line(
            "frequency",
            "power",
            source=tb_other_periodogram,
            alpha=0.7,
            color="grey",
            name="Grid",
        )
        # color="#1F77B4"
        fig_other_periodogram.segment(
            x0="minimum_frequency",
            y0="other_prd_cuttoff",
            x1="maximum_frequency",
            y1="other_prd_cuttoff",
            source=self.tb_constants_val,
            name="threshold_line",
            color="red",
            line_width=2,
        )

        return tb_other_periodogram, fig_other_periodogram

    def plot_vertical_lines(self):

        color_map = self.mode_color_map
        color_mapper = CategoricalColorMapper(
            factors=list(color_map.keys()), palette=list(color_map.values())
        )

        self.env.fig_other_periodogram.ray(
            x="Frequency",
            y=0,
            source=self.tb_se_second_source,
            length=2137,
            angle=np.pi / 2,
            legend_field="Mode",
            name="vertical_line",
            color={"field": "Mode", "transform": color_mapper},
        )

    def make_tb_echelle_diagram(self):
        """
        Initialize or refresh the echelle diagram ColumnDataSource.

        On the first call, a fresh ColumnDataSource is always created (even if
        one already exists from a previous Bokeh session). This prevents the
        `_pending_writes` RuntimeError which occurs when a stale Bokeh model
        from an old session has its .data mutated, triggering its old
        document's event system during a new session's initialization.
        """
        print("Making echelle diagram")
        # Always create a fresh ColumnDataSource to avoid cross-session contamination.
        # This is now naturally handled by the per-session Environment instance.
        is_first_init = self.env.tb_echelle_diagram is None

        if is_first_init:
            print("Creating an echelle diagram, Creating column source")
            self.env.tb_echelle_diagram = ColumnDataSource(
                data=dict(
                    image=[],
                    x_f=[],
                    y_f=[],
                    dw=[],
                    dh=[],
                    xmin=[],
                    ymin=[],
                    y_original=[],
                )
            )
        else:
            pass

        if (
            self.env.check_show_echelle.active[0] == 0
            and not self.env.tb_other_periodogram.to_df().empty
        ):
            # print("new an echelle diagram, Cleaning data")
            # Load the values
            # frequency = (
            #     self.env.tb_other_periodogram.data["frequency"]
            #     * self.env.frequency_unit
            # )
            # power = (
            #     self.env.tb_other_periodogram.data["power"]
            #     * self.env.power_unit
            # )
            (
                ep,
                self.x_echelle,
                self.y_echelle,
                y_original,
                self.xx,
                self.yy,
                self.freq_values,
                self.power_values,
            ) = self._clean_echelle(
                deltanu=self.env.dnu_val,
                # minimum_frequency = self.env.minimum_frequency*self.env.frequency_unit,
                # maximum_frequency = self.env.maximum_frequency*self.env.frequency_unit
            )

            x_f = self.x_echelle
            y_f = self.y_echelle
            if x_f.size > 0:
                dw = (x_f.flatten().max() - x_f.flatten().min()).value
                dh = (y_f.flatten().max() - y_f.flatten().min()).value
                xmin = x_f.flatten().min().value
                ymin = y_f.flatten().min().value
            else:
                dw = dh = xmin = ymin = 0
            # print("Echelle Diagram", x_f, y_f, dw, dh, xmin, ymin)
            new_data = ColumnDataSource(
                data=dict(
                    image=[ep.value],
                    x_f=[x_f.value],
                    y_f=[y_f.value],
                    dw=[dw],
                    dh=[dh],
                    xmin=[xmin],
                    ymin=[ymin],
                    y_original=[y_original],
                    # xx=[xx],
                    # yy=[yy],
                    # freq_values=[freq_values],
                )
            )

            self.env.tb_echelle_diagram.data = dict(new_data.data)
        else:
            old_data = ColumnDataSource(
                data=dict(
                    image=[],
                    x_f=[],
                    y_f=[],
                    dw=[],
                    dh=[],
                    xmin=[],
                    ymin=[],
                    y_original=[],
                )
            )
            self.env.tb_echelle_diagram.data = dict(old_data.data)

    def initialize_selection_tables(self):

        # self.tb_se_first_source=ColumnDataSource(data=dict(x=[], y=[],z=[]))
        # self.tb_se_second_source=ColumnDataSource(data=dict(x=[], y=[], z=[]))

        self.tb_se_first_source = ColumnDataSource(
            data=dict(Slicefreq=[], Frequency=[], Power=[], Mode=[], xx=[]),
            name="tb_se_first_source",
        )
        self.tb_se_second_source = ColumnDataSource(
            data=dict(
                Slicefreq=[],
                Frequency=[],
                Power=[],
                Mode=[],
                xx=[],
                mode_color=[],
            ),
            name="tb_se_second_source",
        )
        columns = [
            TableColumn(field="Slicefreq", title="Slice Freq"),
            TableColumn(field="Frequency", title="Frequency"),
            TableColumn(field="Power", title="Power"),
            TableColumn(field="Mode", title="Mode"),
        ]

        self.env.table_se_first = DataTable(
            source=self.tb_se_first_source,
            columns=columns,
            sizing_mode="stretch_width",
            height=520,
            sortable=True,
            selectable="checkbox",
            editable=True,
            stylesheets=["""
                :host {
                    border: 1px solid #ddd !important;
                    border-radius: 2px;
                }
            """],
        )

        self.env.table_se_second = DataTable(
            source=self.tb_se_second_source,
            columns=columns,
            sizing_mode="stretch_width",
            height=520,
            sortable=True,
            selectable="checkbox",
            editable=True,
            stylesheets=["""
                :host {
                    border: 1px solid #ddd !important;
                    border-radius: 2px;
                }
            """],
        )

    def update_selection_tables(self):
        """Placeholder for updating selection tables"""
        print("Commented")

    def make_grid(self):
        """

        Initialize grid source

        """

        if self.env.tb_grid_source is None:
            self.env.tb_grid_source = ColumnDataSource(
                data=dict(
                    xx=[],
                    yy=[],
                    freq_values=[],
                    power_values=[],
                    Mode=[],
                ),
                name="tb_grid_source",
            )

            if not self.mode_shape_selection == "circle":
                # To be done properly
                width = np.array([1])
                height = np.array([1])
                self.env.fig_tpfint.rect(
                    "center_x",
                    "center_y",
                    width.flatten(),
                    height.flatten(),
                    fill_color="gray",
                    fill_alpha=0.2,
                    line_color="blue",
                    name="grid",
                    source=self.env.tb_grid_source,
                )
            else:

                color_map = self.mode_color_map
                color_mapper = CategoricalColorMapper(
                    factors=list(color_map.keys()),
                    palette=list(color_map.values()),
                )

                marker_val = self.env.select_grid_marker.value
                hollow = marker_val.endswith("_hollow")
                base_marker = marker_val.replace("_hollow", "")

                size_val = float(self.env.grid_circle_size.value)

                self.env.fig_tpfint.scatter(
                    x="xx",
                    y="yy",
                    size=size_val,
                    marker=base_marker,
                    fill_alpha=0.0 if hollow else 0.7,
                    name="grid_circle",
                    line_color={"field": "Mode", "transform": color_mapper},
                    fill_color=(
                        {"field": "Mode", "transform": color_mapper}
                        if not hollow
                        else None
                    ),
                    source=self.env.tb_grid_source,
                    **self.env.selection,
                )

                # color_mapper = CategoricalColorMapper(
                #     factors=list(color_map.keys()),
                #     palette=list(color_map.values()))

                # self.env.fig_tpfint.circle(x='xx', y='yy',
                #                            size=2,
                #                            fill_alpha=0.2,
                #                            line_color='blue',
                #                            source=self.env.tb_grid_source)

                # self.env.fig_tpfint.circle(x = 'xx',
                #                            y = 'yy',
                #                            size = 2,
                #                            fill_alpha = 0.2,
                #                            color = {'field': 'Mode',
                #                                   'transform': color_mapper},
                #                            source=self.env.tb_grid_source)

        # print('Griding',self.env.check_make_grid.active)
        if (
            self.env.check_make_grid.active[0] == 0
            and not self.env.tb_other_periodogram.to_df().empty
        ):
            cutt_off = float(self.env.echelle_noise_cuttoff_text.value)
            self.tb_constants_val.data["other_prd_cuttoff"] = list([cutt_off])

            val_min = float(self.env.frequency_minimum_text.value)
            val_max = float(self.env.frequency_maximum_text.value)
            self.tb_constants_val.data.update(
                {
                    "minimum_frequency": [val_min],
                    "maximum_frequency": [val_max],
                }
            )
            print("Threshold min:", val_min, "max:", val_max)
            ind = np.array(self.power_values) >= cutt_off
            xx = np.array(self.xx)[ind]
            yy = np.array(self.yy)[ind]
            ff = np.array(self.freq_values)[ind]
            pp = np.array(self.power_values)[ind]
            mm = ["999"] * (len(pp))

            old_data = ColumnDataSource(
                data=dict(
                    xx=list(xx),
                    yy=list(yy),
                    freq_values=list(ff),
                    power_values=list(pp),
                    Mode=mm,
                )
            )
            self.env.tb_grid_source.data = dict(old_data.data)
            self.apply_modes(table=self.tb_se_first_source.to_df())
            self.apply_modes(table=self.tb_se_second_source.to_df())
            val = int(self.env.grid_circle_size.value)
            print("Circle size", val)
            grid_renderers = self.env.fig_tpfint.select("grid_circle")
            for r in grid_renderers:
                if hasattr(r, "glyph"):
                    r.glyph.size = val
                if hasattr(r, "selection_glyph") and r.selection_glyph:
                    r.selection_glyph.size = val
                if hasattr(r, "nonselection_glyph") and r.nonselection_glyph:
                    r.nonselection_glyph.size = val
            if self.env.check_show_modes_grid.active[0] == 0:
                val = "999"
                df_grid = self.env.tb_grid_source.to_df().query("Mode !=@val")
                old_data = ColumnDataSource(df_grid.to_dict("list"))
                self.env.tb_grid_source.data = dict(old_data.data)

        else:
            old_data = ColumnDataSource(
                data=dict(
                    xx=[],
                    yy=[],
                    freq_values=[],
                    power_values=[],
                    Mode=[],
                )
            )
            self.env.tb_grid_source.data = dict(old_data.data)

    def read_fits_get_fp(self):
        """
        Read the configured source file and return frequency and power arrays.
        """
        self.publish_message(text="Reading Fits")
        ff = np.array([])
        pp = np.array([])
        filename = Path(self.env.tb_source.data["path_fits"][0])
        print("Running read fits", filename)
        if filename.is_file():
            ff, pp = functions.read_frequency_power(
                file_path=str(filename),
                file_type=getattr(self.env, "source_file_type", "fits-image"),
                frequency_selector=getattr(
                    self.env, "source_frequency_column", "0"
                ),
                power_selector=getattr(self.env, "source_power_column", "1"),
                fits_hdu=getattr(self.env, "source_fits_hdu", "0"),
                text_delimiter=getattr(
                    self.env, "source_text_delimiter", "auto"
                ),
                input_units=getattr(
                    self.env, "source_input_frequency_unit", None
                ),
                target_units=getattr(
                    self.env, "source_target_frequency_unit", None
                ),
            )
            ff = self.__swap_byteorder(ff)
            pp = self.__swap_byteorder(pp)
        else:
            print("File does not exist")
        self.publish_message(text="Read Fits")

        return ff, pp

    def __swap_byteorder(self, arr):
        try:
            return arr.byteswap().view(arr.dtype.newbyteorder())
        except AttributeError:
            return arr.byteswap().newbyteorder()

    def calculate_synthetic_psd(self):
        self.publish_message("Calculating Synthetic Spectra; Busy")
        self.env.select_grid_menu.options = ["Obs", "Syn", "Sub"]

        data = functions.calculate_synthetic_spectrum(
            fits_file=self.env.selected_filename_text.text,
            bkg_file=self.env.selected_filename_background_text.text,
            pkb_file=self.env.selected_filename_pkb_text.text,
            n_harvey=2,
            environment=self.env,
        )
        data["sub_psd"] = data["psd"] - data["synthetic_psd"]
        freq_min = float(self.env.frequency_minimum_text.value)
        freq_max = float(self.env.frequency_maximum_text.value)
        data = data.query(
            "freq>@freq_min & freq<@freq_max",
            local_dict={"freq_min": freq_min, "freq_max": freq_max},
        )

        freq = data.freq.values
        power = data.psd.values
        print("frequecny calculation", freq)
        name = "Obs"
        self.add_plot_other_periodogram(
            freq=freq, power=power, name=name, color="blue"
        )

        freq = data.freq.values
        power = data.synthetic_psd.values
        name = "Syn"
        self.add_plot_other_periodogram(
            freq=freq, power=power, name=name, color="green"
        )

        freq = data.freq.values
        power = data.sub_psd.values
        name = "Sub"
        self.add_plot_other_periodogram(
            freq=freq, power=power, name=name, color="red"
        )
        old_data = ColumnDataSource(
            data=dict(
                name=[
                    "Grid",
                    "Obs",
                    "Syn",
                    "Sub",
                    "l=0",
                    "l=1",
                    "l=2",
                    "l=3",
                    "l=4",
                    "l=5",
                    "l=-1",
                ],
                colour=[
                    self.mode_color_map["999"],
                    "blue",
                    "green",
                    "red",
                    self.mode_color_map["0"],
                    self.mode_color_map["1"],
                    self.mode_color_map["2"],
                    self.mode_color_map["3"],
                    self.mode_color_map["4"],
                    self.mode_color_map["5"],
                    self.mode_color_map["-1"],
                ],
                show=[
                    True,
                    True,
                    True,
                    True,
                    True,
                    True,
                    True,
                    True,
                    True,
                    True,
                    True,
                ],
            )
        )
        self.env.tb_plot.data = dict(old_data.data)
        self.env.tb_plot.selected.indices = [0]
        self.publish_message("Ready")

        return data

    def show_plot(self):
        df = self.env.tb_plot.to_df()

        # Temporary palette for the mapper to handle visibility via transparency
        display_palette = dict(self.mode_color_map)
        mode_config_changed = False

        for i, row in df.iterrows():
            name = row["name"]
            visible = row["show"]
            color = row["colour"]

            if name.startswith("l="):
                mode_str = name.replace("l=", "")
                if mode_str in self.mode_color_map:
                    # Update base color map (original color)
                    self.mode_color_map[mode_str] = color
                    # Set display color (transparent if hidden)
                    display_palette[mode_str] = (
                        color if visible else "#00000000"
                    )
                    mode_config_changed = True
                continue

            # Update visibility and color for named renderers (Grid, Obs, Syn, Sub, etc.)
            renderers = self.env.fig_other_periodogram.select(name)
            for r in renderers:
                r.visible = visible
                if hasattr(r, "glyph"):
                    if hasattr(r.glyph, "line_color"):
                        r.glyph.line_color = color
                    if hasattr(r.glyph, "fill_color"):
                        r.glyph.fill_color = color

            # If it's the Grid row, also sync the Echelle grid components
            if name == "Grid":
                self.mode_color_map["999"] = color
                display_palette["999"] = color if visible else "#00000000"
                mode_config_changed = True

        if mode_config_changed:
            new_mapper = CategoricalColorMapper(
                factors=list(display_palette.keys()),
                palette=list(display_palette.values()),
            )

            # Update Echelle Grid markers
            grid_circles = self.env.fig_tpfint.select("grid_circle")
            if grid_circles:
                for gr in grid_circles:
                    if hasattr(gr, "glyph"):
                        mapper_dict = {
                            "field": "Mode",
                            "transform": new_mapper,
                        }
                        gr.glyph.line_color = mapper_dict
                        if (
                            hasattr(gr.glyph, "fill_color")
                            and gr.glyph.fill_color is not None
                        ):
                            gr.glyph.fill_color = mapper_dict

            # Update PSD shared mode lines (standard and inverted)
            for line_name in ["vertical_line", "vertical_inverted_line"]:
                psd_lines = self.env.fig_other_periodogram.select(line_name)
                if psd_lines:
                    for line_renderer in psd_lines:
                        if hasattr(line_renderer, "glyph"):
                            line_renderer.glyph.line_color = {
                                "field": "Mode",
                                "transform": new_mapper,
                            }

        print("Finished visibility and color update")

    def add_plot_other_periodogram(
        self, freq=None, power=None, name=None, color=None
    ):

        # self.env.fig_other_periodogram.line(freq, power,
        #                    name=name,
        #                     alpha=0.7, color=color)
        # self.env.fig_other_periodogram.select(name).visible = False

        existing_line = None
        for renderer in self.env.fig_other_periodogram.renderers:
            if hasattr(renderer, "name") and renderer.name == name:
                existing_line = renderer
                break

        if existing_line:
            # Update the existing line's data source
            existing_line.data_source.data["x"] = freq
            existing_line.data_source.data["y"] = power
            existing_line.data_source.trigger(
                "data",
                existing_line.data_source.data,
                existing_line.data_source.data,
            )
        else:
            # Create a new line
            self.env.fig_other_periodogram.line(
                freq, power, name=name, alpha=0.7, color=color
            )

        # Assuming you still want to set the visibility to False
        self.env.fig_other_periodogram.select(name=name).visible = False

    def update_value(self):
        self.env.minimum_frequency = float(
            self.env.frequency_minimum_text.value
        )
        self.env.maximum_frequency = float(
            self.env.frequency_maximum_text.value
        )
        self.env.maxdnu = float(self.env.frequency_maxdnu_text.value)
        self.env.dnu_val = float(self.env.dnu_text.value)
        self.env.dnu_slider.value = self.env.dnu_val

        print(
            "Values transfering",
            self.env.minimum_frequency,
            self.env.maximum_frequency,
            self.env.maxdnu,
        )

        # print('range', self.env.fig_other_periodogram.x_range.end)

        # print(self.env.fig_other_periodogram.x_range.end)
        self.update_plot(0, 0, 0)

    def force_refresh(self):
        """Forces a data reload from input_tb by passing None as attr."""
        self.update_plot(None, None, None)
        # self.env.fig_other_periodogram.x_range.start = int(self.env.minimum_frequency)
        # self.env.fig_other_periodogram.x_range.end = int(self.env.maximum_frequency)
        # start = int(self.env.minimum_frequency)
        # end = int(self.env.maximum_frequency)

        # self.env.fig_other_periodogram.x_range=(start, end)
        # print(self.env.fig_other_periodogram.x_range.end)

    def check_change_in_frequency_limit(self):
        """
        Return true if there is change in frequency
        """
        # Get new frequency values

        self.env.minimum_frequency = float(
            self.env.frequency_minimum_text.value
        )
        self.env.maximum_frequency = float(
            self.env.frequency_maximum_text.value
        )
        self.grid_selection = self.env.select_grid_menu.value
        if (
            (self.env.minimum_frequency == self.old_frequency_min)
            and (self.env.maximum_frequency == self.old_frequency_max)
            and (self.grid_selection == self.old_grid_selection)
        ):
            print("No change in Min and Max Freq Cuttoff or Grid")
            value = False
        else:
            print("Change in Frequency or Grid")
            value = True

        self.old_frequency_min = self.env.minimum_frequency
        self.old_frequency_max = self.env.maximum_frequency
        self.old_grid_selection = self.grid_selection

        return value

    def update_whole_plot(self, attr, old, new):
        """
        Triggered when a new source is selected.
        """
        # Populate input_tb from the selected file in tb_source
        try:
            path = self.env.tb_source.data["path_fits"][0]
            if path and path != "None":
                ff, pp = functions.read_frequency_power(
                    file_path=path,
                    file_type=self.env.source_file_type,
                    frequency_selector=self.env.source_frequency_column,
                    power_selector=self.env.source_power_column,
                    fits_hdu=self.env.source_fits_hdu,
                    text_delimiter=self.env.source_text_delimiter,
                    input_units=self.env.source_input_frequency_unit,
                    target_units=self.env.source_target_frequency_unit,
                )
                self.env.input_tb.data = dict(frequency=ff, power=pp)
        except Exception as e:
            print(f"Error populating input_tb in update_whole_plot: {e}")

        self.trim_frequency()
        self.clear_se_table1()
        self.clear_se_table2()
        self.update_plot(0, 0, 0)

        self.id_mycatalog = self.env.tb_source.data["id_mycatalog"][0]
        dnu = self.env.dnu_val
        self.env.fig_tpfint.xaxis.axis_label = (
            r"Frequency / {:.3f} Mod. 1".format(dnu)
        )

        self.make_tb_echelle_diagram()

        ep = self.env.tb_echelle_diagram.data["image"] * self.env.power_unit
        ep_values = np.asarray(ep.value)
        if ep_values.size > 0 and np.any(np.isfinite(ep_values)):
            lo, hi = np.nanpercentile(ep_values, [0.1, 99.9])
            vlo, vhi = 0.3 * lo, 1.7 * hi
            pal = getattr(bokeh.palettes, self.env.select_color_palette.value)
            if type(pal) is dict:
                self.palette = pal[9]
            else:
                self.palette = pal

                if self.env.check_reverse_color_palette.active[0] != 1:
                    self.palette = list(reversed(self.palette))
            # Lock color mapper
            if self.env.check_color_map_lock.active[0] == 1:
                print(
                    "Value of colormap check",
                    self.env.check_color_map_lock.active[0],
                )
                color_mapper = LogColorMapper(
                    palette=self.palette, low=lo, high=hi
                )
                self.env.fig_tpfint.select("img").glyph.color_mapper.low = (
                    color_mapper.low
                )
                self.env.fig_tpfint.select("img").glyph.color_mapper.high = (
                    color_mapper.high
                )
                self.env.fig_tpfint.select(
                    "img"
                ).glyph.color_mapper.palette = getattr(color_mapper, "palette")
                self.env.stretch_sliderint.start = vlo
                self.env.stretch_sliderint.end = vhi
                self.env.stretch_sliderint.value = (lo, hi)
        self._update_dnu_slider_bounds()
        if self.env.check_show_horizontal_lines.active[0] == 0:
            self.env.fig_other_periodogram.select("vertical_line").visible = (
                True
            )
        else:
            self.env.fig_other_periodogram.select("vertical_line").visible = (
                False
            )

        self.publish_message(text="Ready")

    def update_plot(self, attr, old, new):
        self.publish_message(text="Updating Plot")
        if self.check_change_in_frequency_limit() or attr is None:
            print("Triming Frequency")
            self.trim_frequency()

        self.id_mycatalog = self.env.tb_source.data["id_mycatalog"][0]
        dnu = self.env.dnu_val
        self.env.fig_tpfint.xaxis.axis_label = (
            r"Frequency / {:.3f} Mod. 1".format(dnu)
        )

        self.make_tb_echelle_diagram()
        if self.env.check_show_echelle.active[0] == 0:

            ep = (
                self.env.tb_echelle_diagram.data["image"] * self.env.power_unit
            )
            # print("Echelle diagram", ep)
            if ep.value.any():
                lo, hi = np.nanpercentile(ep.value, [0.1, 99.9])
                vlo, vhi = 0.3 * lo, 1.7 * hi
                # vstep = (lo - hi) / 500
                # self.palette = getattr(bokeh.palettes,
                #                     self.env.select_color_palette.value)[9]

                pal = getattr(
                    bokeh.palettes, self.env.select_color_palette.value
                )
                if type(pal) is dict:
                    self.palette = pal[9]
                else:
                    self.palette = pal

                if self.env.check_reverse_color_palette.active[0] != 1:
                    self.palette = list(reversed(self.palette))
                if self.env.check_color_map_lock.active[0] == 1:
                    print(
                        "Value of colormap check",
                        self.env.check_color_map_lock.active[0],
                    )
                    color_mapper = LogColorMapper(
                        palette=self.palette, low=lo, high=hi
                    )
                    self.env.fig_tpfint.select(
                        "img"
                    ).glyph.color_mapper.low = color_mapper.low
                    self.env.fig_tpfint.select(
                        "img"
                    ).glyph.color_mapper.high = color_mapper.high
                    self.env.fig_tpfint.select(
                        "img"
                    ).glyph.color_mapper.palette = getattr(
                        color_mapper, "palette"
                    )
                    self.env.stretch_sliderint.start = vlo
                    self.env.stretch_sliderint.end = vhi
                    self.env.stretch_sliderint.value = (lo, hi)
                self._update_dnu_slider_bounds()
            if self.env.check_show_horizontal_lines.active[0] == 0:
                self.env.fig_other_periodogram.select(
                    "vertical_line"
                ).visible = True
                # self.env.fig_other_periodogram.select('vertical_inverted_line').visible = True
            else:
                self.env.fig_other_periodogram.select(
                    "vertical_line"
                ).visible = False
                # self.env.fig_other_periodogram.select('vertical_inverted_line').visible = False

        self.make_grid()
        self.publish_message(text="Ready")

    def _validate_numax(self, numax):
        """
        Raises exception if `numax` is None and `self.numax` is not set.
        """
        if numax is None:
            try:
                return self.numax
            except AttributeError:
                raise AttributeError(
                    "You need to call `Seismology.estimate_numax()` first."
                )
        return numax

    def _validate_deltanu(self, deltanu):
        """
        Raises exception if `deltanu` is None and `self.deltanu` is not set.
        """
        if deltanu is None:
            try:
                print("Check here")
                return self.env.dnu_val
            except AttributeError:
                raise AttributeError(
                    "You need to call `Seismology.estimate_deltanu()` first."
                )
        return deltanu

    def _update_dnu_slider_bounds(self):
        """Keep Delta Nu slider inside valid echelle geometry limits."""
        if getattr(self.env, "dnu_slider", None) is None:
            return

        freq = np.asarray(
            self.env.tb_other_periodogram.data.get("frequency", []),
            dtype=float,
        )
        if freq.size < 3:
            return

        freq_span = float(freq[-1] - freq[0])
        resolution = float(np.median(np.diff(freq)))

        if (
            not np.isfinite(freq_span)
            or freq_span <= 0
            or not np.isfinite(resolution)
            or resolution <= 0
        ):
            return

        min_dnu = max(0.01, resolution)
        max_dnu = min(float(self.env.maxdnu), freq_span)
        if max_dnu < min_dnu:
            max_dnu = min_dnu

        self.env.dnu_slider.start = min_dnu
        self.env.dnu_slider.end = max_dnu

        clamped_dnu = min(max(self.env.dnu_slider.value, min_dnu), max_dnu)
        if abs(self.env.dnu_slider.value - clamped_dnu) > 1e-12:
            self.env.dnu_slider.value = clamped_dnu
            self.env.dnu_val = clamped_dnu
            self.env.dnu_text.value = str(clamped_dnu)

    def _clean_echelle(
        self,
        deltanu=None,
        numax=None,
        minimum_frequency=None,
        maximum_frequency=None,
        smooth_filter_width=None,
        scale="linear",
    ):
        """
        Takes input seismology object and creates the necessary arrays for an echelle

        diagram. Validates all the inputs.

        Parameters
        ----------
        deltanu : float
            Value for the large frequency separation of the seismic mode
            frequencies in the periodogram. Assumed to have the same units as
            the frequencies, unless given an Astropy unit.
            Is assumed to be in the same units as frequency if not given a unit.
        numax : float
            Value for the frequency of maximum oscillation. If a numax is
            passed, a suitable range one FWHM of the mode envelope either side
            of the will be shown. This is overwritten by custom frequency ranges.
            Is assumed to be in the same units as frequency if not given a unit.
        minimum_frequency : float
            The minimum frequency at which to display the echelle
            Is assumed to be in the same units as frequency if not given a unit.
        maximum_frequency : float
            The maximum frequency at which to display the echelle.
            Is assumed to be in the same units as frequency if not given a unit.
        smooth_filter_width : float
            If given a value, will smooth periodogram used to plot the echelle
            diagram using the periodogram.smooth(method='boxkernel') method with
            a filter width of `smooth_filter_width`. This helps visualise the
            echelle diagram. Is assumed to be in the same units as the
            periodogram frequency.
        ax : `~matplotlib.axes.Axes`
            A matplotlib axes object to plot into. If no axes is provided,
            a new one will be created.
        scale: str
            Set z axis to be "linear" or "log". Default is linear.

        Returns
        -------
        ep : np.ndarray
            Echelle diagram power
        x_f : np.ndarray
            frequencies for X axis
        y_f : np.ndarray
            frequencies for Y axis
        """
        deltanu = self._validate_deltanu(deltanu)
        deltanu = deltanu * self.periodogram.frequency.unit

        freq = self.env.tb_other_periodogram.data["frequency"]
        power = self.env.tb_other_periodogram.data["power"]

        data_frame = pd.DataFrame()
        data_frame["freq"] = freq
        data_frame["power"] = power

        minf = int(self.env.minimum_frequency)
        maxf = int(self.env.maximum_frequency)
        data_frame = data_frame.query(
            "freq<@maxf & freq>@minf", local_dict={"minf": minf, "maxf": maxf}
        )
        if data_frame.empty:
            ep = np.array([])
            x_f = np.array([])
            y_f = np.array([])
            freq_values = np.array([])
            power_values = np.array([])
            y_original = np.array([])
            xx = np.array([])
            yy = np.array([])
        else:
            ep, x_f, y_f, xx, yy, freq_values, power_values = (
                self.apollinaire_echelle(
                    data_frame.freq.values,
                    data_frame.power.values,
                    deltanu.value,
                )
            )

            if len(y_f) < 1:
                ep = np.array([])
                x_f = np.array([])
                y_f = np.array([])
                freq_values = np.array([])
                power_values = np.array([])
                y_original = np.array([])
                xx = np.array([])
                yy = np.array([])
            else:
                y_original = y_f
                if len(y_f) < 2:
                    mean_diff = float(deltanu.value)
                else:
                    mean_diff = np.mean(np.diff(y_f))
                y_f_extra = y_f - (mean_diff / 2)
                val = y_f[-1] + (mean_diff / 2)
                center_y = np.append(y_f_extra, val)
                y_f = center_y

        ep = ep * self.env.power_unit
        x_f = x_f * self.env.frequency_unit
        y_f = y_f * self.env.frequency_unit
        return ep, x_f, y_f, y_original, xx, yy, freq_values, power_values

    def inverted_slider(self):

        if self.env.inverted_slider is None:
            sld_val = float(self.env.inverted_slider_max_value_text.value)
            self.env.inverted_slider = Slider(
                start=-1 * sld_val,
                end=sld_val,
                value=0,
                step=1,
                title="Inverted Slider",
                sizing_mode="stretch_width",
            )

            color_map = self.mode_color_map
            color_mapper = CategoricalColorMapper(
                factors=list(color_map.keys()),
                palette=list(color_map.values()),
            )

            df_second = self.tb_se_second_source.to_df()
            df_second["initial_y"] = 0
            length = 10
            factor = -1
            df_second["final_y"] = factor * df_second["Power"] + length
            df_second = df_second[
                ["Frequency", "initial_y", "final_y", "Power", "Mode"]
            ]
            self.inverted_slider_source = ColumnDataSource(
                df_second.to_dict("list")
            )

            self.env.fig_other_periodogram.segment(
                x0="Frequency",
                y0="initial_y",
                x1="Frequency",
                y1="final_y",
                name="vertical_inverted_line",
                source=self.inverted_slider_source,
                line_dash="dotted",
                color={"field": "Mode", "transform": color_mapper},
            )

            self.env.inverted_slider.on_change(
                "value", self.update_inverted_slider
            )
        # else:
        #     self.update_inverted_slide(0,0,0)

    def update_inverted_slider(self, attr, old, new):
        """
        Update x value
        """
        self.env.inverted_line_xvalue_text.value = str(
            self.env.inverted_slider.value
        )
        self.update_inverted_line()

    def update_inverted_line(self):
        """
        Update inverted line
        """
        sld_val = float(self.env.inverted_slider_max_value_text.value)
        self.env.inverted_slider.start = -1 * sld_val
        self.env.inverted_slider.end = 1 * sld_val

        df_second = self.tb_se_second_source.to_df()
        df_second["initial_y"] = float(
            self.env.inverted_line_initial_y_text.value
        )
        length = float(self.env.inverted_line_length_text.value)
        factor = float(self.env.inverted_line_scale_text.value)
        df_second["final_y"] = factor * df_second["Power"] + length
        df_second = df_second[
            ["Frequency", "initial_y", "final_y", "Power", "Mode"]
        ]
        value = float(self.env.inverted_line_xvalue_text.value)
        df_second["Frequency"] = df_second["Frequency"] + value
        old_data = ColumnDataSource(df_second.to_dict("list"))
        self.inverted_slider_source.data = dict(old_data.data)

        if self.env.check_show_inverted_lines.active[0] == 0:
            # print('Showing inverted line')
            # self.env.fig_other_periodogram.select('vertical_line').visible = True
            self.env.fig_other_periodogram.select(
                "vertical_inverted_line"
            ).visible = True
        else:
            # print('Turn off inverted line')
            old_data = ColumnDataSource(data=dict())
            self.inverted_slider_source.data = dict(old_data.data)

            # self.env.fig_other_periodogram.select('vertical_line').visible = False
            self.env.fig_other_periodogram.select(
                "vertical_inverted_line"
            ).visible = False

    def _make_echelle_elements(
        self,
        deltanu,
        cmap="hot",
        minimum_frequency=None,
        maximum_frequency=None,
        smooth_filter_width=None,
        scale="linear",
        width=490,
        height=340,
        title="Echelle",
    ):
        """
        Helper function to make the elements of the echelle diagram for bokeh plotting.
        """

        freq = (
            self.env.tb_other_periodogram.data["frequency"]
            * self.env.frequency_unit
        )
        ep = self.env.tb_echelle_diagram.data["image"] * self.env.power_unit

        fig = figure(
            sizing_mode="stretch_width",
            height=600,
            # x_range=(0, 1), y_range=(y_f[0].value, y_f[-1].value),
            title=title,
            tools="pan,box_zoom,reset,lasso_select",
            toolbar_location="above",
            border_fill_color="white",
            tooltips=self.env.TOOLTIPS,
        )

        fig.yaxis.axis_label = r"Frequency [{}]".format(freq.unit.to_string())
        fig.xaxis.axis_label = r"Frequency / {:.3f} Mod. 1".format(deltanu)
        if ep.value.any():
            lo, hi = np.nanpercentile(ep.value, [0.1, 99.9])
        else:
            lo = 10
            hi = 100
        vlo, vhi = 0.3 * lo, 1.7 * hi
        vstep = (lo - hi) / 500
        color_mapper = LogColorMapper(palette=self.palette, low=lo, high=hi)

        fig.image(
            image="image",
            x="xmin",
            y="ymin",
            dw="dw",
            dh="dh",
            color_mapper=color_mapper,
            name="img",
            source=self.env.tb_echelle_diagram,
        )
        stretch_slider = RangeSlider(
            start=vlo,
            end=vhi,
            step=vstep,
            title="",
            value=(lo, hi),
            #  orientation='vertical',
            width=700,
            height=50,
            direction="ltr",
            show_value=True,
            name="stretch",
        )

        def stretch_change_callback(attr, old, new):
            """TPF stretch slider callback."""
            fig.select("img")[0].glyph.color_mapper.high = new[1]
            fig.select("img")[0].glyph.color_mapper.low = new[0]

        stretch_slider.on_change("value", stretch_change_callback)

        return fig, stretch_slider

    def _on_dnu_slider_change(self, attr, old, new):
        """Callback to take action when dnu slider changes"""
        dnu = SeismologyQuantity(
            quantity=self.env.dnu_slider.value * self.env.frequency_unit,
            name="deltanu",
            method="echelle",
        )

        self.make_tb_echelle_diagram()
        self.make_grid()

        self.env.fig_tpfint.xaxis.axis_label = (
            r"Frequency / {:.3f} Mod. 1".format(dnu)
        )
        self.env.dnu_val = self.env.dnu_slider.value
        self.env.dnu_text.value = str(self.env.dnu_slider.value)

    def _on_dnu_right_small_click(self):
        """Step forward in dnu by a small amount"""
        existing_value = self.env.dnu_slider.value
        if existing_value < self.env.maxdnu:
            self.env.dnu_slider.value = existing_value + 0.1

    def _on_dnu_left_small_click(self):
        """Step back in dnu by a small amount"""
        existing_value = self.env.dnu_slider.value
        if existing_value > self.env.dnu_slider.start:
            self.env.dnu_slider.value = existing_value - 0.1

    def _on_dnu_right_click(self):
        """Step forward in dnu"""
        existing_value = self.env.dnu_slider.value
        if existing_value < self.env.maxdnu:
            self.env.dnu_slider.value = existing_value + 1

    def _on_dnu_left_click(self):
        """Step back in dnu"""
        existing_value = self.env.dnu_slider.value
        if existing_value > self.env.dnu_slider.start:
            self.env.dnu_slider.value = existing_value - 1

    def _on_grid_color_change(self, attr, old, new):
        """Callback to change grid color"""
        color = self.env.select_grid_color.color
        self.mode_color_map["999"] = color

        # Update Echelle grid circles
        grid_circles = self.env.fig_tpfint.select("grid_circle")
        if grid_circles:
            new_mapper = CategoricalColorMapper(
                factors=list(self.mode_color_map.keys()),
                palette=list(self.mode_color_map.values()),
            )
            for gr in grid_circles:
                if hasattr(gr, "glyph"):
                    mapper_dict = {
                        "field": "Mode",
                        "transform": new_mapper,
                    }
                    gr.glyph.line_color = mapper_dict
                    # Also update fill_color if it's not a hollow marker
                    marker_val = self.env.select_grid_marker.value
                    if not marker_val.endswith("_hollow"):
                        if hasattr(gr.glyph, "fill_color"):
                            gr.glyph.fill_color = mapper_dict

        # Update Echelle grid rectangles
        grid_rects = self.env.fig_tpfint.select("grid")
        for gr in grid_rects:
            if hasattr(gr, "glyph"):
                if hasattr(gr.glyph, "fill_color"):
                    gr.glyph.fill_color = color
                if hasattr(gr.glyph, "line_color"):
                    gr.glyph.line_color = "black"

    def _on_grid_marker_change(self, attr, old, new):
        """Callback to change grid marker shape"""
        marker_val = self.env.select_grid_marker.value
        hollow = marker_val.endswith("_hollow")
        base_marker = marker_val.replace("_hollow", "")

        grid_circles = self.env.fig_tpfint.select("grid_circle")
        for gr in grid_circles:
            if hasattr(gr, "glyph"):
                gr.glyph.marker = base_marker
                if hollow:
                    gr.glyph.fill_alpha = 0.0
                    gr.glyph.fill_color = None
                else:
                    gr.glyph.fill_alpha = 0.7
                    if hasattr(gr.glyph, "line_color"):
                        if isinstance(gr.glyph.line_color, dict):
                            gr.glyph.fill_color = dict(gr.glyph.line_color)
                        else:
                            gr.glyph.fill_color = gr.glyph.line_color

    def _on_grid_circle_size_change(self, attr, old, new):
        """Callback to change grid circle size"""
        try:
            val = int(new)
            grid_circles = self.env.fig_tpfint.select("grid_circle")
            for gr in grid_circles:
                if hasattr(gr, "glyph"):
                    gr.glyph.size = val
                if hasattr(r := gr, "selection_glyph") and r.selection_glyph:
                    r.selection_glyph.size = val
                if (
                    hasattr(r := gr, "nonselection_glyph")
                    and r.nonselection_glyph
                ):
                    r.nonselection_glyph.size = val
        except ValueError:
            pass

    def interact_echelle(self, notebook_url="localhost:8888", **kwargs):
        """
        Display an interactive Jupyter notebook widget showing an Echelle
        diagram.

        This feature only works inside an active Jupyter Notebook, and
        requires an optional dependency, ``bokeh`` (v1.0 or later).
        This dependency can be installed using e.g. `conda install bokeh`.

        Parameters
        ----------
        notebook_url : str
            Location of the Jupyter notebook page (default: "localhost:8888")
            When showing Bokeh applications, the Bokeh server must be
            explicitly configured to allow connections originating from
            different URLs. This parameter defaults to the standard notebook
            host and port. If you are running on a different location, you
            will need to supply this value for the application to display
            properly. If no protocol is supplied in the URL, e.g. if it is
            of the form "localhost:8888", then "http" will be used.
        """

        dnu = SeismologyQuantity(
            quantity=self.env.dnu_val
            * u.Unit(self.env.source_target_frequency_unit),
            name="deltanu",
            method="echelle",
        )
        self.dnu_interact = dnu

        self.env.fig_tpfint, self.env.stretch_sliderint = (
            self._make_echelle_elements(deltanu=dnu, **kwargs)
        )
        # maxdnu = self.periodogram.frequency.max().value/5
        maxdnu = self.env.maxdnu
        # Interactive slider widgets
        self.env.dnu_slider = Slider(
            start=0.01,
            end=maxdnu,
            value=dnu.value,
            step=0.01,
            title="Delta Nu",
            width=290,
        )
        self.env.r_button = Button(
            label=">", button_type=self.env.button_type, width=30
        )
        self.env.l_button = Button(
            label="<", button_type=self.env.button_type, width=30
        )
        self.env.rr_button = Button(
            label=">>", button_type=self.env.button_type, width=30
        )
        self.env.ll_button = Button(
            label="<<", button_type=self.env.button_type, width=30
        )

        self.env.select_grid_color = ColorPicker(
            title="Grid Color",
            color=self.mode_color_map["999"],
            width=65,
            sizing_mode="fixed",
        )

        # Attach callbacks
        self.env.dnu_slider.on_change("value", self._on_dnu_slider_change)
        self.env.r_button.on_click(self._on_dnu_right_small_click)
        self.env.l_button.on_click(self._on_dnu_left_small_click)
        self.env.rr_button.on_click(self._on_dnu_right_click)
        self.env.ll_button.on_click(self._on_dnu_left_click)

        self.env.select_grid_color.on_change(
            "color", self._on_grid_color_change
        )
        self.env.grid_circle_size.on_change(
            "value", self._on_grid_circle_size_change
        )
        self.env.select_grid_marker.on_change(
            "value", self._on_grid_marker_change
        )

    def apollinaire_echelle(
        self,
        freq,
        PSD,
        dnu,
        twice=False,
        fig=None,
        index=111,
        figsize=(16, 16),
        title=None,
        smooth=10,
        cmap="cividis",
        cmap_scale="linear",
        mode_freq=None,
        mode_freq_err=None,
        vmin=None,
        vmax=None,
        scatter_color="white",
        fmt="+",
        ylim=None,
        shading="gouraud",
        mfc="none",
        ms=20,
        index_offset=None,
        mec=None,
        xlabel=None,
        ylabel=None,
        **kwargs,
    ):
        """
        Build the echelle diagram of a given PSD.

        :param freq: input vector of frequencies.
        :type freq: ndarray

        :param PSD: input vector of power. Must be of same size than freq.
        :type PSD: ndarray

        :param dnu: the large frequency separation use to cut slices
            into the diagram.
        :type dnu: float

        :param twice: slice using 2 x *dnu* instead of *dnu*, default False.
        :type twice: bool

        :param fig: figure on which the echelle diagram will be plotted. If ``None``, a new figure
            instance will be created. Optional, default ``None``.
        :type fig: matplotlib Figure

        :param index: position of the echelle diagram Axe in the figure. Optional, default ``111``.
        :type index: int

        :param figsize: size of the echelle diagram to plot.
        :type figsize: tuple

        :param title: title of the figure. Optional, default ``(16, 16)``
        :type title: str

        :param smooth: size of the rolling window used to smooth the PSD. Default 10.
        :type smooth: int

        :param cmap: select one available color map provided by matplotlib, default ``cividis``
        :type cmap: str

        :param cmap_scale: scale use for the colormap. Can be 'linear' or 'logarithmic'.
            Optional, default 'linear'.
        :type cmap_scale: str

        :param mode_freq: frequency array of the modes to represent on the diagram. It can be single
            array or a tuple of array.
        :type mode_freq: ndarray or tuple of array

        :param mode_freq_err: frequency uncertainty of the modes to represent on the diagram. It can be
            a single array or a tuple of array.
        :type mode_freq_err: ndarray or tuple of array

        :param vmin: minimum value for the colormap.
        :type vmin: float

        :param vmax: maximum value for the colormap.
        :type vmax: float

        :param scatter_color: color of the scatter point of the mode frequencies. Optional, default ``white``.
        :type scatter_color: str

        :param fmt: the format of the errorbar to plot. Can be a single string or a tuple of string with the same
            dimension that ``mode_freq``.
        :type fmt: str or tuple

        :param ylim: the y-bounds of the echelle diagram.
        :type ylim: tuple

        :param mew: marker edge width. Optional, default 1.
        :type mew: float

        :param markersize: size of the markers used for the errorbar plot. Optional, default 10.
        :type markersize: float

        :param capsize: length of the error bar caps. Optional, default 2.
        :type capsize: float

        :return: the matplotlib Figure with the echelle diagram.
        :rtype: matplotlib Figure
        """

        # if cmap_scale not in ['linear', 'logarithmic'] :
        #     raise Exception ("cmap_scale should be set to 'linear' or 'logarithmic'.")

        # if cmap_scale=='logarithmic':
        #     norm = colors.LogNorm (vmin=vmin, vmax=vmax)
        # elif cmap_scale=='linear' :
        #     norm = colors.Normalize (vmin=vmin, vmax=vmax)

        # if smooth != 1 :
        #     PSD = pd.Series(data=PSD).rolling (window=smooth, min_periods=1,
        #                             center=True).mean().to_numpy()

        # if twice==True :
        #     dnu = 2.*dnu
        # print('freq',freq)
        if len(freq) < 3 or len(PSD) < 3:
            log.warning(
                "Skipping echelle build due to insufficient data points: "
                "len(freq)=%s, len(PSD)=%s",
                len(freq),
                len(PSD),
            )
            return np.array([]), np.array([]), np.array([]), [], [], [], []

        res = freq[2] - freq[1]

        # if index_offset is not None :
        #     PSD = PSD[index_offset:]
        #     freq = freq[index_offset:]

        n_slice = int(np.floor_divide(freq[-1] - freq[0], dnu))
        len_slice = int(np.floor_divide(dnu, res))

        if n_slice < 1 or len_slice < 1:
            log.warning(
                "Skipping echelle build for invalid deltanu geometry: "
                "dnu=%s, freq_span=%s, res=%s, n_slice=%s, len_slice=%s",
                dnu,
                freq[-1] - freq[0],
                res,
                n_slice,
                len_slice,
            )
            return np.array([]), np.array([]), np.array([]), [], [], [], []

        if n_slice * len_slice > PSD.size:
            len_slice -= 1

        ed = PSD[: len_slice * n_slice]
        ed = np.reshape(ed, (n_slice, len_slice))

        freq_ed = freq[: len_slice * n_slice]
        freq_ed = np.reshape(freq_ed, (n_slice, len_slice))
        # x_freq = freq_ed[0,:] - freq_ed[0,0]
        x_freq = freq_ed[0, :]
        y_freq = freq_ed[:, 0]

        value = freq_ed
        x = x_freq
        y = y_freq
        xx, yy = np.meshgrid(x, y, sparse=False)
        xx = xx.reshape(-1, 1).flatten().tolist()
        yy = yy.reshape(-1, 1).flatten().tolist()
        freq_values = value.reshape(-1, 1).flatten().tolist()

        power_values = ed.reshape(-1, 1).flatten().tolist()

        return ed, x_freq, y_freq, xx, yy, freq_values, power_values

    def clear_se_table1(self):
        """This function clear table1 of the program"""

        old_data = ColumnDataSource(
            data=dict(Slicefreq=[], Frequency=[], Power=[], Mode=[], xx=[])
        )
        self.tb_se_first_source.data = dict(old_data.data)

    def clear_se_table2(self):
        """This function clear table1 of the program"""

        old_data = ColumnDataSource(
            data=dict(Slicefreq=[], Frequency=[], Power=[], Mode=[], xx=[])
        )
        self.tb_se_second_source.data = dict(old_data.data)

    def find_peak_frequencies(self):
        """
        Find peak of the frequencies
        """

        df_se = pd.DataFrame()
        yy = self.env.tb_grid_source.data["yy"]
        xx = self.env.tb_grid_source.data["xx"]
        freq_values = self.env.tb_grid_source.data["freq_values"]
        power_values = self.env.tb_grid_source.data["power_values"]

        df_se["freq_values"] = freq_values
        df_se["power_values"] = power_values
        df_se["xx"] = xx
        df_se["yy"] = yy

        se_indices = self.env.tb_grid_source.selected.indices
        real_freq = df_se.freq_values.loc[se_indices]
        real_power = df_se.power_values.loc[se_indices]
        xx_f = df_se.xx.loc[se_indices]
        yy_f = df_se.yy.loc[se_indices]
        df_se_freq = pd.DataFrame()
        df_se_freq["indices"] = se_indices
        df_se_freq["real_freq"] = real_freq.values
        df_se_freq["real_power"] = real_power.values
        df_se_freq["xx"] = xx_f.values
        df_se_freq["yy"] = yy_f.values

        # print(' All selected freq', df_se_freq)
        max_indices = df_se_freq.groupby("yy")["real_power"].idxmax()
        # print(' Max indices', max_indices)
        df_se_freq = df_se_freq.loc[max_indices]
        # print(' filetered', df_se_freq)

        self.env.tb_other_periodogram.selected.indices = list([])

        self.env.tb_grid_source.selected.indices = df_se_freq[
            "indices"
        ].to_list()
        self.clear_se_table1()
        self.get_all_selection_button()
        self.get_all_selection_button()

    def selection_prd_to_grid_fig(self, attrname, old, new):
        """
        Selected frequencies from prd to grid
        """

        se_indices = self.env.tb_other_periodogram.selected.indices

        df_prd = self.env.tb_other_periodogram.to_df()
        df_prd = df_prd.loc[se_indices]

        selected_freq = list(
            np.round(df_prd.frequency.values, self.env.freq_round)
        )

        se_indices = self.env.tb_grid_source.selected.indices
        df_grid = self.env.tb_grid_source.to_df()

        df_grid["freq_values"] = df_grid["freq_values"].round(
            self.env.freq_round
        )

        df_grid = df_grid.query(
            "freq_values==@selected_freq",
            local_dict={"selected_freq": selected_freq},
        )

        all_list = df_grid.index.to_list() + se_indices

        # print('all list', all_list)
        if set(all_list) != set(se_indices):
            self.env.tb_grid_source.selected.indices = list(set(all_list))
            self.update_plot(0, 0, 0)
        self.env.fig_other_periodogram.x_range.start = int(
            self.env.minimum_frequency
        )
        self.env.fig_other_periodogram.x_range.end = int(
            self.env.maximum_frequency
        )

    def selection_grid_to_table_fig(self, attrname, old, new):
        """
        Selection from grid to table
        """

        se_indices = self.env.tb_grid_source.selected.indices
        # print('Grid2Table se_indices', se_indices)
        df_grid = self.env.tb_grid_source.to_df()
        df_grid = df_grid.loc[se_indices]
        df_table = self.tb_se_first_source.to_df()
        df_grid.rename(
            columns={
                "yy": "Slicefreq",
                "freq_values": "Frequency",
                "power_values": "Power",
            },
            inplace=True,
        )
        df_grid = df_grid[["Slicefreq", "Frequency", "Power", "Mode", "xx"]]
        all_data = pd.concat([df_table, df_grid], ignore_index=True)
        # print(all_data)
        all_data["Frequency"] = all_data["Frequency"].round(
            self.env.freq_round
        )
        all_data = all_data.drop_duplicates(subset=["Frequency"])
        # print(all_data.to_dict())
        # old_data = ColumnDataSource.from_df(all_data)
        df = all_data
        old_data = ColumnDataSource(
            data=dict(
                Slicefreq=df["Slicefreq"].to_list(),
                Frequency=df["Frequency"].to_list(),
                Power=df["Power"].to_list(),
                Mode=df["Mode"].to_list(),
                xx=df["xx"].to_list(),
            )
        )

        self.tb_se_first_source.data = dict(old_data.data)

    def selection_grid_to_prd_fig(self, attrname, old, new):
        """
        This function will not be used, slowing the system
        This function moves selected indices in grid to periodogram

        """

        df_se = pd.DataFrame()
        yy = self.env.tb_grid_source.data["yy"]
        xx = self.env.tb_grid_source.data["xx"]
        freq_values = self.env.tb_grid_source.data["freq_values"]

        df_se["xx"] = xx
        df_se["yy"] = yy
        df_se["freq_values"] = freq_values

        se_indices = self.env.tb_grid_source.selected.indices
        slice_freq = df_se.loc[se_indices]["yy"]
        mod_val = df_se.loc[se_indices]["xx"]

        real_freq = slice_freq + (self.env.dnu_val * mod_val)
        real_freq = real_freq.round(self.env.freq_round)
        real_freq = mod_val

        real_freq = df_se.freq_values.loc[se_indices]

        real_freq = np.round(real_freq, self.env.freq_round)
        real_freq = real_freq.tolist()

        df_prd = self.env.tb_other_periodogram.to_df()
        df_prd["frequency"] = df_prd["frequency"].round(self.env.freq_round)
        df_prd = df_prd.query(
            "frequency==@real_freq", local_dict={"real_freq": real_freq}
        )
        all_list = (
            self.env.tb_other_periodogram.selected.indices
            + df_prd.index.to_list()
        )

        if set(all_list) != set(
            self.env.tb_other_periodogram.selected.indices
        ):
            self.env.tb_other_periodogram.selected.indices = list(
                set(all_list)
            )

    def selection_table_to_prd_fig(self, attrname, old, new):
        """
        Table to prd selection
        """

        df_table = self.tb_se_first_source.to_df()
        selected_freq = df_table["Frequency"].round(self.env.freq_round)
        selected_freq = selected_freq.to_list()

        df_prd = self.env.tb_other_periodogram.to_df()
        df_prd["frequency"] = df_prd["frequency"].round(self.env.freq_round)
        df_prd = df_prd.query(
            "frequency==@selected_freq",
            local_dict={"selected_freq": selected_freq},
        )

        all_list = (
            self.env.tb_other_periodogram.selected.indices
            + df_prd.index.to_list()
        )

        if set(all_list) != set(
            self.env.tb_other_periodogram.selected.indices
        ):
            self.env.tb_other_periodogram.selected.indices = list(
                set(all_list)
            )

    def selection_table2_to_prd_fig(self, attrname, old, new):
        """
        Table to prd selection
        """

        df_table = self.tb_se_second_source.to_df()
        selected_freq = df_table["Frequency"].round(self.env.freq_round)
        selected_freq = selected_freq.to_list()

        df_prd = self.env.tb_other_periodogram.to_df()
        df_prd["frequency"] = df_prd["frequency"].round(self.env.freq_round)
        df_prd = df_prd.query("frequency==@selected_freq")

        all_list = (
            self.env.tb_other_periodogram.selected.indices
            + df_prd.index.to_list()
        )

        if set(all_list) != set(
            self.env.tb_other_periodogram.selected.indices
        ):
            self.env.tb_other_periodogram.selected.indices = list(
                set(all_list)
            )

    def get_all_selection_button(self):
        """
        Move selection around
        """
        self.publish_message("Getting Selection")
        self.selection_grid_to_prd_fig(0, 0, 0)
        self.selection_prd_to_grid_fig(0, 0, 0)
        self.selection_grid_to_table_fig(0, 0, 0)
        self.publish_message("Ready")

    def clear_se_grid_prd(self):
        """
        Clear all selections
        """
        self.env.tb_other_periodogram.selected.indices = list([])
        self.env.tb_grid_source.selected.indices = list([])
        self.update_plot(0, 0, 0)

    def click_mode_apply_button(self):
        """
        Change mode selection for selected rows in Table 1 or Table 2.
        """
        self._set_busy(True)
        try:
            new_mode = self.env.select_mode_menu.value
            new_color = self.mode_color_map.get(str(new_mode), "grey")

            freqs_to_update = []

            # 1. Update Table 1 selections
            indices1 = self.tb_se_first_source.selected.indices
            if indices1:
                df1 = self.tb_se_first_source.to_df()
                for idx in indices1:
                    df1.at[idx, "Mode"] = new_mode
                    if "mode_color" in df1.columns:
                        df1.at[idx, "mode_color"] = new_color
                    freqs_to_update.append(df1.at[idx, "Frequency"])
                self.tb_se_first_source.data = df1.to_dict("list")

            # 2. Update Table 2 selections
            indices2 = self.tb_se_second_source.selected.indices
            if indices2:
                df2 = self.tb_se_second_source.to_df()
                for idx in indices2:
                    df2.at[idx, "Mode"] = new_mode
                    if "mode_color" in df2.columns:
                        df2.at[idx, "mode_color"] = new_color
                    freqs_to_update.append(df2.at[idx, "Frequency"])
                self.tb_se_second_source.data = df2.to_dict("list")

            if not freqs_to_update:
                print("No rows selected for mode application")
                return

            # 3. Update master grid (tb_grid_source) to reflect changes in Echelle diagram
            df_grid = self.env.tb_grid_source.to_df()
            df_grid["freq_tmp"] = df_grid["freq_values"].round(
                self.env.freq_round
            )
            rounded_freqs = [
                round(f, self.env.freq_round) for f in freqs_to_update
            ]
            mask_grid = df_grid["freq_tmp"].isin(rounded_freqs)
            df_grid.loc[mask_grid, "Mode"] = new_mode
            df_grid = df_grid.drop(columns=["freq_tmp"])
            self.env.tb_grid_source.data = df_grid.to_dict("list")

            # 4. Update PSD plot (tb_other_periodogram) to reflect changes in mode colors
            df_other = self.env.tb_other_periodogram.to_df()
            df_other["freq_tmp"] = df_other["frequency"].round(
                self.env.freq_round
            )
            mask_other = df_other["freq_tmp"].isin(rounded_freqs)
            df_other.loc[mask_other, "Mode"] = new_mode
            df_other = df_other.drop(columns=["freq_tmp"])
            self.env.tb_other_periodogram.data = df_other.to_dict("list")

            # 5. Refresh UI
            self.update_plot(0, 0, 0)

            # Clear selections after applying
            self.tb_se_first_source.selected.indices = []
            self.tb_se_second_source.selected.indices = []
            self.env.tb_other_periodogram.selected.indices = []
        finally:
            self._set_busy(False)

    def click_move_se_1_2_button(self):
        """
        Move selected frequencies from table 1 to table 2
        """
        indices = self.tb_se_first_source.selected.indices
        if not indices:
            print("No rows selected in Table 1")
            return

        df_table1 = self.tb_se_first_source.to_df()

        # Rows to move
        df_to_move = df_table1.iloc[indices].copy()

        # Rows to keep in Table 1
        df_remaining = df_table1.drop(df_table1.index[indices])

        # Update Table 1
        self.tb_se_first_source.data = df_remaining.to_dict("list")
        self.tb_se_first_source.selected.indices = []  # Clear selection

        # Update Table 2
        df_table2 = self.tb_se_second_source.to_df()
        df_new_table2 = pd.concat([df_table2, df_to_move], ignore_index=True)

        # Ensure mode_color is present if it's missing in some rows
        if "mode_color" in self.tb_se_second_source.data:
            df_new_table2["mode_color"] = [
                self.mode_color_map.get(str(m), "grey")
                for m in df_new_table2["Mode"]
            ]

        self.tb_se_second_source.data = df_new_table2.to_dict("list")

    def _set_busy(self, busy=True):
        if busy:
            self.env.message_banner.text = "<b>Busy... Processing request</b>"
            self.env.mode_apply_button.label = "Applying..."
            self.env.mode_apply_button.disabled = True
            self.env.delete_row_button.disabled = True
        else:
            self.env.mode_apply_button.label = "Apply Mode"
            self.env.mode_apply_button.disabled = False
            self.env.delete_row_button.disabled = False
            self.publish_message(text="Ready")

    def click_delete_row_button(self):
        """
        Delete selected rows from Table 1 and Table 2.
        Also sync with master grid and PSD plot.
        """
        self._set_busy(True)
        try:
            freqs_to_delete = []

            # 1. Delete from Table 1
            indices1 = self.tb_se_first_source.selected.indices
            if indices1:
                df1 = self.tb_se_first_source.to_df()
                freqs_to_delete.extend(
                    df1.iloc[indices1]["Frequency"].to_list()
                )
                df_remaining1 = df1.drop(df1.index[indices1])
                self.tb_se_first_source.data = df_remaining1.to_dict("list")
                self.tb_se_first_source.selected.indices = []

            # 2. Delete from Table 2
            indices2 = self.tb_se_second_source.selected.indices
            if indices2:
                df2 = self.tb_se_second_source.to_df()
                freqs_to_delete.extend(
                    df2.iloc[indices2]["Frequency"].to_list()
                )
                df_remaining2 = df2.drop(df2.index[indices2])
                self.tb_se_second_source.data = df_remaining2.to_dict("list")
                self.tb_se_second_source.selected.indices = []

            if not freqs_to_delete:
                print("No rows selected for deletion")
                return

            # 3. Sync with Master Grid (tb_grid_source)
            df_grid = self.env.tb_grid_source.to_df()
            df_grid["freq_tmp"] = df_grid["freq_values"].round(
                self.env.freq_round
            )
            rounded_freqs = [
                round(f, self.env.freq_round) for f in freqs_to_delete
            ]
            mask_grid = df_grid["freq_tmp"].isin(rounded_freqs)
            df_grid.loc[mask_grid, "Mode"] = (
                "999"  # Changed from "None" to "999" to match default mode
            )
            df_grid = df_grid.drop(columns=["freq_tmp"])
            self.env.tb_grid_source.data = df_grid.to_dict("list")

            # 4. Sync with PSD plot (tb_other_periodogram)
            df_other = self.env.tb_other_periodogram.to_df()
            df_other["freq_tmp"] = df_other["frequency"].round(
                self.env.freq_round
            )
            mask_other = df_other["freq_tmp"].isin(rounded_freqs)
            df_other.loc[mask_other, "Mode"] = (
                "999"  # Changed from "None" to "999" to match default mode
            )
            df_other = df_other.drop(columns=["freq_tmp"])
            self.env.tb_other_periodogram.data = df_other.to_dict("list")

            # 5. Refresh UI
            self.update_plot(0, 0, 0)
            self.env.tb_other_periodogram.selected.indices = []
        finally:
            self._set_busy(False)

    def click_move_se_2_1_button(self):
        """
        Move selected frequencies from table 2 back to table 1
        """
        indices = self.tb_se_second_source.selected.indices
        if not indices:
            print("No rows selected in Table 2")
            return

        df_table2 = self.tb_se_second_source.to_df()

        # Rows to move
        df_to_move = df_table2.iloc[indices].copy()

        # Rows to keep in Table 2
        df_remaining = df_table2.drop(df_table2.index[indices])

        # Update Table 2
        self.tb_se_second_source.data = df_remaining.to_dict("list")
        self.tb_se_second_source.selected.indices = []  # Clear selection

        # Update Table 1
        df_table1 = self.tb_se_first_source.to_df()

        # Be careful to remove mode_color from moved rows if Table 1 doesn't want it
        if "mode_color" in df_to_move.columns:
            df_to_move = df_to_move.drop(columns=["mode_color"])

        df_new_table1 = pd.concat([df_table1, df_to_move], ignore_index=True)
        self.tb_se_first_source.data = df_new_table1.to_dict("list")

    def assign_mode_color(self, modes):
        val = list(modes)
        val = map(str, val)

        val = list(
            map(lambda x: x.replace("999", self.mode_color_map[-1]), val)
        )
        val = list(map(lambda x: x.replace("0", self.mode_color_map[0]), val))
        val = list(map(lambda x: x.replace("1", self.mode_color_map[1]), val))
        val = list(map(lambda x: x.replace("2", self.mode_color_map[2]), val))
        return val

    def trim_frequency(self):
        # print("Value of selection:", self.env.select_mode_menu.value)
        if self.env.select_grid_menu.value == "Obs":
            ff = np.array(self.env.input_tb.data["frequency"])
            pp = np.array(self.env.input_tb.data["power"])
        if self.env.select_grid_menu.value == "Syn":
            data = self.calculate_synthetic_psd()
            ff = data.freq.values
            pp = data.synthetic_psd.values
        if self.env.select_grid_menu.value == "Sub":
            data = self.calculate_synthetic_psd()
            data["sub_psd"] = data["psd"] - data["synthetic_psd"]
            ff = data.freq.values
            pp = data.sub_psd.values
        # print("Frequency and power", ff, pp)

        min_freq = int(self.env.minimum_frequency) * self.env.frequency_unit
        max_freq = int(self.env.maximum_frequency) * self.env.frequency_unit
        print(
            f"DEBUG trim_frequency: ff shape={ff.shape}, min={min_freq}, max={max_freq}"
        )
        if ff.size > 0:
            print(f"DEBUG trim_frequency: ff range=[{ff.min()}, {ff.max()}]")

        ind = (ff <= max_freq.value) & (ff >= min_freq.value)
        print(f"DEBUG trim_frequency: ind count={np.sum(ind)}")

        f = ff[ind]
        p = pp[ind]
        if f.any():
            # print('frequency',f)
            f = f * self.env.frequency_unit
            p = p * self.env.power_unit
            mm = ["999"] * (len(p))

            # self.env.minimum_frequency = f.min().value
            # self.env.maximum_frequency = f.max().value

            # print(f,p)
            period = lk_prd_module.Periodogram(f, p)
            self.periodogram = period

            old_data = ColumnDataSource(
                data=dict(
                    frequency=list(self.periodogram.frequency.value),
                    power=list(self.periodogram.power.value),
                    Mode=mm,
                    # cuttoff=list(np.array([0])),
                )
            )
            self.env.tb_other_periodogram.data = dict(old_data.data)
        else:
            print("No frequencies in the range")
            old_data = ColumnDataSource(
                data=dict(
                    frequency=list([]),
                    power=list([]),
                    Mode=list([]),
                    # cuttoff=list(np.array([0])),
                )
            )

            self.env.tb_other_periodogram.data = dict(old_data.data)

    def save_table_2(self):

        df = self.tb_se_second_source.to_df()
        id = self.env.tb_source.data["id"][0]
        data_folder = self.env.tb_source.data["data_folder"][id]
        # id_mycatalog = self.env.tb_source.data["id_mycatalog_all"][id]
        file_name = data_folder + "/" + "modes_parameter_selected.csv"
        df.to_csv(file_name, index=False)
        # length_of_columns = len(df.columns)

        df_pkb = pd.DataFrame(columns=self.pkb_columns_selected)

        df_pkb["nu"] = df["Frequency"].to_list()
        df_pkb["H"] = df["Power"].to_list()
        df_pkb["l"] = df["Mode"].astype(float).to_list()
        deltanu = self.env.dnu_val
        df_pkb["n"] = np.rint(df["Frequency"] / deltanu).astype(int)
        print("df_pkb", df_pkb)

        print("deltanu", deltanu)
        print(df_pkb["n"])
        df_pkb = df_pkb.fillna(0)

        pkb_array = df_pkb.values
        length_prefered = len(self.pkb_columns_selected)
        fmt = ["%.0f"] * 2 + ["%.4f"] * (length_prefered - 2)

        file_name = data_folder + "/" + "modes_parameter_selected.pkb"

        apol.peakbagging.save_pkb(file_name, pkb_array, fmt=fmt)
        print("PKB file saved to ", file_name)
        fulltext = "Saving " + file_name
        self.publish_message(text=fulltext)

    def load_pkb_to_second_tab(self, file_name):
        "Loads pkb, csv or txt and return df for second tab"
        ext = os.path.splitext(file_name)[1].lower()
        if ext == ".csv":
            df_load = pd.read_csv(file_name)
        else:
            try:
                df_load = pd.read_csv(file_name, sep=r"\s+")
                if (
                    "nu" not in df_load.columns
                    and "Frequency" not in df_load.columns
                ):
                    raise ValueError("Unknown columns")
            except Exception:
                pkb_array = np.atleast_2d(np.loadtxt(file_name, skiprows=0))
                if pkb_array.shape[1] == 26:
                    df_load = pd.DataFrame(
                        pkb_array, columns=self.env.pkb_columns_extended
                    )
                elif pkb_array.shape[1] == 18:
                    df_load = pd.DataFrame(
                        pkb_array, columns=self.env.pkb_columns
                    )
                else:
                    raise ValueError(
                        f"Unsupported file format (columns={pkb_array.shape[1]})."
                    )

        # Check if it's a raw table save or a PKB
        if "Frequency" in df_load.columns and "Power" in df_load.columns:
            print("Detected raw table format")
            df_load["Mode"] = df_load["Mode"].astype(str)
            # Ensure required columns for Table 2 are present
            required = ["Slicefreq", "Frequency", "Power", "Mode", "xx"]
            for col in required:
                if col not in df_load.columns:
                    # Fill missing columns with reasonable defaults or 0
                    if col == "Mode":
                        df_load[col] = "999"
                    else:
                        df_load[col] = 0
            return df_load[required]

        # Otherwise treat as PKB format and proceed to matching
        df_pkb = df_load
        if len(df_pkb.columns) == 26:
            self.pkb_columns_selected = self.env.pkb_columns_extended
        else:
            self.pkb_columns_selected = self.env.pkb_columns

        df_pkb["nu"] = df_pkb["nu"].astype(float).round(4)

        df_grid = self.env.tb_grid_source.to_df()

        # Fallback if the grid is filtered or empty
        if df_grid.empty or (
            len(df_grid) < 10
            and hasattr(self, "freq_values")
            and self.freq_values is not None
        ):
            print(
                "DEBUG: Grid is empty or too small, using internal frequency values for matching."
            )
            df_grid = pd.DataFrame(
                {
                    "freq_values": self.freq_values.round(self.env.freq_round),
                    "xx": self.xx,
                    "yy": self.yy,
                    "power_values": self.power_values,
                    "Mode": ["999"] * len(self.freq_values),
                }
            )

        if df_grid.empty:
            raise ValueError(
                "No Echelle grid available to match PKB frequencies. Please load a PSD first."
            )

        df_grid["freq_values"] = df_grid["freq_values"].round(
            self.env.freq_round
        )

        def find_nearest_value(x):
            return df_grid["freq_values"].iloc[
                (df_grid["freq_values"] - x).abs().idxmin()
            ]

        df_pkb["closest_frequency"] = df_pkb["nu"].apply(find_nearest_value)
        df_pkb_merged = df_pkb.merge(
            df_grid,
            left_on="closest_frequency",
            right_on="freq_values",
            how="left",
        )
        df_pkb_merged["freq_diff"] = abs(
            df_pkb_merged.nu - df_pkb_merged.closest_frequency
        )
        df_pkb_merged["Mode"] = df_pkb_merged["l"].astype(int)
        df_pkb_merged["Mode"] = df_pkb_merged["Mode"].astype(str)
        print(df_pkb_merged)
        if not df_pkb_merged.query("freq_diff>1").empty:

            print(
                "Warning frequency with greater than difference 1 is present, change frequency limit"
            )
        df_pkb = df_pkb_merged.query("freq_diff<1")
        print(
            f"DEBUG load_pkb_to_second_tab: df_pkb size after filter={len(df_pkb)}"
        )
        if len(df_pkb) == 0:
            print(
                f"DEBUG load_pkb_to_second_tab: df_pkb_merged size={len(df_pkb_merged)}"
            )
            if len(df_pkb_merged) > 0:
                print(
                    f"DEBUG load_pkb_to_second_tab: min freq_diff={df_pkb_merged['freq_diff'].min()}"
                )

        df_pkb.rename(
            columns={
                "yy": "Slicefreq",
                "freq_values": "Frequency",
                "power_values": "Power",
            },
            inplace=True,
        )
        df_pkb = df_pkb[["Slicefreq", "Frequency", "Power", "Mode", "xx"]]

        return df_pkb

    def load_table_2(self):
        self.publish_message(text="Loading pkb")
        id = self.env.tb_source.data["id"][0]
        data_folder = self.env.tb_source.data["data_folder"][id]
        # id_mycatalog = self.env.tb_source.data["id_mycatalog_all"][id]
        # file_name = data_folder+'/'+'modes_param.pkb'
        file_name = data_folder + "/" + "modes_parameter_selected.pkb"
        # modes_parameter_selected.pkb

        df = self.load_pkb_to_second_tab(file_name)

        old_data = ColumnDataSource(
            data=dict(
                Slicefreq=df["Slicefreq"].to_list(),
                Frequency=df["Frequency"].to_list(),
                Power=df["Power"].to_list(),
                Mode=df["Mode"].to_list(),
                xx=df["xx"].to_list(),
            )
        )
        self.tb_se_second_source.data = dict(old_data.data)
        self.apply_modes(df)
        print("Loaded", file_name)
        fulltext = "Loaded " + file_name
        self.publish_message(text=fulltext)

    def publish_message(self, text=""):
        self.env.message_banner.text = text

    def apply_modes(self, table):
        self.publish_message(text="Applying Modes")
        df_second = table
        df_second["Mode"] = df_second["Mode"].astype(str)
        df_second["Frequency"] = df_second["Frequency"].round(
            self.env.freq_round
        )
        for mode in df_second.Mode.unique():
            print("Applying", mode, df_second.Mode.unique())
            list_freq = df_second.query("Mode==@mode")["Frequency"].to_list()

            df_grid = self.env.tb_grid_source.to_df()
            df_grid["freq_values"] = df_grid["freq_values"].round(
                self.env.freq_round
            )
            ind = df_grid.query(
                "freq_values == @list_freq",
                local_dict={"list_freq": list_freq},
            ).index

            df_grid.loc[ind, "Mode"] = str(mode)
            old_data = ColumnDataSource(df_grid.to_dict("list"))
            self.env.tb_grid_source.data = dict(old_data.data)

            # ind = self.env.tb_other_periodogram.selected.indices

            df_other = self.env.tb_other_periodogram.to_df()
            df_other["frequency"] = df_other["frequency"].round(
                self.env.freq_round
            )
            ind = df_other.query("frequency == @list_freq").index

            df_other.loc[ind, "Mode"] = str(mode)
            old_data = ColumnDataSource(df_other.to_dict("list"))
            self.env.tb_other_periodogram.data = dict(old_data.data)
        self.publish_message(text="Ready")

    def load_from_file(self):
        """
        Load pkb from file using FileOpener
        """

        self.publish_message(text="Loading from File")

        file_path = ""
        manual_path_widget = getattr(
            self.env, "settings_manual_pkb_path", None
        )
        if manual_path_widget is not None:
            file_path = manual_path_widget.value.strip()

        if not file_path:
            opener = FileOpener(default_engine=self.env.dialog_engine)
            file_path = opener.get_open_filename(
                title="Select PKB File",
                filetypes=[
                    ("PKB Files", "*.pkb *.csv *.txt"),
                    ("All Files", "*.*"),
                ],
            )
        if file_path:
            file_path = str(Path(file_path).expanduser().resolve())
            self.publish_message(text="Loading from File")
            df = self.load_pkb_to_second_tab(file_path)

            mode_colors = [
                self.mode_color_map.get(str(m), "grey") for m in df["Mode"]
            ]

            old_data = ColumnDataSource(
                data=dict(
                    Slicefreq=df["Slicefreq"].to_list(),
                    Frequency=df["Frequency"].to_list(),
                    Power=df["Power"].to_list(),
                    Mode=df["Mode"].to_list(),
                    xx=df["xx"].to_list(),
                    mode_color=mode_colors,
                )
            )
            self.tb_se_second_source.data = dict(old_data.data)
            self.apply_modes(df)
            print("Loaded", file_path)
            self.env.selected_filename_pkb_text.text = file_path
            manual_path_widget = getattr(
                self.env, "settings_manual_pkb_path", None
            )
            if manual_path_widget is not None:
                manual_path_widget.value = file_path
            self.publish_message(text="File Loaded, Ready")

    def load_bkg_param_from_file(self):
        """
        Load BKG from background file using FileOpener
        """

        self.publish_message(text="Loading BKG from File")

        file_path = ""
        manual_path_widget = getattr(
            self.env, "settings_manual_bkg_path", None
        )
        if manual_path_widget is not None:
            file_path = manual_path_widget.value.strip()

        if not file_path:
            opener = FileOpener(default_engine=self.env.dialog_engine)
            file_path = opener.get_open_filename(
                title="Select Background File",
                filetypes=[("All Files", "*.*")],
            )
        if file_path:
            file_path = str(Path(file_path).expanduser().resolve())
            self.env.selected_filename_background_text.text = file_path
            manual_path_widget = getattr(
                self.env, "settings_manual_bkg_path", None
            )
            if manual_path_widget is not None:
                manual_path_widget.value = file_path
        self.publish_message(text="Ready")

    def save_as_table_2(self):
        self.publish_message(text="Saving File")
        opener = FileOpener(default_engine=self.env.dialog_engine)
        filename = opener.get_save_filename(
            title="Save Mode Selection Table",
            filetypes=[
                ("PKB Files", "*.pkb"),
                ("CSV Files", "*.csv"),
                ("Text Files", "*.txt"),
                ("All Files", "*.*"),
            ],
        )
        if filename:
            self._save_to_path(filename)

    def save_to_default_file(self):
        """Save Table 2 to the path manually specified in the Home tab."""
        filename = self.env.settings_manual_output_path.value.strip()
        if not filename:
            self.publish_message(
                text="Error: No output path specified in Home tab"
            )
            return

        self.publish_message(text=f"Saving to {filename}")
        self._save_to_path(filename)

    def _save_to_path(self, filename):
        """Core logic to save Table 2 data to a given file path."""
        try:
            df = self.tb_se_second_source.to_df()
            ext = os.path.splitext(filename)[1].lower()

            if ext == ".csv":
                df.to_csv(filename, index=False)
                fulltext = f"Table saved as CSV to {filename}: Ready"
            elif ext == ".txt":
                df.to_csv(filename, sep=" ", index=False)
                fulltext = f"Table saved as TXT to {filename}: Ready"
            else:
                # Default to PKB format
                df_pkb = pd.DataFrame(columns=self.pkb_columns_selected)
                df_pkb["nu"] = df["Frequency"].to_list()
                df_pkb["H"] = df["Power"].to_list()
                df_pkb["l"] = df["Mode"].astype(float).to_list()
                deltanu = self.env.dnu_val
                df_pkb["n"] = np.rint(df["Frequency"] / deltanu).astype(int)
                df_pkb = df_pkb.fillna(0)

                pkb_array = df_pkb.values
                length_prefered = len(self.pkb_columns_selected)
                fmt = ["%.0f"] * 2 + ["%.4f"] * (length_prefered - 2)
                apol.peakbagging.save_pkb(filename, pkb_array, fmt=fmt)
                fulltext = f"PKB file saved to {filename}: Ready"

            print(fulltext)
            self.publish_message(text=fulltext)
        except Exception as e:
            err_msg = f"Save failed: {e}"
            print(err_msg)
            self.publish_message(text=err_msg)

    def toggle_periodogram_axis_scale(self, attr, old, new):

        print("Changing periodogram scale: Not working")
        from bokeh.models import Range1d, LogScale, LinearScale

        if 0 in new:
            # Set to logarithmic scale
            self.env.fig_other_periodogram.y_range = Range1d(
                start=0.1, end=10, bounds="auto"
            )
            self.env.fig_other_periodogram.x_range = Range1d(
                start=0.1, end=10, bounds="auto"
            )
            self.env.fig_other_periodogram.y_scale = LogScale()
            self.env.fig_other_periodogram.x_scale = LogScale()
        else:
            # Set to linear scale
            self.env.fig_other_periodogram.y_range = Range1d(
                start=0, end=10, bounds="auto"
            )
            self.env.fig_other_periodogram.x_range = Range1d(
                start=0, end=10, bounds="auto"
            )
            self.env.fig_other_periodogram.y_scale = LinearScale()
            self.env.fig_other_periodogram.x_scale = LinearScale()

    def update_axis_scale(self, attr, old, new):
        """Update the axis scale of the PSD plot."""
        if self.env.fig_other_periodogram is None:
            return

        if self.env.select_x_axis_type.value == "log":
            self.env.fig_other_periodogram.x_scale = LogScale()
        else:
            self.env.fig_other_periodogram.x_scale = LinearScale()

        if self.env.select_y_axis_type.value == "log":
            self.env.fig_other_periodogram.y_scale = LogScale()
        else:
            self.env.fig_other_periodogram.y_scale = LinearScale()
