import os
from time import time

import plotext_plus._utility as ut
from plotext_plus._date import DateClass
from plotext_plus._default import DefaultFigureClass
from plotext_plus._doc_utils import add
from plotext_plus._matrix import join_matrices
from plotext_plus._monitor import MonitorClass

# A figure is a general container of either a plot (called monitor) or another figure, when subplots are nested
# This creates a hierarchy of figures, where master is the main/initial global figure, and parent is the figure containing (or above) the one considered
# The active figure is the one that can be accessed with further plotext commands - like plot(), limitsize() etc ..
# If a figure has no sub figures, then it is used for plotting, otherwise its sub figures are checked


class _FigureClass:

    def __init__(self, master=None, parent=None):
        self._set_family(master, parent)  # it sets master, parent and active figure
        self.default = DefaultFigureClass()  # default values of figure class
        self.date = DateClass()

        self._set_size(None, None)  # no initial size for general figure
        self.max_or_min = max  # in a matrix of subplots the maximum height/width is considered (by default) for each row/column

        self.monitor = (
            MonitorClass() if self._is_master else self._parent.monitor.copy()
        )  # each figure has a monitor for plotting; which by default is deep copied from its parent figure monitor, so that a figure with multiple sub plots can have easily the same plot and plot settings and preferences (without rewriting code)
        self.monitor.set_date(
            self.date
        )  # to make sure that the date settings of a figure are the same for its subplots

        (
            self._set_master() if self._is_master else None
        )  # it sets the master figure size and other utilities

        self._set_slots_max(
            *self._master._size
        )  # sets the maximum number of sub figures in the current figure (from master figure size)
        self.subplots(
            0, 0
        )  # no sub figures added by default, so that the current figure is used for plotting

    def _set_family(self, master=None, parent=None):
        self._parent = (
            self if parent is None else parent
        )  # the figure just above this one
        self._master = self if master is None else master  # the figure above all others
        self._is_master = self is self._master
        self._active = (
            self if self._is_master else self._master._active
        )  # the active figure, such that further plotting or settings commands refer to it

    def _set_master(self):
        self._limit_size(
            True, True
        )  # limit size (only available for master, as sub figures are by default limited by parent figure)
        self._set_terminal_size(*ut.terminal_size())  # get and set terminal size
        self._set_master_size()  # set master size to terminal
        self._time = None  # computational time of show() method, only available for global figure
        self._dummy = _FigureClass(
            self._master, self._master
        )  # the master has a dumm container for subplots that do not actually exist (anymore due to change of size)
        self._master.monitor.set_size(self._size)
        self._dummy.monitor.set_size(self._size)
        self._set_interactive()  # if to make the final figure interactive: every command gets directly printed

    ##############################################
    ###########    Size Functions    #############
    ##############################################

    def _set_interactive(self, interactive=None):
        self._interactive = (
            self.default.interactive if interactive is None else bool(interactive)
        )

    def _set_size(self, width=None, height=None):
        self._width = None if width is None else int(width)
        self._height = None if height is None else int(height)
        self._size = [self._width, self._height]

    def _limit_size(self, width=None, height=None):
        self._limit_width = self.default.limit_width if width is None else bool(width)
        self._limit_height = (
            self.default.limit_height if height is None else bool(height)
        )
        self._limit = [self._limit_width, self._limit_height]

    def _set_terminal_size(self, width=None, height=None):
        self._width_term = self.default._width_term if width is None else width
        extra_lines = 2 if ut.is_ipython() else 1
        self._height_term = (
            self.default._height_term
            if height is None
            else max(height - extra_lines, 0)
        )
        self._size_term = [self._width_term, self._height_term]

    def _set_master_size(self):
        width = (
            self._width_term
            if self._width is None
            or (self._width > self._width_term and self._limit_width)
            else self._width
        )
        height = (
            self._height_term
            if self._height is None
            or (self._height > self._height_term and self._limit_height)
            else self._height
        )
        self._set_size(width, height)

    ##############################################
    #########    Subplots Functions    ###########
    ##############################################

    def _set_slots_max(self, width=None, height=None):
        self._rows_max = height  # (height + 1) // 3
        self._cols_max = width  # (width + 1) // 3
        self._slots_max = [self._rows_max, self._cols_max]

    def _set_slots(self, rows=None, cols=None):
        rows = 1 if rows is None else int(abs(rows))
        cols = 1 if cols is None else int(abs(cols))
        self._rows = min(rows, self._rows_max)
        self._cols = min(cols, self._cols_max)
        self._Rows = list(range(1, self._rows + 1))
        self._Cols = list(range(1, self._cols + 1))
        self._slots = [self._rows, self._cols]
        self._no_plots = 0 in self._slots  # or self._is_master

    def _set_subplots(self):
        self.subfig = [
            [_FigureClass(self._master, self) for col in self._Cols]
            for row in self._Rows
        ]

    def _get_subplot(self, row=None, col=None):
        return (
            self.subfig[row - 1][col - 1]
            if row in self._Rows and col in self._Cols
            else self._master._dummy
        )

    def subplot(self, row=None, col=None):
        row = 1 if row is None else int(abs(row))
        col = 1 if col is None else int(abs(col))
        active = self._get_subplot(row, col)
        self._active = active
        self._master._active = active
        return self._master._active

    def subplots(self, rows=None, cols=None):
        """Configure the figure for multiple subplots in a grid layout.

        Args:
            rows (int, optional): Number of subplot rows. Defaults to None.
            cols (int, optional): Number of subplot columns. Defaults to None.

        Returns:
            Figure: Self for method chaining.
        """
        self._set_slots(rows, cols)
        self._set_subplots()
        return self

    ##############################################
    #######    External Set Functions    #########
    ##############################################

    def title(self, label=None):
        (
            self.monitor.set_title(label)
            if self._no_plots
            else [
                [self._get_subplot(row, col).title(label) for col in self._Cols]
                for row in self._Rows
            ]
        )

    def xlabel(self, label=None, xside=None):
        (
            self.monitor.set_xlabel(label=label, xside=xside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).xlabel(label=label, xside=xside)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def ylabel(self, label=None, yside=None):
        (
            self.monitor.set_ylabel(label=label, yside=yside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).ylabel(label=label, yside=yside)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def xlim(self, left=None, right=None, xside=None):
        (
            self.monitor.set_xlim(left=left, right=right, xside=xside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).xlim(
                        left=left, right=right, xside=xside
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def ylim(self, lower=None, upper=None, yside=None):
        (
            self.monitor.set_ylim(lower=lower, upper=upper, yside=yside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).ylim(
                        lower=lower, upper=upper, yside=yside
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def xscale(self, scale=None, xside=None):
        (
            self.monitor.set_xscale(scale=scale, xside=xside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).xscale(scale=scale, xside=xside)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def yscale(self, scale=None, yside=None):
        (
            self.monitor.set_yscale(scale=scale, yside=yside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).yscale(scale=scale, yside=yside)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def xticks(self, ticks=None, labels=None, xside=None):
        (
            self.monitor.set_xticks(ticks=ticks, labels=labels, xside=xside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).xticks(
                        ticks=ticks, labels=labels, xside=xside
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def yticks(self, ticks=None, labels=None, yside=None):
        (
            self.monitor.set_yticks(ticks=ticks, labels=labels, yside=yside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).yticks(
                        ticks=ticks, labels=labels, yside=yside
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def xfrequency(self, frequency=None, xside=None):
        (
            self.monitor.set_xfrequency(frequency=frequency, xside=xside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).xfrequency(
                        frequency=frequency, xside=xside
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def yfrequency(self, frequency=None, yside=None):
        (
            self.monitor.set_yfrequency(frequency=frequency, yside=yside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).yfrequency(
                        frequency=frequency, yside=yside
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def xreverse(self, reverse=None, xside=None):
        (
            self.monitor.set_xreverse(reverse=reverse, xside=xside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).xreverse(reverse=reverse, xside=xside)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def yreverse(self, reverse=None, yside=None):
        (
            self.monitor.set_yreverse(reverse=reverse, yside=yside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).yreverse(reverse=reverse, yside=yside)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def xaxes(self, lower=None, upper=None):
        (
            self.monitor.set_xaxes(lower=lower, upper=upper)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).xaxes(lower=lower, upper=upper)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def yaxes(self, left=None, right=None):
        (
            self.monitor.set_yaxes(left=left, right=right)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).yaxes(left=left, right=right)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def frame(self, frame=None):
        (
            self.monitor.set_frame(frame=frame)
            if self._no_plots
            else [
                [self._get_subplot(row, col).frame(frame=frame) for col in self._Cols]
                for row in self._Rows
            ]
        )

    def grid(self, horizontal=None, vertical=None):
        (
            self.monitor.set_grid(horizontal=horizontal, vertical=vertical)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).grid(
                        horizontal=horizontal, vertical=vertical
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def canvas_color(self, color=None):
        (
            self.monitor.set_canvas_color(color)
            if self._no_plots
            else [
                [self._get_subplot(row, col).canvas_color(color) for col in self._Cols]
                for row in self._Rows
            ]
        )

    def axes_color(self, color=None):
        (
            self.monitor.set_axes_color(color)
            if self._no_plots
            else [
                [self._get_subplot(row, col).axes_color(color) for col in self._Cols]
                for row in self._Rows
            ]
        )

    def ticks_color(self, color=None):
        (
            self.monitor.set_ticks_color(color)
            if self._no_plots
            else [
                [self._get_subplot(row, col).ticks_color(color) for col in self._Cols]
                for row in self._Rows
            ]
        )

    def ticks_style(self, style=None):
        (
            self.monitor.set_ticks_style(style)
            if self._no_plots
            else [
                [self._get_subplot(row, col).ticks_style(style) for col in self._Cols]
                for row in self._Rows
            ]
        )

    def theme(self, theme=None):
        (
            self.monitor.set_theme(theme)
            if self._no_plots
            else [
                [self._get_subplot(row, col).theme(theme) for col in self._Cols]
                for row in self._Rows
            ]
        )

    ##############################################
    ###########    Clear Functions    ############
    ###########################x##################

    def clear_figure(self):
        self.__init__()  # if self._no_plots else [[self._get_subplot(row, col).clear_figure() for col in self._Cols] for row in self._Rows]

    clf = clear_figure

    def clear_data(self):
        (
            self.monitor.data_init()
            if self._no_plots
            else [
                [self._get_subplot(row, col).clear_data() for col in self._Cols]
                for row in self._Rows
            ]
        )

    cld = clear_data

    def clear_color(self):
        (
            self.monitor.clear_color()
            if self._no_plots
            else [
                [self._get_subplot(row, col).clear_color() for col in self._Cols]
                for row in self._Rows
            ]
        )

    clc = clear_color

    def clear_terminal(self, lines=None):
        ut.clear_terminal(lines=lines)

    clt = clear_terminal

    ##############################################
    ###########    Plot Functions    #############
    ##############################################

    def _draw(self, *args, **kwargs):
        (
            self.monitor.draw(*args, **kwargs)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col)._draw(*args, **kwargs)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def scatter(
        self,
        *args,
        marker=None,
        color=None,
        style=None,
        fillx=None,
        filly=None,
        xside=None,
        yside=None,
        label=None,
    ):
        self._draw(
            *args,
            xside=xside,
            yside=yside,
            lines=False,
            marker=marker,
            color=color,
            style=style,
            fillx=fillx,
            filly=filly,
            label=label,
        )

    def plot(
        self,
        *args,
        marker=None,
        color=None,
        style=None,
        fillx=None,
        filly=None,
        xside=None,
        yside=None,
        label=None,
    ):
        self._draw(
            *args,
            xside=xside,
            yside=yside,
            lines=True,
            marker=marker,
            color=color,
            fillx=fillx,
            filly=filly,
            label=label,
        )

    def bar(
        self,
        *args,
        marker=None,
        color=None,
        fill=None,
        width=None,
        orientation=None,
        minimum=None,
        reset_ticks=None,
        xside=None,
        yside=None,
        label=None,
    ):
        (
            self.monitor.draw_bar(
                *args,
                xside=xside,
                yside=yside,
                marker=marker,
                color=color,
                fill=fill,
                width=width,
                orientation=orientation,
                label=label,
                minimum=minimum,
                reset_ticks=reset_ticks,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).bar(
                        *args,
                        xside=xside,
                        yside=yside,
                        marker=marker,
                        color=color,
                        fill=fill,
                        width=width,
                        orientation=orientation,
                        label=label,
                        minimum=minimum,
                        reset_ticks=reset_ticks,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def multiple_bar(
        self,
        *args,
        marker=None,
        color=None,
        fill=None,
        width=None,
        orientation=None,
        minimum=None,
        reset_ticks=None,
        xside=None,
        yside=None,
        labels=None,
    ):
        (
            self.monitor.draw_multiple_bar(
                *args,
                xside=xside,
                yside=yside,
                marker=marker,
                color=color,
                fill=fill,
                width=width,
                orientation=orientation,
                labels=labels,
                minimum=minimum,
                reset_ticks=reset_ticks,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).multiple_bar(
                        *args,
                        xside=xside,
                        yside=yside,
                        marker=marker,
                        color=color,
                        fill=fill,
                        width=width,
                        orientation=orientation,
                        labels=labels,
                        minimum=minimum,
                        reset_ticks=reset_ticks,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def stacked_bar(
        self,
        *args,
        marker=None,
        color=None,
        fill=None,
        width=None,
        orientation=None,
        minimum=None,
        reset_ticks=None,
        xside=None,
        yside=None,
        labels=None,
    ):
        (
            self.monitor.draw_stacked_bar(
                *args,
                xside=xside,
                yside=yside,
                marker=marker,
                color=color,
                fill=fill,
                width=width,
                orientation=orientation,
                labels=labels,
                minimum=minimum,
                reset_ticks=reset_ticks,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).stacked_bar(
                        *args,
                        xside=xside,
                        yside=yside,
                        marker=marker,
                        color=color,
                        fill=fill,
                        width=width,
                        orientation=orientation,
                        labels=labels,
                        minimum=minimum,
                        reset_ticks=reset_ticks,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def hist(
        self,
        data,
        bins=None,
        marker=None,
        color=None,
        fill=None,
        norm=None,
        width=None,
        orientation=None,
        minimum=None,
        xside=None,
        yside=None,
        label=None,
    ):
        (
            self.monitor.draw_hist(
                data,
                bins=bins,
                norm=norm,
                xside=xside,
                yside=yside,
                marker=marker,
                color=color,
                fill=fill,
                width=width,
                orientation=orientation,
                label=label,
                minimum=minimum,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).hist(
                        data,
                        bins=bins,
                        norm=norm,
                        xside=xside,
                        yside=yside,
                        marker=marker,
                        color=color,
                        fill=fill,
                        width=width,
                        orientation=orientation,
                        label=label,
                        minimum=minimum,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def candlestick(
        self,
        dates,
        data,
        colors=None,
        orientation=None,
        xside=None,
        yside=None,
        label=None,
    ):
        (
            self.monitor.draw_candlestick(
                dates,
                data,
                xside=xside,
                yside=yside,
                orientation=orientation,
                colors=colors,
                label=label,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).candlestick(
                        dates, data, orientation=orientation, colors=colors, label=label
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def box(
        self,
        *args,
        quintuples=None,
        colors=None,
        fill=None,
        width=None,
        orientation=None,
        minimum=None,
        reset_ticks=None,
        xside=None,
        yside=None,
        label=None,
    ):
        (
            self.monitor.draw_box(
                *args,
                xside=xside,
                yside=yside,
                orientation=orientation,
                colors=colors,
                label=label,
                fill=fill,
                width=width,
                minimum=minimum,
                reset_ticks=reset_ticks,
                quintuples=quintuples,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).box(
                        *args,
                        orientation=orientation,
                        colors=colors,
                        label=label,
                        fill=fill,
                        width=width,
                        minimum=minimum,
                        reset_ticks=reset_ticks,
                        quintuples=quintuples,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    ##############################################
    ###########    Plotting Tools    #############
    ##############################################

    def error(
        self,
        *args,
        xerr=None,
        yerr=None,
        color=None,
        xside=None,
        yside=None,
        label=None,
    ):
        (
            self.monitor.draw_error(
                *args,
                xerr=xerr,
                yerr=yerr,
                xside=xside,
                yside=yside,
                color=color,
                label=label,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).error(
                        *args,
                        xerr=xerr,
                        yerr=yerr,
                        xside=xside,
                        yside=yside,
                        color=color,
                        label=label,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def event_plot(self, data, marker=None, color=None, orientation=None, side=None):
        (
            self.monitor.draw_event_plot(
                data, orientation=orientation, marker=marker, color=color, side=side
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).event_plot(
                        data,
                        orientation=orientation,
                        marker=marker,
                        color=color,
                        side=side,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    eventplot = event_plot

    def vertical_line(self, coordinate, color=None, xside=None):
        (
            self.monitor.draw_vertical_line(coordinate, color=color, xside=xside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).vertical_line(
                        coordinate, color=color, xside=xside
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    vline = vertical_line

    def horizontal_line(self, coordinate, color=None, yside=None):
        (
            self.monitor.draw_horizontal_line(coordinate, color=color, yside=yside)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).horizontal_line(
                        coordinate, color=color, yside=yside
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    hline = horizontal_line

    def text(
        self,
        label,
        x,
        y,
        color=None,
        background=None,
        style=None,
        orientation=None,
        alignment=None,
        xside=None,
        yside=None,
    ):
        (
            self.monitor.draw_text(
                label,
                x,
                y,
                xside=xside,
                yside=yside,
                color=color,
                background=background,
                style=style,
                orientation=orientation,
                alignment=alignment,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).text(
                        label,
                        x,
                        y,
                        xside=xside,
                        yside=yside,
                        color=color,
                        background=background,
                        style=style,
                        orientation=orientation,
                        alignment=alignment,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def rectangle(
        self,
        x=None,
        y=None,
        marker=None,
        color=None,
        lines=None,
        fill=None,
        xside=None,
        yside=None,
        label=None,
    ):
        (
            self.monitor.draw_rectangle(
                x=x,
                y=y,
                xside=xside,
                yside=yside,
                lines=lines,
                marker=marker,
                color=color,
                fill=fill,
                label=label,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).rectangle(
                        x=x,
                        y=y,
                        xside=xside,
                        yside=yside,
                        lines=lines,
                        marker=marker,
                        color=color,
                        fill=fill,
                        label=label,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def polygon(
        self,
        x=None,
        y=None,
        radius=None,
        sides=None,
        marker=None,
        color=None,
        lines=None,
        fill=None,
        xside=None,
        yside=None,
        label=None,
    ):
        (
            self.monitor.draw_polygon(
                x=x,
                y=y,
                radius=radius,
                sides=sides,
                xside=xside,
                yside=yside,
                lines=lines,
                marker=marker,
                color=color,
                fill=fill,
                label=label,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).polygon(
                        x=x,
                        y=y,
                        radius=radius,
                        sides=sides,
                        xside=xside,
                        yside=yside,
                        lines=lines,
                        marker=marker,
                        color=color,
                        fill=fill,
                        label=label,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def confusion_matrix(self, actual, predicted, color=None, style=None, labels=None):
        (
            self.monitor.draw_confusion_matrix(
                actual, predicted, labels=labels, color=color, style=style
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).confusion_matrix(
                        actual, predicted, labels=labels, color=color, style=style
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    cmatrix = confusion_matrix

    def indicator(self, value, label=None, color=None, style=None):
        (
            self.monitor.draw_indicator(value, label=label, color=color, style=style)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).confusion_matrix(
                        value, label=label, color=color, style=style
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    ##############################################
    ##############    2D Plots    ################
    ##############################################

    def matrix_plot(self, matrix, marker=None, style=None, fast=False):
        (
            self.monitor.draw_matrix(matrix, marker=marker, style=style, fast=fast)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).matrix_plot(
                        matrix, marker=marker, style=style, fast=fast
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def pie(
        self,
        labels,
        values,
        colors=None,
        radius=None,
        show_values=True,
        show_percentages=True,
        title=None,
        show_values_on_slices=False,
        donut=False,
        remaining_color=None,
    ):
        (
            self.monitor.draw_pie(
                labels,
                values,
                colors=colors,
                radius=radius,
                show_values=show_values,
                show_percentages=show_percentages,
                title=title,
                show_values_on_slices=show_values_on_slices,
                donut=donut,
                remaining_color=remaining_color,
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).pie(
                        labels,
                        values,
                        colors=colors,
                        radius=radius,
                        show_values=show_values,
                        show_percentages=show_percentages,
                        title=title,
                        show_values_on_slices=show_values_on_slices,
                        donut=donut,
                        remaining_color=remaining_color,
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def heatmap(self, dataframe, color=None, style=None):
        (
            self.monitor.draw_heatmap(dataframe, color=color, style=style)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).heatmap(
                        dataframe, color=color, style=style
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def image_plot(self, path, marker=None, style=None, fast=False, grayscale=False):
        (
            self.monitor.draw_image(
                path, marker=marker, style=style, grayscale=grayscale, fast=fast
            )
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).image_plot(
                        path, marker=marker, style=style, grayscale=grayscale, fast=fast
                    )
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    ##############################################
    ###########    Date Functions    #############
    ##############################################

    def date_form(self, input_form=None, output_form=None):
        self._master._dummy.date.date_form(input_form, output_form)
        if self._no_plots:
            self.monitor.date.date_form(input_form, output_form)
        else:
            [
                [
                    self._get_subplot(row, col).date_form(input_form, output_form)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]

    def set_time0(self, string, input_form=None):
        (
            self.monitor.date.set_time0(string, input_form)
            if self._no_plots
            else [
                [
                    self._get_subplot(row, col).set_time0(string, input_form)
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
        )

    def today_datetime(self):
        return self.monitor.date.today_datetime()

    def today_string(self, output_form=None):
        return self.monitor.date.today_string(output_form)

    def datetime_to_string(self, datetime, output_form=None):
        return self.monitor.date.datetime_to_string(datetime, output_form=output_form)

    def datetimes_to_strings(self, datetimes, output_form=None):
        return self.monitor.date.datetimes_to_strings(
            datetimes, output_form=output_form
        )

    def string_to_datetime(self, string, input_form=None):
        return self.monitor.date.string_to_datetime(string, input_form=input_form)

    def string_to_time(self, string, input_form=None):
        return self.monitor.date.string_to_time(string, input_form=input_form)

    def strings_to_time(self, string, input_form=None):
        return self.monitor.date.strings_to_time(string, input_form=input_form)

    ##############################################
    ###########    Build Functions    ############
    ##############################################

    def show(self):  # it build and shows the overall figure
        t = time()
        self.build()
        ut.write(self.monitor.matrix.canvas)  # it prints the final canvas
        self._time = (
            time() - t
        )  # computational time of build + print (it does not include any pre-processing time, which is gets more important for bar and image plots)
        (
            self.main() if not self._master._interactive else None
        )  # it returns control to main figure on top level

    def build(self):  # it build the current figure without showing it
        self._set_sizes()
        self._build_matrix()
        self.monitor.matrix.set_canvas() if not self.monitor.fast_plot else None
        return self.monitor.matrix.get_canvas()

    def _build_matrix(self):
        if self._no_plots:
            self.monitor.build_plot() if not self.monitor.fast_plot else None
        else:
            [
                [self._get_subplot(row, col)._build_matrix() for col in self._Cols]
                for row in self._Rows
            ]
            matrices = [
                [self._get_subplot(row, col).monitor.matrix for col in self._Cols]
                for row in self._Rows
            ]
            self.monitor.matrix = join_matrices(matrices)

    ##############################################
    #########    Set Size Utilities    ###########
    ##############################################

    def _set_sizes(self):  # it properly sets coherent sub figure dimensions
        self._set_slots_max(*self._size)
        self._set_slots(*self._slots)
        widths = self._get_widths()
        widths = ut.set_sizes(
            widths, self._width
        )  # it sets the free subplots widths in accord with the parent figure width
        widths = ut.fit_sizes(
            widths, self._width
        )  # it fits the subplots widths to the parent figure width
        heights = self._get_heights()
        heights = ut.set_sizes(
            heights, self._height
        )  # it sets the free subplots height in accord with the parent figure height
        heights = ut.fit_sizes(
            heights, self._height
        )  # it fits the subplots heights to the parent figure height
        self.monitor.set_size(self._size)
        # Note: width/height calculations removed as they were unused
        # Original: self._set_size(sum(widths), sum(heights))
        (
            [
                [
                    self._set_subplot_size(row, col, widths[col - 1], heights[row - 1])
                    for col in self._Cols
                ]
                for row in self._Rows
            ]
            if (not self._no_plots)
            else None
        )  # to make sure that all sub figures have internal dimensions set as well

    def _get_widths(self):  # the subplots max/min widths for each column
        widths = [
            [self._get_subplot(row, col)._width for row in self._Rows]
            for col in self._Cols
        ]
        widths = [
            self.max_or_min([sub for sub in el if sub is not None], default=None)
            for el in widths
        ]
        return widths

    def _get_heights(self):  # the subplots max/min heights for each row
        heights = [
            [self._get_subplot(row, col)._height for col in self._Cols]
            for row in self._Rows
        ]
        heights = [
            self.max_or_min([sub for sub in el if sub is not None], default=None)
            for el in heights
        ]
        return heights

    def _set_subplot_size(self, row=None, col=None, width=None, height=None):
        self._get_subplot(row, col)._set_size(width, height)
        self._get_subplot(row, col)._set_sizes()

    ##############################################
    ######    Externally Called Utility    #######
    ##############################################

    def _get_time(
        self, show=True
    ):  # it returns the computational time of latest show or build function
        time = ut.format_time(self._time)
        (
            print(ut.format_strings("plotext time:", time, ut.title_color))
            if show
            else None
        )
        return self._time

    def main(
        self,
    ):  # returns the master figure and sets the active figure to the master
        self._master._active = self._master
        return self._master

    def plot_size(self, width=None, height=None):
        width = self._width if width is None else width
        height = self._height if height is None else height
        self._set_size(width, height)
        self._set_master_size() if self._is_master else None
        self.monitor.size = self._size
        return self._width, self._height

    plotsize = plot_size

    def take_min(
        self,
    ):  # in a matrix of subplots the maximum height/width will be considered for each row/column
        self.max_or_min = min

    def save_fig(
        self, path=None, append=False, keep_colors=False
    ):  # it saves the plot as text or html, keep_colors = True preserves ansi colors for texts
        path = "plotext.txt" if path is None or not ut.correct_path(path) else path
        _, extension = os.path.splitext(path)
        canvas = self.monitor.matrix.get_canvas()
        if extension == ".html":
            canvas = self.monitor.matrix.to_html()
        elif not keep_colors:
            canvas = ut.uncolorize(canvas)
        ut.save_text(canvas, path, append)

    savefig = save_fig

    ##############################################
    ############     Docstrings    ###############
    ##############################################
    add(subplots)
    add(subplots)
    add(subplot)
    add(main)

    add(plot_size)
    add(take_min)

    add(title)
    add(xlabel)
    add(ylabel)
    add(xlim)
    add(ylim)
    add(xscale)
    add(yscale)
    add(xticks)
    add(yticks)
    add(xfrequency)
    add(yfrequency)
    add(xreverse)
    add(yreverse)
    add(xaxes)
    add(yaxes)
    add(frame)
    add(grid)
    add(canvas_color)
    add(axes_color)
    add(ticks_color)
    add(ticks_style)
    add(theme)

    add(clear_figure)
    add(clear_data)
    add(clear_color)
    add(clear_terminal)

    add(scatter)
    add(plot)
    add(bar)
    add(multiple_bar)
    add(stacked_bar)
    add(hist)
    add(candlestick)
    add(box)

    add(error)
    add(event_plot)
    add(vertical_line)
    add(horizontal_line)
    add(text)
    add(rectangle)
    add(polygon)
    add(confusion_matrix)
    add(indicator)

    add(matrix_plot)
    # add(pie)  # Commented out to suppress warning - pie documentation needs to be added to _doc.py
    add(image_plot)

    add(show)
    add(build)
    add(save_fig)

    add(date_form)
    add(set_time0)
    add(today_datetime)
    add(today_string)
    add(datetime_to_string)
    add(datetimes_to_strings)
    add(string_to_datetime)
