from PyQt5 import QtWidgets, uic, QtGui
from PyQt5.QtCore import QUrl, Qt
from PyQt5.QtWidgets import QFileDialog, QMessageBox, QTableWidgetItem, QAbstractItemView, QListWidgetItem, QSplashScreen, QShortcut
from PyQt5.QtWidgets import QDialog, QTableView, QDialogButtonBox
from PyQt5.QtGui import QDesktopServices, QPixmap
from PyQt5 import QtWidgets, QtCore
from PyQt5.QtCore import QObject, QThread, pyqtSignal, QAbstractProxyModel
from PyQt5.QtGui import QKeySequence
from PyQt5.QtWidgets import QColorDialog
from PyQt5.QtGui import QPalette, QColor, QFont
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QLabel, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QComboBox, QLineEdit, QTableView, QSizePolicy
from PyQt5.QtWidgets import (
    QApplication, QDialog, QCheckBox, QLabel, QPushButton, QHBoxLayout
)
from PyQt5.QtWidgets import QMenu, QAction

from PyQt5.QtWidgets import QStyledItemDelegate, QComboBox
from PyQt5.QtCore import Qt, QModelIndex

import tempfile
import shutil
from pathlib import Path

import ast 

import traceback
import difflib
import sys
import os
import yaml

import time
import sympy
from sympy import sympify, simplify, SympifyError
import re as regex
from re import sub
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
from graphviz import Source
from sympy.parsing.sympy_parser import parse_expr
from sympy import *
from sympy import simplify, init_printing, printing
import pandas as pd
import pyperclip
from collections import defaultdict
import subprocess

# sub-modules
from .pandasmodel import PandasModel, CustomHeaderView
from .mamba_interpreter2 import convert_code
from .mainloop_editor import MainLoopEditor
from .output_display import OutputDisplay

from .draw_widget import MyDrawWidget, Box
from .agent_editor import CodeEditor
from .yaml_editor import SettingsEditor
from .theme_manager import ThemeManager

from .mamba_interpreter2 import convert_code
from .mainloop_editor import MainLoopEditor
from .output_display import OutputDisplay

from .reordering import DataFrameSorterDialog
# from .qt_material import apply_stylesheet

import numpy as np
import pandas as pd
from sfctools.api.namespace_io import read_data_to_ns, read_coeffs_to_ns
from sfctools.api.bimets import Equations
from types import SimpleNamespace


"""
Qt GUI for sfctools
author: Thomas Baldauf, DLR-VE
last code review: March 2025 by TB 

This file includes 
    - CheckBoxDialog: A generic checkbox dialog
    - SearchDialog for finding agents in the graph view 
    - MatrixViewer for displaying matrices in Qt 
    - CashFlowViewer, BalanceViewer, IncomeViewer for displaying these 
    - EQsWrapper: stores Bimets euqation definitions
    - DataWrapper: stores calibration Data specifications
    - GuiSettingsDialog: manages GUI settings 
    - BimetsDialog: manages settings for Bimets API
    - AccountingDialog: allows user to edit SFC transactions
    - SfcGUIMainWindow: manages the whole application
"""


def _qcolor_to_mpl(qc: QtGui.QColor, force_opaque=False):
    """Convert QColor to (r, g, b, a) tuple in 0–1 range."""
    if force_opaque:
        return (qc.redF(), qc.greenF(), qc.blueF(), 1.0)
    return (qc.redF(), qc.greenF(), qc.blueF(), qc.alphaF())


def apply_mpl_theme(fig, ax, theme_manager):
    """
    Apply the current ThemeManager colors to a Matplotlib figure + axes.

    Parameters
    ----------
    fig : matplotlib.figure.Figure
    ax : matplotlib.axes.Axes
    theme_manager : ThemeManager
        The active theme manager (self.theme_manager).
    """

    theme = getattr(theme_manager, "theme", "bright").lower()

    if theme == "dark":
        bg   = (0.2, 0.2, 0.2, 1.0)       # black/dark
        text = (0.85, 0.85, 0.85, 1.0)    # light gray
        grid = (0, 0, 0, 0)               # transparent (no grid)
    else:
        bg   = (1.0, 1.0, 1.0, 1.0)       # white
        text = (0.0, 0.0, 0.0, 1.0)       # black
        grid = (0, 0, 0, 0)               # transparent (no grid)

    # Figure and axes backgrounds
    fig.patch.set_facecolor(bg)
    ax.set_facecolor(bg)

    # Text and tick colors
    ax.tick_params(colors=text)
    ax.xaxis.label.set_color(text)
    ax.yaxis.label.set_color(text)
    ax.title.set_color(text)

    # Spines (axes borders)
    for spine in ax.spines.values():
        spine.set_edgecolor(text)

    leg = ax.get_legend()
    if leg:
        leg.get_frame().set_facecolor(bg)
        leg.get_frame().set_edgecolor(bg)
        for text_item in leg.get_texts():
            text_item.set_color(text)
    
    return fig, ax

def my_parse_bsm_expr(expr: str):
    """
    Parse strings like '-fb + intf + intgb - inth + intlh'
    -> list of (sign, name) tuples: [(-1,'fb'), (+1,'intf'), ...]
    """
    tokens = regex.findall(r'([+-])?\s*([A-Za-z0-9_]+)', expr.replace(" ", ""))
    out = []
    for sign, name in tokens:
        s = -1 if sign == '-' else 1
        out.append((s, name))
    return out
    
class DataFrameDialog(QDialog):
    _instance = None  # class-level variable

    @classmethod
    def open_dialog(cls, df, parent=None):
        if cls._instance is None or not cls._instance.isVisible():
            cls._instance = cls(df, parent)
            cls._instance.show()
        else:
            cls._instance.raise_()
            cls._instance.activateWindow()
    
    def closeEvent(self, event):
        type(self)._instance = None
        super().closeEvent(event)
        
    
    def __init__(self, df, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Difference between model and data")
        self.resize(500, 400)

        layout = QVBoxLayout(self)

        self.table = QTableView(self)
        # Use your custom header
        header = CustomHeaderView(Qt.Horizontal, parent=self.table, mainparent=self)
        self.table.setHorizontalHeader(header)

        # Hook up your model
        try:
            self.model = PandasModel(df)
            self.model.view = self.table
            self.table.setModel(self.model)

            self.table.setSelectionBehavior(QTableView.SelectRows)
            self.table.setAlternatingRowColors(False)
            self.table.horizontalHeader().setStretchLastSection(True)
            self.table.resizeColumnsToContents()
            
            #self.table.adjustSizeToTable()
            self.table.resizeColumnsToContents()
            self.table.resizeRowsToContents()

        except Exception as e:
            self.parent().notify("Exception: %s" % str(e), title="Exception")
            self.close() 
        
        layout.addWidget(self.table)

        buttons = QDialogButtonBox(QDialogButtonBox.Close, parent=self)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)

        for b in buttons.buttons():
            size_hint = b.sizeHint()
            new_width = int(size_hint.width() * 1.5)
            b.setFixedWidth(new_width)
        

class ColumnSelectorDelegate(QStyledItemDelegate):
    def __init__(self, available_columns, parent=None):
        super().__init__(parent)
        self.available_columns = list(available_columns)

    def createEditor(self, parent, option, index: QModelIndex):
        combo = QComboBox(parent)
        combo.setEditable(False)
        combo.addItems(self.available_columns)
        return combo

    def setEditorData(self, editor: QComboBox, index: QModelIndex):
        current = index.data(Qt.EditRole) or index.data(Qt.DisplayRole) or ""
        ix = editor.findText(str(current))
        editor.setCurrentIndex(ix if ix >= 0 else -1)

    def setModelData(self, editor: QComboBox, model, index: QModelIndex):
        value = editor.currentText()
        model.setData(index, value, Qt.EditRole)


class CheckBoxDialog(QDialog):
    def __init__(self, options, parent=None):
        """
        A generic checkbox dialog.

        Args:
            options (list): A list of option labels to display.
        """
        super(CheckBoxDialog, self).__init__(parent)
        self.setMinimumWidth(250)

        # Store the checkboxes for later access
        self.checkboxes = []

        # Set up the layout
        layout = QVBoxLayout()

        # Dynamically add checkboxes based on the options list
        for option in options:
            # label = QLabel(f"{option}:")
            checkbox = QCheckBox(f"{option}")
            # layout.addWidget(label)
            layout.addWidget(checkbox)
            self.checkboxes.append(checkbox)

        # Add OK and Cancel buttons
        button_layout = QHBoxLayout()
        self.ok_button = QPushButton("OK")
        self.cancel_button = QPushButton("Cancel")
        button_layout.addWidget(self.ok_button)
        button_layout.addWidget(self.cancel_button)
        layout.addLayout(button_layout)

        # Set the layout for the dialog
        self.setLayout(layout)

        # Connect the buttons to their respective slots
        self.ok_button.clicked.connect(self.accept)
        self.cancel_button.clicked.connect(self.reject)


    def get_checkbox_states(self):
        """
        Returns the states of all checkboxes in the dialog.

        Returns:
            dict: A dictionary mapping option labels to their checkbox states.
        """
        return {f"checkbox{index+1}": checkbox.isChecked() for index, checkbox in enumerate(self.checkboxes)}


class SearchDialog(QtWidgets.QDialog):
    instance = None

    def __init__(self, parent=None):
        super(SearchDialog, self).__init__(parent)

        # # print("init search dialog")
        self.__class__.instance = self

        path = os.path.dirname(os.path.abspath(__file__))
        uic.loadUi(os.path.join(path, 'search_dialog.ui'), self)

        self.pushButton.pressed.connect(self.start_search)
        # self.parent = parent

    def start_search(self):
        search_name = self.lineEdit.text()
        # print("search for", search_name)

        # search agent

        for box in self.parent().drawcanvas.boxes:
            if box.name == search_name:

                self.parent().transaction_select_where(search_name)

                self.parent().drawcanvas.highlighted = box
                self.parent().update()
                break

    def CloseEvent(self, event):
        # # print("close search dialg.")
        self.__class__.instance = None
        super().CloseEvent()


class MatrixViewer(QtWidgets.QDialog):
    instance = {"FlowMatrix": None, "BalanceMatrix": None}

    def __new__(cls, mode=None, **kwargs):
        # print("NEW MATRIX VIEWER. INSTANCE:", cls.instance)
        if mode is None or cls.instance[mode] is None:
            curr_inst = super(MatrixViewer, cls).__new__(
                cls, mode=mode, **kwargs)
            return curr_inst

        return cls.instance[mode]

    def __init__(self, mode, parent=None):
        # Call the inherited classes __init__ method
        super(MatrixViewer, self).__init__(parent)

        self.__class__.instance[mode] = self

        path = os.path.dirname(os.path.abspath(__file__))
        uic.loadUi(os.path.join(path, 'matrix_viewer.ui'), self)

        self.mode = mode
        # self.set_model(model)
        # print("RESET MODEL")
        # self.pandas_model = None
        # self.setStyleSheet(parent.theme_manager.get_stylesheet("main"))
        # self.setStyleSheet(parent.theme_manager.get_background_style())

        self.shortcut = QShortcut(QKeySequence("Ctrl+S"), self)
        self.shortcut.activated.connect(self.save_data)

        self.shortcut = QShortcut(QKeySequence("Ctrl+L"), self)
        self.shortcut.activated.connect(self.clipboard_data)

    def closeEvent(self, evnt):
        self.__class__.instance[self.mode] = None
        super().closeEvent(evnt)

    def clipboard_data(self):
        # copy the table to latex string

        def convert_list(y):
            # print("convert ", y, type(y))
            if isinstance(y, list):
                z = []
                for i in y:
                    if isinstance(i, list):
                        z.append("".join(i))
                    elif isinstance(i, tuple):
                        z.append("".join(list(i)))
                    else:
                        z.append(i)
                return z

            elif isinstance(y, str):
                return [y]
            else:
                try:
                    return [str(y)]
                except:
                    pass

            return []

        def entry_to_str(x):
            z = "+".join(convert_list(x))
            if z == "":
                return ""

            y = str(sympy.simplify(z))
            # print("join", x, "->", z, "->" , y)
            y = "$" + y + "$"
            return y

        try:
            df = self.pandas_model.raw_data
            # .to_clipboard() # excel=False,sep=",")
            df = df.applymap(entry_to_str).replace("=", "")

            pyperclip.copy(df.to_latex().replace("Δ", "$\Delta$"))

        except Exception as e:
            self.parent().notify(str(e), title="Error")

    def save_data(self):
        # save the table to an excel file
        filename = QFileDialog.getSaveFileName(self, 'Save file',
                                               os.getcwd(), "excel Files (*.xlsx)")[0]

        try:
            df = self.pandas_model.raw_data
            # df.applymap(lambda x: str(x).replace("=", "")).to_clipboard() # excel=False,sep=",")
            df.to_excel(filename)
        except Exception as e:
            self.parent().notify(str(e), title="Error")

    def set_model(self, model):
        # print("SET MODEL", model)

        try:
                
            if not hasattr(self, "pandas_model"):  # is None:
                self.pandas_model = model
                self.tableView.setModel(model)
            try:
                self.pandas_model.set_data(model.raw_data)
            except Exception as e:
                self.parent().notify(str(e), title="Error")

            self.tableView.resizeRowsToContents()
            self.tableView.show()

            self.update()
        except Exception as e:
            print(str(e))


    def get_data(self):

        try:
            if self.pandas_model:
                return self.pandas_model.raw_data
        except Exception as e:
            print(str(e))
            
        return None


class CashFlowViewer(QtWidgets.QDialog):
    instances = {}

    def closeEvent(self, event):
        try:
            del self.__class__.instances[self.name]
        except:
            pass 
        event.accept()  # let the window close

    def __init__(self, parent=None, name="Unknown"):

        self.name = name

        # Call the inherited classes __init__ method
        super(CashFlowViewer, self).__init__(parent)
        path = os.path.dirname(os.path.abspath(__file__))
        uic.loadUi(os.path.join(path, 'income_viewer.ui'), self)
        self.setWindowTitle("Cash Flow Statement of %s" % name)

        self.__class__.instances[name] = self

    def set_model(self, model):
        # print("SET MODEL", model)
        self.pandas_model = model
        # self.tableView.setModel(model)

    def get_data(self):
        return self.pandas_model.raw_data


class BalanceSheetViewer(QtWidgets.QDialog):
    instances = {}

    def closeEvent(self, event):
        try:
            del self.__class__.instances[self.name]
        except:
            pass 
        event.accept()  # let the window close
    
    def __init__(self, parent=None, name="Unknown", has_button=False):

        name = name.capitalize()
        self.name = name

        # Call the inherited classes __init__ method
        super(BalanceSheetViewer, self).__init__(parent)
        path = os.path.dirname(os.path.abspath(__file__))
        uic.loadUi(os.path.join(path, 'income_viewer.ui'), self)
        self.setWindowTitle("Balance Sheet of %s" % name)

        self.__class__.instances[name] = self

        if has_button:
            self.button = QPushButton("  Save  ", self)
            self.button.setToolTip("Save current linkages")

            self.button2 = QPushButton("  Check  ", self)
            self.button2.setToolTip("Check data consistency")

            main_layout = self.layout()
            if main_layout is None:
                main_layout = QVBoxLayout(self)
                self.setLayout(main_layout)

            button_bar = QVBoxLayout()
            button_bar.addStretch(1)
            for b in [self.button, self.button2]:
                button_bar.addWidget(b)
            main_layout.addLayout(button_bar)
            
            for button in [self.button, self.button2]:
                size_hint = button.sizeHint()
                new_width = int(size_hint.width() * 1.35)
                button.setFixedWidth(new_width)

            self.button.clicked.connect(self.on_save_clicked) 
            self.button2.clicked.connect(self.on_check_clicked) 
    
    def on_check_clicked(self):
        """
        Go through all data entries and check differences
        between the initial value + changes in all years
        and the actual observed time series for that stock
        """
        if not hasattr(self, "pandas_model"):
            return 
        df = self.pandas_model.df 
        
        def is_main_col(col):
            return not (col.endswith(" [Data]") or col.endswith(" [Value]"))
        bases = [c for c in df.columns if is_main_col(c)]
        
        # retrieve balance entries 
        balance_entries = []
        for _, row in df.iterrows():
            for base in bases:
                new_entry = str(row[base]).strip()
                if new_entry != '':
                    balance_entries.append(new_entry)
        
        # retrieve entries 
        agent_name = self.name[:-3].capitalize()
        if agent_name not in self.__class__.instances:
            self.parent().notify("Please keep the balance sheet window open. aborting operation.", title="Aborting.")
            return 
        bs = self.__class__.instances[agent_name].pandas_model.df
        # print("bs", bs.pandas_model.df)

        results_bs_a = {}
        results_bs_l = {}
        text_a = str(bs["Assets"].iloc[0])
        text_l = str(bs["Liabilities"].iloc[0])
        print("text_a", text_a)
        print("text_l", text_l)
        
        pattern = r"(\w+):\n(.*?)(?=\n{2,}\w+:|$)"
        matches = regex.findall(pattern, text_a, regex.DOTALL)
        result_a = {key: value.strip() for key, value in matches}
        for k, v in result_a.items():
            results_bs_a[k] = v 
        
        matches = regex.findall(pattern, text_l, regex.DOTALL)
        result_l = {key: value.strip() for key, value in matches}
        for k, v in result_l.items():
            results_bs_l[k] = v 

        print("results_bs", results_bs_a)
        print("L", results_bs_l)

        if not self.parent().data_wrapper:
            self.parent().notify("No data wrapper", title="Error")
            return data
        
        data = self.parent().data_wrapper.df.copy()
        start_year = data.index[0]
        end_year = data.index[-1]
        try: 
            data = data.sort_values(by="Year")
        except:
            pass
        
        # self.parent().notify("agent Name: %s" % agent_name, title="name")
        
        d = {"Assets": {}, "Liabilities": {}}
        d_compare = {"Assets": {}, "Liabilities": {}}

        err_str = ""
        global_success = True 
        for entry, res in [("Assets", results_bs_a), ("Liabilities", results_bs_l)]:
            for k, v in res.items():
                
                try:
                    
                    res = my_parse_bsm_expr(v)

                    success = False 
                    init_data = self.parent().SETTING_balance_init_data
                    if agent_name in init_data:
                        if entry in init_data[agent_name]:
                            if k in init_data[agent_name][entry]:
                            
                                start_val_name = init_data[agent_name][entry][k]
                                
                                if (start_val_name in data) and (start_year in data[start_val_name].index):

                                    start_val = data[start_val_name].loc[start_year]
                                    istart_year = int(start_year)
                                    iend_year = int(end_year)
                                    
                                    print("--")
                                    y = [start_val]
                                    for year in range(istart_year+1, iend_year+1):
                                        y_j = start_val 
                                        print("y_j0 = ", start_val)
                                        for sign, r in res:
                                            r_val = data.loc[istart_year+1:year, r].sum()
                                            y_j += sign * r_val 
                                            print("       + ", sign*r_val)
                                        # y_j = np.round(y, 2)
                                        print("y_j", y_j)
                                        y.append(y_j)

                                    d[entry][k] = np.round(np.array(y), 2)

                                    d_compare[entry][k] = data.loc[istart_year:iend_year, start_val_name]
                                    success = True 
                
                    if not success:
                        global_success = False
                
                except Exception as e:
                   # self.parent().notify("Exception: %s" % str(e), title="Processing error")
                   print("Exception: %s" % str(e))
                   global_success = False 

                   err_str += str(e)
        
        if not global_success:
            self.parent().notify("Exception: could not build table, or data only partially available.\nHave you set up any data yet?\n%s" % err_str, title="Processing error")

        print("d")
        print(d)

        merged = {**d["Assets"], **d["Liabilities"]}
        df = pd.DataFrame(merged)

        
        merged2 = {**d_compare["Assets"], **d_compare["Liabilities"]}
        df2 = pd.DataFrame(merged2)

        df.index = df2.index 
        print(df)
        print(df2)

        df_compare = df - df2 
        try:
            df_compare = df_compare.round(2)
        except Exception as e:
            print("Exception: %s" % str(e))

        dlg = DataFrameDialog(df_compare,parent=self)
        dlg.setModal(False)
        dlg.show()

    
    def on_save_clicked(self):
        """
        Read the tableview and build: for example
        {
          "Assets": {"OtherAssets": "oah", "Deposits": "mh"},
          "Liabilities": {"NW": "vh"}
        }
        """
        if not hasattr(self, "pandas_model"):
            return 
        df = self.pandas_model.df  # assumes your PandasModel exposes .df
        def is_main_col(col):
            return not (col.endswith(" [Data]") or col.endswith(" [Value]"))
        bases = [c for c in df.columns if is_main_col(c)]
        linkages = {}
        for base in bases:
            data_col = f"{base} [Data]"
            if data_col not in df.columns:
                continue
            mapping = {}
            for _, row in df.iterrows():
                name = row.get(base, "")
                sel  = row.get(data_col, "")
                if pd.isna(name) or str(name).strip() == "":
                    continue
                if pd.isna(sel) or str(sel).strip() == "":
                    continue
                key = name #  regex.sub(r"[_\W]+", "", str(name))
                mapping[key] = str(sel)
            if mapping:
                linkages[base] = mapping 
        # self.parent().notify(str(linkages), title="New Linkages")
        ref_agent = self.parent().get_selected_agent()
        if ref_agent is not None:
            self.parent().SETTING_balance_init_data[ref_agent] = linkages
        print(dict(self.parent().SETTING_balance_init_data))
        self.parent().gen_balance_matrix()

    def set_model(self, model):

        w = self.tableView
        N_rows = w.horizontalHeader().count()
        N_cols = w.verticalHeader().count()

        widths = [w.columnWidth(i) for i in range(N_cols)]
        heights = [w.rowHeight(i) for i in range(N_rows)]
        # print(widths,heights)

        self.pandas_model = model
        self.tableView.setModel(model)

        self.tableView.resizeRowsToContents()

        N_rows = w.horizontalHeader().count()
        N_cols = w.verticalHeader().count()

        for i in range(min(len(widths), N_cols)):
            w.setColumnWidth(w.columnAt(i), widths[i])
        for i in range(min(len(heights), N_rows)):
            w.setRowHeight(i, heights[i])

    def get_data(self):
        return self.pandas_model.raw_data


class IncomeViewer(QtWidgets.QDialog):
    instances = {}

    def closeEvent(self, event):
        try:
            del self.__class__.instances[self.name]
        except:
            pass 
        event.accept()  # let the window close

    def __init__(self, parent=None, name="Unknown"):

        # Call the inherited classes __init__ method
        super(IncomeViewer, self).__init__(parent)
        path = os.path.dirname(os.path.abspath(__file__))
        uic.loadUi(os.path.join(path, 'income_viewer.ui'), self)
        self.setWindowTitle("Income Sheet of %s" % name)

        self.name = name
        self.__class__.instances[name] = self

    def set_model(self, model):
        # print("SET MODEL", model)
        self.pandas_model = model
        self.tableView.setModel(model)

    def get_data(self):
        return self.pandas_model.raw_data


class EQsWrapper:

    def __init__(self, parent):
        #
        self.data = {"NAME": [], "TYPE": [], "EXPRESSION": [],
                     "COEFFS": [], "RESTRICT": [], "CONDITION": [], "COMMENT": []}
        self.parent = parent

    def udpate_data(self):
        # update data from current version
        table_view = self.parent.EQtableView

        # model = table_view.model()
        model = self.parent.eq_model
        if model is None:
            print("no eq_model yet")
            return

        # Initialize an empty list to hold the data
        data = {"NAME": [], "TYPE": [], "EXPRESSION": [], "COEFFS": [],
                "RESTRICT": [], "CONDITION": [], "COMMENT": []}

        # Loop through rows and columns to extract data
        for row in range(model.rowCount()):
            row_data = []
            for column in range(model.columnCount()):
                # Get the data from the model's index
                index = model.index(row, column)
                row_data.append(model.data(index))

            data["NAME"].append(row_data[0])
            data["TYPE"].append(row_data[1])
            data["EXPRESSION"].append(row_data[2])
            data["COEFFS"].append(row_data[3])
            try:
                data["RESTRICT"].append(row_data[4])
            except Exception as e:
                data["RESTRICT"].append("")
                print("Exception:", str(e))
            try:
                data["CONDITION"].append(row_data[5])
            except Exception as e:
                data["CONDITION"].append("")
                print("Exception:", str(e))
            try:
                data["COMMENT"].append(row_data[6])
            except Exception as e:
                data["COMMENT"].append("")
                print("Exception:", str(e))

        # Get the header labels (if any)
        # headers = [model.headerData(i, 1) for i in range(model.columnCount())]
        headers = [model.headerData(i, Qt.Horizontal, Qt.DisplayRole)
                   for i in range(model.columnCount())]

        # df = pd.DataFrame(data, columns=headers)
        # self.data = df.to_dict()
        # print("new data\n", self.data)
        self.data = data

    def get_data(self):
        return self.data

    def get_df(self):
        return pd.DataFrame(self.data)


class DataWrapper:
    # data class for empirical data for sfc fitting

    def __init__(self, df):

        print("DataWRapper", df)
        self.df = df
        self.ts = []
        self.ts_df = {"NAME": [], "START": [], "END": [], "FREQ": [], "EXTMODE": [], "EXTVAL": [
        ], "ADD_FACT_START": [], "ADD_FACT_VALS": [], "EXO_START": [], "EXO_END": []}

        try:
            self.df = self.df.sort_index()
        except:
            pass
        
        # extract time series info for each data column
        if "Unnamed: 0" in self.df:
            del self.df["Unnamed: 0"]

        if len(self.df.index) < 2:
            self.notify(
                "It seems as if the provided data is too short!", title="Error")
            return
        
        data_start = self.df.index[0]
        data_end = self.df.index[-1]

        for c in self.df.columns:
            if c == "Unnamed: 0":
                continue

            data_freq = "A"
            ts_i = (c, data_start, data_end, data_freq)
            self.ts.append(ts_i)
            self.ts_df["NAME"].append(str(c))
            self.ts_df["START"].append(data_start)
            self.ts_df["END"].append(data_end)
            self.ts_df["FREQ"].append(data_freq)

            self.ts_df["EXTMODE"].append("")
            self.ts_df["EXTVAL"].append("")
            self.ts_df["ADD_FACT_START"].append("")
            self.ts_df["ADD_FACT_VALS"].append("")
            
            self.ts_df["EXO_START"].append("")
            self.ts_df["EXO_END"].append("")

        self.ts_df = pd.DataFrame(self.ts_df)  # convert to dataframe
        print("READ DATA\n", self.ts_df.to_string())


class GuiSettingsDialog(QtWidgets.QDialog):
    instance = None

    def __new__(cls, *args, **kwargs):
        if cls.instance:
            return cls.instance
        return super().__new__(cls, *args, **kwargs)

    def __init__(self, parent, myfont=None):
        super().__init__(parent=parent)

        self.__class__.instance = self
        path = os.path.dirname(os.path.abspath(__file__))
        loadpath = os.path.join(path, "dlg_settings.ui")
        self.setWindowFlags(self.windowFlags() & ~
                            Qt.WindowContextHelpButtonHint)

        self.font = "Seoge UI"
        if myfont is not None:
            self.font = myfont
            self.set_font_for_all_widgets()

        # Ensure 100% scaling
        QtWidgets.QApplication.setAttribute(
            QtCore.Qt.AA_EnableHighDpiScaling, False)
        QtWidgets.QApplication.setAttribute(
            QtCore.Qt.AA_UseHighDpiPixmaps, False)

        content = []
        success = False

        # remove font-size to fix bug in qt designer
        with open(loadpath, "r") as file:
            for line in file.readlines():
                if "<pointsize>-1</pointsize>" not in line:
                    content.append(line)

            success = True

        if success:
            with open(loadpath, "w") as file:
                file.write("".join(content))

        uic.loadUi(loadpath, self)  # load UI
        self.okButton.pressed.connect(self.load_from_gui)

        self.update_colors()
        # color buttons
        self.bc1.pressed.connect(lambda: self.change_color(1))
        self.bc2.pressed.connect(lambda: self.change_color(2))
        self.bc3.pressed.connect(lambda: self.change_color(3))
        self.bc4.pressed.connect(lambda: self.change_color(4))
        self.bc5.pressed.connect(lambda: self.change_color(5))
        self.bc6.pressed.connect(lambda: self.change_color(6))
        self.bc7.pressed.connect(lambda: self.change_color(7))
        self.bc8.pressed.connect(lambda: self.change_color(8))

        self.btn_csave.pressed.connect(
            lambda: self.parent().theme_manager.save_colors())
        self.btn_cload.pressed.connect(lambda: self.load_theme())
        self.btn_crestore.pressed.connect(lambda: self.restore())

        self.filterCombo.itemSelectionChanged.connect(
            lambda: self.update_filter())
        self.clearFilterButton.pressed.connect(self.clear_filter)
        self.fill_filter()

        # font size selectors
        self.spinBoxFontSize.valueChanged.connect(lambda: self.change_font())
        self.spinBoxFontSize_2.valueChanged.connect(lambda: self.change_font())

        self.spinBoxFontSize.setValue(self.parent().SETTING_fontsize)
        self.spinBoxFontSize_2.setValue(self.parent().SETTING_fontsize_2)

        # checkboxes
        self.checkBox_2.setChecked(self.parent().SETTING_show_labels)
        self.checkBox_4.setChecked(self.parent().SETTING_highlight_labels)
        self.checkBox_2.stateChanged.connect(self.update_checkboxes)
        self.checkBox_4.stateChanged.connect(self.update_checkboxes)

        try:  # depricated items
            for item in [self.checkBox,
                         self.checkBox_raster_2,
                         self.checkBox_raster,
                         self.checkBox_3,
                         self.checkBox_5,
                         self.checkBox_6,
                         self.checkBox_7,
                         self.checkBox_8,
                         self.checkBox_9]:
                item.setVisible(False)
        except:
            pass

    def update_checkboxes(self):
        self.parent().SETTING_show_labels = self.checkBox_2.isChecked()
        self.parent().SETTING_highlight_labels = self.checkBox_4.isChecked()

        self.parent().drawcanvas.update()

    def change_font(self):
        self.parent().SETTING_fontsize = int(self.spinBoxFontSize.value())
        self.parent().SETTING_fontsize_2 = int(self.spinBoxFontSize_2.value())
        self.parent().drawcanvas.update()

    def fill_filter(self):
        for f in self.parent().filter_entries:
            self.filterCombo.addItem(f)

    def update_filter(self):
        print("update filter", self.parent().filter_entries)
        self.parent().filter_gui = [item.text()
                                    for item in self.filterCombo.selectedItems()]
        print("new filter", self.parent().filter_gui)
        self.parent().drawcanvas.update()

    def clear_filter(self):
        self.parent().filter_gui = self.parent().filter_entries
        for i in range(self.filterCombo.count()):
            self.filterCombo.item(i).setSelected(
                False)  # setCheckState(Qt.Unchecked)

    def load_theme(self):
        self.parent().theme_manager.load_colors()
        self.update_colors()

    def restore(self):
        self.parent().theme_manager.restore()
        self.update_colors()

    def update_colors(self):
        btn_table = {
            1: self.bc1,
            2: self.bc2,
            3: self.bc3,
            4: self.bc4,
            5: self.bc5,
            6: self.bc6,
            7: self.bc7,
            8: self.bc8,
        }

        for i, color in enumerate(self.parent().theme_manager.colors[self.parent().theme_manager.theme]):
            btn_table[i+1].setStyleSheet("background-color: rgb(%s,%s,%s);" %
                                         (color.red(), color.green(), color.blue()))

        self.parent().update()

    def change_color(self, which):
        self.parent().change_color(which)
        self.update_colors()

    def load_from_gui(self):

        # self.parent().theme_manager[] = self.bc1.palette().button().color()
        # print("set color", self.parent().bc1)

        self.__class__.instance = None
        self.close()

    def load_to_gui(self):
        return


