__version__ = "0.9"
__author__ = "Thomas Baldauf"
__email__ = "thomas.baldauf@dlr.de"
__license__ = "MIT"
__birthdate__ = '15.11.2021'
__status__ = 'prod'  # options are: dev, test, prod

from ..core.settings import Settings
from ..core.world import World
from ..core.clock import Clock
from ..core.flow_matrix import FlowMatrix
from ..datastructs import HashedIdxList
import pandas as pd
import warnings
import numpy as np
from collections import defaultdict
from enum import Enum, IntEnum
import copy
import inspect
import itertools
from ..sfc_structs import SfcArray, Sfc2DArray, Sfc3DArray
from ..datastructs.autopickle import AutoPickle

from scipy.sparse import csr_matrix, lil_matrix
import matplotlib.patches as mpatches

# import fastenum


class BalanceEntry:  # (Enum):
    ASSETS = 0
    LIABILITIES = 1
    EQUITY = 2


class BankruptcyEvent(Enum):  # (fastenum.Enum):
    """
    bankruptcy cases
    """
    NEGATIVE_ASSETS = 0
    NEGATIVE_LIABILITIES = 1
    NEGATIVE_EQUITY = 2
    NEGATIVE_NET_WORTH = 3


class BalanceSheet():
    """
    This is the balance sheet. It contains entries for assets and liabilities.
    more general information can be found here https://www.investopedia.com/terms/b/balancesheet.asp for example.
    """

    _tolerance = 1e-6  # tolerance for engage()
    _tolerance_rel = 1e-4  # relative tolerance of deviation from total assets
    _bankruptcy_warnings = True  # print a warning when an agent went bankrupt?
    # (optional fast mode skips the consistency check)
    check_consistency = True

    @classmethod
    def set_indexing_mode(cls, mode):
        """
        set the indexing mode for agents' balance sheet entries. 
        either 'int' or 'str' 
        in 'int mode' agent[(1,0)] is used, 
        in 'str mode' agent[("somAsset", 0)] is used

        :param mode: 'str' or 'int'
        """
        if mode == "str":
            HashedIdxList.use_str_hashes = True

    @classmethod
    def set_bankruptcy_warnings(cls, value: bool):
        """
        Enable/disable the bankruptcy warnings
        """
        cls._bankruptcy_warnings = value

    @classmethod
    def change_tolerance(cls, tol: float, rel_tol: float):
        """
        Change the tolerance by which the balance is allowed to diverge
        This will set the new values globally. The tolerance is checked in a two-step process:
        Frist, the absolute tolerance violation is checked, i.e. if any balance sheet side sums up to more or less than the other side.
        If the absolute tolerance is violated, an error is thrown if also the relative tolerance is exceeded. The latter is the second step of the checking process.
        If the relative deviation is violated but the absolute level is not exceeded, no error wil be thrown.

        :param tol: new absolute tolerance
        :param rel_tol: relative tolerance (relative devaition from total assets)
        """

        assert tol > 0
        cls._tolerance = tol
        cls._tolerance_rel = rel_tol

    class Proxy:
        """
        Proxy class for balance sheet. It can be either engaged (consistent and 'active') or disengaged (inconsistent and 'disabled').
        The consistency is automatically checked inside the 'engage()' call when switched from disengaged to engaged.
        """

        def __init__(self, parent):
            self.parent = parent

        def __enter__(self):
            self.parent._engaged = False  # disengage()

        def __exit__(self, exc_type, exc_val, exc_tb):
            self.parent.engage()
            self.parent._engaged = True  # engage()

    class FastProxy:
        """
        Fast proxy (see 'Proxy') which does not do engage()
        """

        def __init__(self, parent):
            self.parent = parent

        def __enter__(self):
            self.parent._engaged = False  # disengage()

        def __exit__(self, exc_type, exc_val, exc_tb):
            self.parent._engaged = True  # engage()

    def __init__(self, owner, items=None):
        """
        Balance sheet constructor. Creates a new balance sheet

        :param owner: the owner agent of this balance sheet. Can be None for standalone purposes, but it is recommended to pass an Agent instance here.
        :param items: initial list of items in the balance sheet
        """
        self.owner = owner  # stores owner instance, if any

        if items is None:
            items = []

        # np.nan NOTE if nan is set this can lead to misbehavior in engage.
        default_value = 0.0
        sheet_data = [[default_value] * 3] + [[default_value] * 3 for k in range(1, 1 + len(
            items))]  # the actual data structure is a three-column table for assets, equity and liabilities
        data = [e for e in sheet_data]

        hashes = ["Total"] + items
        self._sheet = HashedIdxList(
            hashes, data, default_val=lambda: [default_value] * 3)

        self.items = items
        for item in items:
            FlowMatrix().add_stock(item)

        # self._sheet[BalanceEntry.TOTAL] = [default_value]*3 # the 'total' row at the bottom
        # print(self._sheet)

        # <- this can deactivate the consistency check temporarily for modification. Manual modification is not allowed (therefore private).
        self._engaged = True
        # <- this stores if the owner went bankrupt. manual modification should not be needed (therefore private).
        self._bankrupt = False
        # self.tolerance -> self.__class__.tolerance # numerical tolerance for consistency cross-check

        # link agents to specific entries in the balance sheet
        # nested by [BalanceEntry]->[ItemName]->[Agent]->[float Balance]
        self._changelog = defaultdict(lambda: defaultdict(
            lambda: defaultdict(lambda: np.nan)))

    def add_changelog(self, agent, name, which, x):
        """
        inserts a link between a specific balance sheet item to a
        for example, a bank might want to record which agent its deposits belong to...
        Data structure: [BalanceEntry]->[ItemName]->[Agent]->[float Balance]

        :param agent: agent instance to store here
        :param name: name of the item, e.g. 'Deposits'
        :param which: corresponding BalanceEntry
        :param x: quantity which is inserted, can be positive, zero or negative
        """

        # assert isintance(x,int) or isinstance(x,float), "wrong data type"
        y = self._changelog[which][name][agent]

        # track total changelog
        if np.isnan(y):
            # first time this is called? overwrite nan
            self._changelog[which][name][agent] = x
            self._changelog[which]["Tot."][agent] = x
        else:
            self._changelog[which][name][agent] = x + y  # after first time
            self._changelog[which]["Tot."][agent] = x + y

    def add_cl(self, agent, name, which, x):
        """
        inserts a link between a specific balance sheet item to a
        for example, a bank might want to record which agent its deposits belong to...
        Data structure: [BalanceEntry]->[ItemName]->[Agent]->[float Balance]

        :param agent: agent instance to store here
        :param name: name of the item, e.g. 'Deposits'
        :param which: corresponding BalanceEntry
        :param x: quantity which is inserted, can be positive, zero or negative
        """
        self.add_changelog(agent, name, which, x)

    @property
    def sheet(self):
        return self._sheet  # .toarray()

    @property
    def raw_data(self):
        """
        get the raw data of this data structure in dictionary format.
        This will create a copy of the original data, so the user won't operate on the data directly (which is forbidden)
        """

        if not World.use_sfc_array:

            # d = copy.deepcopy(self._sheet.data)
            raw = {}
            for h in self._sheet.hashes:
                if h != "Total":
                    raw[h] = {i: self._sheet[h][i] for i in [0, 1, 2]}
            # del d[BalanceEntry.TOTAL]
            return raw

        """
        If there are SfcArrays present, fetch the info from those only 
        and ignore the local balance sheet data
        """

        raw = {}
        entries = [BalanceEntry.ASSETS,
                   BalanceEntry.LIABILITIES, BalanceEntry.EQUITY]
        cls_name = self.owner.__class__.__name__
        agent_id = self.owner.sfc_id

        for entry in entries:
            data1d = SfcArray.sorted_instances[cls_name][entry]
            for data in data1d:
                if data.title not in raw:
                    raw[data.title] = {i: 0 for i in entries}
                raw[data.title][entry] += data[agent_id]

            data2d = Sfc2DArray.sorted_instances[cls_name][entry]
            for data in data2d:
                if data.title not in raw:
                    raw[data.title] = {i: 0 for i in entries}
                raw[data.title][entry] += float(data.fetch(cls_name)[agent_id])

            data3d = Sfc3DArray.sorted_instances[cls_name][entry]
            for data in data3d:
                if data.title not in raw:
                    raw[data.title] = {i: 0 for i in entries}
                raw[data.title][entry] += float(data.fetch(cls_name)
                                                [agent_id, Clock().get_time()])
        # print(self, "RAW")
        # print(raw)
        return raw

    def get_cl(self, name, which, agent=None):
        """
        returns the internally stored links to other agents made via balance sheet transactions

        :param agent: agent instance to store here. if None, all agents' entries will be returned
        :param name: name of the item, e.g. 'Deposits'
        :param which: corresponding BalanceEntry
        :return: float, current balance or dict of balances for agents
        """
        return self.get_last_changelog(name, which, agent)

    def get_last_changelog(self, name, which, agent=None):
        """
        returns the internally stored links to other agents made via balance sheet transactions

        :param agent: agent instance to store here. if None, all agents' entries will be returned
        :param name: name of the item, e.g. 'Deposits'
        :param which: corresponding BalanceEntry
        :return: float, current balance or dict of balances for agents
        """

        if agent is not None:
            return self._changelog[which][name][agent]
        return self._changelog[which][name]

    # def items(self):
    #    return self.raw_data.items()

    # def values(self):
    #    return self.raw_data.values()

    # def keys(self):
    #    return self.raw_data.keys()

    def __repr__(self):
        return "<Balance of %s>" % self.owner

    def __str__(self):
        return self.__repr__()

    def __getitem__(self, key):
        """
        Get an entry of the balance sheet
        :param key: str, name of the asset
        :return: a dict with ASSETS, EQUITY, LIABILITIES entries of this item in this particular agent. NOTE In most cases, at least 2 of the entries will be zero.
        """
        # print("--> get", key, str(self._sheet))
        return self._sheet[key[0]][key[1]]

    @property
    def modify(self):
        """Modification decorator. Temporarily disengages the data structure. This can be used in a with block:

        .. code-block:: python

            with balance_sheet_1.modify:
                with balance_sheet_2.modify:
                    balance_sheet_1.change_item(...)
                    balance_sheet_2.change_item(...)

        """
        if self.__class__.check_consistency:
            return self.Proxy(self)
        else:
            return self.FastProxy(self)

    @property
    def modify_fast(self):
        return self.FastProxy(self)

    # abbreviated syntax
    @property
    def mod(self):
        return self.modify

    @property
    def fmod(self):
        return self.modify_fast

    def restore_after_bankruptcy(self, verbose=False):
        # helper function to reset bankrupt flag to False again after bankruptcy has been fixed (e.g. replacement of agent by new fictitious entrant)
        if verbose:
            warnings.warn("%s restore after bankrupcy\n" %
                          self.to_string())  # TODO < test this

        self._bankrupt = False

    def disengage(self):
        """
        'unlocks' the balance sheet -> can be modified now without error
        """
        self._engaged = False

    def engage(self):
        """
        locks the balance sheet -> no longer can be modified
        performs also a cross-check if consistency is maintained
        """

        if not self.__class__.check_consistency:
            return

        eps = self.__class__._tolerance  # tolerance level for cross-check, 1e-6 by default

        # print("engage", self._sheet[0])
        a = self._sheet[0][0]
        l = self._sheet[0][1]
        e = self._sheet[0][2]

        # print(self._sheet)
        # print(self._sheet[0][0], self._sheet[0][1], self._sheet[0][2])

        if abs((a - l) - e) > self.__class__._tolerance:
            raise RuntimeError(
                "Balance sheet inconsistency above absolute tolerance. Balance sheet is corrupted after cross-check. Deviation: %.4f\n%s" % (
                    abs((a - l) - e), self.to_string()))

        if a > 1e-10:
            if abs((a - l) - e) / abs(a) > self.__class__._tolerance_rel:
                raise RuntimeError(
                    "Balance sheet inconsistency above relative tolerance. Balance sheet is corrupted after cross-check. Deviation: %.4f\n%s" % (
                        abs((a - l) - e) / abs(a), self.to_string()))

        elif l < -self.__class__._tolerance:
            if self.__class__._bankruptcy_warnings:
                warnings.warn("filed bankruptcy for %s" % self.owner)
            self.owner.file_bankruptcy(
                event=BankruptcyEvent.NEGATIVE_LIABILITIES)

        elif e < -self.__class__._tolerance:
            if self.__class__._bankruptcy_warnings:
                warnings.warn("filed bankruptcy for %s" % self.owner)
            self.owner.file_bankruptcy(event=BankruptcyEvent.NEGATIVE_EQUITY)

        n_item = len(self.items)

        for j, data in enumerate(self._sheet.data):
            if j == 0 or j >= n_item:
                continue
            entry = self.items[j]

            # if entry == BalanceEntry.TOTAL:
            #    continue
            # if entry != "Total"
            # print("entry", entry)
            a_i = data[0]  # assets
            l_i = data[1]  # liabilities
            e_i = data[2]  # equity

            eps = -self.__class__._tolerance

            conds = {a_i < eps: BankruptcyEvent.NEGATIVE_ASSETS,
                     l_i < eps: BankruptcyEvent.NEGATIVE_LIABILITIES,
                     e_i < eps: BankruptcyEvent.NEGATIVE_EQUITY}

            for cond, e in conds.items():

                if cond:
                    if not self._bankrupt:
                        self._bankrupt = True
                        if self.__class__._bankruptcy_warnings:
                            warnings.warn(
                                "filed bankruptcy for %s in entry %s" % (self.owner, entry))
                        self.owner.file_bankruptcy(event=e)

    def get_balance(self, key, kind):  # , kind=None):
        """
        Gets amount of a certain entry for certain key

        :param key: name of entry (e.g. 'Cash' or 'Apples')
        :param kind: preferrably BalanceEntry: Asset, Liabilities or Equity, optionally str 'Assets', 'Equity' or 'Liabilities'

        :return: float, value in balance sheet
        """

        return self._sheet[key][kind]

        """try:
              [...]
        except:
            if isinstance(kind, str):
                mydict = {
                    "Assets": AST,
                    "Equity": EQT,
                    "Net Worth": BalanceEntry.NET_WORTH,
                    "Liabilities": LIA,
                }
                kind = mydict[kind]
                return_value = self._sheet[key][kind.value]

            return return_value"""

    def to_string(self, nice_format=True):
        """
        Coverts the whole balance sheet to a pandas dataframe and subsequently to a string

        :param nice_format: True (default), use a fancy formatting in three boxes vs direct dataframe to string conversion
        :return: str, balance sheet string representation
        """
        if nice_format:

            def filter_empty(series):
                # try:
                indices = [str(v).strip() != ".-" for v in series]
                return series[indices]
                # except:
                #    return df

            # 1. retrieve strings from all 3 components
            df = self.to_dataframe()
            df.index.name = ""
            df_a = "ASSETS\n-------\n" + \
                filter_empty(df["Assets"]).to_string().replace("NaN", " ~ ")
            df_l = "LIABILITIES\n-------------\n" + \
                filter_empty(df["Liabilities"]).to_string().replace(
                    "NaN", " ~ ")
            df_e = "EQUITY/NET WORTH\n------------------\n" + filter_empty(
                df["Equity / Net Worth"]).to_string().replace("NaN", " ~ ")

            # 2. 'glue' together the string blocks
            df_el = df_l + "\n\n" + df_e

            lines_a = df_a.split("\n")
            lines_l = df_el.split("\n")

            max_len_a = np.max([len(i) for i in lines_a])
            max_len_l = np.max([len(i) for i in lines_l])

            final_str = "-" * (max_len_a + max_len_l + 5)
            final_str += "\n%s" % self + "\n\n"
            # final_str += "ASSETS" + " "*(max_len_a-2) +  "LIABILITIES\n"
            # final_str += "------" + " "*(max_len_a-2) +  "-----------\n"

            title_switch = False
            for i in range(max(len(lines_a), len(lines_l))):
                if i < len(lines_a):
                    final_str += lines_a[i] + " " * \
                        (max_len_a - len(lines_a[i]))

                else:
                    final_str += " " * (max_len_a)

                final_str += "  |  "

                if i < len(lines_l):
                    final_str += lines_l[i]

                final_str += "\n"

            final_str += "-" * (max_len_a + max_len_l + 5)

            return_str = "\n"
            final_lines = final_str.split("\n")
            maxlen = max([len(i) for i in final_lines])
            for line in final_lines:
                lstr = line
                return_str += "|" + lstr + " " * (maxlen - len(lstr)) + "|\n"
            return_str += "\n"
            return return_str

        else:
            return "\n\n" + self.to_dataframe().to_string().replace("NaN", " ~ ") + "\n\n"

    def change_item(self, name, which, value, relative=True, check_type=False, suppress_stock=False):
        """
        Chanes value of an item in the balance sheet.

        :param name: name of the asset / entry to change
        :param value: value is added to entry. can also be negative
        :param relative: log as relative change
        :param check_type: performs a type check on value (value is int or float?)
        :param which: BalanceEntry: Asset, Liability, Equity
        """

        if self._engaged:  # security lock when balance is still disengaged
            raise PermissionError(
                "Cannot change item in engaged balance sheet! This would lead to inconsistencies. Please disengage before calling change_item or use 'modify' proxy.")

        if check_type:
            type_cond = isinstance(value, int) or isinstance(value, float)

            if type_cond:
                if value == 0:  # security lock if this is a 'ghost transaction'
                    curframe = inspect.currentframe()
                    # try:
                    calframe = inspect.getouterframes(curframe, 2)[1]
                    calframe2 = inspect.getouterframes(curframe, 2)[2]
                    info = str(calframe.function) + ", line " + \
                        str(calframe.lineno) + " > " + str(calframe2.filename)
                    # except:
                    #    info = "unknown frame"

                    warnings.warn("""
    >> Blocked ghost item in balance (value=0): %s, %s, %s,
            Called %s in file %s, line %s,
            Function %s in file %s, line %s\n""" % (
                        self, name, which, str(calframe.function), str(
                            calframe.filename), str(calframe.lineno),
                        str(calframe2.function), str(calframe2.filename), str(calframe2.lineno)))
                    return

            if np.isnan(self._sheet[name][which]):
                warnings.warn("Found NaN value in %s, %s, %s" %
                              (self, name, which))

            if np.isnan(self[0][which]):
                warnings.warn("Found NaN value in %s, %s, %s" %
                              (self, "TOTAL", which))

        if relative:
            entry = self._sheet[name]
            entry[which] += value
            self._sheet[name] = entry

            entry = self._sheet.get_value_by_idx(0)  # [0]
            entry[which] += value
            self._sheet.set_value_by_idx(0, entry)

        else:
            entry = self._sheet[name]
            entry[which] = value
            self._sheet[name] = entry

            entry = self._sheet.get_value_by_idx(0)
            entry[which] = value
            self._sheet.set_value_by_idx(0, entry)

        if (not suppress_stock) and FlowMatrix.enabled:

            if which == 0:
                FlowMatrix().capital_flow_data["Δ %s" %
                                               name][self.owner] += -value  # -value

            elif which == 1:
                FlowMatrix().capital_flow_data["Δ %s" %
                                               name][self.owner] += value  # value

    def check_cl(self, item, balance_entry=None, verbose=False):
        # see check_consistency_changelog
        self.check_consistency_changelog(item, balance_entry, verbose)

    def print_changelog(self, item, balance_entry=None, return_str=False):

        if balance_entry is None:
            bes = [0, 1, 2]
        else:
            bes = [balance_entry]

        s0 = ""
        for be in bes:

            s = ""

            if len(self.get_last_changelog(item, be)) == 0:
                # s+= " (no data) "
                pass
            else:
                s = "\n----------------------------------------------\n"
                s += "Changelog of %s (%s, %s)\n" % (self, item, be)

                data = {"Agent": [], "Value": []}
                for agent, value in self.get_last_changelog(item, be).items():
                    data["Agent"].append(agent)
                    data["Value"].append(value)

                df = pd.DataFrame(data)
                s += df.to_string(index=False)

                s += "\n---------------------------------------------\n"

            s0 += s

        if s0 == "":
            s0 = "(empty changelog)"

        if not return_str:
            print(s0)
        else:
            return s0

    def check_consistency_changelog(self, item, balance_entry=None, verbose=False):
        """
        checks the changelog consistency with the aggregate balance sheet

        :param item: name of the item 
        :param balance_entry a BalanceEntry identifier
        :param verbose: print out the values (default False)

        """

        if self._bankrupt:
            if verbose:
                print("<cannot check consistency, is not bankrupt...>")
            return

        if self._engaged:
            if verbose:
                print("<cannot check consistency, is not engaged....>")
            return

        if balance_entry is None:
            bes = [0, 1, 2]
        else:
            bes = [balance_entry]

        for be in bes:

            balance = self.get_balance(item, be)
            changelog = 0
            count = 0

            if len(self.get_last_changelog(item, be)) == 0:
                continue

            for k, v in self.get_last_changelog(item, be).items():

                if verbose:
                    print("   ___> ", k, be, ">", v)

                changelog += v
                count += 1

            if count > 0:

                if verbose:
                    print("   deviation balance-changelog: ",
                          (balance - changelog))

                if changelog < 1e-9:
                    tol = self.__class__._tolerance
                    dev = abs(balance - changelog)
                else:
                    # tol = abs(0.01 * changelog)
                    tol = self.__class__._tolerance_rel
                    dev = abs(balance - changelog) / changelog

                if verbose:
                    print(item, be, "deviation", dev)

                if dev > tol:
                    s = ""
                    for be in bes:
                        s += self.print_changelog(item,
                                                  be, return_str=True) + "\n"
                    s += "\n" + self.to_string() + "\n"
                    raise RuntimeError(
                        "Deviation between changelog and balance in item %s (%s) of %s detected:\nDeviation: %.2f (Tolerance %.2f)\nBalance: %.2f\nChangelog: %.2f\n%s" % (
                            item, be, self, dev, tol, balance, changelog, s))
                    s += self.to_string() + "\n"

    """
    TODO (?) insert operations like 'Aktivtausch', 'Passivtausch', 'Bilanzverlängerung'(?)
    """

    def to_dataframe(self):
        """
        Create a dataframe from this data structure. Warning: this is computationally heavy and should not be used in loops!

        :return: pandas dataframe
        """

        df = pd.DataFrame({"Entry": [], "Assets": [], "Liabilities": [],
                           "Equity / Net Worth": []})  # construct an empty 'dummy' data frame
        entries = []  # allocate list of commodities entered in the balance sheet

        null_sym = "   .-   "  # symbol for zero entries

        # print(self._sheet)
        # iterate through the balance sheet items and 'fill the table'
        # print(self._sheet.data)
        # print("raw data", self.raw_data)
        # enumerate(self.raw_data.data):  # self._sheet.items():
        for j, row in self.raw_data.items():
            if j == 0:
                continue
            # print("j", j)
            # key = self.raw_data.keys()[j] # self._sheet.hashes[j]
            key = j

            if key == "Total":
                continue
            # if key != BalanceEntry.TOTAL: # consider all rows except the 'total' entry

            entries.append(key)  # add to entries list
            AST, LIA, EQT = 0, 1, 2
            new_row = pd.DataFrame({"Entry": [key],
                                    "Assets": [row[AST]],
                                    "Equity / Net Worth": [row[EQT]],
                                    "Liabilities": [row[LIA]]})  # construct a new row
            df = pd.concat([df, new_row.replace(0.0, null_sym)])

        df = df.sort_index()  # sort the entries by index

        # Compute 'Total' row
        # dummy for modifying factors.
        # NOTE is 1 always for now, but might change with exchange rates or any other conversion factors (?)
        factors = [1.0 for key in entries]
        my_row = pd.DataFrame({"Entry": ["Total"],
                               "Assets": [np.dot(df["Assets"].replace(null_sym, 0.0), factors)],
                               "Equity / Net Worth": [np.dot(df["Equity / Net Worth"].replace(null_sym, 0.0), factors)],
                               "Liabilities": [np.dot(df["Liabilities"].replace(null_sym, 0.0), factors)]})
        df = pd.concat([df, my_row])  # adds the 'Total' row

        df = df.set_index("Entry")  # sets index column correctly
        # gives the balance sheet an understandable title
        df.index.name = "BALANCE SHEET OF %s" % self.owner

        return df

    def depreciate(self):
        """
        (BETA) this will look up all the required depreciation rates in the
        settings and depreciate the balance sheet.
        Equity will become less, liabilities will stay.
        """

        settings = Settings()  # read depreciation rates from settings

        for item_name in self._sheet.keys():

            depr_rate = settings.config_data["params"][item_name]["depreciation"]
            # Settings entry is 'param->(item name)->depreciation->value'

            depr_q = self._sheet[item_name][EQT] * depr_rate
            # Warning: Depreciation is percentage of current stock. Non-linear!

            if not np.isnan(self._sheet[item_name][EQT]):
                self._sheet[item_name][EQT] -= depr_q

            if not np.isnan(self._sheet[item_name][AST]):
                self._sheet[item_name][AST] -= depr_q

        # TODO maybe automatically log this on income statement of the underlying agent here?

    @property
    def leverage(self) -> float:
        """
        computes the financial leverage value (debt-to-assets ratio)
        of this balance sheet. In this definition, it ranges between 0 and 1

        .. math::
            Leverage = Liabilities / Total Assets 

        :return: float, leverage
        """

        E = self.net_worth
        L = self.total_liabilities

        if E + L == 0:
            return np.inf

        # E = A - L
        # => E + L = A
        lev = L / (E + L)

        return max(0, lev)

    @property
    def net_worth(self) -> float:
        """net worth = sum of equity
        """
        return_val = self._sheet[0][2]  # "Equity"]
        if np.isnan(return_val):
            return 0.0
        return return_val

    @property
    def total_assets(self):
        """
        total assets = sum of assets column
        """
        return_val = self._sheet[0][0]  # "Assets"]
        if np.isnan(return_val):
            return 0.0
        return return_val

    @property
    def total_liabilities(self):
        """
        total liabilities = sum of all liabilities in liability column
        """
        return_val = self._sheet[0][1]  # "Liabilities"]
        if np.isnan(return_val):
            return 0.0
        return return_val

    def plot(self, show_labels=True, cols_assets=None, cols_liabs=None, cols_eq=None, show_legend=True,
             fname=None, ax=None, label_fmt="{0:.1f}"):
        """
        creates a matplotlib plot of the balance sheet

        :param show_labels: show labels (numbers) above the bars
        :param label_fmt: (default '{0:.1f}'), format of labels (if shown)
        :param show_legend: (True) show a legend with the balance sheet entries

        By default, assets are red, equity is blue and liabilites are green
        but custom colors can be given

        :param cols_assets: (None) list of mpl colors for assets. Default is ["salmon", "indianred", "rosybrown", "orangered", "red", "indianred"]
        :param cols_liabs: (None) list of mpl colors for liabilities. Default is [ "lightgreen", "honeydew","seagreen", "darkgreen", "darkslategray"]
        :param cols_eq: (None) list of mpl colors for equity. Default is  ["lightsteelblue", "lavender", "powderblue", "steelblue", "royalblue", "blue"]

        if you want to save to file instead of showing as plot window, specify a fname
        :param fname: if None (default) is given, plot will be shown directly. If fname is given, plot will be saved to a file


        if you want to redirect the plot to a certain axis, you can enter the axis via the parameter ax
        :param ax: axis to plot on. setting this parameter different from None will make 'fname' ineffective
        """
        import matplotlib.pyplot as plt
        import matplotlib.patches as mpatches
        from ..misc.mpl_plotting import matplotlib_barplot

        AST, LIA, EQT = 0, 1, 2

        if self.owner is not None:
            title = "%s" % self.owner
        else:
            title = ""

        colors = []

        if cols_liabs is None:
            greens = ["lightgreen", "honeydew",
                      "seagreen", "darkgreen", "darkslategray"]
        else:
            greens = cols_liabs

        if cols_eq is None:
            blues = ["lightsteelblue", "lavender",
                     "powderblue", "steelblue", "royalblue", "blue"]
        else:
            blues = cols_eq

        if cols_assets is None:
            reds = ["salmon", "indianred", "rosybrown",
                    "orangered", "red", "indianred"]
        else:
            reds = cols_assets

        greens.reverse()
        blues.reverse()
        reds.reverse()

        mycolors = {0: blues, 1: reds, 2: greens}
        counter = {0: 0, 1: 0, 2: 0}
        colors = []

        data = self.raw_data
        df = pd.DataFrame(data).T

        legend_patches = []
        for j, col in enumerate(df.columns):
            for idx, row in df.iterrows():
                colr = mycolors[j][counter[j] % len(mycolors[j])]
                if row[col] != 0:
                    colors.append(colr)
                    print(idx, "-->", j, "val",
                          row[col], counter[j] % len(mycolors[j]), colr)
                    legend_patches.append(
                        mpatches.Patch(color=colr, label=idx))
                    counter[j] += 1

        print(colors)

        df["Assets"] = df[AST]
        df["Liabilities & Equity"] = df[LIA] + df[EQT]
        df = df[["Assets", "Liabilities & Equity"]]
        print(df)

        fig = matplotlib_barplot(df.T, xlabel="", ylabel="", color=colors, title=title, stacked=True,
                                 show_labels=show_labels, fmt=label_fmt,
                                 legend="off", size=(4, 4), show=False, ax=ax)

        if ax is None:
            ax = plt.gca()

        legend_patches.reverse()
        ax.legend(handles=legend_patches,
                  bbox_to_anchor=(1.3, 1), framealpha=0.0)

        # plt.axis("off")
        ax.spines[['right', 'top', 'left']].set_visible(False)

        plt.gca().set_xticks([0, 1])
        plt.gca().set_xticklabels(["A", "L"])

        plt.tight_layout()

        if ax is None:
            if fname is None:
                plt.show()
            else:
                plt.savefig(fname)
                plt.close()
        return ax

    @classmethod
    def plot_list(cls, list_of_balance_sheets, dt=1, xlabel=None, ylabel=None, title=None, show_liabilities=True,
                  show=True,
                  neg_colors=None, pos_colors=None, figsize=(8, 4), color_mode="cycle"):
        """
        Plots a list of balance sheets (e.g. a collected temporal series)

        :param list_of_balance_sheets: a list of BalanceSheet instances
        :param dt: step, (how many values to skip in between)
        :param xlabel: x axis label
        :param ylabel: y axis label
        :param title: title of the figure
        :param show_liabilities: boolean switch to show passive side of balance sheet as downward-pointing bars (default True)
        :param show: show the plot in a new window? if False, only the matplotlib figure is returned
        :param neg_colors, pos_colors: you can optionally give a list of colors for plotting here
        :param figsize: figure size (default 8,4)
        :param color_mode: 'cycle' or 'dict'. In cycle mode, a list of colors should be provided, else a dict should be provided
        """
        import matplotlib.pyplot as plt
        import matplotlib.patches as mpatches
        from ..misc.mpl_plotting import matplotlib_barplot

        class MyCicle():
            # poor man's cycle datastruct
            def __init__(self, x):
                self.x = x
                self.i = -1

            def __next__(self):
                self.i += 1
                if self.i == len(self.x):
                    self.i = 0

                return self.get()

            def get(self):
                return self.x[self.i]

            def reset(self):
                self.i = -1

        # for data_i in list_of_balance_sheets:
        #    print(data_i)

        dfs = [pd.DataFrame(data_i) for data_i in list_of_balance_sheets]

        plt.figure(figsize=figsize)

        if xlabel is not None:
            plt.xlabel(xlabel)

        if ylabel is not None:
            plt.ylabel(ylabel)

        if title is not None:
            plt.title(title)

        maxy = 0.01  # np.-1inf
        miny = -0.01  # np.inf

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

        plt.grid(False)
        # plot upper half

        if isinstance(pos_colors, dict) and isinstance(neg_colors, dict):
            color_mode = "dict"

        if pos_colors is None:
            if color_mode == "cycle":
                colors_a = MyCicle(
                    ["maroon", "red", "lightcoral", "gold", "crimson", "orange"])  # plot colors

        else:
            colors_a = pos_colors

        if neg_colors is None:
            if color_mode:
                colors_l = MyCicle(
                    ["black", "gray", "navy", "dodgerblue", "royalblue", "slateblue"])  # plot colors

        else:
            colors_l = neg_colors

        if color_mode == "dict":
            colors_a = pos_colors
            colors_l = neg_colors

        if color_mode == "dict" and (colors_l is None or colors_a is None):
            raise RuntimeError(
                "In color mode 'dict', please provide a dictionary for the colors. Alternatively, use 'cycle' mode.")

        min_val = 0.0
        max_val = 0.0

        AST, LIA, EQT = 0, 1, 2

        legend_patches_a = []
        legend_patches_l = []

        for t, df in enumerate(dfs):

            vals_a = df.loc[AST]
            bottom = 0

            for idx, val in vals_a.items():

                if val != 0.0:
                    if color_mode == "dict":
                        mycolor = colors_a[idx]
                    else:
                        mycolor = next(colors_a)

                    plt.bar(t, vals_a[idx], color=mycolor, bottom=bottom)
                    bottom += vals_a[idx]

                    if t == 0:
                        legend_patches_a.append(
                            mpatches.Patch(color=mycolor, label=idx))
            if bottom > max_val:
                max_val = bottom

            if color_mode == "cycle":
                colors_a.reset()

            vals_l = df.loc[EQT] + df.loc[LIA]
            bottom = 0
            for idx, val in vals_l.items():

                if val != 0.0:
                    if color_mode == "dict":
                        mycolor = colors_l[idx]
                    else:
                        mycolor = next(colors_l)

                    plt.bar(t, -vals_l[idx], color=mycolor, bottom=bottom)
                    bottom += -vals_l[idx]

                    if t == 0:
                        legend_patches_l.append(
                            mpatches.Patch(color=mycolor, label=idx))

            if bottom < min_val:
                min_val = bottom

            if color_mode == "cycle":
                colors_l.reset()

        legend_patches_a.reverse()
        legend_patches = legend_patches_a + legend_patches_l
        plt.legend(handles=legend_patches, loc=(1.04, 0))

        plt.ylim([1.1 * min_val, 1.1 * max_val])
        plt.axhline(0.0, color="black")
        plt.xticks(rotation=90)

        # downward ticks
        # https://stackoverflow.com/questions/50571287/how-to-create-upside-down-bar-graphs-with-shared-x-axis-with-matplotlib-seabor

        ticks = plt.gca().get_yticks()
        myticks_abs = [(abs(tick)) for tick in ticks]
        myticks = [((tick)) for tick in ticks]
        plt.gca().set_yticks(myticks)
        plt.gca().set_yticklabels(myticks_abs)

        plt.tight_layout()

        if show:
            plt.show()

        return plt.gcf()