class BimetsDialog(QtWidgets.QDialog):
    instance = None

    def __new__(cls, *args, **kwargs):
        if cls.instance:
            return cls.instance
        return super().__new__(cls, *args, **kwargs)

    def __init__(self, parent, myfont=None):
        super().__init__(parent=parent)

        self.__class__.instance = self
        path = os.path.dirname(os.path.abspath(__file__))
        loadpath = os.path.join(path, "dlg_bimets.ui")
        self.setWindowFlags(self.windowFlags() & ~
                            Qt.WindowContextHelpButtonHint)
        
        self.font = "Seoge UI"
        if myfont is not None:
            self.font = myfont
            self.set_font_for_all_widgets()

        # Ensure 100% scaling
        QtWidgets.QApplication.setAttribute(
            QtCore.Qt.AA_EnableHighDpiScaling, False)
        QtWidgets.QApplication.setAttribute(
            QtCore.Qt.AA_UseHighDpiPixmaps, False)

        content = []
        success = False

        # remove font-size to fix bug in qt designer
        with open(loadpath, "r") as file:
            for line in file.readlines():
                if "<pointsize>-1</pointsize>" not in line:
                    content.append(line)

            success = True

        if success:
            with open(loadpath, "w") as file:
                file.write("".join(content))

        uic.loadUi(loadpath, self)  # load UI
        self.load_to_gui()

        self.okButton.pressed.connect(self.load_from_gui)

    def load_to_gui(self):
        self.lineEdit_R_model_dir.setText(self.parent().SETTING_R_path)
        self.comboBoxSimType.setCurrentText(self.parent().SETTING_R_SimType)
        self.comboBoxAlgo.setCurrentText(self.parent().SETTING_R_Algo)
        year_start_est, year_end_est = self.parent().SETTING_R_RANGE_FIT
        self.lineEdit_year_start_est.setText(str(year_start_est))
        self.lineEdit_year_end_est.setText(str(year_end_est))
        year_start_sim, year_end_sim = self.parent().SETTING_R_RANGE_SIM
        self.lineEdit_year_start_sim.setText(str(year_start_sim))
        self.lineEdit_year_end_sim.setText(str(year_end_sim))

    def load_from_gui(self):
        self.parent().SETTING_R_path = self.lineEdit_R_model_dir.text()
        self.parent().SETTING_R_SimType = self.comboBoxSimType.currentText()  # "RESCHECK+STATIC"
        self.parent().SETTING_R_Algo = self.comboBoxAlgo.currentText()
        self.parent().SETTING_R_RANGE_FIT = (
            self.lineEdit_year_start_est.text(), self.lineEdit_year_end_est.text())
        self.parent().SETTING_R_RANGE_SIM = (
            self.lineEdit_year_start_sim.text(), self.lineEdit_year_end_sim.text())

        # close gui
        self.__class__.instance = None
        self.close()

    def set_font_for_all_widgets(self):
        font = self.font
        self.setFont(font)

        def set_font_recursive(widget):
            if isinstance(widget, (QLabel, QPushButton, QComboBox, QTableView, QLineEdit)):
                widget.setFont(font)
            for child in widget.findChildren(QWidget):
                set_font_recursive(child)
        set_font_recursive(self)


class AccountingDialog(QtWidgets.QDialog):

    def set_font_for_all_widgets(self):
        font = self.font
        self.setFont(font)

        def set_font_recursive(widget):
            if isinstance(widget, (QLabel, QPushButton, QComboBox, QTableView, QLineEdit)):
                widget.setFont(font)
            for child in widget.findChildren(QWidget):
                set_font_recursive(child)
        set_font_recursive(self)

    def __init__(self, parent, data=None, myfont=None):

        super(AccountingDialog, self).__init__(parent=parent)
        path = os.path.dirname(os.path.abspath(__file__))
        loadpath = os.path.join(path, "accounting_editor.ui")
        self.setWindowFlags(self.windowFlags() & ~
                            Qt.WindowContextHelpButtonHint)

        self.font = "Seoge UI"
        if myfont is not None:
            self.font = myfont
            self.set_font_for_all_widgets()

        # Ensure 100% scaling
        QtWidgets.QApplication.setAttribute(
            QtCore.Qt.AA_EnableHighDpiScaling, False)
        QtWidgets.QApplication.setAttribute(
            QtCore.Qt.AA_UseHighDpiPixmaps, False)

        # self.setWindow
        content = []
        success = False

        # remove font-size to fix bug in qt designer
        with open(loadpath, "r") as file:
            for line in file.readlines():
                if "<pointsize>-1</pointsize>" not in line:
                    content.append(line)

            success = True

        if success:
            with open(loadpath, "w") as file:
                file.write("".join(content))

        uic.loadUi(loadpath, self)  # load UI
        self.resize(1062, 582)

        self.okButton.pressed.connect(self.load_from_gui)
        self.btnDuplicate.pressed.connect(self.duplicate)

        # insert data
        print("DATA", data)
        self.new_edit = True
        if data is not None:
            # load data into current window
            self.load_to_gui(data)
            self.new_edit = False

        self.show()  # show window

    def load_to_gui(self, data):
        self.nameText.setText(data["shortname"])

        self.agent1Edit.setText(data["agent1"])
        self.agent2Edit.setText(data["agent2"])
        try:
            self.agent3Edit.setText(data["agent3"])
            self.agent4Edit.setText(data["agent4"])
        except:
            pass

        self.AssetListEdit1.setText(data["a1"])
        self.AssetListEdit2.setText(data["a2"])
        try:
            self.AssetListEdit3.setText(data["a3"])
            self.AssetListEdit4.setText(data["a4"])
        except:
            pass

        self.LiabilityListEdit1.setText(data["l1"])
        self.LiabilityListEdit2.setText(data["l2"])
        try:
            self.LiabilityListEdit3.setText(data["l3"])
            self.LiabilityListEdit4.setText(data["l4"])
        except:
            pass

        self.EquityListEdit1.setText(data["e1"])
        self.EquityListEdit2.setText(data["e2"])
        try:
            self.EquityListEdit3.setText(data["e3"])
            self.EquityListEdit4.setText(data["e4"])
        except:
            pass

        self.incomeCombo1.setCurrentIndex(
            self.incomeCombo1.findText(data["income1"] or "None"))
        self.incomeCombo2.setCurrentIndex(
            self.incomeCombo2.findText(data["income2"] or "None"))
        try:
            self.incomeCombo3.setCurrentIndex(
                self.incomeCombo3.findText(data["income3"] or "None"))
            self.incomeCombo4.setCurrentIndex(
                self.incomeCombo4.findText(data["income4"] or "None"))
        except:
            pass

        self.CFCombo1.setCurrentIndex(
            self.CFCombo1.findText(str(data["cashflow1"] or "None")))
        self.CFCombo2.setCurrentIndex(
            self.CFCombo2.findText(str(data["cashflow2"] or "None")))
        try:
            self.CFCombo3.setCurrentIndex(
                self.CFCombo3.findText(str(data["cashflow3"] or "None")))
            self.CFCombo4.setCurrentIndex(
                self.CFCombo4.findText(str(data["cashflow4"] or "None")))
        except:
            pass

        self.registerFlowBoxEdit.setCurrentIndex(
            self.registerFlowBoxEdit.findText(str(data["log transaction"])))

        trtype = data["kind"] or "KA->KA"
        self.editTypeCombo.setCurrentIndex(
            self.editTypeCombo.findText(str(trtype)))

        self.editQuantityField.setText(data["quantity"])
        self.editSubjectField.setText(data["subject"])

        if "description" in data:
            self.editDescriptionField.setText(data["description"])
        else:
            self.editDescriptionField.setText("")

        if "add_args" in data:
            self.editAddArgsField.setText(data["add_args"])
        else:
            self.editAddArgsField.setText("")

        if "add_code" in data:
            self.editAddCodeField.setPlainText(data["add_code"])
        else:
            self.editAddCodeField.setPlainText("")

        unidir = str(data["uni-directional"]) or "None"
        self.UnidirCombo.setCurrentIndex(self.UnidirCombo.findText(unidir))

    def abort_edit(self):
        if self.new_edit:
            self.parent().entry_data.pop()
            self.parent().selection_idx = None  # len(self.parent().entry_data) - 1

    def duplicate(self):
        old_edit=self.new_edit
        self.new_edit = True
        self.nameText.setText(self.nameText.text()+"_duplicate")
        self.load_from_gui()
        self.new_edit = old_edit

    def load_from_gui(self):

        # if self.parent().selection_idx is None:
        #    return

        if self.new_edit:  # edit a new entry
            new_entry = {k: "" for k in ["agent1", "agent2", "agent3", "agent4", "a1", "a2", "a3", "a4", "l1", "l2", "l3", "l4", "e1", "e2", "e3", "e4", "l1", "l2", "l3", "l4",
                                         "shortname", "income1", "income2", "income3", "income4", "cashflow1", "cashflow2", "cashflow3", "cashflow4",
                                         "log transaction", "kind", "quantity", "subject", "description", "add_args", "add_code", "uni-directional"]}
            new_entry["agent1"] = "Unknown"
            new_entry["agent2"] = "Unknown"
            new_entry["subject"] = "No Subject"
            new_entry["quantity"] = "x"
            new_idx = self.parent().transactionView.rowCount()
            curr_idx = new_idx
            self.parent().entry_data.append(new_entry)
            self.parent().selection_idx = len(self.parent().entry_data) - 1

        else:  # edit existing (= currently selected) entry
            curr_idx = self.parent().selection_idx

        data = self.parent().entry_data[curr_idx]

        old_name = data["shortname"]
        data["shortname"] = self.nameText.text()

        new_agent1 = self.agent1Edit.text()  # data["agent1"]
        new_agent2 = self.agent2Edit.text()
        new_agent3 = self.agent3Edit.text()
        new_agent4 = self.agent4Edit.text()

        #  "agent2", "shortname", "quantity", "subject"]:
        if new_agent1 == "":
            self.parent().notify("Please insert a valid agent name (sender)", title="Error")
            self.abort_edit()
            return

        if new_agent2 == "":
            self.parent().notify("Please insert a valid agent name (receiver)", title="Error")
            self.abort_edit()
            return

        if self.editQuantityField.text() == "":
            self.parent().notify(
                "Please insert a valid mathematical symbol under 'Symbol'", title="Error")
            self.editQuantityField.setText("x")
            self.abort_edit()
            return

        if self.editSubjectField.text() == "":
            self.parent().notify("Please insert a valid subject", title="Error")
            self.editSubjectField.setText("No Subject")
            self.abort_edit()
            return

        if not self.parent().drawcanvas.check_exist(new_agent1):
            if new_agent1 != "":
                yes = self.parent().ask_question(
                    '', "The agent %s does not exist.\nDo you wish to continue and automatically create a new agent?" % new_agent1)
                if not yes:
                    new_agent1 = data["agent1"]  # keep old data
                    self.agent1Edit.setText(new_agent1)

        if not self.parent().drawcanvas.check_exist(new_agent2):
            if new_agent2 != "":
                yes = self.parent().ask_question(
                    '', "The agent %s does not exist.\nDo you wish to continue and automatically create a new agent?" % new_agent2)
                if not yes:
                    new_agent2 = data["agent2"]  # keep old data
                    self.agent2Edit.setText(new_agent2)
        try:
            if not self.parent().drawcanvas.check_exist(new_agent3):
                if new_agent3 != "":
                    yes = self.parent().ask_question(
                        '', "The agent %s does not exist.\nDo you wish to continue and automatically create a new agent?" % new_agent3)
                    if not yes:
                        new_agent3 = data["agent3"]  # keep old data
                        self.agent3Edit.setText(new_agent3)
                    else:
                        # new_box3 = self.drawcanvas.add_agent(new_agent3)
                        self.parent().add_helper(new_agent3)

            if not self.parent().drawcanvas.check_exist(new_agent4):
                if new_agent4 != "":
                    yes = self.parent().ask_question(
                        '', "The agent %s does not exist.\nDo you wish to continue and automatically create a new agent?" % new_agent4)
                    if not yes:
                        new_agent4 = data["agent4"]  # keep old data
                        self.agent4Edit.setText(new_agent4)
                    else:
                        # new_box4 = self.drawcanvas.add_agent(new_agent4)
                        self.parent().add_helper(new_agent4)

        except Exception as e:
            print("Exception:, ", str(e))

        data["agent1"] = new_agent1
        data["agent2"] = new_agent2
        try:
            data["agent3"] = new_agent3
            data["agent4"] = new_agent4
        except AttributeError:
            pass

        data["a1"] = self.AssetListEdit1.toPlainText()
        data["a2"] = self.AssetListEdit2.toPlainText()
        try:
            data["a3"] = self.AssetListEdit3.toPlainText()
            data["a4"] = self.AssetListEdit4.toPlainText()
        except AttributeError:
            pass

        data["l1"] = self.LiabilityListEdit1.toPlainText()
        data["l2"] = self.LiabilityListEdit2.toPlainText()
        try:
            data["l3"] = self.LiabilityListEdit3.toPlainText()
            data["l4"] = self.LiabilityListEdit4.toPlainText()
        except AttributeError:
            pass

        data["e1"] = self.EquityListEdit1.toPlainText()
        data["e2"] = self.EquityListEdit2.toPlainText()
        try:
            data["e3"] = self.EquityListEdit3.toPlainText()
            data["e4"] = self.EquityListEdit4.toPlainText()
        except AttributeError:
            pass

        data["income1"] = self.incomeCombo1.currentText()
        data["income2"] = self.incomeCombo2.currentText()
        try:
            data["income3"] = self.incomeCombo3.currentText()
            data["income4"] = self.incomeCombo4.currentText()
        except AttributeError:
            pass

        data["cashflow1"] = self.CFCombo1.currentText()
        data["cashflow2"] = self.CFCombo2.currentText()
        try:
            data["cashflow3"] = self.CFCombo3.currentText()
            data["cashflow4"] = self.CFCombo4.currentText()
        except AttributeError:
            pass

        data["log transaction"] = self.registerFlowBoxEdit.currentText()

        data["kind"] = self.editTypeCombo.currentText()
        data["quantity"] = self.editQuantityField.text()

        data["subject"] = self.editSubjectField.text()

        data["description"] = self.editDescriptionField.text()
        data["add_args"] = self.editAddArgsField.text()
        data["add_code"] = self.editAddCodeField.toPlainText()

        data["uni-directional"] = self.UnidirCombo.currentText()

        # update summary table
        # self.parent().update_table()
        # self.parent().select_transaction_data(self.parent().selection_idx)
        curr_idx = self.parent().selection_idx

        try:
            self.parent().drawcanvas.old_positions[data["shortname"]] = self.parent(
            ).drawcanvas.old_positions[old_name]
            self.parent().drawcanvas.old_positions[data["shortname"] + "<label>"] = self.parent(
            ).drawcanvas.old_positions[old_name + "<label>"]
            self.parent().drawcanvas.label_position_data[data["shortname"]+"<label>"] = self.parent(
            ).drawcanvas.label_position_data[old_name+"<label>"]
            self.parent().drawcanvas.old_positions[data["shortname"] + "<support>"] = self.parent(
            ).drawcanvas.old_positions[old_name + "<support>"]
            self.parent().drawcanvas.old_positions[data["shortname"] + "<support><label>"] = self.parent(
            ).drawcanvas.old_positions[old_name + "<support><label>"]
        except:
            pass

        # label_position_data
        if old_name != data["shortname"]:
            try:
                del self.parent().drawcanvas.old_positions[old_name]
                del self.parent(
                ).drawcanvas.old_positions[old_name + "<label>"]
                del self.parent(
                ).drawcanvas.label_position_data[old_name + "<label>"]
                del self.parent(
                ).drawcanvas.old_positions[old_name + "<support>"]
                del self.parent(
                ).drawcanvas.old_positions[old_name + "<support><label>"]
            except:
                pass

        self.parent().transactionView.selectRow(curr_idx)
        self.parent().update_table()
        self.parent().transactionView.selectRow(curr_idx)
        self.parent().transaction_select()

        if self.new_edit:
            self.new_edit = False


class SfcGUIMainWindow(QtWidgets.QMainWindow):
    """
    Main Window of the gui
    """

    def set_font_for_all_widgets(self):
        font = self.font
        self.setFont(font)

        # Iterate over all child widgets and set the font for labels and buttons
        def set_font_recursive(widget):
            # Check if it's a QLabel or QPushButton
            if isinstance(widget, (QLabel, QPushButton, QComboBox, QTableView, QLineEdit)):
                widget.setFont(font)

            # Recursively set font for all child widgets
            for child in widget.findChildren(QWidget):
                set_font_recursive(child)

        # Start the recursive function from the parent window (self)
        set_font_recursive(self)

    def __init__(self, myfont=None):

        # Call the inherited classes __init__ method
        super(SfcGUIMainWindow, self).__init__()
        
        # print("load...")
        path = os.path.dirname(os.path.abspath(__file__))
        loadpath = os.path.join(
            path, "mainwindow.ui")

        self.font = "Seoge UI"
        if myfont is not None:
            self.font = myfont
            self.set_font_for_all_widgets()

        # Ensure 100% scaling
        QtWidgets.QApplication.setAttribute(
            QtCore.Qt.AA_EnableHighDpiScaling, False)
        QtWidgets.QApplication.setAttribute(
            QtCore.Qt.AA_UseHighDpiPixmaps, False)

        content = []
        success = False
        self.test_mode = False  # used for testing

        # remove font-size to fix bug in qt designer
        with open(loadpath, "r") as file:
            for line in file.readlines():
                if "<pointsize>-1</pointsize>" not in line:
                    content.append(line)
            success = True

        if success:
            with open(loadpath, "w") as file:
                file.write("".join(content))

        uic.loadUi(loadpath, self)  # Load the .ui file

        self.flipped = False
        my_widget = MyDrawWidget(self.frame, self)

        # BSM and TFM key shortcuts
        self.shortcut_bsm_c = QShortcut(
            QKeySequence("Ctrl+C"), self.BSMtableView)
        self.shortcut_bsm_c.activated.connect(
            lambda: self.clipboard_data(self.BSMtableView))
        self.shortcut_tfm_c = QShortcut(
            QKeySequence("Ctrl+C"), self.TFMtableView)
        self.shortcut_tfm_c.activated.connect(
            lambda: self.clipboard_data(self.TFMtableView))

        # define custom QtTable headers
        custom_header = CustomHeaderView(Qt.Horizontal, self.TFMtableView, mainparent=self)
        custom_header.setDefaultAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.TFMtableView.setHorizontalHeader(custom_header)
        custom_header2 = CustomHeaderView(Qt.Horizontal, self.BSMtableView, mainparent=self)
        custom_header2.setDefaultAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.BSMtableView.setHorizontalHeader(custom_header2)

        # data dicitonary for code
        self.code_data = {}

        # logger for changes in transactions
        self.made_changes = False
        try:
            self.actionGenerate_BalanceSheet_Matrix.triggered.connect(
                lambda: self.gen_balance_matrix(show=True))
            self.actionGenerate_Balance_Matrix_Detailed.triggered.connect(
                lambda: self.gen_balance_matrix(show=True))
        except:
            pass

        self.FilterlineEdit.returnPressed.connect(self.update_table)
        self.FilterlineEditData.textChanged.connect(self.update_data_filter)
        self.FilterlineEditData.returnPressed.connect(self.update_data_filter)
        self.FilterlineEditEQs.textChanged.connect(self.update_eqn_filter)
        self.FilterlineEditEQs.returnPressed.connect(self.update_eqn_filter)
        self.exportButton.pressed.connect(lambda: self.export_transactions())

        self.exportR_xlsxButton.setVisible(False)
        self.copyR_LateXButton.setVisible(False)
        self.fitPlotButton.setVisible(False)
        self.buildModelButton.pressed.connect(
            lambda: self.fit_eqs(build_only=True))

        # settings
        self.SETTING_show_labels = True  # NOTE former checkbox 2
        self.SETTING_highlight_labels = False  # checkbox 4
        self.SETTING_show_arrows = True  # checkBox
        self.SETTING_use_raster = False  # checkbox_raster
        self.SETTING_show_raster = False  # checkBox_raster_2
        self.SETTING_show_support = True  # checkBox_6 NOTE now always True
        self.SETTING_show_handles = True  # checkBox_3 NOTE now always True
        self.SETTING_subject_labels = False  # checkBox_8
        self.SETTING_text_at_handle = True  # checkBox_5
        self.SETTING_show_preview = True  # checkBox_9
        self.SETTING_white_bg = False  # checkbox 7

        # bimets settings
        self.SETTING_R_path = "RModel/"
        self.SETTING_R_SimType = "RESCHECK+STATIC"
        self.SETTING_R_Algo = "Gauss-Seidel"
        self.SETTING_R_RANGE_FIT = (2002, 2017)
        self.SETTING_R_RANGE_SIM = (2010, 2017)
        self.SETTING_fontsize = 12
        self.SETTING_fontsize_2 = 8
        self.SETTING_show_labels = True
        self.SETTING_highligh_labels = False

        # accounting setting
        self.SETTING_ca_ka = defaultdict(lambda: True)
        self.SETTING_balance_init_data = defaultdict()

        self.account_switch.pressed.connect(lambda: self.update_ca_ka_account())

        self.actionEditSetting.triggered.connect(self.edit_gui_settings)
        self.actionEdit_Settings_yaml.triggered.connect(self.edit_settings)

        self.BSMaggcheck.stateChanged.connect(
            lambda: self.gen_balance_matrix(show=False))

        # main thread of this application NOTE might be more useful for later versions
        self.thread = QThread()
        self.transaction_data = None
        self.theme_manager = ThemeManager(self)

        try:
            self.actionChristmas.triggered.connect(
                self.theme_manager.activate_christmas_mode)
        except:
            pass

        self.filter_gui = []
        self.current_file = None
        self.drawcanvas = my_widget
        # self.drawcanvas.mode = "select"
        self.backup_str = ""
        # self.setMouseTracking(True)
        self.graphshowincome.pressed.connect(self.gen_ics_single)
        self.genCFSButton.pressed.connect(self.gen_cashflow_single)
        self.genBSButton.pressed.connect(self.gen_balance_single)
        self.actionBrowse.triggered.connect(self.browse)
        self.actionSave.triggered.connect(self.save)
        self.actionSave_as.triggered.connect(self.save_dlg)
        self.actionTex.triggered.connect(self.gen_tex_ics)
        self.actionSave.triggered.connect(self.save_and_build)

        self.shortcut = QShortcut(QKeySequence("Ctrl+S"), self)
        self.shortcut.activated.connect(self.save_and_build)
        self.shortcut_search = QShortcut(QKeySequence("Ctrl+F"), self)
        self.shortcut_search.activated.connect(self.search_dlg)
        
        self.shortcut_refresh = QShortcut(QKeySequence("Ctrl+R"), self)
        self.shortcut_refresh.activated.connect(self.refresh_tables)
        
        self.actionSwitch_Dark_Bright_Mode.triggered.connect(self.switch_theme)
        
        self.actionTransactions_and_Data_View.setCheckable(True)
        self.actionEq_and_Agents_view.setCheckable(True)
        self.actionTransactions_and_Data_View.setChecked(False)
        self.actionEq_and_Agents_view.setChecked(False)
        self.dockWidget_2.setVisible(False)
        self.dockWidget.setVisible(False)

        self.actionTransactions_and_Data_View.triggered.connect(self.toggle_view_transdata)
        self.actionEq_and_Agents_view.triggered.connect(self.toggle_view_eqagent)

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.show_dock_context_menu)
        
        try:
            self.actionAbout.triggered.connect(lambda: self.notify(
                "This sfctols-attune, developed at German Aerospace Center, Institute of Networked Energy Systems (DLR-VE).\nMIT license.\nThanks for using! Please report bugs to thomas.baldauf@dlr.de", title="About this software"))
            self.actionFlowMatrix_to_Excel_file.triggered.connect(
                lambda: self.gen_transaction_matrix(mode="excel"))
        except:
            pass

        self.transactionView.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.transactionView.doubleClicked.connect(
            lambda: self.transaction_select(double_click=True))
        self.transactionView.currentCellChanged.connect(
            self.transaction_select)

        self.transactionView.setColumnWidth(0, 120)
        self.EQtableView.setColumnWidth(2, 450)

        # self.arrangeButton.pressed.connect(self.arrange_pretty)
        self.removeButton.pressed.connect(self.remove_item)

        self.data_wrapper = None
        self.plotSelectedDataButton.pressed.connect(self.plot_data)
        self.plotSelectedDataFitButton.pressed.connect(
            lambda: self.plot_data(add_fit=True))
        self.plotSelectedModelButton.pressed.connect(
            lambda: self.plot_data(add_fit=True, show_data=False))

        self.mainloop_str = ""
        self.settings_str = """
metainfo:
	author: your name here
	date: 2070
	info: example settings

hyperparams:
       - name: example
         value: 42.0
         description: an example parameter
"""
        try:
            self.editSettingsButton.triggered.connect(self.edit_settings)
        except:
            pass

        self.addButton.pressed.connect(self.add_new)  # add an agent
        # add a helper agent (no transactions)
        self.addButton2.pressed.connect(self.add_helper)
        self.graphDeleteButton.pressed.connect(self.remove_helper)

        # self.graphEditButton.pressed.connect(self.edit_graph_agent)

        try:
            self.actionExport_Graph_to_PDF.triggered.connect(
                self.drawcanvas.export_pdf)
        except:
            pass

        try:
            self.actionSwap_Line_Interpolation.triggered.connect(
                self.drawcanvas.swap_interpol_style)

        except:
            pass

        self.actionRun.triggered.connect(self.run_project)
        self.udpateButton.pressed.connect(self.update_transaction_data)

        self.filename = None    # current .yaml file
        self.entry_data = []  # store current entries
        self.selection_idx = None

        # self.matrixView = MatrixViewer(self)

        # move transactions up/down in list
        self.moveUpBtn.pressed.connect(self.move_up)
        self.moveDnBtn.pressed.connect(self.move_down)

        # move equations up/down in list
        self.moveUpBtn_2.pressed.connect(self.move_eq_up)
        self.moveDnBtn_2.pressed.connect(self.move_eq_down)

        # settings editor is always running to check for file changes
        self.settings_editor = SettingsEditor(self, self.settings_str)
        self.settings_editor.setWindowFlag(Qt.Tool, True)
        self.settings_editor.show()
        self.settings_editor.hide()

        # theme manager setup
        # theme manager has 'dark' and 'bright' mode
        self.dataBrowseButton.pressed.connect(self.browse_data)

        self.eq_wrapper = EQsWrapper(self)
        self.eq_model = None
        self.addEQButton.pressed.connect(self.add_eq)
        self.add_R_EQButton.pressed.connect(self.add_R_eq)
        self.remEQButton.pressed.connect(self.rem_eq)

        # fit buttons
        # self.fitButton.pressed.connect(lambda: self.fit_eqs(False))
        self.fitAllButton.pressed.connect(lambda: self.fit_eqs(True))
        # self.EQtableView.doubleClicked.connect(self.onEqChanged)
        self.config_bimets_button.pressed.connect(self.show_bimets_config)

        print("cwd", os.getcwd())
        module_dir = os.path.dirname(__file__)
        print("sfctoos-dir", module_dir)
        self.icon = QIcon(os.path.join(module_dir, "DLR_Logo.png"))
        self.setWindowIcon(self.icon)

        self.theme_manager.theme = "dark"
        self.switch_theme()  # make bright
        self.theme_manager.restore()  # instantiate theme manager

        self.switch_theme()
        self.switch_theme()  # make dark and bright again

        # save the bimets model and data output
        self.bimets_model = None  # extract the Python object containing the model specs
        self.bimets_model_code_pt1 = None  # extract the generated code
        # extract the generated code (part 2)
        self.bimets_model_code_pt2 = None
        self.bimets_data = None  # extract the data output

        self.data_model = None

        # data info for the add factors etc.
        self.data_user_params = None

        self.lower_shrinked = 0
        self.right_shrinked = 0

        # checkboxes for analytical vs. data view 
        self.checkBoxAnalyticalTFM.stateChanged.connect(lambda state: self.gen_transaction_matrix())
        self.checkBoxAnalyticalBSM.stateChanged.connect(lambda state: self.gen_balance_matrix())

        self.spinBox_refYear.valueChanged.connect(lambda state: self.gen_balance_matrix())
        self.spinBox_refYear2.valueChanged.connect(lambda state: self.gen_transaction_matrix())

        self.btn_balance_data_link.pressed.connect(self.config_bsm_linkages)

        self.btn_rearrange_Tfm.pressed.connect(lambda: self.config_order("tfm"))
        self.btn_rearrange_Bsm.pressed.connect(lambda: self.config_order("bsm"))

        self.sorting_tables = {
            "tfm": {
                "rows": [], 
                "cols": [],
            },
            "bsm": {
                "rows": [], 
                "cols": [],
            }
        }

        self.RcheckButton.pressed.connect(lambda: self.check_eqs())
        
        # hihihi
        self.btn_Ts_cross_bsm.pressed.connect(lambda: self.gen_balance_cross_timeseries())
        self.btnTFMplot.pressed.connect(lambda: self.gen_tfm_cross_timeseries())


    def check_eqs(self):
        EQS = self.get_selected_eqs()
        print("CHECK EQS")
        print(EQS)

        try:
                
            v_temp = SimpleNamespace()
            eqs = Equations()
            eqs.set_namespace(v_temp)

            # work on a copy to avoid side effects
            data = self.data_wrapper.df.copy()
            if "Year" not in data.columns:
                data["Year"] = data.index

            read_data_to_ns(data, v_temp)
            eqs.read_data(data)
            eqs.link_namespace_dict(v_temp)

            # build the system
            for name, eq_data in EQS.items():
                # eq string like "GDP = C + G + I + X - M"
                eqs.add(eq_data["EQ"][0], coeffs=None, namespace=v_temp)

            # evaluate identities
            rows = []
            for name, eq_data in EQS.items():
                if eq_data["type"][0] != "IDENTITY":
                    continue
                rhs_vals = [eqs.eval_rhs(name, t) for t in range(len(data))]
                rhs = np.asarray(rhs_vals, dtype=float)
                lhs = np.asarray(data[name], dtype=float)
                rmse = np.sqrt(np.nanmean((rhs - lhs) ** 2))
                rows.append({"Name": name, "RMSE": rmse})

            df = pd.DataFrame(rows).sort_values(by="RMSE", ascending=False)

            if df.empty:
                self.notify("Did not select any identities.", title="Error")
                return

            if len(df) > 1:
                dlg = DataFrameDialog(df, parent=self)
                dlg.table.setSortingEnabled(True)
                dlg.show()
            else:
                # exactly one identity; recompute series for that name to show LHS/RHS
                only_name = df["Name"].iloc[0]
                rhs_vals = [eqs.eval_rhs(only_name, t) for t in range(len(data))]
                rhs = np.asarray(rhs_vals, dtype=float)
                lhs = np.asarray(data[only_name], dtype=float)

                rmse_val = float(df["RMSE"].iloc[0])
                rmse_label = f"RMSE {rmse_val}"

                comp = pd.DataFrame({"LHS": lhs, "RHS": rhs}, index=data.index)
                dlg = DataFrameDialog(comp, parent=self)
                label = QLabel(rmse_label, dlg)
                layout = dlg.layout()
                layout.addWidget(label)
                dlg.show()

        except Exception as e:
            self.notify(f"Exception: {e}", title="Exception")

    
    def refresh_tables(self):
        self.gen_balance_matrix()
        self.gen_transaction_matrix()

    def sort_table(self, which):
        if which == "bsm": 
            ro = self.sorting_tables["bsm"]["rows"]
            co = self.sorting_tables["bsm"]["cols"]
            df = self.bsm_model.df0.copy()

            if len(co) == 0 or len(ro) == 0:
                return 
            
            # if len(co) != 0:
            #     co = [c for c in co if c < len(df.columns)]
            #     df = df.iloc[:, co] 

            if len(ro) != 0:
                ro = [r for r in ro if r in df.index]
                for r in df.index:
                    if r not in ro:
                        ro.append(r)
                for x in ["Total", "NW"]:
                    if x in df.index and not x in ro:
                        ro += [x]
                df = df.loc[ro]

            desired_order = co
            idx = list(df.columns)
            n = len(idx)

            print("COLS", df.columns)
            # 1) Scan the index and record blocks by integer positions
            #    blocks_pos[name] -> list of row positions (e.g., [i] or [i, i+1] if next is "\nKA")
            blocks_pos = {}
            i = 0
            while i < n:
                lab = idx[i]
                if lab.endswith('\nA '):
                    name = lab[:-3]  # strip the trailing '\nA '
                    block = [i]
                    # pair with the immediately following "\nL " row if it exists
                    if i + 1 < n and idx[i + 1] == '\nL ':
                        block.append(i + 1)
                        i += 2
                    else:
                        i += 1
                    blocks_pos[name] = block
                else:
                    i += 1
            
            # 2) Build the new order of positions using the desired order
            new_pos = []
            seen = set()
            for name in desired_order:
                if name in blocks_pos:
                    for p in blocks_pos[name]:
                        new_pos.append(p)
                        seen.add(p)
                else:
                    # If a desired name exists as a plain row (e.g., "Households" without "\nCA"), include it
                    for p, lab in enumerate(idx):
                        if p not in seen and (lab == name):
                            new_pos.append(p)
                            seen.add(p)

            # 3) Append leftover rows (anything not already included), preserving their original order
            leftovers = [p for p in range(n) if p not in seen]
            new_pos.extend(leftovers)

            for i, c in enumerate(df.columns):
                if i not in new_pos:
                    new_pos += [i]

            #print("new pos", new_pos)
            # 4) Reorder via iloc (works with non-unique labels)
            df = df.iloc[:, new_pos]
            #print("df", df)

            # apply formatting
            df = df.applymap(
                lambda x: f"{x:,.1f}" if isinstance(x, (int, float)) else x
            )

            model = PandasModel(df)
            self.bsm_model = model
            self.BSMtableView.setModel(self.bsm_model)

        elif which == "tfm":
            if not hasattr(self, 'tfm_model'):
                return 

            ro = self.sorting_tables["tfm"]["rows"]
            co = self.sorting_tables["tfm"]["cols"]
            df = self.tfm_model.df0.copy()

            if len(co) == 0 or len(ro) == 0:
                return 

            # if len(co) != 0:
            #     co = [c for c in co if c < len(df.columns)]
            #     df = df.iloc[:, co] 

            if len(ro) != 0:
                ro = [r for r in ro if r in df.index]
                for r in df.index:
                    if r not in ro: 
                        ro.append(r)
                df = df.loc[ro]

            # # sort columns 
            # # Build a new index list preserving "\nKA" lines after each "\nCA"
            desired_order = co
            idx = list(df.columns)
            n = len(idx)

            # 1) Scan the index and record blocks by integer positions
            #    blocks_pos[name] -> list of row positions (e.g., [i] or [i, i+1] if next is "\nKA")
            blocks_pos = {}
            i = 0
            while i < n:
                lab = idx[i]
                if lab.strip().endswith('\nCA'):
                    name = lab[:-3]  # strip the trailing '\nCA'
                    block = [i]
                    # pair with the immediately following "\nKA" row if it exists
                    if i + 1 < n and idx[i + 1].endswith('\nKA'):
                        block.append(i + 1)
                        i += 2
                    else:
                        i += 1
                    blocks_pos[name] = block
                else:
                    i += 1

            # 2) Build the new order of positions using the desired order
            new_pos = []
            seen = set()
            for name in desired_order:
                if name in blocks_pos:
                    for p in blocks_pos[name]:
                        new_pos.append(p)
                        seen.add(p)
                else:
                    # If a desired name exists as a plain row (e.g., "Households" without "\nCA"), include it
                    for p, lab in enumerate(idx):
                        if p not in seen and (lab == name):
                            new_pos.append(p)
                            seen.add(p)

            # 3) Append leftover rows (anything not already included), preserving their original order
            leftovers = [p for p in range(n) if p not in seen]
            new_pos.extend(leftovers)

            for i, c in enumerate(df.columns):
                if i not in new_pos:
                    new_pos += [i]

            # 4) Reorder via iloc (works with non-unique labels)
            df = df.iloc[:, new_pos]

            # apply formatting
            df = df.applymap(
                lambda x: f"{x:,.1f}" if isinstance(x, (int, float)) else x
            )

            model = PandasModel(df)
            self.tfm_model = model
            self.TFMtableView.setModel(self.tfm_model)

            

    def config_order(self, which):
        # configure order of BSM/TFM columns and rows
        if which == "bsm": 
            df = self.BSMtableView.model().df
        elif which == "tfm":
            df = self.TFMtableView.model().df
        dlg = DataFrameSorterDialog.open(self, df, which=which)
        dlg.show()
            

    def get_selected_agent(self):
        selected_agent = self.drawcanvas.highlighted
        if selected_agent:
            if not selected_agent.ishelper:
                return selected_agent.name.capitalize()
        return None 

    def config_bsm_linkages(self):
        # configuration of the linkages of initial stock values to the data...
        try:
                
            selected_agent = self.drawcanvas.highlighted
            if selected_agent:
                if not selected_agent.ishelper:
                    k = selected_agent.name.capitalize()
                    databsm = self.gen_balance_single(show_window=True)
                    result = {}
                    all_items = []
                    for key, values in databsm.items():
                        extracted = []
                        for text in values:
                            # find words ending with ':' (e.g. Other_Assets:, Deposits:, NW:)
                            matches = regex.findall(r'([A-Za-z0-9_]+):', text)
                            extracted.extend(matches)
                            all_items.extend(matches)
                        if extracted:
                            result[key] = extracted
                    max_len = max(len(v) for v in result.values())
                    for kk, v in result.items():
                        result[kk] = v + [''] * (max_len - len(v))
                    df = pd.DataFrame(result)
                    
                    print("df\n", df)

                    df2 = {}
                    print("init data")
                    print(self.SETTING_balance_init_data)

                    for c in df.columns:
                        entry = df[c]
                        if k in self.SETTING_balance_init_data:
                            entry = self.SETTING_balance_init_data[k].get(c, df[c])
                            print(" --> entry **", entry)
                            print("all_items", all_items)
                        
                        entry_keys = [ky for ky in all_items if ky in list(df[c])]
                        entry_keys = entry_keys + [np.nan]*(len(all_items)-len(entry_keys))
                        entry_vals = [entry.get(ky, np.nan) for ky in entry_keys]
                        df2[c] = entry_keys 
                        df2[c+" [Data]"] = entry_vals
                    
                    print("df2", df2)
                    
                    df2 = pd.DataFrame(df2)
                    df2.dropna(how='all', inplace=True)
                    df2 = df2.fillna("")

                    model = PandasModel(df2)
                    viewer = BalanceSheetViewer(self, name=k+"_B0", has_button=True)

                    ts_df = self.data_wrapper.df  # same DF as above
                    available_cols = ts_df.columns.tolist()

                    delegate = ColumnSelectorDelegate(available_cols, viewer.tableView)  # assume your dialog has a QTableView named .table

                    # Find and set delegate on every *Data column
                    data_col_indexes = []
                    for col_idx, header in enumerate(df2.columns):  # assuming PandasModel exposes .df (or adapt)
                        if header.endswith(" [Data]"):
                            viewer.tableView.setItemDelegateForColumn(col_idx, delegate)
                            data_col_indexes.append(col_idx)

                    # Slot: recompute paired *Value when a Data cell changes
                    def recompute_value_for(index_top_left, index_bottom_right, roles):
                        # iterate through changed indexes
                        for row in range(index_top_left.row(), index_bottom_right.row() + 1):
                            for col in range(index_top_left.column(), index_bottom_right.column() + 1):
                                header = df2.columns[col]
                                if not header.endswith(" [Data]"):
                                    continue
                                # paired value column
                                base = header[:-4]  # strip "Data"
                                value_col = base + " [Value]"
                                if value_col not in df2.columns:
                                    continue
                                selected_series = df2.at[row, header]
                                if isinstance(selected_series, str) and selected_series in ts_df.columns:
                                    first_idx = ts_df[selected_series].first_valid_index()
                                    val = ts_df.at[first_idx, selected_series] if first_idx is not None else float('nan')
                                else:
                                    val = float('nan')
                                # write to model.df and emit dataChanged for the value cell
                                df2.at[row, value_col] = val
                                value_col_idx = df2.columns.get_loc(value_col)
                                tl = model.index(row, value_col_idx)
                                br = model.index(row, value_col_idx)
                                model.dataChanged.emit(tl, br, [Qt.DisplayRole])

                    # Connect the signal
                    model.dataChanged.connect(recompute_value_for)
                    viewer.set_model(model)
                    viewer.show()
            else:
                self.notify("Please select an agent first", title="No agent selected!")
        except Exception as e:
            self.notify("Cannot generate balance sheet: %s" % str(e), title="Exception")
            
    def update_ca_ka_account(self):
        # switch info if agent has a separate CA/KA or just a simple account
        selected_agent = self.drawcanvas.highlighted
        if selected_agent:
            if not selected_agent.ishelper:
                k = selected_agent.name.capitalize()
                self.SETTING_ca_ka[k] = not self.SETTING_ca_ka[k]

                # reset past sorting of TFM dataframe columns
                # self.sorting_tables["tfm"]["rows"] = []
                # self.sorting_tables["tfm"]["cols"] = []
            
                self.gen_transaction_matrix()

        else:
            self.notify("Please select an agent first", title="No agent selected!")

    def show_dock_context_menu(self, pos):
        sender_dock = self.sender()
        
        # Get global position of the click
        global_pos = sender_dock.mapToGlobal(pos)

        # Check if the global mouse position is inside `frame`
        if self.frame.geometry().contains(self.frame.mapFromGlobal(global_pos)):
            print("Mouse over frame — suppressing menu.")
            return  # Do not show the menu

        # Otherwise show the menu
        menu = QMenu(sender_dock)

        action_eqagent = QAction("Show Equations and Agents View", self)
        action_eqagent.setCheckable(True)
        action_eqagent.setChecked(self.dockWidget.isVisible())
        action_eqagent.triggered.connect(self.toggle_view_eqagent)

        action_transdata = QAction("Show Transactions and Data View", self)
        action_transdata.setCheckable(True)
        action_transdata.setChecked(self.dockWidget_2.isVisible())
        action_transdata.triggered.connect(self.toggle_view_transdata)

        menu.addAction(action_eqagent)
        menu.addAction(action_transdata)

        menu.exec_(global_pos)


    def toggle_view_transdata(self):
        is_visible = self.dockWidget_2.isVisible()
        self.dockWidget_2.setVisible(not is_visible)
        self.actionTransactions_and_Data_View.setChecked(not is_visible)

    def toggle_view_eqagent(self):
        is_visible = self.dockWidget.isVisible()
        self.dockWidget.setVisible(not is_visible)
        self.actionEq_and_Agents_view.setChecked(not is_visible)
            
    def edit_gui_settings(self):
        settings_dialog = GuiSettingsDialog(self)
        settings_dialog.show()

    def show_bimets_config(self):
        # show a new bimets config dialog
        bimets_dlg = BimetsDialog(self, myfont=self.font)
        bimets_dlg.show()

    def clipboard_data(self, table_view):
        # copy the table to latex string
        try:
            model = table_view.model()
        except:
            return

        def convert_list(y):
            # print("convert ", y, type(y))
            if isinstance(y, list):
                z = []
                for i in y:
                    if isinstance(i, list):
                        z.append("".join(i))
                    elif isinstance(i, tuple):
                        z.append("".join(list(i)))
                    else:
                        z.append(i)
                return z

            elif isinstance(y, str):
                return [y]
            else:
                try:
                    return [str(y)]
                except:
                    pass
            return []

        def entry_to_str(x):
            z = "+".join(convert_list(x))
            if z == "":
                return ""

            y = str(sympy.simplify(z))
            # print("join", x, "->", z, "->" , y)
            y = "$" + y + "$"
            return y

        try:
            df = model.raw_data
            # .to_clipboard() # excel=False,sep=",")
            df = df.applymap(entry_to_str).replace("=", "")
            pyperclip.copy(df.to_latex().replace("Δ", "$\Delta$"))

            self.statusBar().showMessage("Copied LaTeX table to clipboard.")

        except Exception as e:
            self.notify(str(e), title="Error")

    def hide_show_right(self):
        return
        # TODO remove in future versions safely

    def hide_show_lower(self):
        # hide or show lower half of the gui
        # TODO remove safely in fugure versions
        return

    def onEqChanged(self, index):

        print("eq changed")
        row = index.row()
        column = index.column()
        current_val = self.eq_model.raw_data.iloc[row, column]
        # self.eq_model.setData(index, current_val)
        self.eq_model.setData(index, " ")

    def rem_eq(self):
        try:
            # remove selected equation from the equation list

            eq_df = self.eq_wrapper.get_df()
            if self.eq_model is None:
                self.eq_model = PandasModel(eq_df, self.EQtableView)
                self.EQtableView.setModel(self.eq_model)

            self.eq_model.removeSelectedRows()
            self.eq_wrapper.udpate_data()

            # self.EQtableView.setModel(model)
        except Exception as e:
            self.notify("something went wrong.", title="Error")

    def add_eq(self, row_data=None):
        # add an equation to the equation list
        # self.eq_wrapper.add_line()
        try:
            eq_df = self.eq_wrapper.get_df()
            if self.eq_model is None:
                self.eq_model = PandasModel(eq_df, self.EQtableView)
                self.EQtableView.setModel(self.eq_model)

            self.eq_model.addRow(row_data=row_data)
            self.eq_wrapper.udpate_data()

            min_colwidth = 350
            if self.EQtableView.columnWidth(2) < min_colwidth:
                self.EQtableView.setColumnWidth(2, min_colwidth)
            self.update()
        except Exception as e:
            self.notify("something went wrong.", title="Error")

    def add_R_eq(self):
        """
        browse file and try to add equations from the file by reading the EQ> lines
        """

        equations = defaultdict(
            lambda: {"equation": "", "type": "", "coeff": "", "comment": ""})

        current_context = None  # Tracks whether we are in an IDENTITY or BEHAVIORAL block
        eq_name = None

        options = QFileDialog.Options()
        file_path, _ = QFileDialog.getOpenFileName(
            self, "Select a File", "", "R Files (*.R);;Text Files (*.txt)", options=options)

        if not file_path:
            print("Exception: no file or file not valid")
            # TODO notify status bar
            return

        with open(file_path, 'r') as file:
            for line in file:
                line = line.strip()

                # Detect the context (IDENTITY or BEHAVIORAL)
                if line.startswith("IDENTITY>"):
                    current_context = "IDENTITY"
                    # Extract the equation name
                    eq_name = line.split(">")[1].strip()
                    # print("context<IDENTIT>", eq_name)

                elif line.startswith("BEHAVIORAL>"):
                    current_context = "BEHAVIORAL"
                    eq_name = line.split(">")[1].strip()
                    # print("context<BEHAVIORAL>", eq_name)

                # Extract equation when encountering EQ>
                elif line.startswith("EQ>"):
                    if eq_name:
                        equations[eq_name]["equation"] = line.split(">")[
                            1].strip()
                        equations[eq_name]["type"] = current_context

                elif line.startswith("COMMENT>"):
                    if eq_name:
                        equations[eq_name]["comment"] = line.split(">")[
                            1].strip()
                    
                # Capture COEFF if it exists for behavioral equations
                if current_context == "BEHAVIORAL" and line.startswith("COEFF>"):
                    # Extract the COEFF value
                    coeff = line.split(">")[1].strip()
                    if eq_name:
                        if coeff:
                            equations[eq_name]['coeff'] = coeff
                        else:
                            equations[eq_name]['coeff'] = ''

        print("Extracted Equations:\n-----------------")
        if self.eq_model:
            print(self.eq_model.raw_data["NAME"])
            print("y" in self.eq_model.raw_data["NAME"])

        no_all = False
        yes_all = False

        for k, e in equations.items():
            # print(k, e)
            # print("raw datat")
            confirm_overwrite = True
            if self.eq_model:
                if k.strip() in list(self.eq_model.raw_data['NAME']):
                    # print(self.eq_model.raw_data)
                    # self.notify("Found %s already in list of equations. keep?", # title="Notification")
                    if yes_all:
                        answer = "yes"
                    elif no_all:
                        answer = "no"
                    else:
                        answer = self.ask_question_all(
                            '', "There is already an entry %s. Do you want to overwrite?" % k.strip())
                        if answer == 'yes_all':
                            yes_all = True
                        elif answer == 'no_all':
                            no_all = True

                    if answer == 'no' or answer == 'no_all':
                        print("answer", answer)
                        confirm_overwrite = False
                    elif answer == 'yes' or answer == 'yes_all':
                        print("confirm_overwrite", answer)
                        confirm_overwrite = True

            if confirm_overwrite:  # allowed to overwrite or add new?

                # remove old data if existent
                original_index = None
                if self.eq_model:
                    df = self.eq_model.raw_data

                    if k.strip() in list(df['NAME']):
                        original_index = df.index  # replacing an old entry: remember order of entries
                    df = df[df['NAME'] != k.strip()]
                    self.eq_model.set_data(df)

                # add new data
                self.add_eq(
                    row_data=[k.strip(), e["type"], e["equation"], e["coeff"], "", "", e["comment"]])

                if original_index is not None:  # sort back to old order
                    df = self.eq_model.raw_data
                    df = df.reindex(original_index)
                    self.eq_model.set_data(df)

            self.update()
        print("--------------------------------")

    def export_bimets_xlsx(self):
        if self.bimets_model is None:
            self.notify(
                "There is not yet a fitted model. Please fit model before saving outputs.", title="Error")
            return
        if self.bimets_data is None:
            self.notify(
                "No output data found. Please fit model without any errors before saving outputs.", title="Error")
            return
        # browse for saving
        print(self.bimets_data)

    def plot_data(self, add_fit=False, show_data=True):
        # see if any rows are selected, if yes, plot

        # clear all figures
        try:
            plt.close('all')
        except:
            pass

        if self.data_wrapper is None:
            return
        selected_indexes = self.dataTableView.selectionModel().selectedRows()

        data = {}
        for index in selected_indexes:
            name = self.data_wrapper.ts_df.iloc[index.row()]["NAME"]
            data[name] = self.data_wrapper.df[name]
        df = pd.DataFrame(data)

        try:
            # print("DF\n", df)
            y_start = int(self.lineEdit_year_start_data.text())
            y_end = int(self.lineEdit_year_end_data.text())
            df = df[df.index >= y_start]
            df = df[df.index <= y_end]

        except Exception as e:
            print("Exception: ", str(e))

        if add_fit:
            df2 = self.bimets_data
            if df2 is None:
                self.notify("No fit for this data entry.", title="Error")

                return
            try:
                df2 = df2.set_index("Year")
            except:
                try:
                    df2 = df2.set_index("Period")
                except:
                    self.notify(
                        "Dataframe Index column must be named 'Year' or 'Period'", title="Error")

            print("df2", df2)

            # convert to numeric 
            print("df2\n", df2)
            df2 = df2.apply(pd.to_numeric, errors='ignore')
            
        try:

            try:
                plt.gcf()
            except:
                pass
            try:
                plt.style.use("sfctools")

            except:
                pass
            # df.plot.line()
            years_data = df.index 
            for c in df.columns:
                try:
                    if show_data:
                        if self.checkBox_growthrate.isChecked():
                            plt.plot(years_data, df[c].diff()/df[c], label=c + " (Data)")
                            # plt.plot(df[c].diff()/df[c], label="Actual")
                        else:
                            plt.plot(years_data, df[c], label=c + " (Data)")
                            # plt.plot(df[c], label="Actual")
                    if add_fit:
                        years_model = df2.index - 1
                        if self.checkBox_growthrate.isChecked():
                            plt.plot(years_model, df2[c].diff()/df2[c],
                                     label=c + ("(Model)"))
                            # plt.plot(df2[c].diff()/df2[c],
                            #          label="Model")
                        else:
                            plt.plot(years_model, df2[c], label=c + ("(Model)"))
                            # plt.plot(df2[c], label="Model")
                except Exception as e:
                    print("Exception", str(e))

            plt.legend()
            plt.xlabel("Time")
            if self.checkBox_growthrate.isChecked():
                plt.ylabel("Growth Rate")
            else:
                plt.ylabel("Value")
            plt.gcf().set_size_inches(w=6, h=3.8)
            plt.tight_layout()
            fig = plt.gcf()
            manager = plt.get_current_fig_manager()

            manager.window.setWindowIcon(self.icon)
            manager.set_window_title('Data plot')

            if add_fit:
                y_min = min(np.min(df.index), np.min(df2.index))
                y_max = max(np.max(df.index), np.max(df2.index))
            else:
                y_min = np.min(df.index)
                y_max = np.max(df.index)

            if y_min % 5 != 0:
                y_min_adjusted = y_min - (y_min % 5)
            else:
                y_min_adjusted = y_min
            if y_max % 5 != 0:
                y_max_adjusted = y_max + (5 - (y_max % 5))
            else:
                y_max_adjusted = y_max
            xticks = list(range(y_min_adjusted, y_max_adjusted + 1, 5))
            if (xticks[0] - y_min) >= 3:
                xticks = [y_min] + xticks
            xticklabels = [str(s) for s in xticks]
            plt.xticks(xticks, xticklabels)

            fig = plt.gcf()
            ax = plt.gca()
            try:
                fig, ax = apply_mpl_theme(fig, ax, self.theme_manager)
            except:
                print("COuld not apply theme manager :/")
            plt.tight_layout()
            plt.show()

        except Exception as e:
            self.notify("You did not select a row or something else went wrong.\n%s" % str(e),
                        title="Error while plotting")

    def browse_data(self, file_path=None, block_dlg=False):
        # browse data file for fitting

        first_load = self.data_model is None

        if file_path is None:
            file_path, _ = QFileDialog.getOpenFileName(
                self, "Select File", "", "Excel or csv Files (*.xlsx; *.csv)")

            # clear up file path
            current_path = os.getcwd()
            try:
                relative_path = Path(file_path).resolve().relative_to(
                    Path(current_path).resolve())
                print("relative path", file_path)
                file_path = str(relative_path)
                print("found shortcut path", file_path)
            except Exception as e:
                print("Exception:", str(e))

        if file_path:
            self.datafilePathLabel.setText(f"{file_path}")

            # load data file
            if file_path.endswith(".xlsx"):
                print("read excel...")
                df = pd.read_excel(file_path)
            elif file_path.endswith(".csv"):
                print("read csv...")
                df = pd.read_csv(file_path)

                try:
                    # convert this to excel and save back to disk
                    excel_file_path = file_path.replace('.csv', '.xlsx')
                    df.to_excel(excel_file_path, index=False)
                except:
                    pass

            else:
                self.notify(
                    "%s is no valid file or no valid file ending (should be xlsx or csv)" % file_path, title="Error")
                return

            # try to set year as index
            try:
                try:
                    df = df.set_index("Year")
                except:
                    try:
                        df = df.set_index("Period")
                    except:
                        self.notify(
                            "Dataframe Index column must be named 'Year' or 'Period'", title="Error")

                if "Unnamed: 0" in df:
                    del df["Unnamed: 0"]
            except Exception as e:
                print("Warning: no 'Year' column detected.")

            print("Data\n", df)
            self.data_wrapper = DataWrapper(df)
            self.update_data_table()

            # prompt if user wants to keep old parameters?
            if not block_dlg:
                if (not first_load) and (self.transaction_data is not None):
                    yes = self.ask_question(
                        '', "Do you want to keep the data args from the previous file?")
                    if yes:
                        self.update_data_args(
                            self.transaction_data["data_args"])

        else:
            # no file selected
            pass

    def update_data_table(self):
        print("UPDATE DATA TABLE.")
        ts_df = self.data_wrapper.ts_df
        if self.data_model is None:
            print("creating new data model...")
            self.data_model = PandasModel(ts_df, view=self.dataTableView, blocked_cols=[
                                          "NAME", "START", "END", "FREQ"])
            self.dataTableView.setModel(self.data_model)

        else:
            self.data_model.set_data(ts_df)
        # self.update()

    def notify_test(self):
        self.notify("test", title="Test")

    def export_transactions(self):

        my_alt_fname = "/" + self.export_path_edit.text()

        if my_alt_fname.strip() == "":
            self.notify("Please insert a valid path for export",
                        title="Invalid Path")
            return
        try:
            print("export to", my_alt_fname)
            self.gen_code(alt_fname=my_alt_fname,
                          ask_overwrite=True, add_cl_checks=None)
            self.statusBar().showMessage("Exported  transactions to " + str(my_alt_fname))
        except Exception as e:
            self.notify("An error occurred during export:\n%s.\nDid you specify a valid path?" % str(
                e), title="Export Error")

    def update_alt_label(self):
        s = ""
        try:
            if self.agent1EditField.text() == self.agent2EditField.text():
                s = "other_" + self.agent2EditField.text()
        except:
            pass

        # self.altName.setText(s)

    def search_dlg(self):
        """
        search for an agent
        """

        if SearchDialog.instance is None:
            new_dlg = SearchDialog(self)

        else:
            new_dlg = SearchDialog.instance

        new_dlg.show()
        # self.notify("Test Search", title="Search Agent")

    def change_color(self, which):
        color = QColorDialog.getColor()

        if color.isValid():
            self.theme_manager.colors[self.theme_manager.theme][which-1] = color

    def clear_filter(self):
        # self.filterCombo.clearSelection() TODO fix
        pass
    
    def gen_tfm_cross_timeseries(self):
        if not self.checkBoxAnalyticalTFM.isChecked():
            self.notify("Please select analytical mode", title="Not available.")
            return 

        selection = self.TFMtableView.selectionModel().selectedIndexes()

        if selection:
            idx = min(selection, key = lambda i: (i.row(), i.column()))

            def _map_to_source(index):
                model = index.model()
                idx = index
                while isinstance(model, QAbstractProxyModel):
                    idx = model.mapToSource(idx)
                    model = model.sourceModel()
                return model, idx
            source_model, src_idx = _map_to_source(idx)
            row_name = source_model.headerData(src_idx.row(), Qt.Vertical, Qt.DisplayRole).strip()
            col_label = source_model.headerData(src_idx.column(), Qt.Horizontal, Qt.DisplayRole).strip()
            if col_label.strip() == "KA":
                col_label = source_model.headerData(src_idx.column()-1, Qt.Horizontal, Qt.DisplayRole).strip()
            agent_name = col_label.split("\n")[0].strip().capitalize()
            print("agent_name", agent_name)
            
            value = source_model.data(src_idx, Qt.DisplayRole)

            def to_list(s: str):
                s = s.strip()
                if not s.startswith("[") or not s.endswith("]"):
                    return [s]  # not a list-looking string
                try:
                    return ast.literal_eval(s)  # works if quoted
                except (ValueError, SyntaxError):
                    return [x.strip() for x in s.strip("[]").split(",") if x.strip()]
                    
            cell_value = " + ".join(f"({x})" for x in to_list(value))
            cell_value = simplify(cell_value)

            print("ROW NAME", row_name)
            if "Δ" in row_name:
                # check if it is a liability: 
                print("Delta in row name", row_name)
                try:
                    flip_sign = True 
                    bal_item = row_name.split("Δ")[1].strip()
                    print("bal_item", bal_item)
                    
                    if bal_item == "NW":
                        flip_sign = True
                    else:
                        if agent_name in self.SETTING_balance_init_data:
                            # A_list = self.SETTING_balance_init_data[agent_name]["Assets"]
                            L_list = self.SETTING_balance_init_data[agent_name]["Liabilities"]
                            if bal_item in L_list:
                                print("found", bal_item, "in Liabilities of", agent_name, "-> flipping sign")
                                flip_sign = False
                    
                    if flip_sign:
                        cell_value = f"-({cell_value})"
                        cell_value = simplify(cell_value)
                except:
                    pass     
            # print("cell value", cell_value)

            try:
                start_year = int(self.lineEdit_year_start_data.text())
                year_ref = int(self.lineEdit_year_end_data.text())

            except Exception as e:
                self.notify("Error. please specify a valid data range.\n%s" % str(e), title="Error")
                return 
            
            try:
                data_loaded = self.data_wrapper.df
            
            except:
                self.notify("Cannot find data. Have you loaded any data yet?", title="Error")    
                return 

            data_success = False 
            selected_indexes = self.dataTableView.selectionModel().selectedRows()
            # print("selected rows", selected_indexes)
            if len(selected_indexes) > 0:
                index = selected_indexes[0]
                #print("index", index)
                name = self.data_wrapper.ts_df.iloc[index.row()]["NAME"]
                myvar = name
                # #print("myvar", myvar)
            
                data_loaded = data_loaded.loc[data_loaded.index >= start_year]
                data_loaded = data_loaded.loc[data_loaded.index <= year_ref+1] 
                data_loaded = data_loaded[myvar]
                
                #print("data loaded", data_loaded)
                #print("cell value", cell_value)

                data_success = True 

            def clean_name(n, extract_sign=False):
                if extract_sign: 
                    if "-" in n:
                        sign = -1 
                    else:
                        sign = +1 
                name = n.replace("(", "").replace(")", "").replace("+", "").replace("-", "").strip()
                if extract_sign:
                    return name, sign
                return name 
            
            def my_parse_tfm_expr(expr: str):
                """
                Parse strings like '-fb + intf + intgb - inth + intlh'
                -> list of (sign, name) tuples: [(-1,'fb'), (+1,'intf'), ...]
                """
                tokens = regex.findall(r'([+-])?\s*([A-Za-z0-9_]+)', expr.replace(" ", ""))
                out = []
                for sign, name in tokens:
                    s = -1 if sign == '-' else 1
                    out.append((s, clean_name(name)))
                return out
                
            
            data = self.data_wrapper.df.copy()
            data.columns = [clean_name(c) for c in data.columns] 
            #print("data")
            #print(data)

            unique_names = {clean_name(n) for n in self.data_wrapper.ts_df["NAME"].unique()}
            #print("unique names", unique_names)

            year_idx = list(range(start_year, year_ref+1))
            vals = []
            err = False 
            err_msg = ""
            num_val = None 

            terms = my_parse_tfm_expr(str(cell_value))

            for y in year_idx:
                #print("y = ", y )
                try:
                    err = True 

                    # TODO insert code here from gen_transaction_matrix
                    # xxx ..
                    out = []
                    ref_row = data.loc[y]

                    # print("ref_row", ref_row)
                    failed = False 
                    not_found = []

                    # print("terms", terms)
                    for sign, nm in terms:
                        if nm in ref_row.index and nm in unique_names:
                            val = ref_row.at[nm]
                            new_val = sign * val 
                            out.append(new_val)
                        else:
                            failed=True 
                            not_found.append(nm)
                    # print("not found", not_found, "failed", failed)

                    if len(out) > 0:
                        vals.append(np.round(sum(out), 2))
                    else:
                        vals.append(np.nan)

                    # print("sum(out) ->", vals[-1])
                    
                    err = False 
                
                except Exception as e:
                    err = True 
                    err_msg = str(e)
                    pass 

            if err:
                self.notify("Something went wrong %s " % str(err_msg), title="Error")

            
            vals = np.array(vals)
            print("vals", vals)

            if not np.all(np.isnan(vals)):
                try:
                    plt.style.use("sfctools")
                except:
                    print("no sfc plotstyle found")
                
                fig =plt.figure(figsize=(8,5)) 
                ax = plt.gca() 
                
                #plt.plot(year_idx, vals)
                if data_success:
                    plt.title(myvar)
                    min_len = min(len(year_idx), len(data_loaded))
                    plt.plot(year_idx[:min_len], data_loaded[:min_len], linestyle="--", marker="o", markersize=10, label="Data")
                try:
                    plt.plot(year_idx, vals, linestyle="-", marker="x",  label="Constructed Series from Model")
                except Exception as e0:
                    try:
                        min_len = min(len(year_idx), len(vals))
                        plt.plot(year_idx[:min_len], vals[:min_len], linestyle="-", marker="x",  label="Constructed Series from Model")
                    except Exception as e:
                        self.notify("Could not plot data\n%s"%str(e), title="Error")
                plt.legend(loc=(1.04, 0))

                # plt.xlim([start_year, year_ref + 2])
                xticks = list(range(start_year, year_ref+1, 5))
                xticklabels = [str(s) for s in xticks]
                plt.xticks(xticks, xticklabels)
                plt.ylabel("Value")
                fig, ax = apply_mpl_theme(fig, ax, self.theme_manager)
                
                plt.tight_layout()

                plt.show()
            else:
                self.notify("Something went wrong. Is any data loaded?\nVals:%s"%(str(vals)), title="Error")



    def gen_balance_cross_timeseries(self):
        if not self.checkBoxAnalyticalBSM.isChecked():
            self.notify("Please select analytical mode", title="Not available.")
            return 

        #    return # analytical 
        # get selected item 
        
        selection = self.BSMtableView.selectionModel().selectedIndexes()

        if selection:
            idx = min(selection, key = lambda i: (i.row(), i.column()))
            # print("row, col", idx.row(), idx.column())

            # model = self.BSMtableView.model()
            # source_model = model
            # row_name = model.headerData(idx.row(), Qt.Vertical, Qt.DisplayRole).strip()
            # col_label = model.headerData(idx.column(), Qt.Horizontal, Qt.DisplayRole).strip()

            # print("row, col*", row_name, col_label)

            def _map_to_source(index):
                """Walk through any proxy chain to get the source model/index."""
                model = index.model()
                idx = index
                while isinstance(model, QAbstractProxyModel):
                    idx = model.mapToSource(idx)
                    model = model.sourceModel()
                return model, idx
            
            source_model, src_idx = _map_to_source(idx)

            row_name = source_model.headerData(src_idx.row(), Qt.Vertical, Qt.DisplayRole).strip()
            col_label = source_model.headerData(src_idx.column(), Qt.Horizontal, Qt.DisplayRole).strip()

            col_name = col_label.split("\n")[0].strip()
            try:
                col2_name = col_label.split("\n")[1].strip()
                renamer = {"A": "Assets", "L": "Liabilities"}
                col2_name = renamer[col2_name]
            except:
                col2_name = "Liabilities"
            value = source_model.data(src_idx, Qt.DisplayRole)
            #self.notify(str(value)+ "\n%s,%s, %s" % (row_name, col_name, col2_name), title="Value")

            agent_name=col_name.strip()
            
            if agent_name == "L":
                col_label = source_model.headerData(src_idx.column()-1, Qt.Horizontal, Qt.DisplayRole).strip()
                col_name = col_label.split("\n")[0].strip()
                agent_name = col_name 
            
            entry_name = col2_name 
            varname = row_name 
            agent_name = agent_name.capitalize()

            if agent_name in self.SETTING_balance_init_data:
                if entry_name in self.SETTING_balance_init_data[agent_name]:
                    var_start = self.SETTING_balance_init_data[agent_name][entry_name]
                    if varname in var_start:
                        myvar = var_start[varname]
                        # self.notify(myvar, title="var_start")

                        # generate time series for whole range 
                        try:
                            start_year = int(self.lineEdit_year_start_data.text())
                            year_ref = int(self.lineEdit_year_end_data.text())

                        except Exception as e:
                            self.notify("Error. please specify a valid data range.\n%s" % str(e), title="Error")
                            return 

                        
                        try:
                            data_loaded = self.data_wrapper.df
                            
                            
                        except:
                            self.notify("Cannot find %s in data. Have you loaded any data yet?" % myvar, title="Error")    
                            return 

                        data_loaded = data_loaded.loc[data_loaded.index >= start_year]
                        data_loaded = data_loaded.loc[data_loaded.index <= year_ref+1] 
                        data_loaded = data_loaded[myvar]

                        cell_value = source_model.data(src_idx, Qt.DisplayRole)
                        print("cell_value", cell_value)

                        year_idx = list(range(start_year, year_ref+1))
                        print("year_idx", year_idx)

                        vals = []
                        err = False 
                        start_value_known = False 
                        num_val = None 
                        for y in year_idx:
                            try:
                                err = True 
                                start_value_known, num_val = self.extract_data_from_cell(start_year, y, agent_name, entry_name, varname, cell_value, verbose=False)
                                err = False 
                            except Exception as my_e:
                                self.notify("Exception:\n%s" % str(my_e), title="Exception") 
                            if not start_value_known:
                                self.notify("Exception: No start value known for %s.\nAborting..." % varname, title="Exception") 
                                err = True 
                                break 
                            print("num_val", num_val)
                            if not np.isnan(num_val):
                                vals.append(num_val)
                            else:
                                vals.append(np.nan)
                        vals = np.array(vals)[1:]

                        if not err:
                            print("balance_cross no err")

                            try:
                                plt.style.use("sfctools")
                            except:
                                print("no sfc plotstyle found")
                            
                            fig =plt.figure(figsize=(8,5)) 
                            ax = plt.gca() 
                            
                            plt.title(myvar)
                            #plt.plot(year_idx, vals)
                            print("data loaded", data_loaded)
                            min_len = min(len(year_idx), len(data_loaded))
                            if min_len > 0:
                                plt.plot(year_idx[:min_len], data_loaded[:min_len], linestyle="--", marker="o", markersize=10, label="Data")

                            print("vals", vals)
                            min_len = min(len(year_idx), len(vals))
                            if min_len > 0:
                                plt.plot(year_idx[:min_len], vals[:min_len], linestyle="-", marker="x",  label="Constructed Series from Model")
                            plt.legend(loc=(1.04, 0))

                            xticks = list(range(start_year, year_ref+1, 5))
                            xticklabels = [str(s) for s in xticks]
                            plt.xticks(xticks, xticklabels)
                            plt.ylabel("Value")
                            try:
                                fig, ax = apply_mpl_theme(fig, ax, self.theme_manager)
                            except:
                                pass 
                            
                            plt.tight_layout()

                            plt.show()
                        else:
                            self.notify("Something went wrong. Is any data loaded?", title="Error")

                    else:
                        self.notify("No data linked or found for %s in %s of %s" % (varname, entry_name, agent_name), title="Not found")
                        #pass 

                    
                    
                    #varname = df.index[i].strip()
                    #if varname in var_start:
                    #    num_val = float(data[var_start[varname]].loc[start_year])
                                # restore original checkbox state 
        #self.checkBoxAnalyticalBSM.setChecked(original_checkbox_state)
        #self.gen_balance_matrix()
        #self.update()

    
    def extract_data_from_cell(self, start_year, ref_year, agent_name, entry_name, varname, cell, verbose=False):
        
        if not self.data_wrapper:
            if verbose: 
                self.notify("No data loaded. Please do so and re-try", title="Error")
            return False, None 

        df = self.bsm_model.df 
        
        data = self.data_wrapper.df.copy()
        # print("data", data)
        # self.parent().notify(str(data), "Data")
        data = data.sort_index()
        start_year = data.index[0]
        try: 
            data = data.sort_values(by="Year")
        except:
            pass 
        
        # print("bsm cell", cell)
        res = my_parse_bsm_expr(cell)
        # print("--> res", res)
        
        start_value_known = False 
        num_val = 0
        err = None 

        # find starting value?
        try:
            
            # print("entry name, agent name", entry_name, agent_name, df.columns[j])
            if agent_name in self.SETTING_balance_init_data:
                if entry_name in self.SETTING_balance_init_data[agent_name]:
                    var_start = self.SETTING_balance_init_data[agent_name][entry_name]
                    
                    if varname in var_start:
                        num_val = float(data[var_start[varname]].loc[start_year])
                        # if entry_name == "Liabilities":
                        #    num_val = -num_val # flip sign for liabilities
                        # print("starting value for ", j, "is", num_val)
                        start_value_known = True
        
        except Exception as e:
            self.notify(str(e), title="Error")
            err = str(e)
        
        for sign, r in res:
            r_val = data.loc[start_year+1:ref_year, r].sum()
            # print("sign, r", sign, r, r_val)
            num_val += sign * r_val 
        num_val = np.round(num_val, 2)

        return start_value_known, num_val
    

    def gen_balance_matrix(self, show=False):
        # Generates a balance sheet matrix for all agents.
        detailed = not self.BSMaggcheck.isChecked()
        combined_balance = defaultdict(
            lambda: {"Item": [], "Assets": [], "Liabilities": []})

        def process_entries(entries, agent, subject, is_asset, quantity):
            for entry in entries.split("\n"):
                entry = entry.strip()
                if entry.startswith("+") or entry.startswith("-"):
                    sign = entry[0]
                    item = entry[1:].strip().split(" ")[0] + subject
                    balance_type = "Assets" if is_asset else "Liabilities"

                    combined_balance[agent]["Item"].append(item)
                    combined_balance[agent][balance_type].append(
                        sign + quantity + "\n")
                    combined_balance[agent]["Liabilities" if is_asset else "Assets"].append(
                        "")  # Empty for other type
        try:
            for filedata in self.entry_data:
                agent1, agent2 = filedata["agent1"].strip(
                ), filedata["agent2"].strip()
                subject = f" ({filedata['subject']})\n" if detailed else "\n"
                quantity = filedata["quantity"]

                # Process assets and liabilities for both agents
                process_entries(
                    filedata["a1"], agent1, subject, is_asset=True, quantity=quantity)
                process_entries(
                    filedata["l1"], agent1, subject, is_asset=False, quantity=quantity)
                process_entries(
                    filedata["a2"], agent2, subject, is_asset=True, quantity=quantity)
                process_entries(
                    filedata["l2"], agent2, subject, is_asset=False, quantity=quantity)

                # Process additional optional entries (a3/l3 and a4/l4)
                # for agent, a_key, l_key in [(agent1, "a3", "l3"), (agent2, "a3", "l3"), (agent1, "a4", "l4"), (agent2, "a4", "l4")]:
                try:
                    agent3 = filedata["agent3"].strip()
                    process_entries(
                        filedata["a3"], agent3, subject, is_asset=True, quantity=quantity)
                    process_entries(
                        filedata["l3"], agent3, subject, is_asset=False, quantity=quantity)
                except Exception as e:
                    pass

                try:
                    agent4 = filedata["agent4"].strip()
                    process_entries(
                        filedata["a4"], agent4, subject, is_asset=True, quantity=quantity)
                    process_entries(
                        filedata["l4"], agent4, subject, is_asset=False, quantity=quantity)
                except Exception as e:
                    pass

            dfs = []
            for k, v in combined_balance.items():
                dv = pd.DataFrame(v)
                dv = dv.rename(
                    columns={"Liabilities": "L (%s)" % k, "Assets": "A (%s)" % k})

                if not detailed:
                    aggregation_functions = {}
                    for col in dv.columns:
                        if col == "Item":
                            aggregation_functions[col] = "first"
                        else:
                            aggregation_functions[col] = "sum"
                    dv = dv.groupby(dv['Item']).aggregate(
                        aggregation_functions)
                    dv = dv.drop(columns="Item")
                dfs.append(pd.DataFrame(dv))

            try:
                df = pd.concat(dfs, axis=1)  # ,keys =combined_balance.keys())
            except:
                return  # ignore case when there is no data yet and nothing to concatenate

            # col_order = []
            # for agent in self.sorting_tables["bsm"]["cols"]:
            #     a = agent # .capitalize()
            #     for xi in ["A (%s)" % a, "L (%s)" % a]:
            #         if xi in df.columns:
            #             col_order += [xi]
            # df = df[col_order]

            # combine back to 'multi-index'
            df.columns = [
                f"\n{col.split('(')[0]}" if col.startswith('L')
                # Add spaces for right alignment
                else f"{' ' * 0}{col.split('(')[1][:-1]}\n{col.split('(')[0]}"
                for col in df.columns
            ]
            
            # (optional) fill in data 
            if not self.checkBoxAnalyticalBSM.isChecked():
                if self.data_wrapper:
                    data = self.data_wrapper.df.copy()

                    # print("data", data)
                    # self.parent().notify(str(data), "Data")
                    data = data.sort_index()
                    start_year = data.index[0]
                    try: 
                        data = data.sort_values(by="Year")
                    except:
                        pass 
                
                    year_ref = int(self.spinBox_refYear.value())

                    agent_name = None 
                    for i, row in enumerate(df.itertuples(index=False, name=None)):
                        for j, cell in enumerate(row):
                            if cell is not None:
                                try:

                                    d_a_l = {"A": "Assets", "L":"Liabilities"}
            
                                    # print("df.columns", df.columns)
                                    # print("df", df)
                                    if df.columns[j].endswith("A "):
                                        entry_name = "Assets"
                                        agent_name = df.columns[j] 
                                        agent_name = str(agent_name).split('\n')[0]
                                        agent_name = agent_name.capitalize()
                                    elif df.columns[j].endswith("L "):
                                        entry_name = "Liabilities"
                                    else:
                                        raise KeyError(df.columns[j])
                                        
                                    varname = df.index[i].strip()

                                    start_value_known, num_val = self.extract_data_from_cell(start_year, year_ref, agent_name, entry_name, varname, cell)

                                    
                                    if start_value_known or num_val == 0:
                                        df.iat[i,j] = num_val 
                                    else: 
                                        if err is None:
                                            df.iat[i,j] = "(" + str(num_val) + ")"
                                        else:
                                            df.iat[i,j] = err
                                except Exception as e:
                                    print("Exception", str(e))
                df = df.round(2)
                
                df = df.fillna(0.0)
                df.loc["Total"] = df.apply(lambda s: pd.to_numeric(s, errors="coerce").sum()
                           if pd.to_numeric(s, errors="coerce").notna().all()
                           else np.nan)

                try:
                    num = df.apply(pd.to_numeric, errors="coerce")
                    odd_sum  = num.iloc[:, ::2].sum(axis=1, min_count=1) 
                    even_sum = num.iloc[:, 1::2].sum(axis=1, min_count=1)
                    total = odd_sum - even_sum
                    df["Total"] = total.where(total.notna(), "N/A")
                except Exception:
                    df["Total"] = "N/A"

            # try:
            if self.checkBoxAnalyticalBSM.isChecked():
                df = df.fillna("0")
                df.loc["Total"] = df.apply(
                    lambda s: " + ".join(
                        f"({x})"
                        for x in s.astype(str)
                        if x.strip() not in {"", "0", "nan", "NaN"}
                    ) or "0"
                )
                #try:
                # df = df.fillna("0")
                # odd_sum = df.iloc[:, ::2].apply(lambda row: " + ".join(f"({x})" for x in row.astype(str)), axis=1)
                # even_sum = df.iloc[:, 1::2].apply(lambda row: " + ".join(f"({x})" for x in row.astype(str)), axis=1)
                # df["Total"] = odd_sum.combine(even_sum, lambda o, e: f"({o}) - ({e})")
                # df["Total"] = df["Total"].apply(lambda s: str(simplify(s)))

                #except Exception:
                #   df["Total"] = "N/A"

                ZERO_LIKE = {"", "0", "0.0", "nan", "NaN", "None", "()", "( )"}

                def _clean_token(x):
                    if pd.isna(x):
                        return None
                    s = str(x).strip()
                    return None if s in ZERO_LIKE else s

                def _sum_expr(row: pd.Series) -> str:
                    terms = [t for t in (_clean_token(v) for v in row) if t]
                    if not terms:
                        return "0"
                    # wrap each term; no term is empty by construction, so no "()"
                    return " + ".join(f"({t})" for t in terms)

                def _diff_expr(o: str, e: str) -> str:
                    if o == "0" and e == "0":
                        return "0"
                    if e == "0":
                        return o
                    if o == "0":
                        return f"-({e})"
                    return f"({o}) - ({e})"

                # --- build expressions (avoid "()") ---
                odd_sum  = df.iloc[:, ::2].apply(_sum_expr, axis=1)    # cols 0,2,4,...
                even_sum = df.iloc[:, 1::2].apply(_sum_expr, axis=1)   # cols 1,3,5,...
                df["Total"] = odd_sum.combine(even_sum, _diff_expr)
                try:            
                    def safe_simplify(s: str) -> str:
                        try:
                            return str(simplify(sympify(s)))
                        except Exception: 
                            return s
                    df["Total"] = df["Total"].map(safe_simplify)
                    df.loc["Total"] = df.loc["Total"].map(safe_simplify)
                except Exception:
                    pass
                
                    
            # except Exception as e:
            #     self.notify("%s" % str(e), title="Error")

            df.loc["NW"] = ''
            for i, col in enumerate(df.columns):
                if i > 0 and i % 2 == 1:
                    if not self.checkBoxAnalyticalBSM.isChecked():
                        # numerical net wealth <-> convert entries to float
                        try:
                            df.iloc[-1, i] = float(df.iloc[-2, i-1]) - float(df.iloc[-2, i] )
                        except Exception as e:
                            df.iloc[-1, i] = "Error\n%s" % str(e) 
                    else: 
                        # analytical net wealth <-> convert net wealth to simplified analytical total 
                        try:
                            new_expr = simplify(str(df.iloc[-2, i-1]) + f"-({df.iloc[-2, i]})")
                            df.iloc[-1, i] = new_expr
                            
                        except Exception as e2:
                            df.iloc[-1, i] = "Error\n%s" % str(e2) 

                else:
                    df.iloc[-1, i] = ""

            df = df.replace(np.nan, "")

            # print("BSM DF")
            
            model = PandasModel(df)
            exists_instance = MatrixViewer.instance["BalanceMatrix"] is not None
            mview = MatrixViewer(mode="BalanceMatrix", parent=self)
            mview.set_model(model)

            self.bsm_model = model
            self.BSMtableView.setModel(self.bsm_model)
            header = self.BSMtableView.horizontalHeader()
            header.setDefaultAlignment(Qt.AlignRight | Qt.AlignVCenter)

            # mview.webView.setHtml(df_html)
            if not self.test_mode:
                if exists_instance:
                    mview.update()

            if show and not exists_instance:
                mview.show()

        except Exception as e:
            self.notify(title="Error", message="Error:" + str(e))

        self.sort_table("bsm")

        return df

    def edit_mainloop(self):
        # TODO remove safely in future versione
        return

    def restore_original_size(self):
        # self.setFixedSize(1821,1010)
        # self.setMaximumSize(40000,40000)
        # TODO remove safely in future versions
        pass

    def flip_height(self):
        if self.flipped:
            W = self.frameGeometry().width()
            H = self.frameGeometry().height()

            self.setMinimumSize(self.last_width, self.last_height)
            # self.setMaximumSize(self.last_width,self.last_height)
            self.setMaximumSize(40000, 40000)
            # self.setMinimumSize(400,400)

            # W = self.frameGeometry().width()
            # self.FlipHeightButton.setGeometry(int(W)-81-25,5,81,21)

            self.flipped = False
        else:
            W = self.frameGeometry().width()
            H = self.frameGeometry().height()
            self.last_width = W
            self.last_height = H

            self.setFixedSize(800, 70)

            # W = self.frameGeometry().width()
            # self.FlipHeightButton.setGeometry(int(W)-81-25,5,81,21)

            self.flipped = True

    def change_alert(self):
        self.made_changes = True
        # self.udpateButton.setText("Update Values *")

    def show_output_display(self):
        # shows the logger display for output quantities

        try:

            path = os.path.dirname(self.current_file)+"/output/"
            # print("[DEBUG MESSAGE] open output display in path", path)

            output_display = OutputDisplay(self, path=path)
        except Exception as e:
            self.notify(str(e), title="Error")

    def save_and_build(self):

        # -- create new status signal

        # self.worker.switch_state()
        # ---

        self.save()

        a = []
        for ce in CodeEditor.instances.values():
            names = ce.save_and_build()
            if names is not None:
                a.append(names)

        for ai in a:
            if ai[0] in CodeEditor.instances:
                CodeEditor.instances[ai[0]] = None
                del CodeEditor.instances[ai[0]]
            CodeEditor.instances[ai[1]] = ai[2]
            CodeEditor.instances[ai[1]].update()

        # print("\nNEW INSTANCES\n", CodeEditor.instances)

        try:
            self.build_project(silent=True)
            self.statusBar().showMessage("Saved " + self.current_file)
            self.settings_editor.mtime = time.time()

        except Exception as e:
            # self.notify(str(e),title="Exception")
            print(str(e))

        # re-generate balance sheet matrix
        self.gen_balance_matrix(show=False)
        self.gen_transaction_matrix(show=False, silent=True)

    def run_project(self):

        self.save_and_build()
        # self.show_output_display()
        self.setCursor(Qt.WaitCursor)

        try:
            # self.tabWidget.setCurrentIndex(1)

            # filename = os.path.dirname(self.current_file) + "/python_code/mainloop.py"
            filename = os.path.dirname(
                os.path.dirname(self.current_file)) + "/run.py"
            folder = os.path.dirname(os.path.dirname(self.current_file)) + "/"

            if " " in filename or " " in folder:
                self.notify(title="Invalid file name",
                            message="There are spaces in your folder or file name. This might cause problems.")

            # print("lookup", filename, "in", folder)

            old_dir = os.getcwd()
            # # print("old_dir",old_dir)

            os.chdir(folder)
            cmd = "conda activate attune /k python %s" % filename
            # print("COMMAND", cmd)
            os.system("start cmd     %s" % cmd)

            os.chdir(old_dir)

        except Exception as e:

            self.notify(str(e), title="")
            self.setCursor(Qt.ArrowCursor)

        self.setCursor(Qt.ArrowCursor)

    def build_project(self, silent=False, overwrite=False):

        # folder = QFileDialog.getExistingDirectory (self, 'Build Project...', os.getcwd())

        if self.current_file is None:
            self.notify(
                message="Cannot build project. Please save to file first.", title="Error")
            return

        folder = os.path.dirname(self.current_file)
        # print("build project in ", folder)

        try:
            os.mkdir(folder + "/mamba_code/")
        except:
            pass

        try:
            os.mkdir(folder + "/python_code/")
        except:
            pass

        try:
            os.mkdir(folder + "/output/")
        except:
            pass

        try:

            for agent, code in self.drawcanvas.code_data.items():

                if agent in CodeEditor.instances:  # only convert edited code

                    print("BUILD agent", agent)
                    print("CODE", code)
                    print("**#")
                    # write mamba code for completeness
                    path = folder + "/mamba_code/%s.txt" % agent  # former .txt

                    # print("path",path)

                    with open(path, "w") as file:
                        # file.write(code.replace("\t", "    "))
                        file.write(code)

                    # write python code
                    path = folder + "/python_code/%s.py" % agent.lower()

                    # print("path",path)

                    with open(path, "w") as file:
                        new_code = convert_code(code.split("\n"))[0]
                        new_code = new_code.encode("utf-8").decode('cp1252')
                        # file.write(new_code.replace("\t", "    ")) # convert using the mamba interpreter
                        # convert using the mamba interpreter
                        file.write(new_code)

            # write settings file
            with open(folder + "/settings.yml", "w") as file:
                file.write(self.settings_str)

            # write the runner file
            # if self.mainloop_str == "" or overwrite:
            #     with open(folder+"/run.py", "w") as file:
            #         file.write(
            #             "from python_code.mainloop import run\n\nrun()\n")

            # write main simulation file

            #             with open(folder + "/python_code/mainloop.py", "w") as file:
            #                 # with open(folder + "./mainloop.py","w") as file:
            #                 if self.mainloop_str == "" or overwrite:

            #                     if overwrite:
            #                         yes = self.ask_question(
            #                             '', "WARNING - You are about to create a fresh main script. This could potentially overwrite previous versions (if any).\nThis operation cannot be reverted. Continue?")
            #                         if not yes:
            #                             return

            #                     if overwrite and self.mainloop_str != "":
            #                         with open(folder + "/python_code/mainloop_backup.py", "w") as backupfile:
            #                             # with open(folder + "./mainloop_backup.py","w") as backupfile:
            #                             backupfile.write(
            #                                 self.mainloop_str.replace("\t", "    "))

            #                     simu_str = """
            # \"\"\"
            # This the main ABM Simulation file
            # Cretaed with the sfctools GUI

            # @author: <Your name>
            # @date: <Your date>
            # \"\"\"

            # from sfctools import Agent, World, Settings, Clock, FlowMatrix
            # """
            #                     for agent in self.drawcanvas.code_data.keys():
            #                         simu_str += "from python_code.%s import %s\n" % (
            #                             agent.lower(), agent.capitalize())

            #                     simu_str += """

            # def iter():
            #     \"\"\"
            #     this is one iteration
            #     \"\"\"

            #     # TODO modify iteration here
            # """

            #                     for agent in self.drawcanvas.code_data.keys():
            #                         a = agent.capitalize()
            #                         simu_str += "\tfor a in World().get_agents_of_type('%s'):\n\t\t# print('Do something with' + str(a))\n\n" % (a)
            #                     simu_str += """\n
            # def run():
            #     \"\"\"
            #     this is the main simulation loop
            #     \"\"\"
            # """

            #                     simu_str += """\n

            #     Settings().read(\"settings.yml\") # read settings file

            #     \"\"\"
            #     Simulation parameters
            #     \"\"\"

            #     # number of agents to be created
            # """

            #                     for agent in self.drawcanvas.code_data.keys():
            #                         simu_str += "\tN_%s = 1\n" % agent.capitalize()
            #                     simu_str += """
            #     # TODO^ set the correct value

            #     # number of simulation steps
            #     T = 100

            #     # TODO^ set the correct values

            #     # create Agents:
            # """
            #                     for agent in self.drawcanvas.code_data.keys():
            #                         a = agent.capitalize()
            #                         simu_str += "\t[%s() for i in range(N_%s)]\n" % (a, a)

            #                     simu_str += "\n"
            #                     simu_str += "\t# inter-link agents \n"
            #                     simu_str += "\tWorld().link()\n\n"
            #                     simu_str += """
            #     for i in range(T):
            #         iter()

            #         # TODO write outputs here ...

            #         Clock().tick()

            #     # print(FlowMatrix().to_string())
            # """
            #                     self.mainloop_str = simu_str

            #                 else:
            #                     simu_str = self.mainloop_str

            #                 # print("SIMU STR", simu_str)
            #                 file.write(simu_str)  # .replace("\t","    "))

            #                 if not silent:
            #                     self.notify("Project files built!", title="Success")

            #                 # self.gen_code() # generate transactions.py

        except Exception as e:
            exception_type, exception_object, exception_traceback = sys.exc_info()
            self.notify("Could not build project.\n" + str(exception_object) +
                        str(exception_traceback), title="Error on Project Build")

    def auto_backup(self):
        try:
            folder = self.current_file
            if folder is not None:
                # makes a backup file
                self.saveas(folder[:-6] + "_autobackup.sfctl")
            else:
                # self.notify("You are working on a blank project file. Please save your changes soon.", title="Warning")
                self.statusBar().showMessage(
                    "WARNING: You are working on a blank project file. Please save your changes soon.")
        except Exception as e:
            self.notify(str(e), title="Error on AutoBackup")

    def edit_settings(self):
        if self.settings_str == "" or self.settings_str is None:
            self.notify(
                message="Something went wrong. Have you built the project?", title="Error")
            return

        # my_edit = SettingsEditor(parent=self, text=self.settings_str)
        self.settings_editor.show()
        self.settings_editor.rebuild_table()

    #def edit_graph_agent(self):
    #    self.drawcanvas.edit_agent()

    def arrange_pretty(self):
        self.update_table()
        self.drawcanvas.arrange_pretty()

    def update_graphics_data(self, box, conn):
        pass

    def switch_theme(self):

        # from qt_material import apply_stylesheet

        if self.theme_manager.theme == "dark":
            self.theme_manager.theme = "bright"

        elif self.theme_manager.theme == "bright":
            self.theme_manager.theme = "dark"

        self.setStyleSheet(self.theme_manager.get_stylesheet("main"))

        # self.theme_manager.restore_buttons()

        try:
            self.dockWidget_3.setStyleSheet(
                self.theme_manager.get_background_style())
            self.dockWidget_4.setStyleSheet(
                self.theme_manager.get_background_style())
            self.dockWidget_5.setStyleSheet(
                self.theme_manager.get_background_style())
            self.widget.setStyleSheet(
                self.theme_manager.get_background_style())
        except:
            pass

        try:
            self.dockWidget_7.setStyleSheet(
                self.theme_manager.get_background_style())
        except:
            pass
        try:
            self.tabWidget_2.setStyleSheet(
                self.theme_manager.get_background_style())
            self.tabWidget.setStyleSheet(
                self.theme_manager.get_background_style())
        except:
            pass

        for ce in CodeEditor.instances.values():
            ce.setStyleSheet(self.theme_manager.get_background_style())
        try:
            if MainLoopEditor.instance is not None:
                inst = MainLoopEditor.instance
                inst.setStyleSheet(self.theme_manager.get_background_style())
                inst.setStyleSheet(self.theme_manager.get_stylesheet("main"))

        except:

            pass

        self.update_table()
        self.set_font_for_all_widgets()
        # self.transactionView.setStyleSheet(self.theme_manager.get_table_style())

    def ask_question(self, title, message):
        # ask a question yes / no
        msg = QMessageBox(QMessageBox.Question,
                          title, message, buttons=QMessageBox.Yes | QMessageBox.No,
                          parent=self)
        msg.setWindowTitle(title)
        msg.setStyleSheet(self.theme_manager.get_notification_style())
        msg.exec_()
        reply = msg.standardButton(msg.clickedButton())

        if reply == msg.Yes:
            return True
        else:
            return False

    def ask_question_all(self, title, message):
        # ask a question yes / no / yes (all)/ no (all)

        msg = QMessageBox(QMessageBox.Question,
                          title, message,
                          buttons=QMessageBox.Yes | QMessageBox.No | QMessageBox.YesToAll | QMessageBox.NoToAll,
                          parent=self)
        msg.setWindowTitle(title)
        msg.setStyleSheet(self.theme_manager.get_notification_style())
        msg.setMinimumWidth(500)
        button_yes = msg.button(QMessageBox.Yes)
        button_no = msg.button(QMessageBox.No)
        button_yes_to_all = msg.button(QMessageBox.YesToAll)
        button_no_to_all = msg.button(QMessageBox.NoToAll)
        for button in [button_yes, button_no, button_yes_to_all, button_no_to_all]:
            button.setMinimumWidth(120)  # Adjust width as needed

        msg.exec_()

        reply = msg.standardButton(msg.clickedButton())

        if reply == msg.Yes:
            return 'yes'
        elif reply == msg.No:
            return 'no'
        elif reply == msg.YesToAll:
            return 'yes_all'  # or any identifier for Yes (All)
        elif reply == msg.NoToAll:
            return 'no_all'  # or any identifier for No (All)
        else:
            return None  # Fallback case if no valid response

    def notify(self, message, title):
        # msg = QMessageBox(self)
        msg = QMessageBox(self)
        msg.setWindowTitle(title)
        msg.setText(message)
        # msg.setStyleSheet(self.stylesheet)
        # get background color from theme manager
        # get foreground color from theme manager
        style = self.theme_manager.get_notification_style()
        msg.setStyleSheet(style)

        msg.setAttribute(Qt.WA_DeleteOnClose, True)
        msg.finished.connect(msg.deleteLater)

        msg.show()

    def cleanup_transaction_data(self, data):
        """
        cleans up transaction data from old file versions
        """
        try:
            # print("old_data",data)
            # data = dict(data)
            new_data = {"agents": {}, "transactions": [], "box_positions": {
            }, "label_positions": {}, "theme": None, "options": None}  # data.copy()
            # new_data["transactions"] = data["transactions"].copy()

            for k, v in data.items():

                if k == "options":
                    new_data["options"] = data[k]

                if k == "theme":
                    # {"globaltheme": bright/dark, "colors": colors} # load the color theme
                    new_data["theme"] = data[k]

                if k == "agents":
                    for k2, v2 in data[k].items():

                        # 1. rename agent
                        # print("RENAME AGENTS...")
                        new_code_lines = []

                        for line in v2.split("\n"):
                            if "AGENT" in line:  # and len(line) >= 9:
                                nline = line[:9] + camel(line[9]) + line[10:]
                                # print("   renamed", line,nline)

                            elif "CLASS" in line:
                                nline = line[:9] + camel(line[9]) + line[10:]
                                # print("   renamed", line,nline)

                            else:
                                nline = line  # "NOT FOUND" # line
                            # # print("nline",nline,"old",v2)

                            new_code_lines.append(nline)

                            pass

                        new_code = "\n".join(new_code_lines)

                        agent_name = camel(str(k2))
                        # print("    agent name", str(k2), "->" , camel(str(k2)))

                        # try if agent is placed somewhere
                        if str(k2) in data["box_positions"]:

                            new_data["agents"][agent_name] = new_code

                            # 2. assign new box position
                            # print("ASSIGN NEW BOX POSITIONS...")

                            old_pos = data["box_positions"][str(k2)]
                            new_data["box_positions"][agent_name] = old_pos

                else:
                    new_data[k] = v

            # print("RENAME TRANSACTIONS...")
            # 3. rename transactions
            try:
                for i, t in enumerate(list(data["transactions"])):
                    new_data["transactions"][i]["agent1"] = camel(
                        data["transactions"][i]["agent1"])  # .capitalize()
                    new_data["transactions"][i]["agent2"] = camel(
                        data["transactions"][i]["agent2"])  # .capitalize()
            except Exception as e:
                exception_type, exception_object, exception_traceback = sys.exc_info()
                filename = exception_traceback.tb_frame.f_code.co_filename
                line_number = exception_traceback.tb_lineno
                self.notify("Something wen wrong when inserting the transactions:\n" +
                            str(e) + "\nLine:" + str(line_number), title="Error")

            return new_data
        except Exception as e:
            exception_type, exception_object, exception_traceback = sys.exc_info()
            filename = exception_traceback.tb_frame.f_code.co_filename
            line_number = exception_traceback.tb_lineno

            self.notify("Coult not optn the project file. Is it an old version?\n Error in Line %i: " %
                        line_number + str(e), title="Error")

    def notify_tooltip(self, tt):
        print("notify",  tt)
        self.notify(title="Tip", message=str(tt))

    def resizeEvent(self, event):
        # print("resize")

        # self.drawcanvas.parent.resize_frame()
        QtWidgets.QMainWindow.resizeEvent(self, event)

        # resize transaction
        w = 741
        h = 191 + max(0, self.frameGeometry().height()-985)

        try:
            self.transactionView.parent = self.dockWidget_5
        except:
            pass
        # self.transactionView.setGeometry(30,65,w,h) # 30,670,w,h)

        # self.label_17.setGeometry(10,self.frameGeometry().height()-150,131,89) # logo
        # self.label_17.setVisible(False)

        # self.radioButton.setGeometry(0,self.frameGeometry().height()-28,22,22)

    def browse(self):
        """
        browse a file
        """

        try:
            filename = QFileDialog.getOpenFileName(
                self, 'Open file', os.getcwd(), "attune Files (*.sfctl)")[0]
        except:
            self.notify(
                "Seems as if the projects folder does not yet exist. please create it manually in the parent directory of src", title="Projects folder")

        print("filename", filename)
        if filename is None or (filename == ""):
            return

        # remove all tabs from tab widget
        for i in range(2, self.tabWidget.count()):  # 1 = Notes, 2 = Equations
            tab = self.tabWidget.widget(2)
            self.tabWidget.removeTab(2)
            del tab

        CodeEditor.instances = {}

        filename_backup = self.filename
        current_file_backup = self.current_file

        self.filename = filename
        self.current_file = filename

        # self.mainloop_editor.start_watchdog(self.current_file)
        settings_file = os.path.join(
            os.path.dirname(self.current_file), "settings.yml")
        if os.path.exists(settings_file):
            self.settings_editor.start_watchdog(settings_file)
        
        print("CHECK 1")

        try:
            # print("FILENAME",filename)
            # load entries into the list

            with open(filename, 'r') as stream:
                try:
                    transaction_data = yaml.safe_load(stream)
                    transaction_data = self.cleanup_transaction_data(transaction_data)
                    self.transaction_data = transaction_data

                except Exception as e:
                    self.notify(
                        "Error retreiving the transaction data from the source file. Aborting.\n"+str(e), title="Error")
                
                print("CHECK 2")

                # print("transaction_data",transaction_data)
                try:

                    # try to load notes
                    if "notes" in transaction_data:
                        self.notesEdit.setPlainText(transaction_data["notes"])

                    print("CHECK 2a")

                    # try to load color theme
                    if transaction_data["theme"] is not None:
                        try:
                            theme_fname = transaction_data["theme"]["colors"]
                            gtheme = transaction_data["theme"]["globaltheme"]
                            if self.theme_manager.theme != gtheme:
                                self.switch_theme()

                            dirname = os.path.dirname(filename)
                            self.theme_manager.load_colors(theme_fname, dirname)

                        except:
                            self.statusBar().showMessage("Could not load color theme. Fallback to stanard theme")

                    print("CHECK 2b")

                    # new format
                    self.entry_data = transaction_data["transactions"]
                    self.drawcanvas.clear(clearall=True)
                    # add the helper agents

                    # self.notify(str(transaction_data), title="Transaction Data")

                    for k, v in transaction_data["agents"].items():
                        self.drawcanvas.add_agent(k)  # .capitalize())
                        # print("Add agent", k) #.capitalize())

                    print("CHECK 2c")
                    # add the transactions (connectors and boxes)
                    self.update_table()

                    # load agent's codes

                    # first, compare if the code in the file tree (if any) corresponds to the code in the .sfctl file

                    # check if file tree is existent
                    my_agents = list(transaction_data["agents"].keys())

                    print("CHECK 2d")
                    # get directory from filename
                    try:
                        # print("CHECK DIRECTORY FOR UPDATES", filename)
                        filepath = os.path.dirname(os.path.abspath(filename))
                        mamba_path = filepath + "/mamba_code"
                        # print("mamba path",mamba_path)

                        msg = "I have detected a deviation between the /mamba_code/ directory and the code stored in this file!"
                        msg += " Affected files:\n\n"

                        sub_names = next(os.walk(mamba_path), (None, None, []))[
                            2]  # [] if no file
                        alt_transaction_data = {}
                        alt_diffs = {}


                        print("CHECK 3")

                        for fi in sub_names:

                            # check filename for agent
                            aname = camel(fi[:-4])  # .capitalize()

                            # print("File ",aname)
                            if aname in my_agents:

                                # read this file
                                with open(mamba_path+"/" + fi, "r") as file:
                                    code = file.read()

                                    # # print("dev mamba folder: ",code)
                                    # # print("dev sfctl file:", transaction_data["agents"][aname])

                                    if code.strip() != transaction_data["agents"][aname].strip():

                                        # print("DEVIATION detected in ",aname)

                                        alt_transaction_data[aname] = code

                                        diff = [li for li in difflib.ndiff(
                                            code.strip(), transaction_data["agents"][aname].strip()) if li[0] != ' ']
                                        alt_diffs[aname] = str(diff)

                                        strdiff = str(diff).strip()

                                        if len(strdiff) > 40:
                                            strdiff = strdiff[:7] + "..."
                                        else:
                                            strdiff = strdiff

                                        msg += fi + ": " + strdiff + "\n"
                                        # print(diff)
                                        # print("\n")

                        print("CHECK 4")

                        # msg += "\nDo you want to adapt the external changes?"
                        # # self.notify(msg,title="Changes Detected")

                        # if len(alt_transaction_data) > 0:

                        #     qm = QMessageBox(self)
                        #     qm.setWindowTitle("Caution")
                        #     # qm.setText(message)
                        #     style = self.theme_manager.get_notification_style()
                        #     qm.setStyleSheet(style)

                        #     # ret = qm.question(self,'Caution', msg , qm.Yes | qm.No)
                        #     # # NOTE was deactivated due to malfunctioning behavior TODO fix and uncomment

                        #     # if ret == qm.Yes:
                        #     #     for k, v in alt_transaction_data.items():
                        #     #         transaction_data["agents"][k] = alt_transaction_data[k]
                        #     # else:
                        #     #     pass

                    except Exception as e:
                        # print("SOMETHING WENT WRONG")
                        print(str(e))

                    #
                    # print("UPDATE DRAW CANVAS")
                    try:
                        self.drawcanvas.code_data = transaction_data["agents"]
                        # print("CODE DATA\n", self.drawcanvas.code_data)

                        self.drawcanvas.box_position_data = transaction_data["box_positions"]
                        self.drawcanvas.label_position_data = transaction_data["label_positions"]

                        # print("BOX POSITIONS...")
                        for k, v in self.drawcanvas.box_position_data.items():
                            # print("(1)", k,v)
                            self.drawcanvas.old_positions[k] = (v["x"], v["y"])
                            # self.notify(str(v), title="test")
                        for k, v in self.drawcanvas.label_position_data.items():
                            # print("(2)", k,v)
                            self.drawcanvas.old_positions[k] = (v["x"], v["y"])
                            # self.notify(str(v), title="test")
                            
                    except: 
                        pass 
                    # print("CODE DATA",transaction_data["agents"])
                    print("CHECK 5")

                    # move the boxes to the saved positions
                    # transaction_data["box_positions"])
                    try:
                        self.drawcanvas.reposition()
                    except:
                        print("Could not reposition drawing canvas")


                    try:
                        # transaction_data["label_positions"])
                        self.drawcanvas.reposition_labels()
                    except:
                        print("Could not load label positions. Resetting to zero.")

                    # print("Settings", self.settings_str)
                    self.settings_str = transaction_data["settings"]

                    try:
                        self.mainloop_str = transaction_data["mainloop"]
                    except:
                        print("NO MAINLOOP FOND. SKIPPING")

                    self.gen_balance_matrix(show=False)

                    print("CHECK 6")
                    

                    # load equations
                    if "eqs" in transaction_data:
                        print("EQS:", transaction_data["eqs"])

                        try:
                            keys = ["NAME", "TYPE", "EXPRESSION",
                                    "COEFFS", "RESTRICT", "CONDITION", "COMMENT"]
                            # sorted_data = {k: transaction_data["eqs"][k] for k in keys}
                            sorted_data = {}
                            for k in keys:
                                if k in transaction_data["eqs"]:
                                    sorted_data[k] = transaction_data["eqs"][k]
                                else:
                                    sorted_data[k] = ""

                            self.eq_wrapper.data = sorted_data
                            self.eq_model = PandasModel(pd.DataFrame(
                                sorted_data), view=self.EQtableView, sorting=keys)
                            self.EQtableView.setModel(self.eq_model)
                            self.eq_wrapper.udpate_data()
                            print("eq model***", self.eq_model)

                        except Exception as e:
                            print("EXCEPTION:", str(e))

                    try:
                        self.statusBar().showMessage("Opened file " + filename)
                    except Exception as e:
                        print("EXCEPTION:", str(e))

                    # load data path
                    if "data_path" in transaction_data:
                        print("DATA PATH:", transaction_data["data_path"])
                        self.browse_data(
                            transaction_data["data_path"], block_dlg=True)

                    # merge browsed data with the user specifications
                    if "data_args" in transaction_data:
                        self.update_data_args(transaction_data["data_args"])

                    # bimets fitting arts
                    if "bimets_args" in transaction_data:
                        self.update_bimets_args(
                            transaction_data["bimets_args"])

                    # transactions export path
                    if "trans_path" in transaction_data:
                        self.export_path_edit.setText(
                            transaction_data["trans_path"])

                    self.drawcanvas.center_boxes()

                except Exception as e:
                    print("EXCEPTION*", str(e))

                    # old format as fallback
                    self.entry_data = transaction_data["transactions"]
                    self.drawcanvas.code_data = transaction_data["agents"]
                    self.update_table()

                    # print("wARNING - FALLBACK TO OLD FILE FORMAT!")
                    # print(str(e))

                # self.fileEdit.setText(filename)

                if "ca_ka_info" in transaction_data:
                   self.SETTING_ca_ka.update(transaction_data["ca_ka_info"])
                
                if "balance_init_data" in transaction_data:
                    self.SETTING_balance_init_data = transaction_data["balance_init_data"]

                if "sorting_tables" in transaction_data:
                    # self.notify("sorting is there\n%s" % str(transaction_data[sorting_tables]), title="Ok")
                    self.sorting_tables = transaction_data["sorting_tables"]
                    print("sorting tables", self.sorting_tables)
                    self.notify("sorting %s" % str(self.sorting_tables))
                    self.sort_table("bsm")
                    self.sort_table("tfm")

        except Exception as e:

            # self.notify(message="Something went wrong when opening the file:\n %s"%str(e),title="Error")
            self.filename = filename_backup
            self.curent_file = current_file_backup
            if str(self.current_file) != "":
                self.statusBar().showMessage("Cancelled. Restored file '%s'" %
                                             str(self.current_file))

        try:
            self.drawcanvas.center_boxes()
        except:
            pass

        self.notify("Loaded file\n%s" % settings_file, title="Load file")
        self.label_fname.setText(filename)

        # update transaction table
        self.update_table()

        # update theme
        self.theme_manager.restore()

        try:
            # fname, file_extension = os.path.splitext(self.filename)
            fname, file_extension = os.path.splitext(filename)
            print("fname", fname)
            self.theme_manager.load_colors(fname + ".sfctheme")

        except Exception as e:
            # print("could not load theme: ", str(e))
            self.notify("could not load color theme.\n%s" % str(e), title="Error")
            self.statusBar().showMessage("Could not load theme: %s" % str(e))

        # update boxes
        for box in self.drawcanvas.boxes:
            box.edit_agent()

        # update display options
        try:
            if "options" in transaction_data:
                if transaction_data["options"] is not None:
                    self.set_options(transaction_data["options"])
        except Exception as e:
            print(str(e))

        # print("update col width")

        # update code editors
        for ce in CodeEditor.instances.values():
            ce.update_line_label()
            ce.update_interpreter()
            ce.repaint()

        self.tabWidget.update()
        self.tabWidget.setCurrentIndex(0)

    def update_data_args(self, data_args):
        df_args = pd.DataFrame(data_args)
        print("LOAD DF ARGS\n", df_args)

        df0 = self.data_model.raw_data
        for c in df_args.columns:
            for i, _ in df0.iterrows():
                try:
                    df0.loc[i, c] = df_args.loc[i, c]
                except Exception as e:
                    print("Exception:" + str(e))
        self.data_model.set_data(df0)

    def update_bimets_args(self, bimets_args):
        # update the fitting arguments
        try:
            ys_e, ye_e, ys_s, ye_s, bimets_dir = bimets_args

            self.SETTING_R_RANGE_FIT = (ys_e, ye_e)
            self.SETTING_R_RANGE_SIM = (ys_s, ye_s)
            self.SETTING_R_path = bimets_dir

        except Exception as e:
            print("WARNING: Could not load simulation and fitting periods: ", str(e))

    def entry_to_vals(self, entry, agent, which):

        if not bool(regex.findall("([+-]+)", entry)):
            self.notify(message="Sign corrupted in entry %s (%s of %s)" %
                        (entry, which, agent), title="Error")
            return None, None, None

        if regex.findall("([+-]+)", entry)[0] == "-":
            sign = "-"
        else:
            sign = "+"

        words = regex.findall("([\w\s.><=]+)", entry)

        item = ""
        silent = False
        for word in words:
            if word != "s":
                item += word.strip() + " "
            elif word == "s":
                silent = True

        split_item = item.split(" ")
        item = split_item[0].strip()

        # third-party agent can be defined here as cross-reference
        ref_agent = None
        direction = -1
        other = ""
        if len(split_item) > 2:
            ref_agent = split_item[1].strip()
            # print("ref_agent", ref_agent)
            if ref_agent == "":
                ref_agent = None
            elif ref_agent.startswith(">"):
                # log the change on the other agent
                ref_agent = ref_agent[1:]
                direction = 1
            elif ref_agent.startswith("<"):
                # log the change on this agent
                ref_agent = ref_agent[1:]
                direction = 0
            elif ref_agent.startswith("="):
                # log the change in both directions
                ref_agent = ref_agent[1:]
                direction = 2
            other = split_item[2].strip()

        # self.notify("%s %s %s %s %s %s" % (sign, item, silent, ref_agent, direction, other), title="test")
        return sign, item, silent, ref_agent, direction, other

    def update_data_filter(self):
        self.data_filter_idx = 0
        searchtext = self.FilterlineEditData.text()
        if searchtext != "":
            # search for keyword in data model and try to focus on this
            try:
                self.data_model.try_focus(searchtext, self.data_filter_idx)
            except Exception as e:
                print("Exception:", str(e))

    def update_eqn_filter(self):
        searchtext = self.FilterlineEditEQs.text()
        if searchtext != "":
            # search for keyword in data model and try to focus on this
            try:
                self.eq_model.try_focus(searchtext, columns=[0])
            except Exception as e:
                print("Exception:", str(e))

    def update_table(self):
        # self.selection_idx = None
        self.transactionView.setRowCount(0)

        # insert new rows if needed
        row_count = self.transactionView.rowCount()

        if self.entry_data is None:
            self.notify(
                "Data is empty or corrupted. Something went wrong", title="Fatal Error")
            return

        if row_count < len(self.entry_data):
            for i in range(len(self.entry_data)-row_count):
                self.transactionView.insertRow(row_count)

        self.drawcanvas.clear()
        # self.filterCombo.clear() #
        self.filter_entries = []

        # print("-> ENTRY DATA", self.entry_data)

        try:
            # print("   ... -> found transactions entry")
            # self.notify("found", title="found")

            for i, data in enumerate(self.entry_data):
                # exists1 = self.drawcanvas.check_exist(data["agent1"].capitalize())
                # exists2 = self.drawcanvas.check_exist(data["agent2"].capitalize())

                # print("DATA", i, data)

                new_agent1 = camel(data["agent1"])
                new_agent2 = camel(data["agent2"])
                box1 = self.drawcanvas.add_agent(new_agent1)  # .capitalize())
                box2 = self.drawcanvas.add_agent(new_agent2)  # .capitalize())

                # correct number of connections for existing agents
                box1.n_connections = 0
                box2.n_connections = 0

            # update the data
            for i, data in enumerate(self.entry_data):
                # print(data)

                self.transactionView.setItem(
                    i, 0, QTableWidgetItem(data["subject"]))
                self.transactionView.setItem(i, 1, QTableWidgetItem(
                    camel(data["agent1"])))  # .capitalize()))
                self.transactionView.setItem(i, 2, QTableWidgetItem(
                    camel(data["agent2"])))  # .capitalize()))
                self.transactionView.setItem(
                    i, 3, QTableWidgetItem(data["shortname"]))
                self.transactionView.setItem(
                    i, 4, QTableWidgetItem(data["kind"]))
                self.transactionView.setItem(
                    i, 5, QTableWidgetItem(str(data["uni-directional"])))
                self.transactionView.setItem(
                    i, 6, QTableWidgetItem(str(data["quantity"])))
                # self.transactionView.selectionModel().selectedRows()

                box1 = self.drawcanvas.add_agent(
                    camel(data["agent1"]))  # .capitalize())
                box2 = self.drawcanvas.add_agent(
                    camel(data["agent2"]))  # .capitalize())

                my_items = []

                allentries = "\n".join(
                    [data["l1"], data["a1"], data["e1"], data["l2"], data["a2"], data["e2"]])
                for sub_item in allentries.split("\n"):
                    for sub_entry in sub_item.split("\n"):
                        entry = sub_entry.replace(
                            "-", "").replace("+", "").lstrip()
                        if entry != "":
                            my_items.append(entry)

                self.drawcanvas.add_connection(
                    box1, box2, name=data["shortname"], subject=data["subject"], items=my_items)

                # reduce the filter values
                x = []
                for val in my_items + [data["agent1"], data["agent2"]]:
                    x.append(val.split(" ")[0].strip())
                my_items = list(set(x))

                # update the filter values
                for entry in my_items:
                    if entry not in self.filter_entries:
                        if entry not in [data["agent1"], data["agent2"]]:
                            self.filter_entries.append(entry)

        except Exception as e:
            tb_str = tb_str = "".join(traceback.format_tb(e.__traceback__))
            self.notify(str(e)+"\n" + "\n" + str(tb_str), title="Error")

        try:
            # search for a match in the filter
            found_idxs = []
            search_str = self.FilterlineEdit.text()

            if search_str != "":
                try:
                    for i in range(self.transactionView.rowCount()):

                        for j in range(self.transactionView.columnCount()):

                            entry_str = self.transactionView.item(i, j).text()

                            if entry_str.find(search_str) > -1:

                                found_idxs.append(i)
                except Exception as e:
                    print("Exception:", str(e))

            for found_idx in found_idxs:
                # highlight occurances
                for j in range(self.transactionView.columnCount()):
                    try:

                        if self.theme_manager.theme == "bright":

                            # light mode
                            self.transactionView.item(found_idx, j).setBackground(
                                QtGui.QColor(227, 166, 166))
                            self.transactionView.item(found_idx, j).setForeground(
                                QtGui.QColor(180, 18, 18))
                            

                        else:

                            # dark mode
                            self.transactionView.item(found_idx, j).setBackground(
                                QtGui.QColor(120, 120, 120))
                            self.transactionView.item(found_idx, j).setForeground(
                                QtGui.QColor(50, 50, 50))

                    except:
                        pass
                        # avoids None pointer when a transaction is deleted
        except:
            pass

        # if len(self.filter_entries) > 0:
        #    print(self.filter_entries)
        #    # sys.exit(0)
        if GuiSettingsDialog.instance:
            GuiSettingsDialog.instance.fill_filter()
        # self.notify("Filter GUI", self.filter_gui)

        #  self.notify(str(self.filter_entries), title="Entries")
        # TODO fix this

        self.gen_transaction_matrix(silent=True)
        df = self.gen_balance_matrix()

        # construct the sfc conditions from the balance matrix
        if df is not None:
            print(df)

        header = self.transactionView.horizontalHeader()
        header.setDefaultAlignment(Qt.AlignLeft | Qt.AlignVCenter)

        self.drawcanvas.reposition()
        self.drawcanvas.reposition_labels()

        self.drawcanvas.update()
        self.made_changes = False

    def move_eq_up(self):
        if self.eq_model is not None:
            print("swap up")
            self.eq_model.swap_up()

    def move_eq_down(self):
        if self.eq_model is not None:
            print("swap down")
            self.eq_model.swap_down()

    def move_down(self):
        # move selected transaction up in table
        selected_rows = sorted(set(index.row() for index in
                                   self.transactionView.selectedIndexes()))

        if len(selected_rows) == 0:
            # print("no selected rows")
            return

        selection = selected_rows[0]

        number_of_items = self.transactionView.rowCount()
        number_of_columns = self.transactionView.columnCount()

        try:

            if selection < number_of_items-1:
                currentRow = self.transactionView.currentRow()

                for j in range(number_of_columns):
                    currentItem = self.transactionView.takeItem(currentRow, j)
                    switchItem = self.transactionView.takeItem(currentRow+1, j)

                    self.transactionView.setItem(
                        currentRow + 1, j, currentItem)
                    self.transactionView.setItem(currentRow, j, switchItem)

                    switch_entry = self.entry_data[currentRow+1]
                    current_entry = self.entry_data[currentRow]

                    self.entry_data[currentRow] = switch_entry
                    self.entry_data[currentRow+1] = current_entry

                self.transactionView.clearSelection()
                self.transactionView.setCurrentCell(currentRow+1, 0)

        except Exception as e:
            self.notify("error: "+str(e), title="Error")

    def move_up(self):
        # move selected transaction up in table
        selected_rows = sorted(set(index.row() for index in
                                   self.transactionView.selectedIndexes()))

        if len(selected_rows) == 0:
            # print("no selected rows")
            return

        selection = selected_rows[0]

        number_of_items = self.transactionView.rowCount()
        number_of_columns = self.transactionView.columnCount()

        try:

            if selection >= 1:
                currentRow = self.transactionView.currentRow()

                for j in range(number_of_columns):
                    currentItem = self.transactionView.takeItem(currentRow, j)
                    switchItem = self.transactionView.takeItem(currentRow-1, j)

                    self.transactionView.setItem(
                        currentRow - 1, j, currentItem)
                    self.transactionView.setItem(currentRow, j, switchItem)

                    switch_entry = self.entry_data[currentRow-1]
                    current_entry = self.entry_data[currentRow]

                    self.entry_data[currentRow] = switch_entry
                    self.entry_data[currentRow-1] = current_entry

                self.transactionView.clearSelection()
                self.transactionView.setCurrentCell(currentRow-1, 0)

        except Exception as e:
            self.notify("error: "+str(e), title="Error")

    def transaction_select(self, double_click=None):

        # selected_rows = self.transactionView.selectionModel().selectedRows()
        selected_rows = sorted(set(index.row() for index in
                                   self.transactionView.selectedIndexes()))

        if len(selected_rows) == 0:
            # print("no selected rows")
            return

        selection = selected_rows[0]

        if isinstance(double_click, bool) and double_click and self.selection_idx == selection:  # same as before
            self.update_transaction_data()
            # self.notify("%s" % str(double_click), title="test")
            # print("select")

        else:
            self.row_select(selection)
        self.made_changes = False
        self.update_alt_label()

    def transaction_select_where(self, expr):
        # print("data select where",expr)

        for i in range(self.transactionView.rowCount()):
            my_entry = self.transactionView.item(i, 3).text()

            # print("...",i,my_entry)
            if my_entry == expr or my_entry.lower() == expr.lower():

                self.row_select(i)
                return

    def row_select(self, selection):

        print("row select", selection)
        # color the selected row
        # gb(233, 234, 227);

        self.selection_idx = None

        if self.made_changes:
            yes = self.ask_question(
                "Warning", "You are about to change to another transaction and withdraw the changes you made. Continue?")
            if not yes:
                return

        try:
            self.transactionView.clearSelection()
            self.transactionView.setCurrentCell(selection, 3)

        except:
            pass

        try:
            self.selection_idx = selection
            data = self.entry_data[selection]
            self.drawcanvas.highlight_connector(data["shortname"])

        except Exception as e:
            self.notify(str(e), title="Error")

        self.made_changes = False
        # self.update_alt_label()

    def update_transaction_data(self):
        # update the data showon in the transaction table

        self.made_changes = False
        if self.selection_idx is None:
            return

        acc_dlg = AccountingDialog(
            self, data=self.entry_data[self.selection_idx], myfont=self.font)
        return

        self.entry_data[self.selection_idx]["a1"] = self.AssetListLeftEdit.toPlainText(
        )
        self.entry_data[self.selection_idx]["a2"] = self.AssetListRightEdit.toPlainText(
        )
        self.entry_data[self.selection_idx]["l1"] = self.LiabilityListLeftEdit.toPlainText()
        self.entry_data[self.selection_idx]["l2"] = self.LiabilityListRightEdit.toPlainText()
        self.entry_data[self.selection_idx]["e1"] = self.EquityListLeft.toPlainText()
        self.entry_data[self.selection_idx]["e2"] = self.EquityListRight.toPlainText()
        self.entry_data[self.selection_idx]["a1"] = self.AssetListLeftEdit.toPlainText(
        )
        self.entry_data[self.selection_idx]["a2"] = self.AssetListRightEdit.toPlainText(
        )
        self.entry_data[self.selection_idx]["l1"] = self.LiabilityListLeftEdit.toPlainText()
        self.entry_data[self.selection_idx]["l2"] = self.LiabilityListRightEdit.toPlainText()
        self.entry_data[self.selection_idx]["e1"] = self.EquityListLeft.toPlainText()
        self.entry_data[self.selection_idx]["e2"] = self.EquityListRight.toPlainText()

        only_rename = True
        if self.entry_data[self.selection_idx]["agent1"] != self.agent1EditField.text():
            only_rename = False
        if self.entry_data[self.selection_idx]["agent2"] != self.agent2EditField.text():
            only_rename = False
        if not self.drawcanvas.check_exist(self.agent1EditField.text()):
            if self.agent1EditField.text().strip() != "":
                yes = self.ask_question(
                    '', "The agent %s does not exist.\nDo you wish to continue and automatically create a new agent?" % self.agent1EditField.text())
                if not yes:
                    return
        if not self.drawcanvas.check_exist(self.agent2EditField.text()):
            if self.agent2EditField.text().strip() != "":
                yes = self.ask_question(
                    '', "The agent %s does not exist.\nDo you wish to continue and automatically create a new agent?" % self.agent2EditField.text())
                if not yes:
                    return

        # .capitalize() # upper because agents are always uppercase
        self.entry_data[self.selection_idx]["agent1"] = camel(
            self.agent1EditField.text())
        self.entry_data[self.selection_idx]["agent2"] = camel(
            self.agent2EditField.text())  # .capitalize()
        self.agent1EditField.setText(
            camel(self.agent1EditField.text()))  # .capitalize())
        self.agent2EditField.setText(
            camel(self.agent2EditField.text()))  # .capitalize())

        self.entry_data[self.selection_idx]["uni-directional"] = \
            str(self.UnidirCombo.currentText())
        self.entry_data[self.selection_idx]["log transaction"] =\
            str(self.registerFlowBoxEdit.currentText())
        self.entry_data[self.selection_idx]["cashflow1"] = \
            str(self.CFLeftCombo.currentText())
        self.entry_data[self.selection_idx]["cashflow2"] = \
            str(self.CFRightCombo.currentText())
        self.entry_data[self.selection_idx]["kind"] = \
            str(self.editTypeCombo.currentText())
        self.entry_data[self.selection_idx]["income1"] = \
            str(self.incomeLeftCombo.currentText())
        self.entry_data[self.selection_idx]["income2"] = \
            str(self.incomeRightCombo.currentText())

        if only_rename:
            self.drawcanvas.rename_connection(
                self.entry_data[self.selection_idx]["shortname"], str(self.editShortnameField.text()))
        else:
            # , str(self.editSubjectField.text()))
            self.drawcanvas.remove_connection(
                self.entry_data[self.selection_idx]["shortname"])
            box1 = self.drawcanvas.add_agent(
                self.entry_data[self.selection_idx]["agent1"])
            box2 = self.drawcanvas.add_agent(
                self.entry_data[self.selection_idx]["agent2"])
            my_items = []
            data = self.entry_data[self.selection_idx]

            allentries = "\n".join(
                [data["l1"], data["a1"], data["e1"], data["l2"], data["a2"], data["e2"]])
            for sub_item in allentries.split("\n"):
                for sub_entry in sub_item.split("\n"):
                    entry = sub_entry.replace("-", "").replace("+", "").strip()
                    if entry != "":
                        my_items.append(entry)
            self.drawcanvas.add_connection(box1, box2, str(self.editShortnameField.text(
            )), subject=self.editSubjectField.text(), items=my_items)

        self.entry_data[self.selection_idx]["quantity"] = \
            str(self.editQuantityField.text())

        self.entry_data[self.selection_idx]["subject"] = \
            str(self.editSubjectField.text())

        self.entry_data[self.selection_idx]["shortname"] = \
            str(self.editShortnameField.text())

        self.entry_data[self.selection_idx]["description"] = \
            str(self.editDescriptionField.text())

        self.entry_data[self.selection_idx]["add_args"] = \
            str(self.editAddArgsField.text())

        self.entry_data[self.selection_idx]["add_code"] = \
            str(self.editAddCodeField.toPlainText())

        self.update_table()
        # print("data changed.")
        self.made_changes = False
        self.row_select(self.selection_idx)
        # self.udpateButton.setText("Update Values")
        self.gen_transaction_matrix()
        self.gen_balance_matrix()

    def save(self):
        try:
            filename = self.current_file  # statusBar().currentMessage()
            if (filename is not None) and filename != "":
                self.label_fname.setText(filename)
                self.saveas(filename)
                # TODO maybe backup(?)
                # self.notify("File saved under %s" % filename,title="Ok")
                # print("FILE SAVED!")
            else:
                self.save_dlg()
        except Exception as e:
            if filename != "":
                self.notify(str(e), title="Error")

    def save_dlg(self):
        current_file_backup = self.current_file
        try:
            filename = QFileDialog.getSaveFileName(self, 'Save file',
                                                   os.getcwd(), "attune Files (*.sfctl)")[0]
            if filename is not None and filename != "":
                self.statusBar().showMessage("Saved file " + filename)
                self.label_fname.setText(filename)
                self.current_file = filename
                self.saveas(filename)
            # TODO maybe backup(?)
        except Exception as e:
            self.notify("Could not save project: %s" % str(e), title="Error")
            self.current_file = current_file_backup

    def get_options(self):

        o1 = self.SETTING_show_labels #= True  # checkbox 2
        o2 = self.SETTING_show_arrows #= True  # checkBox
        o3 = self.SETTING_highlight_labels #= False  # checkbox 4
        o4 = self.SETTING_text_at_handle #= True  # checkBox_5
        o5 = self.SETTING_show_handles #= True  # checkBox_3 NOTE now always True
        o6 = self.SETTING_show_support #= True  # checkBox_6 NOTE now always True
        o7 = self.SETTING_subject_labels #= False  # checkBox_8
        o8 = self.SETTING_show_preview #= True  # checkBox_9
        o9 = self.SETTING_use_raster #= False  # checkbox_raster

        # self.SETTING_show_raster = False # checkBox_raster_2
        # self.SETTING_white_bg = False  # checkbox 7

        # o1 = str(self.checkBox_2.isChecked())
        # o2 = str(self.checkBox.isChecked())
        # o3 = str(self.checkBox_4.isChecked())
        # o4 = str(self.checkBox_5.isChecked())
        # o5 = str(self.checkBox_3.isChecked())
        # o6 = str(self.checkBox_6.isChecked())
        # o7 = str(self.checkBox_8.isChecked())
        # o8 = str(self.checkBox_9.isChecked())
        # o9 = str(self.checkBox_raster.isChecked())
#
        return [o1, o2, o3, o4, o5, o6, o7, o8, o9]

    def set_options(self, new_options):
        # set display options
        if new_options is None:
            return

        o1, o2, o3, o4, o5, o6, o7, o8, o9 = new_options

        def str_to_bool(x):
            if x == "True":
                return True
            return False

        # self.checkBox_2.setChecked(str_to_bool(o1))
        # self.checkBox.setChecked(str_to_bool(o2))
        # self.checkBox_4.setChecked(str_to_bool(o3))
        # self.checkBox_5.setChecked(str_to_bool(o4))
        # self.checkBox_3.setChecked(str_to_bool(o5))
        # self.checkBox_6.setChecked(str_to_bool(o6))
        # self.checkBox_8.setChecked(str_to_bool(o7))
        # self.checkBox_9.setChecked(str_to_bool(o8))
        # self.checkBox_raster.setChecked(str_to_bool(o9))
        # TODO link this to the SETTING___...

    def saveas(self, filename):
        # save as ...

        ftheme = None

        try:
            ftheme = filename[:-5]+"sfctheme"  # absolute file path
            # convert to relative file path
            folder, ftheme = os.path.split(ftheme)
            self.theme_manager.save_colors(ftheme)
        except:
            self.notify(
                "Could not save color theme. proceeding without", title="Error")

        # drawcanvas code_data
        code_data = {}
        for k, v in self.drawcanvas.code_data.items():
            print("code for", k)
            print(v)
            print("       ***          ")

            code_data[k] = str(v.encode("utf-8").decode('cp1252'))
            # fix some potential encoding errors

        # extract data args
        data_args = {}
        try:
            argdict = self.data_model.raw_data.to_dict()
            for k in ["EXTMODE", "EXTVAL", "ADD_FACT_START", "ADD_FACT_VALS", "EXO_START", "EXO_END"]:
                # read the data from the table inserted by the user
                data_args[k] = argdict[k]
        except:
            print("no raw data in data_model. skipping...")

        try:
            # self.lineEdit_year_start_est.text())
            ys_e = int(self.SETTING_R_RANGE_FIT[0])
            # self.lineEdit_year_end_est.text())
            ye_e = int(self.SETTING_R_RANGE_FIT[1])
            # lineEdit_year_start_sim.text())
            ys_s = int(self.SETTING_R_RANGE_SIM[0])
            # self.lineEdit_year_end_sim.text())
            ye_s = int(self.SETTING_R_RANGE_SIM[1])
            bimets_dir = self.SETTING_R_path  # self.lineEdit_R_model_dir.text()
        except:
            ys_e, ye_e, ys_s, ye_s = 0, 0, 0, 0

        bimets_args = [ys_e, ye_e, ys_s, ye_s, bimets_dir]
        # print("data_args", data_args)

        trans_path = str(self.export_path_edit.text())

        try:
            # print("FILENAME", filename)
            self.eq_wrapper.udpate_data()

            # load entries into the list
            with open(filename, 'w') as stream:

                data = {"transactions": self.entry_data,
                        "agents": code_data,
                        "box_positions": self.drawcanvas.box_positions(),
                        "label_positions": self.drawcanvas.label_positions(),
                        "settings": self.settings_str,
                        "mainloop": self.mainloop_str,
                        "theme": {"globaltheme": self.theme_manager.theme, "colors": ftheme},
                        "notes": self.notesEdit.toPlainText(),
                        "options": self.get_options(),
                        "eqs": self.eq_wrapper.get_data(),
                        "data_path": self.datafilePathLabel.text(),
                        "data_args": data_args,
                        "bimets_args": bimets_args,
                        "trans_path": trans_path,
                        "ca_ka_info": dict(self.SETTING_ca_ka),
                        "balance_init_data": dict(self.SETTING_balance_init_data),
                        "sorting_tables": self.sorting_tables
                        }


                yaml.dump(data, stream)

                self.transaction_data = data

        except Exception as e:
            self.notify(message=str(e), title="Error")

    def try_gen_transaction_matrix(self):
        try:
            self.gen_transaction_matrix(show=True)

        except Exception as e:
            self.notify("Error", str(e))

    def gen_transaction_matrix(self, mode="standard", show=False, silent=False):
        """
        generate flow matrix:

        :param mode: standard,excel,latex

        standard: shows matrix graphically
        excel: exports an excel file
        latex: not yet supported NOT AVAILABLE FOR NOW
        html: NOT AVAILABLE FOR NOW
        """

        flow_data = {}

        flow_data["CA"] = defaultdict(
            lambda: defaultdict(list))  # current account
        flow_data["KA"] = defaultdict(
            lambda: defaultdict(list))  # capital account

        # flow_data[KA or CA][subject][agent]
        if len(self.entry_data) == 0:
            if not silent:
                self.notify(
                    message="Cannot generate Matrix. Have you set up any transactions yet?", title="Error")
            return

        try:
            for filedata in self.entry_data:
                """
                evaluate transaction entries with the flow logger
                """
                subject = filedata["subject"].replace("_", " ").title()
                kind = filedata["kind"]
                log_flow = True
                if filedata["log transaction"] == "False":
                    log_flow = False
                #   log_flow = bool(filedata["log transaction"])
                # print("LOG FLOW",log_flow,"raw",filedata["log transaction"])
                quantity = filedata["quantity"]
                agent1 = filedata["agent1"].replace("_", " ").title()
                agent2 = filedata["agent2"].replace("_", " ").title()

                # upper part of matrix
                if log_flow:
                    account_from = kind.split("->")[0]
                    account_to = kind.split("->")[1]

                    flow_data[account_from][subject][agent1].append(
                        "-" + str(quantity))
                    flow_data[account_to][subject][agent2].append(
                        str(quantity))

                # lower part of matrix = changes in assets ans liabilities

                # assets
                def process_items(item_list, agent, flow_data, quantity, flip_sign=False):
                    for item_name in item_list.split("\n"):
                        if item_name:
                            name = item_name[1:].lstrip().split(" ")[0].strip()
                            sign = item_name[0]

                            if flip_sign:
                                flipped_sign = "-" if sign == "+" else "+"
                                sign = flipped_sign

                            if "(s)" not in name:
                                flipped_sign = "-" if sign == "+" else "+"
                                sign = flipped_sign
                                flow_data["KA"][f"Δ {name}"][agent].append(
                                    f"({sign}{quantity})")

                a1 = filedata["a1"]
                l1 = filedata["l1"]
                a2 = filedata["a2"]
                l2 = filedata["l2"]

                process_items(a1, agent1, flow_data, quantity)
                process_items(a2, agent2, flow_data, quantity)
                process_items(l1, agent1, flow_data, quantity, flip_sign=True)
                process_items(l2, agent2, flow_data, quantity, flip_sign=True)

                try:
                    a3 = filedata["a3"]
                    l3 = filedata["l3"]
                    agent3 = filedata["agent3"].replace("_", " ").title()
                    if agent3 != "":
                        process_items(a3, agent3, flow_data, quantity)
                        process_items(l3, agent3, flow_data,
                                      quantity, flip_sign=True)
                except:
                    pass
                try:
                    a4 = filedata["a4"]
                    l4 = filedata["l4"]
                    agent4 = filedata["agent4"].replace("_", " ").title()
                    if agent4 != "":
                        process_items(a4, agent4, flow_data, quantity)
                        process_items(l4, agent4, flow_data,
                                      quantity, flip_sign=True)
                except:
                    pass
            
            # # print(flow_data)
            df_credit = pd.DataFrame(flow_data["CA"]).T
            df_capital = pd.DataFrame(flow_data["KA"]).T

            # mycols_credit = []
            # mycols_capital = []
            # co = self.sorting_tables["tfm"]["cols"]
            # for c in co:
            #     if c in df_credit.columns:
            #         mycols_credit.append(c)
            #     if c in df_capital.columns:
            #         mycols_capital.append(c)
            
            # for c in df_credit.columns:
            #     if c not in mycols_credit:
            #         mycols_credit.append(c)
            # for c in df_capital.columns:
            #     if c not in mycols_capital:
            #         mycols_capital.append(c)
            
            # df_credit = df_credit[mycols_credit]
            # df_capital = df_capital[mycols_capital]

            df_merge = pd.concat([df_credit, df_capital], axis=1, keys=[
                        'CA', 'KA']).swaplevel(0, 1, axis=1).sort_index(axis=1)

            #print("DF_MERGEE")
            #print(df_merge)

            #df_merge = df_merge[mycols_merged]

            # append new columns
            col_dict_ca = {}
            for col in df_credit.columns:
                col_dict_ca[col] = col + " (CA)"

            col_dict_ka = {}
            for col in df_capital.columns:
                col_dict_ka[col] = col + " (KA)"

            df_credit = df_credit.rename(columns=col_dict_ca)
            df_capital = df_capital.rename(columns=col_dict_ka)

            df_merge2 = pd.concat([df_credit, df_capital], axis=1)

            df_merge2 = df_merge2.reindex(sorted(df_merge2.columns), axis=1)
            df_merge2 = df_merge2.reindex(sorted(df_merge2.index), axis=0)

            df = df_merge.fillna(0.0).sort_index()

            def join_elements(my_item):
                if isinstance(my_item, list):
                    my_list = []
                    for subitem in my_item:
                        if subitem != "0":
                            my_list.append(subitem)

                    return "+".join(my_list)

                elif my_item == 0.0:
                    return "0"
                else:
                    return my_item

            df = df.applymap(join_elements)

            # sum over the columns

            def eval_sum(column):
                column = [str(c).replace("^", "**") for c in column]
                my_expr = "+".join(column)
                try:
                    return parse_expr(my_expr)
                except:
                    return my_expr

            df["Total"] = df.T.agg(eval_sum)  # df.T.sum()
            df.loc["Total"] = df.agg(eval_sum)
            # print("TOTAL",df.loc["Total"])
            df_merge2["Total"] = df["Total"]


            #print("dff")
            #print(df)

            # get the new columns

            # iterate through the columns
            # print(df.columns)

            def safe_add(x, y):
                if isinstance(x, float) and np.isnan(x):
                    x = []
                if isinstance(y, float) and np.isnan(y):
                    y = []
                if isinstance(x, list) and isinstance(y, list):
                    result =  x + y
                elif isinstance(x, list):
                    result = x + [y]
                elif isinstance(y, list):
                    result = [x] + y
                else:
                    result = (x or 0) + (y or 0)
                if result == []:
                    return np.nan 
                return result 

            # agent_order = [a for a in self.sorting_tables["tfm"]["cols"] if a]  # keep order

            agents = set()
            for c in df.columns:
                agents.add(c[0])
            
            #print("SORTED AGENTS", self.sorting_tables["tfm"]["cols"])

            # for s in self.sorting_tables["tfm"]["cols"]:
            #     if s in df.columns and s != "Total":
            #         agents.append(s)
            # for c in df.columns:
            #     if c not in agents:
            #         agents.append(c)
            #print("AGENTS", agents)
            #agents = set(agents)

            def merge_cols(d):
                # merge the columns for the non-differentiated agents (sum CA+KA)
                
                for a in agents:
                    if not self.SETTING_ca_ka[a]:
                        # d[a] = d[a + " (CA)"].fillna([]) + d[a + " (KA)"].fillna([])
                        
                        entry1 = a+ " (CA)"
                        entry2 = a+ " (KA)"
                        drop_cols = []
                        
                        if entry1 in d.columns and entry2 in d.columns:
                            d[a] = d[entry1].combine(d[entry2], safe_add)
                            drop_cols = [entry1, entry2]
                        elif entry1 in d.columns:
                            d[a] = d[entry1]
                            drop_cols = [entry1]
                        elif entry2 in d.columns: 
                            d[a] = d[entry2]
                            drop_cols = [entry2]
                        else:
                            print("WARNING: could not combine columns of %s"% str(a))
                            continue 

                        #del d[a + " (CA)"]
                        #del d[a + " (KA)"]
                        d[a] = d[a].replace([], np.nan)
                        
                        d = d.drop(columns=drop_cols)
                        # print("drop", drop_cols)
                    
                return d

            # proper_cols = []
            # for a in agents:
            #     for c in df_merge2.columns:
            #         if a in c:
            #             proper_cols.append(c)
            # print("poper cols", proper_cols)
            # print("dfMERGEEEEE2")
            # print(df_merge2)
            # df_merge2 = df_merge2[proper_cols] # .sort_values(by=proper_cols)
            df_merge2 = merge_cols(df_merge2)

            #print("-<-<-<-")
            #print(df_merge2)
            
            #print("-->")
            #p#rint(df_merge2)

            def move_total_to_end(df):
                df = df.copy()
                if "Total" in df.columns:
                    cols = [c for c in df.columns if c != "Total"] + ["Total"]
                    df = df[cols]
                if "Total" in df.index:
                    rows = [r for r in df.index if r != "Total"] + ["Total"]
                    df = df.loc[rows]
                return df
            
            # compute row totals    
            def safe_add_rows(df, rows):
                result = df.loc[rows[0]].copy()
                for r in rows[1:]:
                    result = result.combine(df.loc[r], safe_add)
                return result

            drows = [r for r in df_merge2.index if "Δ" in r]
            # df_merge2.loc["Δ NW"] = df_merge2.loc[drows].sum(axis=0)
            #print("drows", drows)
            df_merge2.loc["Δ NW"] = safe_add_rows(df_merge2, drows)

            for column in df_merge2.columns:
                if column != "Total":
                    #print("column", column)
                    if "CA" in column: 
                        name= column.split("(CA)")[0].strip()
                        k = (name, "CA")
                        df_merge2.loc["Total", name +" (CA)"] = df.loc["Total", k]
                    elif "KA" in column:
                        name = column.split("(KA)")[0].strip()
                        k = (name, "KA")
                        df_merge2.loc["Total", name +  " (KA)"] = df.loc["Total", k]

            df_merge2 = move_total_to_end(df_merge2)

            def simplify_expr(my_item):
                return simplify(my_item)

            df = df.applymap(simplify_expr)

            df_for_excel = df.copy()

            def abbreviate_zeros_latex(my_item):
                if my_item == "0" or my_item == 0:
                    return " .- "  # .- "
                else:
                    # mode="inline")
                    s = printing.latex(my_item, mode="inline")
                    # print(s)
                    return s

            # df_backup = df.copy()
            df = df.applymap(abbreviate_zeros_latex)

            df_merge2 = df_merge2.replace(np.nan, '', regex=True)
            df_merge2 = df_merge2.replace("0", '', regex=True)
            df_merge2 = df_merge2.replace(0, '', regex=True)
            df_merge2 = df_merge2.replace(0.0, '', regex=True)

            # # print(df.to_string())
            # df.iloc[0,0] = "$\\alpha$"

            pd.options.display.max_colwidth = None

            if mode == "html":  # < NOT YET SUPPORTED

                df_html = """
        <html>
        <head>
        <title>Mathedemo</title>
        <script type="text/x-mathjax-config">
          MathJax.Hub.Config({tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']]}});
        </script>
        <script type="text/javascript"
          src="http://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
        </script>
        <link rel="stylesheet" href="mystyle.css">
        </head>

        <body>
        %s
        </body>
        </html>
        """ % df.to_html()

                pageSource = """
                             <html><head>
                             </head>
                             <body>
                             <p>asdf</p>
                             </body></html>
                             """

                # BLA

                # write page source to html
                if False:
                    # DEPRECIATED
                    path = os.path.dirname(os.path.abspath(__file__))
                    file = open(os.path.join(
                        path, "files/mymatrix.html"), "w", encoding='utf-8')
                    file.write(df_html)
                    file.close()

            # QDesktopServices.openUrl(QUrl("./files/matrix.html"))
            #print("Okk")
            #print(df_merge2)

    
            # Process column names to format as required
            processed_columns = []
            seen_prefixes = set()

            for col in df_merge2.columns:
                prefix = col.split('(')[0].strip()
                if '(KA)' in col:
                    if prefix in seen_prefixes:
                        # Remove the prefix if it has already been seen for a KA column
                        processed_columns.append(
                            f"\n{col.split('(')[1][:-1].strip()}")
                    else:
                        # Keep the prefix and add it to the seen set
                        processed_columns.append(
                            f"{prefix}\n{col.split('(')[1][:-1].strip()}")
                elif '(' in col and ')' in col:
                    # Standard processing for other columns with parentheses
                    processed_columns.append(
                        f"{col.split('(')[0].strip()}\n{col.split('(')[1][:-1].strip()}")
                else:
                    # Leave columns without parentheses unchanged
                    processed_columns.append(col)
                seen_prefixes.add(prefix)

            # Update the column names in the DataFrame
            df_merge2.columns = processed_columns 

            #print("merge23232")
            #print("df_merge2 columns", df_merge2.columns)

            # right_order = []
            # for a in agents:
            #     for c in df_merge2.columns:
            #         if a in c:
            #             right_order.append(c)
            # for c in df_merge2.columns:
            #     if c not in right_order:
            #         right_order.append(c)
            # df_merge2 = df_merge2[right_order]
            # print("right order", right_order)


            if mode == "standard":

                # (optional) replace data 
                if not self.checkBoxAnalyticalTFM.isChecked():
                    try:
                        def clean_name(n, extract_sign=False):
                            if extract_sign: 
                                if "-" in n:
                                    sign = -1 
                                else:
                                    sign = +1 
                            name = n.replace("(", "").replace(")", "").replace("+", "").replace("-", "").strip()
                            if extract_sign:
                                return name, sign
                            return name 
                        
                        def my_parse_tfm_expr(expr: str):
                            """
                            Parse strings like '-fb + intf + intgb - inth + intlh'
                            -> list of (sign, name) tuples: [(-1,'fb'), (+1,'intf'), ...]
                            """
                            tokens = regex.findall(r'([+-])?\s*([A-Za-z0-9_]+)', expr.replace(" ", ""))
                            out = []
                            for sign, name in tokens:
                                s = -1 if sign == '-' else 1
                                out.append((s, clean_name(name)))
                            return out
                        
                        unique_names = {clean_name(n) for n in self.data_wrapper.ts_df["NAME"].unique()}
                        #print("unique names", unique_names)
                        data = self.data_wrapper.df.copy()
                        data.columns = [clean_name(c) for c in data.columns] 
                        cols_to_fill = list(range(min(df_merge2.shape[1], data.shape[1])))
                        data.iloc[:, cols_to_fill] = data.iloc[:, cols_to_fill].astype(object)
                        ref_year = int(self.spinBox_refYear2.value())

                        if ref_year in data.index:
                            ref_row = data.loc[ref_year]
                            for i, row in enumerate(df_merge2.itertuples(index=False, name=None)):
                                for j, cell in enumerate(row):
                                    out = []
                                    not_found = []
                                    failed = False 
                                    if isinstance(cell, list):
                                        # print("cell", cell)
                                        for name in cell:
                                            stripped_name, sign = clean_name(name, extract_sign=True)
                                            # print("  ", stripped_name)
                                            if stripped_name in unique_names: # and stripped_name in ref_row.index:
                                                new_val = sign*float(ref_row.at[stripped_name]) 
                                                # new_val = np.round(new_val, 2)
                                                out.append(new_val)
                                            else:
                                                # self.notify("%s missing. skipping entry." % name, title="Missing data.")
                                                failed = True 
                                                not_found.append(name)
                                    else:
                                        # expression string like '-fb + intf - inth'
                                        # print("cell*", cell)
                                        terms = my_parse_tfm_expr(str(cell))
                                        for sign, nm in terms:
                                            if nm in ref_row.index and nm in unique_names:
                                                val = ref_row.at[nm]
                                                new_val = sign * val 
                                                # new_val = new_val # np.round(new_val, 2)
                                                out.append(new_val)
                                                # print("APPEND", new_val)
                                                # TODO insert case where total is e.g. '2*x' 
                                            else:
                                                failed=True 
                                                not_found.append(nm)
                                    if failed: 
                                        df_merge2.iat[i,j] = "Error. Could not find: %s" % not_found
                                    elif len(out) > 0:
                                        df_merge2.iat[i, j] = np.round(sum(out), 2)
                                    else:
                                        df_merge2.iat[i, j] = " "
                    except Exception as e:
                        self.notify(str(e), title="Exception")
                    
                # df_merge2.applymap(lambda x: "xxx")

                #print("new df_merge")
                #print(df_merge2)

                # why not sorted here 

                model = PandasModel(df_merge2)
                exists_instance = MatrixViewer.instance["FlowMatrix"] is not None
                mview = MatrixViewer(mode="FlowMatrix", parent=self)

                mview.set_model(model)
                self.tfm_model = model
                self.TFMtableView.setModel(self.tfm_model)

                if not self.test_mode:
                    if exists_instance:
                        mview.update()
                    else:
                        if show:
                            mview.show()

            if mode == "latex":  # < NOT YET SUPPORTED
                try:
                    self.convert_df_to_tex(df)
                except Exception as e:
                    self.notify(str(e), title="Error during Tex Conversion")

                # # print("VIEW")
                # # print(df_html)

            def convert_excel(my_item):
                if my_item == "0" or my_item == 0:
                    return " .- "  # .- "
                else:
                    # printing.latex(my_item) # ,mode="inline") #  mode="inline")
                    s = pretty(my_item)
                    # print(s)
                    return s

            if mode == "excel":
                try:
                    efilename = QFileDialog.getSaveFileName(
                        self, 'Save file', os.getcwd(), "Excel Files (*.xlsx)")[0]

                    if efilename is not None and efilename != "":
                        df_for_excel = df_for_excel.applymap(convert_excel)
                        # path = os.path.dirname(os.path.abspath(__file__))
                        # "files/FlowMatrix_formatted.xlsx"))
                        df_for_excel.to_excel(efilename)
                        # df_merge2.to_excel(os.path.join(path,efilename))    # "files/FlowMatrix.xlsx"))

                except Exception as e:
                    self.notify(str(e), title="Error during Excel conversion")

        except Exception as e:
            self.notify(str(e), title="Error during matrix generation")

        self.sort_table("tfm")

    def remove_item(self):

        try:
            selected_rows = sorted(set(index.row() for index in
                                       self.transactionView.selectedIndexes()))

            if len(selected_rows) == 0:
                self.notify("no selected rows", title="Error")
                return

            selection = selected_rows[0]

            yes = self.ask_question(
                '', "Are you sure you want to remove the entry '%s'?" % self.entry_data[selection]["subject"])
            if yes:

                self.drawcanvas.remove_connection(
                    self.entry_data[selection]["shortname"])

                self.transactionView.removeRow(selection)
                del self.entry_data[selection]

        except Exception as e:
            self.notify(str(e), title="Error")

        try:
            self.update_table()

        except Exception as e:
            self.notify(str(e), title="Error")

    def get_selected_eqs(self, fit_all=True):
        # get the selected row
        EQS = {}
        sel_model = self.EQtableView.selectionModel()

        if sel_model:

            rows = sel_model.selectedRows()
            if rows:  # if rows is not None and len(rows) > 0:

                if not fit_all:
                    rows = [rows[0]]

                for row in rows:
                    # print("fit", row)
                    eq_info = dict(self.eq_model.raw_data.loc[row.row(), :])

                    if eq_info["NAME"] not in EQS:
                        EQS[eq_info["NAME"]] = {"type": [],
                                                "EQ":   [],
                                                "COEFF": [],
                                                "RESTRICT": [],
                                                "CONDITION": [],
                                                "COMMENT": [], }

                    EQS[eq_info["NAME"]]["type"].append(eq_info["TYPE"])
                    EQS[eq_info["NAME"]]["EQ"].append(eq_info["EXPRESSION"])

                    if str(eq_info["COEFFS"]).strip() != "":
                        EQS[eq_info["NAME"]]["COEFF"].append(eq_info["COEFFS"])
                    else:
                        if eq_info["TYPE"] == "BEHAVIORAL":
                            self.notify(
                                "Warning: Equation for '%s' is behavioral but no coefficients were given!" % eq_info["NAME"], title="Warning")
                        EQS[eq_info["NAME"]]["COEFF"].append("")

                    for key in ["RESTRICT", "COMMENT", "CONDITION"]:
                        value = str(eq_info[key]).strip()
                        if value != "":
                            try:
                                EQS[eq_info["NAME"]][key].append(value)
                            except KeyError:
                                # Initialize list if key doesn't exist
                                EQS[eq_info["NAME"]][key] = [value]
                        else:
                            try:
                                EQS[eq_info["NAME"]][key].append("")
                            except KeyError:
                                # Initialize list if key doesn't exist
                                EQS[eq_info["NAME"]][key] = [""]

            else:
                self.notify("Please select a row!", title="Notification")
        else:
            return
        
        return EQS 

    def fit_eqs(self, fit_all=True, build_only=False):
        # fit a single equation using bimets API

        self.setCursor(Qt.WaitCursor)

        EQS = self.get_selected_eqs(fit_all=fit_all)

        print("*EQS,\n", len(EQS))
        DATA_PATH = self.datafilePathLabel.text()

        DATA_ARGS = {k: 'df$`%s`' % k for k in self.data_model.raw_data["NAME"].unique(
        )} if self.data_model else {}
        print("DATA_ARGS", len(DATA_ARGS))

        from sfctools.api.bimets import BimetsModel
        FLOWS = []
        STOCKS = {}

        # exogenous variables
        EXO_ARGS_FIXED = {}
        try:
            for i, row in self.data_model.raw_data.iterrows():
                s, e = row["EXO_START"], row["EXO_END"]
                if s != "" and e != "":
                    try:
                        s, e = int(s), int(e)
                        # NOTE only annual frequency so far TODO extent this for tuples to enable sub-annual frequencies
                        EXO_ARGS_FIXED[row["NAME"]] = (s, 1, e, 1)
                    except Exception as e:
                        print("Exception: " + str(e))
            print("EXO ARGS ", len(EXO_ARGS_FIXED))
        except Exception as e:
            if not hasattr(self.data_model, "raw_data"):
                self.notify("No data found. Please check again.",
                            title="Error")
            elif self.data_model.raw_data is None:
                self.notify("No data found. Please check again.",
                            title="Error")
            else:
                self.notify(
                    "Something is possibly wrong with the data. Please check again.", title="Error")
            self.setCursor(Qt.ArrowCursor)
            return
        # extension of exogenous variables
        EXTEND_ARGS = {}
        for i, row in self.data_model.raw_data.iterrows():
            m, v = row["EXTMODE"], row["EXTVAL"]
            try:
                if v != "":
                    EXTEND_ARGS[row["NAME"]] = {
                        'EXTMODE': m, 'FACTOR': float(v)}
                else:
                    if m != "":
                        EXTEND_ARGS[row["NAME"]] = {'EXTMODE': m}
            except Exception as e:
                print("Exception: " + str(e))

        print("EXTEND_ARGS", len(EXTEND_ARGS))

        try:
            # TODO include FLOWS, STOCKS in future versions
            model = BimetsModel(EQS, DATA_ARGS,
                                EXTEND_ARGS, EXO_ARGS_FIXED,
                                verbose=True)
            model.read_data(DATA_PATH)

            self.bimets_model = model

        except Exception as e:
            self.notify(str(e), title="Error in Bimets model")
            self.setCursor(Qt.ArrowCursor)
            return

        ADD_FACT = {}

        # create a temporal directory
        temp_dir = "RModel/"
        try:
            temp_dir = self.lineEdit_R_model_dir.text()
        except:
            pass
        temporary = False

        if temp_dir == "":
            temp_dir = tempfile.mkdtemp()
            temporary = True

        try:
            ys_e, ye_e = self.SETTING_R_RANGE_FIT
            ys_s, ye_s = self.SETTING_R_RANGE_SIM

        except Exception as e:
            self.notify("please provide reasonable fitting and simulation ranges first. Error:\n%s" % str(
                e), title="Input Required")
            self.setCursor(Qt.ArrowCursor)
            return

        #try:
        # my_simtype = self.comboBoxSimType.currentText()
        # except:
        #     my_simtype = "FORECAST"
        #     self.notify("Could not find simtype, using 'FORECAST'\n%s" % str(e), title="Error.")

        my_simtype = self.SETTING_R_SimType

        try:

            # str(self.comboBoxAlgo.currentText())
            my_algo = self.SETTING_R_Algo
            df = model.gen_model(temp_dir,
                                 year_start_est=ys_e, year_end_est=ye_e,
                                 year_start_sim=ys_s, year_end_sim=ye_s, sim_type=my_simtype,
                                 constAdj=ADD_FACT,
                                 algo=my_algo)

            if build_only:
                self.setCursor(Qt.ArrowCursor)  # restore cursor
                self.statusBar().showMessage("Bimets model file successfully built!")
                return

            """print(df)
            print(model.model_txt)
            print("\n\n")
            print(model.dataprep_str)
            print("\n\n")
            print(model.write_out_str)
            print("\n\n")
            print(model.model_code_est)"""  # <- keep for debugging purposes

            # extract the generated code
            """
            Bimets folder
            |____ data
                |___ my_data1.xlsx
                |___ my_data2.xlsx
                |___ my_data3.xlsx

            |____ models
                |___ my_model1.txt
                |___ my_model2.txt  <- self.model_txt

            |____ data_prep.R < self.data_prep_str
            |____ write_output.R < self.write_out_str
            |____ bimets_model.R < self.model_code_est
            """

            R_PATH = "" 
            model.set_R_path(R_PATH)

            import io
            old_stdout = sys.stdout
            old_stderr = sys.stderr
            stdout_buffer = io.StringIO()
            stderr_buffer = io.StringIO()
            sys.stdout = stdout_buffer
            sys.stderr = stderr_buffer

            try:
                output_str = model.run()
            except Exception as e:
                output_str = ""
                print("Exception:", e)
                # self.notify(str(e), title="Exception when running model")

            finally:
                sys.stdout = old_stdout
                sys.stderr = old_stderr

            try:
                res = []  # = ""
                sel_model = self.EQtableView.selectionModel()
                rows = sel_model.selectedRows()
                for row in rows:
                    # print("fit", row)
                    eq_info = dict(self.eq_model.raw_data.loc[row.row(), :])

                    try:
                        df_summary = model.print_summary(eq_info["NAME"], pandas=True, latex=False,
                                                         fix_greeks=False, fix_underscore=False)  # .split(" "))

                        # res_str += df_summary.to_string() + "\n"
                        res += [df_summary]
                    except:
                        pass

                try:
                    summary_str = ""
                    if len(res) > 0:
                        summary_str = pd.concat(res, axis=0).replace(
                            np.nan, "").to_string()

                    # Get the output from the StringIO buffers
                    stdout_output = stdout_buffer.getvalue()
                    stderr_output = stderr_buffer.getvalue()

                    self.fitText.setPlainText(summary_str + "\n-------------------------------------------------------\n\n" + str(
                        output_str) + "\n\n" + str(stdout_output) + "\n\n" + str(stderr_output))
                    
                    # retrieve the output data from the temporary directory
                    df_out_path = os.path.join(temp_dir, "df_model.xlsx")
                    self.bimets_data = pd.read_excel(df_out_path)
                    

                except Exception as e:
                    self.notify("Error: %s\n Current path is %s" % (str(e), os.getcwd()), title="Error")

            except Exception as e:
                self.notify("Something went wrong. Check the equation definitions\n %s\n%s" % (
                    stderr_output, output_str), title="Error")
                self.setCursor(Qt.ArrowCursor)

        finally:
            if temporary:
                shutil.rmtree(temp_dir)  # clean up temporal directory ...

            # Close the StringIO objects
            try:
                stdout_buffer.close()
                stderr_buffer.close()
            except Exception as e:
                print("Error:", str(e))
                self.setCursor(Qt.ArrowCursor)

        # focus on results
        self.tabWidget3.setCurrentIndex(1)
        self.fitText.verticalScrollBar().setValue(
            int(0.8*self.fitText.verticalScrollBar().maximum()))  # scroll to bottom

        self.setCursor(Qt.ArrowCursor)  # restore cursor

    def convert_df_to_tex(self, df):

        # def wrapstring(s):
        #    # wraps string NOTE depricated
        #    return textwrap.wrap(s, width=10)

        def abbreviate_zeros(my_item):
            if my_item == "0" or my_item == 0:
                return " .- "  # .- "
            else:
                my_item = str(my_item)
                max_len = 10
                if len(my_item) > max_len:
                    return my_item[:max_len] + "..."
                else:
                    return my_item

        df_backup = df.copy()

        df_latex = df.to_latex(
            encoding="utf8", longtable=False, escape=False).replace("Δ", "$\Delta$")

        df_latex = df_latex.replace(
            "\\begin{tabular}", "\\begin{tabularx}{\\textwidth}")
        df_latex = df_latex.replace("\\end{tabular}", "\\end{tabularx}")
        df_latex = "\\begin{small}\n" + df_latex + "\n\end{small}"

        df_latex = df_latex.replace("\\left(", "(")
        df_latex = df_latex.replace("\\right)", ")")

        df_latex = df_latex.replace("{l", "{L{3cm}")

        df_latex = """
\\documentclass[a3paper,landscape]{article}
\\usepackage{booktabs}
\\usepackage{adjustbox}
\\usepackage[table]{xcolor}
\\usepackage{longtable}
\\usepackage{tabularx,ragged2e}
\\definecolor{Gray}{gray}{0.90}
\\definecolor{LightGray}{gray}{0.95}
\\definecolor{White}{rgb}{1,1,1}

\\newcolumntype{g}{>{\\columncolor{White}\\arraybackslash}L} %
\\newcolumntype{l}{>{\\columncolor{White}\\arraybackslash}p{3cm}} %centered "X" column
\\newcolumntype{L}[1]{>{\\raggedright\\arraybackslash}p{\\dimexpr#1-2\\tabcolsep-2\\arrayrulewidth+.21pt}}

\\usepackage[left=1cm,right=1.5cm]{geometry}

\\begin{document}\n""" + df_latex + "\n\\end{document}"

        pyperclip.copy(df_latex)
        # self.notify(message="LaTeX code copied to clipboard. Have fun!", title="Have fun")
        self.statusBar().showMessage(
            f"LaTeX code (length: {len(df_latex)}) copied to clipboard!")

        # -------------
        if False:  # DEPRICATED
            try:

                from pdflatex import PDFLaTeX
                path = os.path.dirname(os.path.abspath(__file__))
                my_file = os.path.join(path, "mymatrix")

                file = open(my_file, "w")
                file.write("""
        \\documentclass[a3paper,landscape]{article}
        \\usepackage{booktabs}
        \\usepackage{adjustbox}
        \\usepackage[table]{xcolor}
        \\usepackage{longtable}
        \\usepackage{tabularx,ragged2e}
        \\definecolor{Gray}{gray}{0.90}
        \\definecolor{LightGray}{gray}{0.95}
        \\definecolor{White}{rgb}{1,1,1}

        \\newcolumntype{g}{>{\\columncolor{White}\\arraybackslash}L} %
        \\newcolumntype{l}{>{\\columncolor{White}\\arraybackslash}p{3cm}} %centered "X" column
        \\newcolumntype{L}[1]{>{\\raggedright\\arraybackslash}p{\\dimexpr#1-2\\tabcolsep-2\\arrayrulewidth+.21pt}}

        \\usepackage[left=1cm,right=1.5cm]{geometry}

        \\begin{document}""")
                file.write(df_latex)
                file.write("""
        \\end{document}""")
                file.close()
                # pdfl = PDFLaTeX.from_texfile('my_texfile.tex')
                # pdf, log, completed_process = pdfl.create_pdf(keep_pdf_file=True, keep_log_file=True)
                # os.popen("pdflatex %s" % (os.getcwd()+"/my_texfile.tex"))
                import subprocess
                path = os.path.dirname(os.path.abspath(__file__))
                subprocess.run("pdflatex %s" % (os.path.join(
                    path, "mymatrix.tex"))).check_returncode()

                # os.startfile(os.getcwd() + "\\mymatrix.pdf")
                try:
                    # shutil.rmtree(os.getcwd()+"\\files")
                    # os.mkdir(os.getcwd()+"\\files")

                    path = os.path.dirname(os.path.abspath(__file__))
                    shutil.move(path+"\\mymatrix.tex", path +
                                "\\files\\mymatrix.tex")
                    shutil.move(path + "\\mymatrix.log",
                                path + "\\files\\mymatrix.log")
                    shutil.move(path + "\\mymatrix.aux",
                                path + "\\files\\\mymatrix.aux")
                    shutil.move(path + "\\mymatrix.pdf",
                                path + "\\files\\mymatrix.pdf")
                    # shutil.copy(os.getcwd() + "\\files\\mymatrix.html",os.getcwd() + "\\files\\mymatrix.html")
                    # shutil.copy(os.getcwd() + "\\files\\mystyle.css", os.getcwd() + "\\files\\mystyle.css")
                except Exception as e:
                    self.notify(message=str(
                        e), title="An Error occurred...(ID 1308)")

            except Exception as e:
                self.notify(message=str(
                    e), title="An Error occurred...(ID 1311)")
            # -------------

            pd.options.display.max_colwidth = 2

            df_display = df_backup.applymap(abbreviate_zeros).to_string(
                line_width=None).replace("\_", "_")

            # self.parent.parentApp.getForm("AGGR_FLOW").data_box.values = df_display.split("\n")
            # self.parent.parentApp.switchForm("AGGR_FLOW")

    def gen_code(self, show_notification=False, alt_fname="/python_code/transactions.py", ask_overwrite=False, add_cl_checks=None):
        """
        generates a transactions.py file, containing all relevant transactions as Python functions

        :param show_notifiaction: show notification at end?
        :param alt_fname: export to alternative file path 
        :param ask_overwrite: open a dialog if file already exists
        :param add_cl_checks: if False, the changelog check commands are skipped
        """

        # open dialog
        dialog = CheckBoxDialog(
            ["use SfcArray syntax", "add balance consistency checks"], self)
        result = dialog.exec_()

        checkbox_states = None
        # Check if the user clicked OK or Cancel
        if result == QDialog.Accepted:
            checkbox_states = dialog.get_checkbox_states()
            print("OK clicked")
            for key, value in checkbox_states.items():
                print(f"{key} state: {value}")

        else:
            print("Cancel clicked")
            return

        if checkbox_states is None:
            return
        # if add_cl_checks is None:
        #    add_cl_checks = self.checkBox_export_cl.isChecked()
        # USE_BAL_MOD = self.check_balmod.isChecked() # use mod for faster balance sheet logging (using attributes instead of the balance sheet structure)
        check_vals = list(checkbox_states.values())
        add_cl_checks = check_vals[1]
        USE_BAL_MOD = check_vals[0]
        # self.trans_check_vals = check_vals TODO somehow remember which boxes were checked for next time

        code = ""
        # code += "from typing import Type\n"

        code += "from __future__ import annotations\n"
        # ^NOTE this implements the new type annotation of PEP 563

        code += "from sfctools import Agent, FlowMatrix \nimport numpy as np\n"
        code += "from sfctools import BalanceEntry,Accounts\n"
        code += "from sfctools import CashFlowEntry\n"
        code += "from sfctools import ICSEntry\n"
        code += "from sfctools import Clock\n"
        code += "CA = Accounts.CA\nKA = Accounts.KA\n\n"

        subjects_count = defaultdict(lambda: 0.0)

        for filedata in self.entry_data:
            agent1 = filedata["agent1"]
            agent2 = filedata["agent2"]

            if agent1 == agent2:
                agent2 = "other_" + agent2

            agent3 = filedata.get("agent3") or None
            agent4 = filedata.get("agent4") or None

            a1 = filedata["a1"]  # + "\n"
            e1 = filedata["e1"]  # + "\n"
            l1 = filedata["l1"]  # + "\n"

            a2 = filedata["a2"]  # + "\n"
            e2 = filedata["e2"]  # + "\n"
            l2 = filedata["l2"]  # + "\n"

            a3 = filedata.get("a3") or ""  # + "\n"
            e3 = filedata.get("e3") or ""  # + "\n"
            l3 = filedata.get("l3") or ""  # + "\n"

            a4 = filedata.get("a4") or ""  # + "\n"
            e4 = filedata.get("e4") or ""  # + "\n"
            l4 = filedata.get("l4") or ""  # + "\n"

            flow_check = True
            # print("FLOW CHECK", filedata["shortname"],filedata["log transaction"])
            if isinstance(filedata["log transaction"], str) and filedata["log transaction"].strip() == "True":
                # bool(filedata["log transaction"])
                flow_check = True
                # print(flow_check)
            elif isinstance(filedata["log transaction"], str) and filedata["log transaction"].strip() == "False":
                # bool(filedata["log transaction"])
                flow_check = False
                # print(flow_check)
            else:
                flow_check = filedata["log transaction"]
                # print(flow_check)
            # print("FLOW CHECK", filename, flow_check)
            # print("type", type(flow_check))

            kind = filedata["kind"]

            # subject =  filedata["subject"].replace(" ","_") + "_" + filedata["shortname".replace(" ","_")].lower()
            subject = filedata["shortname".replace(" ", "_")].replace(
                "(", "_").replace(")", "").lower()
            subject_str = filedata["subject".replace(
                " ", "_")].lower().capitalize().replace("_", " ")

            print("export", subject_str, ", agents:",
                  agent1, agent2, agent3, agent4)

            subjects_count[subject] += 1
            if subjects_count[subject] > 1:  # suffix for same names
                subject += "_%i" % int(subjects_count[subject])

            try:
                quantity = filedata["quantity"]
            except:
                quantity = "q"

            commands = []

            if "uni-directional" in filedata:
                unidir = filedata["uni-directional"]

                commands.append(
                    "cond = (isinstance(%s, float) or isinstance(%s, int))" % (quantity, quantity))
                commands.append("if cond: assert not np.isnan(%s)" % quantity)
                # ^ will also pass for np.float64
                commands.append("if cond: assert %s < +np.inf" % (quantity))
                commands.append("if cond: assert %s > -np.inf" % (quantity))

                # print("uni-directional",unidir)
                if str(unidir) == "True":  # do not do bool(unidir)
                    # namaaa = filedata["shortname"]
                    # self.notify( (namaaa+ str(unidir)+ "="+(str(bool(unidir)))),title="check")
                    commands.append(
                        "if cond: assert %s >= 0, 'have to pass positive quantity: unidirectional transaction'" % quantity)

            if not USE_BAL_MOD:
                commands = commands + ["%s.balance_sheet.disengage()" % agent1]
                if True:  # agent1 != agent2:
                    commands += ["%s.balance_sheet.disengage()" % agent2]
                if agent3 is not None:
                    commands = commands + \
                        ["%s.balance_sheet.disengage()" % agent3]
                if agent4 is not None:
                    commands = commands + \
                        ["%s.balance_sheet.disengage()" % agent4]

            crossrefs = []

            def add_crossref(agent, sign, item, item2, silent, ref_agent, direction, other):
                be_dict = {"E": "BalanceEntry.EQUITY",
                           "A": "BalanceEntry.ASSETS", "L": "BalanceEntry.LIABILITIES"}

                # self.notify("crossref %s %s %s %s %s %s %s %s" % (agent, sign, item, item2, silent, ref_agent, direction, other),title="test")
                if direction == 0:  # log for this agent
                    be = be_dict[item2]
                    # item, agent, ref_agent, q, be = cr
                    crossrefs.append(
                        (item, agent, ref_agent, sign + quantity, be, direction))

                elif direction == 1:  # log for other agent
                    if other in be_dict:
                        be = be_dict[other]
                    else:
                        raise TypeError(
                            "Could not identify entry %s, should be A, L or E" % other)

                    # item, agent, ref_agent, q, be = cr
                    crossrefs.append(
                        (item, ref_agent, agent, sign + quantity, be, direction))

                elif direction == 2:  # log both
                    # self.notify("direction 2", title="2")

                    be = be_dict[item2]
                    crossrefs.append(
                        (item, agent, ref_agent, sign + quantity, be, direction))
                    # self.notify(str((item,agent,ref_agent,sign + quantity,be)), title="2")

                    if other in be_dict:
                        be = be_dict[other]
                    else:
                        raise TypeError(
                            "Could not identify entry %s, should be A, L or E" % other)
                    crossrefs.append(
                        (item, ref_agent, agent, sign + quantity, be, direction))
                    # self.notify(str((item,ref_agent,agent,sign + quantity,be)), title="2")

                else:
                    raise TypeError(
                        "Cannot interpret direction %s" % direction)

            ref_agents = []
            ref_items = []

            # Define the categories and their corresponding entry data
            categories = {
                "Equity": {"entries": [e1, e2, e3, e4], "balance_entry": "BalanceEntry.EQUITY", "symbol": "E"},
                "Assets": {"entries": [a1, a2, a3, a4], "balance_entry": "BalanceEntry.ASSETS", "symbol": "A"},
                "Liabilities": {"entries": [l1, l2, l3, l4], "balance_entry": "BalanceEntry.LIABILITIES", "symbol": "L"}
            }

            # Loop through each category and process the entries

            agent_mapping = {
                0: agent1,
                1: agent2,
                2: agent3,
                3: agent4
            }
            for category, data in categories.items():
                entries = data["entries"]
                balance_entry = data["balance_entry"]
                symbol = data["symbol"]

                # agent_index: 0 for agent1, 1 for agent2
                for agent_index, agent_entries in enumerate(entries):
                    # agent = agent1 if agent_index == 0 else agent2
                    agent = agent_mapping[agent_index]
                    if agent is None:
                        continue

                    for entry in agent_entries.split("\n"):
                        if entry.strip():  # Skip empty or newline-only entries
                            sign, item, silent, ref_agent, direction, other = self.entry_to_vals(
                                entry, agent, category)

                            if sign is not None:
                                if not USE_BAL_MOD:
                                    commands.append(
                                        f"{agent}.bal.change_item('{item}', {balance_entry}, {sign + quantity}, suppress_stock={silent})")
                                else:
                                    if direction == 0:
                                        commands.append(
                                            f"{agent}.{item}[{ref_agent}][t] {sign}= {quantity}")
                                    else:
                                        commands.append(
                                            f"{agent}.{item}[t] {sign}= {quantity}")

                                ref_items.append(item)

                            if ref_agent is not None:
                                add_crossref(agent, sign, item, symbol,
                                             silent, ref_agent, direction, other)
                                ref_agents.append(ref_agent)

            if not USE_BAL_MOD:
                commands.append("%s.balance_sheet.engage()" % agent1)
                if True:  # agent1!=agent2:
                    commands.append("%s.balance_sheet.engage()" % agent2)

                if agent3 is not None:
                    commands = commands + \
                        ["%s.balance_sheet.engage()" % agent3]
                if agent4 is not None:
                    commands = commands + \
                        ["%s.balance_sheet.engage()" % agent4]

            add_flow = flow_check
            if add_flow:
                # print(subject_str,"flow check",flow_check)
                kind_enum = "(%s,%s)" % (kind.split(
                    "->")[0].strip(), kind.split("->")[1].strip())
                commands.append("FlowMatrix().log_flow(%s, %s, %s, %s, subject='%s')" % (
                    kind_enum, quantity, agent1, agent2, subject_str))

            # cross references
            # self.notify("\n".join([str(ik) for ik in crossrefs]),title="ok")

            if not USE_BAL_MOD:
                for cr in crossrefs:
                    item, agent, ref_agent, q, be, direction = cr
                    commands.append("%s.balance_sheet.add_changelog(%s, '%s', %s, %s)" % (
                        agent, ref_agent, item, be, q))

            if not USE_BAL_MOD:
                # income statment
                my_income_dict = {  # translates string into ICSEntry (for better efficiency)
                    "Revenue": "ICSEntry.REVENUES",
                    "Non-Op. Income": "ICSEntry.NOI",
                    "Expense": "ICSEntry.EXPENSES",
                    "Tax": "ICSEntry.TAXES",
                    "Interest": "ICSEntry.INTEREST",
                    "Gain": "ICSEntry.GAINS",
                    "Nontax.Profit": "ICSEntry.NONTAX_PROFITS",
                    "Nontax. Profit": "ICSEntry.NONTAX_PROFITS",  # quick and dirty bugfix
                    "Nontax. Loss":  "ICSEntry.NONTAX_LOSSES",
                    "Loss":  "ICSEntry.LOSSES"
                }

                # income statement entries
                loss_entries = ["ICSEntry.EXPENSES",
                                "ICSEntry.LOSSES", "ICSEntry.NONTAX_LOSSES"]
                # Generalized handling for income and cashflow entries
                agents = [agent1, agent2, agent3, agent4]  # List of agents

                # Process income entries
                for i in range(1, 5):
                    income_key = f"income{i}"
                    if income_key in filedata and str(filedata[income_key]) != 'None':
                        income_type = my_income_dict[filedata[income_key]]
                        # Determine the agent dynamically
                        agent = agents[i - 1]
                        sign = "-" if income_type in ["ICSEntry.INTEREST",
                                                      "ICSEntry.TAXES", *loss_entries] else ""

                        commands.append(
                            f"{agent}.income_statement.new_entry({income_type},'{subject}',{sign}{quantity})"
                        )

                # Process cashflow entries
                for i in range(1, 5):
                    cashflow_key = f"cashflow{i}"
                    if cashflow_key in filedata and str(filedata[cashflow_key]) != 'None':
                        # Determine the agent dynamically
                        agent = agents[i - 1]
                        cashflow_type = f"CashFlowEntry.{filedata[cashflow_key].upper()}"
                        sign = "-" if i % 2 == 1 else ""  # Alternate sign for agent1 and agent3

                        commands.append(
                            f"{agent}.cash_flow_statement.new_entry({cashflow_type},'{subject}',{sign}{quantity})"
                        )
            # # print("----")
            # # print("ref agents", ref_agents)
            # # print("agent1, agent2", agent1, agent2)

            ref_agents = list(set(ref_agents))
            check_agents = list(ref_agents)

            for a in list(ref_agents):
                if "." in a:
                    ref_agents.remove(a)
                    check_agents.remove(a)

            str_refa = ""
            if agent1 in ref_agents:
                ref_agents.remove(agent1)
                # # print("found", agent1)
            if agent2 in ref_agents:
                ref_agents.remove(agent2)
                # # print("found", agent2)

            ref_agents = list(set(ref_agents))  # remove double entries
            ref_agents = sorted(ref_agents)
            # # print("---> ", ref_agents)
            # # print("----\n\n")

            if len(ref_agents) > 0:

                if not USE_BAL_MOD:
                    str_refa = ", " + \
                        ", ".join([r + ": Agent" for r in ref_agents])
                else:
                    str_refa = ", " + \
                        ", ".join([r + ": Agent | int" for r in ref_agents])

            if "add_args" in filedata:
                add_args = filedata["add_args"].strip()
                if add_args != "":
                    str_refa += "," + add_args

            # check consistency
            # ref_items.append(item)

            # print("REF_AGENTS", ref_agents)
            # print("CHECK AGENTS", check_agents)
            # print("REF_ITEMS", ref_items)

            changelog_checks = ""
            if not USE_BAL_MOD:
                done = []
                if add_cl_checks:
                    for item in ref_items:
                        for agent in check_agents:
                            if (agent, item) not in done:
                                done.append((agent, item))
                                changelog_checks += "\t%s.balance_sheet.check_consistency_changelog('%s')\n" % (
                                    agent, item)
                            if (agent1, item) not in done:
                                done.append((agent1, item))
                                changelog_checks += "\t%s.balance_sheet.check_consistency_changelog('%s')\n" % (
                                    agent1, item)
                            if (agent2, item) not in done:
                                done.append((agent2, item))
                                changelog_checks += "\t%s.balance_sheet.check_consistency_changelog('%s')\n" % (
                                    agent2, item)
                            if (agent3, item) not in done:
                                done.append((agent2, item))
                                changelog_checks += "\t%s.balance_sheet.check_consistency_changelog('%s')\n" % (
                                    agent3, item)
                            if (agent4, item) not in done:
                                done.append((agent4, item))
                                changelog_checks += "\t%s.balance_sheet.check_consistency_changelog('%s')\n" % (
                                    agent4, item)

            # method = "def %s(%s: Agent, %s: Agent, %s: float%s):\n" % (subject, agent1, agent2, quantity, str_refa)
            # Start with the mandatory parts of the method signature
            method = f"def {subject}("

            # Collect agent arguments
            agents = [f"{agent1}: Agent", f"{agent2}: Agent"]
            if agent3 is not None:
                agents.append(f"{agent3}: Agent")
            if agent4 is not None:
                agents.append(f"{agent4}: Agent")

            # Collect other arguments
            other_args = [f"{quantity}: float"]
            if str_refa:  # Assuming str_refa contains additional argument string
                if str_refa.strip() != "":
                    other_args.append(str_refa)

            # Join agents and other arguments, prioritizing agents first
            method += ", ".join(agents + other_args) + "):\n"
            method = method.replace(", ,", ", ")

            # method += "\n"  # + changelog_check

            if "description" in filedata:  # add the description in the docstring of the function definition
                method += "\t'''\n\t" + \
                    filedata["description"] + \
                    "\n\t'''\n\tt=Clock().get_time()\n"

            for command in commands:
                method += "\t" + command + "\n"  # "    " + command + "\n"

            # additional code
            if "add_code" in filedata:
                add_code = filedata["add_code"].split("\n")

                for command in add_code:
                    method += "\t" + command + "\n"  # "    " + command + "\n"

            method += changelog_checks
            method += "\n\n"

            code += method

        # statusBar().currentMessage())
        folder = os.path.dirname(self.current_file)
        fname = folder + alt_fname

        if ask_overwrite:
            if os.path.isfile(fname):
                yes = self.ask_question(
                    "File exists", "The file %s already exists. overwrite?" % fname)
                if not yes:
                    return
        try:
            with open(fname, "w", encoding="utf-8") as file:
                file.write(code)
        except Exception as e:
            self.notify("Something went wrong: %s.\nDid you specify a valid path?\nDid you save the project?" % str(
                e), title="Error!")

        if show_notification:
            self.notify(message="Code exported to %s" %
                        (fname), title="Success")

    def gen_ics_single(self):
        if self.drawcanvas.highlighted is None:
            self.notify(title="No agent selected",
                        message="No agent selected. Please select one first.")
            return
        if self.drawcanvas.highlighted.ishelper:
            self.notify(title="No agent selected",
                        message="No valid box selected. Please select an agent.")
            return
        my_agent = self.drawcanvas.highlighted.name

        combined_income = {"Subject": [], "Short Name": [], "Symbol": [
        ], "Type": [], "Ranking": []}  # ranking will be removed later
        """
        "Gains": [], "Losses": [], "Revenues": [], "Taxes": [], "Expenses": [],
         "Nontaxable Losses": [], "Nontaxable Profits": [],
                               "Non-Operational Income": [],
                                               "Interest Payments": []
                                               }
        """
        k = 0
        try:
            for filedata in self.entry_data:
                agent1 = filedata["agent1"].strip()
                agent2 = filedata["agent2"].strip()

                ref_agent = None
                ref_income = None

                agent_found = False
                # Iterate over the agents dynamically
                for idx, agent_key in enumerate(["agent1", "agent2", "agent3", "agent4"]):
                    try:
                        if filedata.get(agent_key, "").strip() == my_agent:
                            ref_agent = agent_key
                            ref_income = f"income{idx+1}"
                            print("ref_income", ref_income)
                            agent_found = True
                            break  # Exit the loop as the agent is found
                    except KeyError:
                        continue  # Skip if the agent key is missing

                # assets1 = filedata["a1"] + "\n"
                # liabs1 = filedata["l1"] + "\n" + filedata["e1"] + "\n"
                # assets2 = filedata["a2"] + "\n"
                # liabs2 = filedata["l2"] + "\n" + filedata["e2"] + "\n"

                if (agent_found) and str(filedata[ref_income]) != 'None':
                    mydict = {"Gain": "Gains",
                              "Loss": "Losses",
                              "Expense": "Expenses",
                              "Revenue": "Revenues",
                              "Tax": "Taxes",
                              "Nontax. Profit": "Nontaxable Profits",
                              "Nontax. Loss": "Nontaxable Losses",
                              "Non-Op. Income": "Non-Operational Income",
                              "Interest": "Interest Payments"}

                    try:
                        if agent1 == my_agent:
                            sign = "-"
                        else:  # if agent2 == my_agent:
                            sign = "+"

                        q = sympy.latex(sympy.simplify(
                            sign + filedata["quantity"]), mode="plain")
                        subject = filedata["subject"].replace(
                            "_", " ").capitalize()
                        # print("      found",q,subject)
                        tpe = mydict[filedata[ref_income]]
                        combined_income["Type"].append(tpe)
                        combined_income["Subject"].append(subject)
                        combined_income["Symbol"].append(str(q))
                        combined_income["Short Name"].append(
                            filedata["shortname"].strip())

                        if tpe in ["Gains", "Losses", "Expenses", "Revenues"]:
                            combined_income["Ranking"].append(0)
                        elif tpe in ["Non-Operational Income", "Interest Payments"]:
                            combined_income["Ranking"].append(1)
                        elif tpe in ["Taxes"]:
                            combined_income["Ranking"].append(2)
                        elif tpe in ["Nontaxable Profits", "Nontaxable Losses"]:
                            combined_income["Ranking"].append(3)
                        else:
                            combined_income["Ranking"].append(4)

                    except Exception as e:
                        self.notify(
                            "Cannot handle the symbolic expression %s" % sign + filedata["quantity"], title="Error")

        except Exception as e:
            self.notify(title="Error",
                        message="An error occurred.\n%s" % str(e))

        try:
            result = pd.DataFrame(combined_income).set_index("Subject")

            # sort after ranking
            result = result.sort_values('Ranking')

            # remove ranking
            result = result.drop(columns="Ranking")

            model = PandasModel(result)

            if my_agent not in IncomeViewer.instances:
                myview = IncomeViewer(self, name=my_agent)
            else:
                myview = IncomeViewer.instances[my_agent]
            # mview.setGeometry(100,500)
            myview.set_model(model)
            # mview.webView.setHtml(df_html)

            if not self.test_mode:
                myview.show()

        except Exception as e:
            self.notify(title="Error", message="An error occurred." + str(e))

    def gen_balance_single(self, show_window=True):

        if self.drawcanvas.highlighted is None:
            self.notify(title="No agent selected",
                        message="No agent selected. Please select one first.")
            return

        if self.drawcanvas.highlighted.ishelper:
            self.notify(title="No agent selected",
                        message="No valid box selected. Please select an agent.")
            return

        my_agent = self.drawcanvas.highlighted.name

        changeA = defaultdict(lambda: [])
        changeL = defaultdict(lambda: [])

        k = 0
        try:
            for dat in self.entry_data:
                # print("scaen")
                k += 1

                def process_line(x_a, x_l, x_e):
                    for line in dat[x_a].split("\n"):
                        if len(line.split(" ")) <= 1:
                            continue

                        try:
                            name = line.split(" ")[1]
                            sign = line.split(" ")[0]

                            value = sign+dat["quantity"]
                            changeA[name].append(value)
                        except Exception as e:
                            print(str(e))

                    for line in dat[x_l].split("\n") + dat[x_e].split("\n"):
                        if len(line.split(" ")) <= 1:
                            continue

                        try:
                            name = line.split(" ")[1]
                            sign = line.split(" ")[0]

                            value = sign+dat["quantity"]
                            changeL[name].append(value)
                        except Exception as e:
                            print(str(e))

                if dat["agent1"] == my_agent:
                    x_a = "a1"
                    x_l = "l1"
                    x_e = "e1"
                    process_line(x_a, x_l, x_e)

                if dat["agent2"] == my_agent:
                    x_a = "a2"
                    x_l = "l2"
                    x_e = "e2"
                    process_line(x_a, x_l, x_e)

                if "agent3" in dat and dat["agent3"] == my_agent:
                    x_a = "a3"
                    x_l = "l3"
                    x_e = "e3"
                    process_line(x_a, x_l, x_e)

                if "agent4" in dat and dat["agent4"] == my_agent:
                    x_a = "a4"
                    x_l = "l4"
                    x_e = "e4"
                    process_line(x_a, x_l, x_e)

        except Exception as e:
            self.notify(str(e), title="Error")
        
        changeA = dict(changeA)
        changeL = dict(changeL)
        
        combined_balance = {"Balance": [""],
                            "Assets": [""], "Liabilities": [""]}
        for k, v in changeA.items():
            combined_balance["Assets"][0] += k + ":\n" + "".join(v) + "\n\n"
        for k, v in changeL.items():
            combined_balance["Liabilities"][0] += k + \
                ":\n" + "".join(v) + "\n\n"
            
        if show_window:
            try:
                result = pd.DataFrame(combined_balance).set_index("Balance")
                model = PandasModel(result)

                if my_agent not in BalanceSheetViewer.instances:
                    myview = BalanceSheetViewer(self, name=my_agent)
                else:
                    myview = BalanceSheetViewer.instances[my_agent]
                # mview.setGeometry(100,500)
                myview.set_model(model)

                self.bsm_model = model
                
                # mview.webView.setHtml(df_html)
                if not self.test_mode:
                    myview.show()
            except Exception as e:
                self.notify(title="Error", message="An error occurred." + str(e))

        return combined_balance

    def gen_cashflow_single(self):
        if self.drawcanvas.highlighted is None:
            self.notify(title="No agent selected",
                        message="No agent selected. Please select one first.")
            return
        if self.drawcanvas.highlighted.ishelper:
            self.notify(title="No agent selected",
                        message="No valid box selected. Please select an agent.")
            return
        my_agent = self.drawcanvas.highlighted.name

        combined_income = {"Subject": [], "Short Name": [], "Symbol": [
        ], "Type": [], "Ranking": []}  # ranking will be removed later
        """
        """
        k = 0
        try:
            for filedata in self.entry_data:
                agent1 = filedata["agent1"].strip()
                agent2 = filedata["agent2"].strip()

                # # print("agent1",agent1,"agent2",agent2)
                if agent1 == my_agent:
                    ref_agent = "agent1"
                    ref_income = "cashflow1"

                elif agent2 == my_agent:
                    ref_agent = "agent2"
                    ref_income = "cashflow2"
                else:
                    pass

                # assets1 = filedata["a1"] + "\n"
                # liabs1 = filedata["l1"] + "\n" + filedata["e1"] + "\n"

                # assets2 = filedata["a2"] + "\n"
                # liabs2 = filedata["l2"] + "\n" + filedata["e2"] + "\n"

                if (agent1 == my_agent or agent2 == my_agent) and str(filedata[ref_income]) != 'None':
                    mydict = {"Gain": "Gains",
                              "Loss": "Losses",
                              "Expense": "Expenses",
                              "Revenue": "Revenues",
                              "Tax": "Taxes",
                              "Nontax. Profit": "Nontaxable Profits",
                              "Nontax. Loss": "Nontaxable Losses",
                              "Non-Op. Income": "Non-Operational Income",
                              "Interest": "Interest Payments"}

                    try:
                        if agent1 == my_agent:
                            sign = "-"
                        if agent2 == my_agent:
                            sign = "+"

                        q = sympy.latex(sympy.simplify(
                            sign + filedata["quantity"]), mode="plain")
                        subject = filedata["subject"].replace(
                            "_", " ").capitalize()
                        # # print("      found",q,subject)
                        tpe = filedata[ref_income]
                        combined_income["Type"].append(tpe)
                        combined_income["Subject"].append(subject)
                        combined_income["Symbol"].append(str(q))
                        combined_income["Short Name"].append(
                            filedata["shortname"].strip())
                        combined_income["Ranking"].append(0)
                        """
                        if tpe in ["Gains","Losses","Expenses","Revenues"]:
                            combined_income["Ranking"].append(0)
                        elif tpe in ["Non-Operational Income","Interest Payments"]:
                            combined_income["Ranking"].append(1)
                        elif tpe in ["Taxes"]:
                            combined_income["Ranking"].append(2)
                        elif tpe in ["Nontaxable Profits", "Nontaxable Losses"]:
                            combined_income["Ranking"].append(3)
                        else:
                            combined_income["Ranking"].append(4)
                        """

                    except Exception as e:
                        self.notify(
                            "Cannot handle the symbolic expression %s" % sign + filedata["quantity"] + str(e), title="Error")

        except Exception as e:
            self.notify(title="Error",
                        message="An error occurred.\n %s" % str(e))

        try:
            result = pd.DataFrame(combined_income).set_index("Subject")

            # sort after ranking
            result = result.sort_values('Ranking')

            # remove ranking
            result = result.drop(columns="Ranking")

            model = PandasModel(result)

            if my_agent not in CashFlowViewer.instances:
                myview = CashFlowViewer(self, name=my_agent)
            else:
                myview = CashFlowViewer.instances[my_agent]
            # mview.setGeometry(100,500)
            myview.set_model(model)
            # mview.webView.setHtml(df_html)
            if not self.test_mode:
                myview.show()

        except Exception as e:
            self.notify(title="Error", message="An error occurred." + str(e))

    def gen_tex_ics(self):
        """ 
        Generates latex version of income statement 

        """
        combined_sheets = {}
        """
        {
        agent_name: {
                    "Assets": {
                                asset_name : [entries],
                                ...},

                    "Liabilities and Equity": {
                                name : [entries],
                                ...},

                    }
        }
        """
        combined_income = defaultdict(lambda: {"Gains": [], "Losses": [], "Revenues": [], "Taxes": [], "Expenses": [],
                                               "Nontaxable Losses": [], "Nontaxable Profits": [],
                                               "Non-Operational Income": [],
                                               "Interest Payments": []
                                               })

        """
        {
        agent_name: {
                "Gains":  [entries],
                "Losses":[entries],
                "Expenditures":[entries],
                "Revenues": [entries],
                "Taxes":[entries]
        }
        """

        k = 0
        texstr = ""
        try:
            for filedata in self.entry_data:

                agent1 = filedata["agent1"]
                agent2 = filedata["agent2"]

                assets1 = filedata["a1"] + "\n"
                liabs1 = filedata["l1"] + "\n" + filedata["e1"] + "\n"

                assets2 = filedata["a2"] + "\n"
                liabs2 = filedata["l2"] + "\n" + filedata["e2"] + "\n"

                my_table = pd.DataFrame.from_dict(
                    {"Assets": assets1.split(
                        "\n"), "Liabilities and Equity": liabs1.split("\n")},
                    orient='index').T
                my_table2 = pd.DataFrame.from_dict(
                    {"Assets": assets2.split(
                        "\n"), "Liabilities and Equity": liabs2.split("\n")},
                    orient='index').T

                tex_table1 = my_table.to_latex(index_names=False, index=False)
                tex_table2 = my_table2.to_latex(index_names=False, index=False)

                # ===modify combined sheet

                #
                # AGENT 1
                #

                if agent1 not in combined_sheets:
                    combined_sheets[agent1] = {}

                if "Assets" not in combined_sheets[agent1]:
                    combined_sheets[agent1]["Assets"] = {}
                if "Liabilities and Equity" not in combined_sheets[agent1]:
                    combined_sheets[agent1]["Liabilities and Equity"] = {}

                for change in assets1.split("\n"):
                    try:
                        sign = change[0]  # first letter is sign + or -

                        name = change[1:].strip()
                        q = sympy.latex(sympy.simplify(
                            sign + filedata["quantity"]), mode="inline")
                        subject = "   (" + \
                            filedata["subject"].replace("_", " ") + ")"

                        # print("name", name)
                        # print("combined_sheets[agent1]", combined_sheets[agent1])
                        if name not in combined_sheets[agent1]["Assets"]:
                            combined_sheets[agent1]["Assets"][name] = ""

                        combined_sheets[agent1]["Assets"][name] += q + \
                            subject + "\n"
                    except:
                        pass
                for change in liabs1.split("\n"):
                    try:
                        sign = change[0]  # first letter is sign + or -

                        name = change[1:].strip()
                        try:
                            q = sympy.latex(sympy.simplify(
                                sign + filedata["quantity"]), mode="inline")
                        except Exception as e:
                            self.notify(
                                "Cannot handle the symbolic expression %s" % sign + filedata[
                                    "quantity"], title="Error")

                        subject = "   (" + \
                            filedata["subject"].replace("_", " ") + ")"

                        if name not in combined_sheets[agent1]["Liabilities and Equity"]:
                            combined_sheets[agent1]["Liabilities and Equity"][name] = ""

                        combined_sheets[agent1]["Liabilities and Equity"][name] += q + \
                            subject + "\n"
                    except:
                        pass

                if str(filedata["income1"]) != 'None':
                    mydict = {"Gain": "Gains",
                              "Loss": "Losses",
                              "Expense": "Expenses",
                              "Revenue": "Revenues",
                              "Tax": "Taxes",
                              "Nontax. Profit": "Nontaxable Profits",
                              "Nontax. Loss": "Nontaxable Losses",
                              "Non-Op. Income": "Non-Operational Income",
                              "Interest": "Interest Payments"}

                    try:
                        q = sympy.latex(sympy.simplify(
                            sign + filedata["quantity"]), mode="inline")
                        subject = "   (" + \
                            filedata["subject"].replace("_", " ") + ")"
                        combined_income[agent1][mydict[filedata["income1"]]].append(
                            (str(q), subject))

                    except Exception as e:
                        self.notify(
                            "Cannot handle the symbolic expression %s" % sign + filedata["quantity"], title="Error")

                #
                # AGENT 2
                #

                if agent2 not in combined_sheets:
                    combined_sheets[agent2] = {}

                if "Assets" not in combined_sheets[agent2]:
                    combined_sheets[agent2]["Assets"] = {}
                if "Liabilities and Equity" not in combined_sheets[agent2]:
                    combined_sheets[agent2]["Liabilities and Equity"] = {}

                for change in assets2.split("\n"):

                    try:
                        sign = change[0]  # first letter is sign + or -

                        name = change[1:].strip()
                        try:
                            q = sympy.latex(sympy.simplify(
                                sign + filedata["quantity"]), mode="inline")
                        except Exception as e:
                            self.notify(
                                "Cannot handle the symbolic expression %s" % sign + filedata[
                                    "quantity"], title="Error")

                        subject = "   (" + \
                            filedata["subject"].replace("_", " ") + ")"

                        if name not in combined_sheets[agent2]["Assets"]:
                            combined_sheets[agent2]["Assets"][name] = ""

                        combined_sheets[agent2]["Assets"][name] += q + \
                            subject + "\n"
                    except:
                        pass

                for change in liabs2.split("\n"):
                    try:
                        sign = change[0]  # first letter is sign + or -

                        name = change[1:].strip()
                        try:
                            q = sympy.latex(sympy.simplify(
                                sign + filedata["quantity"]), mode="inline")
                        except Exception as e:
                            self.notify(
                                "Cannot handle the symbolic expression %s" % sign + filedata[
                                    "quantity"], title="Error")

                        subject = "   (" + \
                            filedata["subject"].replace("_", " ") + ")"

                        if name not in combined_sheets[agent2]["Liabilities and Equity"]:
                            combined_sheets[agent2]["Liabilities and Equity"][name] = ""

                        combined_sheets[agent2]["Liabilities and Equity"][name] += q + \
                            subject + "\n"
                    except:
                        pass

                if str(filedata["income2"]) != 'None':
                    mydict = {"Gain": "Gains",
                              "Loss": "Losses",
                              "Expense": "Expenses",
                              "Revenue": "Revenues",
                              "Tax": "Taxes",
                              "Nontax. Profit": "Nontaxable Profits",
                              "Nontax. Loss": "Nontaxable Losses",
                              "Non-Op. Income": "Non-Operational Income",
                              "Interest": "Interest Payments"}
                    q = sympy.latex(sympy.simplify(
                        sign + filedata["quantity"]), mode="inline")
                    subject = "   (" + \
                        filedata["subject"].replace("_", " ") + ")"
                    combined_income[agent2][mydict[filedata["income2"]]].append(
                        (str(q), subject))

                    # ============================

                transfer_str = ""

                if convert_bool(filedata["log transaction"]):
                    transfer_str = "\nFlow:~" + filedata["kind"].strip().replace("->",
                                                                                 "$\\rightarrow$") + "&~\\\\" + "\n"
                    transfer_str += "Subject:~" + \
                        filedata["subject"].strip().title() + "&~\\\\" + "\n"
                    # transfer_str += "Quantity:~" + filedata["quantity"].strip() + "&~\\\\"+ "\n"

                table_str = """\\begin{tabular}{ll}

                %s & %s \\\\
                ~ & ~ \\\\
                """ % (filedata["agent1"], filedata["agent2"])
                table_str += tex_table1 + "&" + tex_table2 + "\\\\\\\\" + transfer_str

                table_str += "\n\n\\end{tabular}"
                texstr += """\\begin{table}[H]\n"""
                texstr += table_str
                texstr += """\caption{%s}\n\end{table}""" % (
                    filedata["subject"] + "(%s)" % filedata["shortname"])

            texstr = texstr.replace("_", " ")
            texstr = texstr.replace("None", "")

            # ============ add combined sheets ======================
            # # print(combined_sheets)

            # pd.DataFrame(combined_sheets).to_excel("test.xlsx") <- for manual testing

            # convert the combined sheets to tex
            texstr_combined = ""
            for agent, data in combined_sheets.items():
                agent_titled = agent.replace("_", " ").title()

                assets = data["Assets"]
                liabs = data["Liabilities and Equity"]

                mys = """
            \\begin{table}[H]
            \\centering
            """
                mys += """
            \\begin{tabular}{|l|l|}
            \hline
            ~&~\\\\
            \\textbf{Assets} & \\textbf{Liabilities and Equity}\\\\
            ~&~\\\\

                        """

                mys += """
            \\begin{tabular}{ll}
                        """

                for item, entry in assets.items():

                    mys += item + "&~ \\\\\hline~&~\\\\\n"

                    for sub_entry in entry.split("\n"):
                        try:
                            lt_subentry = sub_entry
                            mys += "~&" + lt_subentry + "\\\\" + "\n"
                        except:
                            pass

                mys += """
            \end{tabular} & \\begin{tabular}{ll}""" + "\n"

                for item, entry in liabs.items():
                    if item != "Equity":
                        mys += item + "&~ \\\\\hline~&~\\\\\n"

                        for sub_entry in entry.split("\n"):
                            try:
                                lt_subentry = sub_entry
                                mys += "~&" + lt_subentry + "\\\\" + "\n"
                            except:
                                pass

                try:
                    item = "Equity"
                    entry = liabs["Equity"]
                    mys += item + "&~ \\\\\hline~&~\\\\\n"

                    for sub_entry in entry.split("\n"):
                        try:
                            lt_subentry = sub_entry
                            mys += "~&" + lt_subentry + "\\\\" + "\n"
                        except:
                            pass
                except:
                    pass

                mys += """
            \\end{tabular}
            \\\\~&~
            \\\\\hline"""
                mys += """
            \\end{tabular}"""
                mys += """
            \\caption{Balance Sheet of %s}""" % agent_titled + """
            \\end{table}
            """
                texstr_combined += mys

                # =========== add income

                if agent in combined_income:
                    income_data = combined_income[agent]
                    income_str = """
            \\begin{table}[H]\n\centering\n
            \\begin{tabular}{|ll|}\n\hline"""

                    for k in "Revenues", "Gains", "Expenses", "Losses", \
                             "Interest Payments", "Non-Operational Income", "Taxes", "Nontaxable Profits", "Nontaxable Losses":
                        v = income_data[k]

                        income_str += "~&~\\\\"
                        income_str += "\t" + k + "&~\\\\\n"

                        for sub_value in v:
                            income_str += "\t" + "~&" + \
                                sub_value[0] + sub_value[1] + "\\\\\n"
                        if len(v) == 0:
                            income_str += "\t" + "~&" + ".-" + "\\\\\n"

                        if k != "Nontaxable Losses":
                            income_str += "~&~\\\\\hline\n"

                    income_str += """\n
            ~&~\\\\\hline\n\\end{tabular}"""
                    income_str += """\n
            \\caption{Income Statement of %s}""" % agent_titled + """
            \\end{table}"""

                    texstr_combined += income_str

            # =======================================================
            pyperclip.copy(texstr_combined)
            # self.notify(
            #    message="LaTeX code copied to clipboard. Have fun!", title="Have fun")
            self.statusBar().showMessage(
                f"LaTeX code (length: {len(texstr_combined)}) copied to clipboard!")

        except Exception as e:
            self.notify(message=str(e.__context__) + "\n" +
                        str(e), title=e.__class__.__name__)

    def add_helper(self, new_name=None):
        # adds an agent to the scene
        if new_name is None:
            new_name = self.HelperNameEdit.text()
        # TODO add safety mechanism to avoid overwriting an agent
        if new_name == "":
            self.notify("Please enter a valid name", title="IBM Error")
        elif not new_name[0].isupper():
            self.notify(
                "Please enter a valid name with\nuppercase first letter or camelcase, e.g. 'MyAgent'", title="IBM Error")
        else:
            self.drawcanvas.add_agent(new_name)

    def remove_helper(self):
        self.drawcanvas.remove_agent()

    def add_new(self):
        # add new transaction
        acc_dlg = AccountingDialog(self, data=None, myfont=self.font)
        acc_dlg.nameText.setText(self.HelperNameEdit.text())

    def goto_next(self):
        pass

    def udpate_display(self):
        # update transaction designer display
        try:
            filename = self.filename
            with open(filename, 'r') as stream:
                filedata = yaml.safe_load(stream)
                self.agent1Label.text = filedata["agent1"]
                self.agent2Label.text = filedata["agent2"]
                try:
                    my_dict = {
                        "None": 0,
                        "Revenue": 1,
                        "Expense": 2,
                        "Gain": 3,
                        "Loss": 4,
                        "Interest": 5,
                        "Non-Op. Income": 6,
                        "Tax": 7,
                        "Nontax. Profit": 8,
                        "Nontax. Loss": 9
                    }
                except:
                    pass
        except Exception as e:
            print(str(e))

def qt_msg_handler(mode, context, message):
    # only react to the specific painter warning
    if "QPainter::end: Painter ended with" in message:
        sys.stderr.write("\n=== QPainter saved-states leak detected ===\n")
        sys.stderr.write(message + "\n")
        sys.stderr.write("Python stack at warning time:\n")
        sys.stderr.write("".join(traceback.format_stack(limit=25)))
        sys.stderr.write("===========================================\n")
    # Always forward the message so Qt still prints it
    QtCore.qInstallMessageHandler(None)
    QtCore.qDebug(message)  # or just ignore; reinstall handler below
    QtCore.qInstallMessageHandler(qt_msg_handler)

QtCore.qInstallMessageHandler(qt_msg_handler)


def run_app():
    # runs the main application window

    # apply app name
    try:
        import ctypes
        myappid = 'DLR.sfctools.attune'  # arbitrary string
        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
    except:
        pass

    # create font
    try:
        myFont = QFont("Seoge UI", 11)
    except:
        pass

    

    app = QtWidgets.QApplication(sys.argv)
    try:
        QtWidgets.QApplication.instance().setFont(myFont)
    except:
        pass
    
    QtCore.qInstallMessageHandler(qt_msg_handler)


    # load splash image NOTE depricated
    # pixmap = QPixmap("./splash.png")
    # splash = QSplashScreen(pixmap)
    # splash.show()
    # app.setStyle('Fusion')

    # set main window
    window = SfcGUIMainWindow(myFont)
    try:
        window.setFont(myFont)
    except:
        pass
    # splash.finish(window)

    # setup stylesheet NOTE no longer used
    # apply_stylesheet(app, theme='dark_teal.xml', invert_secondary=True)
    # apply_stylesheet(window, theme='dark_teal.xml', invert_secondary=True)

    window.setWindowFlag(Qt.WindowMinimizeButtonHint, True)
    window.setWindowFlag(Qt.WindowMaximizeButtonHint, True)
    window.showMaximized()
    # window.show()
    window.switch_theme()
    window.switch_theme()

    app.exec_()


def camel(s):
    # ensures correct agent naming
    # s = sub(r"(_|-)+", " ", s).title().replace(" ", "")
    # s = sub(r"(_|-)+", " ", s).lower().replace(" ", "")
    if len(s) > 0:
        return ''.join([s[0].capitalize(), s[1:]])
    else:
        return s


def convert_bool(s):
    # convert string boolean to actual boolean
    # TODO check if this is necessary
    if s == "False":
        return False
    elif s == "True":
        return True
    return None


if __name__ == "__main__":

    run_app()
