"""Utilities for creating and storing figures."""
import os.path
import statistics as st
import warnings
from math import isfinite, sqrt
from typing import Any, Callable, Final, Iterable, Sequence, cast

import matplotlib.pyplot as plt  # type: ignore
from matplotlib import rcParams  # type: ignore
from matplotlib.artist import Artist  # type: ignore
from matplotlib.axes import Axes  # type: ignore
from matplotlib.backend_bases import RendererBase  # type: ignore
from matplotlib.backends.backend_agg import RendererAgg  # type: ignore
from matplotlib.figure import Figure, SubFigure  # type: ignore
from pycommons.io.path import Path, directory_path
from pycommons.types import check_int_range, type_error, type_name_of

import moptipy.utils.plot_defaults as pd
from moptipy.utils.lang import Lang

# Ensure that matplotlib uses Type 1 fonts.
# Some scientific conferences, such as GECCO organized by ACM, require this.
# In the language utilities :meth:`~moptipy.utils.lang.Lang.font`, we do
# return acceptable fonts anyway, but it may be better to set this here
# explicitly to avoid any problem.
rcParams["pdf.fonttype"] = 42
rcParams["ps.fonttype"] = 42

#: The golden ratio constant with value 1.618033988749895.
__GOLDEN_RATIO: Final[float] = 0.5 + (0.5 * sqrt(5))


def create_figure(width: float | int | None = 8.6,
                  height: float | int | None = None,
                  dpi: float | int | None = 384.0,
                  **kwargs) -> Figure:
    """
    Create a matplotlib figure.

    :param width: the optional width
    :param height: the optional height
    :param dpi: the dpi value
    :param kwargs: a set of optional arguments
    :return: the figure option
    """
    # Check the size, i.e., width and height and use
    put_size: bool = True
    if width is None:
        if height is None:
            put_size = False
        else:
            width = __GOLDEN_RATIO * height
    elif height is None:
        height = width / __GOLDEN_RATIO

    if put_size:
        if isinstance(height, int):
            height = float(height)
        if not isinstance(height, float):
            raise ValueError(f"Invalid height type {type(height)}.")
        if not (isfinite(height) and (0.1 < height < 10000.0)):
            raise ValueError(f"Invalid height {height}.")
        nheight = int(0.5 + (height * 72)) / 72.0
        if not (isfinite(nheight) and (0.1 < nheight < 10000.0)):
            raise ValueError(f"Invalid height {height} as it maps to "
                             f"{nheight} after to-point-and-back conversion.")

        if isinstance(width, int):
            width = float(width)
        if not isinstance(width, float):
            raise ValueError(f"Invalid width type {type(width)}.")
        if not (isfinite(width) and (0.1 < width < 10000.0)):
            raise ValueError(f"Invalid width {width}.")
        nwidth = int(0.5 + (width * 72)) / 72.0
        if not (isfinite(nwidth) and (0.1 < nwidth < 10000.0)):
            raise ValueError(f"Invalid width {width} as it maps to "
                             f"{nwidth} after to-point-and-back conversion.")

        kwargs["figsize"] = width, height

    if dpi is not None:
        if isinstance(dpi, int):
            dpi = float(dpi)
        if not isinstance(dpi, float):
            raise ValueError(f"Invalid dpi type {type(dpi)}.")
        if not (isfinite(dpi) and (1.0 < dpi < 10000.0)):
            raise ValueError(f"Invalid dpi value {dpi}.")
        kwargs["dpi"] = dpi

    if "frameon" not in kwargs:
        kwargs["frameon"] = False

    if "constrained_layout" not in kwargs:
        kwargs["constrained_layout"] = False

    return Figure(**kwargs)


def __divide_evenly(items: int, chunks: int, reverse: bool) -> list[int]:
    """
    Divide `n` items into `k` chunks, trying to create equally-sized chunks.

    :param items: the number of items to divide
    :param chunks: the number of chunks
    :param reverse: should we put the additional items at the end (True)
        or front (False)
    :returns: the list of items

    >>> print(__divide_evenly(9, 3, reverse=True))
    [3, 3, 3]
    >>> print(__divide_evenly(10, 3, reverse=True))
    [3, 3, 4]
    >>> print(__divide_evenly(10, 3, reverse=False))
    [4, 3, 3]
    >>> print(__divide_evenly(11, 3, reverse=True))
    [3, 4, 4]
    >>> print(__divide_evenly(11, 3, reverse=False))
    [4, 4, 3]
    >>> print(__divide_evenly(12, 3, reverse=False))
    [4, 4, 4]
    """
    # validate that we do not have more chunks than items or otherwise invalid
    # parameters
    if (items <= 0) or (chunks <= 0) or (chunks > items):
        raise ValueError(f"cannot divide {items} items into {chunks} chunks.")

    # First we compute the minimum number of items per chunk.
    # Our basic solution is to put exactly this many items into each chunk.
    # For example, if items=10 and chunks=3, this will fill 3 items into each
    # chunk.
    result: list[int] = [items // chunks] * chunks
    # We then fill the remaining items into the chunks at the front
    # (reverse=False) or end (reverse=True) of the list.
    # We do this by putting exactly one such item into each chunk, starting
    # from the end.
    # Since items modulo chunks must be in [0..chunks), this is possible.
    # This setup will yield the lowest possible standard deviation of items
    # per chunk.
    # In case of items=10 and chunks=3, items % chunks = 1 and the chunks will
    # become [3, 3, 4]. If items=3, no items need to be distributed and we get
    # [3, 3, 3].
    if reverse:
        for i in range(1, (items % chunks) + 1):
            result[-i] += 1
    else:
        for i in range(items % chunks):
            result[i] += 1
    return result


def create_figure_with_subplots(
        items: int,
        max_items_per_plot: int = 3,
        max_rows: int = 3,
        max_cols: int = 1,
        min_rows: int = 1,
        min_cols: int = 1,
        default_width_per_col: float | int | None = 8.6,
        max_width: float | int | None = 8.6,
        default_height_per_row: float | int | None = 8.6 / __GOLDEN_RATIO,
        max_height: float | int | None = 9,
        dpi: float | int | None = 384.0,
        plot_config: dict[str, Any] | None = None,
        **kwargs) \
        -> tuple[Figure, tuple[tuple[Axes | Figure,
                                     int, int, int, int, int], ...]]:
    """
    Divide a figure into nrows*ncols sub-plots.

    :param items: the number of items to divide
    :param max_items_per_plot: the maximum number of items per plot
    :param max_rows: the maximum number of rows
    :param max_cols: the maximum number of columns
    :param min_rows: the minimum number of rows
    :param min_cols: the minimum number of cols
    :param default_width_per_col: the optional default width of a column
    :param default_height_per_row: the optional default height per row
    :param max_height: the maximum height
    :param max_width: the maximum width
    :param dpi: the dpi value
    :param kwargs: a set of optional arguments
    :param plot_config: the configuration to be applied to all sub-plots
    :returns: a tuple with the figure, followed by a series of tuples with
        each sub-figure, the index of the first item assigned to it, the
        index of the last item assigned to it + 1, their row, their column,
        and their overall index
    """
    # First, we do a lot of sanity checks
    check_int_range(items, "items", 1, 1_000_000)
    check_int_range(max_items_per_plot, "max_items_per_plot", 1, 1_000_000)
    check_int_range(max_rows, "max_rows", 1, 100)
    check_int_range(min_rows, "min_rows", 1, max_rows)
    check_int_range(max_cols, "max_cols", 1, 100)
    check_int_range(min_cols, "min_cols", 1, max_cols)
    if (max_cols * max_rows * max_items_per_plot) < items:
        raise ValueError(
            f"Cannot distribute {items} items into at most {max_rows} rows "
            f"and {max_cols} cols with at most {max_items_per_plot} per "
            "plot.")
    if default_width_per_col is not None:
        default_width_per_col = float(default_width_per_col)
        if (not isfinite(default_width_per_col)) \
                or (default_width_per_col <= 0.1) \
                or (default_width_per_col >= 1000):
            raise ValueError(
                f"invalid default_width_per_col {default_width_per_col}")
    if max_width is not None:
        max_width = float(max_width)
        if (not isfinite(max_width)) \
                or (max_width <= 0.1) or (max_width >= 10000):
            raise ValueError(f"invalid max_width {max_width}")
        if (default_width_per_col is not None) \
                and (default_width_per_col > max_width):
            raise ValueError(f"default_width_per_col {default_width_per_col} "
                             f"> max_width {max_width}")
    if default_height_per_row is not None:
        default_height_per_row = float(default_height_per_row)
        if (not isfinite(default_height_per_row)) \
                or (default_height_per_row <= 0.1) \
                or (default_height_per_row >= 10000):
            raise ValueError(
                f"invalid default_height_per_row {default_height_per_row}")
    if max_height is not None:
        max_height = float(max_height)
        if (not isfinite(max_height)) \
                or (max_height <= 0.1) or (max_height >= 10000):
            raise ValueError(f"invalid max_width {max_height}")
        if (default_height_per_row is not None) \
                and (max_height < default_height_per_row):
            raise ValueError(
                f"max_height {max_height} < "
                f"default_height_per_row {default_height_per_row}")

    # setting up the default dimensions
    if default_height_per_row is None:
        default_height_per_row = 1.95
        if max_height is not None:
            default_height_per_row = min(default_height_per_row, max_height)
    if max_height is None:
        max_height = max_rows * default_height_per_row
    if default_width_per_col is None:
        default_width_per_col = 8.6
        if max_width is not None:
            default_width_per_col = min(default_width_per_col, max_width)
    if max_width is None:
        max_width = default_width_per_col
    if (plot_config is not None) and not isinstance(plot_config, dict):
        raise type_error(plot_config, "plot_config", (dict, None))

    # How many plots do we need at least? Well, if we can put
    # max_items_per_plot items into each plot and have items many items, then
    # we need ceil(items / max_items_per_plot) many. This is what we compute
    # here.
    min_plots: Final[int] = (items // max_items_per_plot) \
        + min(1, items % max_items_per_plot)
    # The maximum conceivable number of plots would be items, so this we do
    # not need to compute.
    plots_i: Final[int] = 6
    rows_i: Final[int] = plots_i + 1
    cols_i: Final[int] = rows_i + 1
    height_i: Final[int] = cols_i + 1
    width_i: Final[int] = height_i + 1
    plots_per_row_i: Final[int] = width_i + 1
    chunks_i: Final[int] = plots_per_row_i + 1
    best: tuple[float,  # the plots-per-row std
                float,  # the items-per-plot std
                float,  # the overall area of the figure
                float,  # the std deviation of (w, h*golden_ratio),
                float,  # -plot area
                float,  # the std deviation of (w, h*golden_ratio) per plot
                int,  # the number of plots
                int,  # the number of rows
                int,  # the number of cols
                float,  # the figure height
                float,  # the figure width
                list[int],  # the plots per row
                list[int]] = \
        (1000000.0, 1000000.0, 1000000.0, 1000000.0, 1000000.0, 1000000.0,
         1 << 62, 1 << 62, 1 << 62, 1000000.0, 1000000.0, [0], [0])

    # Now we simply try all valid combinations.
    for rows in range(min_rows, max_rows + 1):
        if rows > items:
            continue  # more rows than items
        for cols in range(min_cols, max_cols + 1):
            # We need cols plots per row, except for the last row, where we
            # could only put a single plot if we wanted.
            min_plots_for_config: int = 1 + ((rows - 1) * cols)
            if min_plots_for_config > items:
                continue  # if this exceeds the maximum number of plots, skip
            # compute the maximum plots we can get in this config
            max_plots_per_config: int = rows * cols
            if max_plots_per_config < min_plots:
                continue  # not enough plots possible

            # now we can iterate over the number of plots we can provide
            for plots in range(max(min_plots_for_config, min_plots),
                               min(max_plots_per_config, items) + 1):
                # We compute the figure width. If we have multiple columns,
                # we try to add 2% plus 0.02" as breathing space per column.
                fig_width = min(max_width, ((1.02 * default_width_per_col)
                                            + 0.02) * cols)
                # The width of a single plot is then computed as follows:
                plot_width = fig_width / cols
                if plot_width < 0.9:
                    continue  # the single plots would be too small
                # We compute the overall figure height. Again we add breathing
                # space if we have multiple rows.
                fig_height = min(max_height, ((1.02 * default_height_per_row)
                                              + 0.02) * rows)
                plot_height = fig_height / rows
                if plot_height < 0.9:
                    continue  # the single plots would be too small
                # So dimension-wise, the plot sizes are OK.
                # How about the distribution of items? We put the rows with
                # plots at the top and the plots with more items at the end.
                plot_distr = __divide_evenly(plots, rows, reverse=False)
                item_distr = __divide_evenly(items, plots, reverse=True)

                current: tuple[float, float, float, float, float, float,
                               int, int, int, float, float,
                               list[int], list[int]] = (
                    st.stdev(plot_distr) if rows > 1 else 0,
                    st.stdev(item_distr) if plots > 1 else 0,
                    fig_height * fig_width,  # the area of the figure
                    st.stdev([fig_width, fig_height * __GOLDEN_RATIO]),
                    -plot_width * plot_height,  # -plot area
                    st.stdev([plot_width, plot_height * __GOLDEN_RATIO]),
                    plots,  # the number of plots
                    rows,  # the number of rows
                    cols,  # the number of cols
                    fig_height,  # the figure height
                    fig_width,  # the figure width
                    plot_distr,  # the plots per row
                    item_distr)  # the data chunks per plot
                if ((current[plots_i] + 1)  # type: ignore
                    < best[plots_i]) or ((  # type: ignore
                        current[plots_i]   # type: ignore
                        <= (best[plots_i] + 1))  # type: ignore
                        and (current < best)):  # type: ignore
                    best = current

    n_plots: Final[int] = best[plots_i]  # type: ignore
    if n_plots > items:
        raise ValueError(
            f"Could not place {items} in {min_cols}..{max_cols} columns "
            f"at {min_rows}..{max_rows} rows with at most "
            f"{max_items_per_plot} items per plot for a max_width={max_width}"
            f" and max_height={max_height}.")

    # create the figure of the computed dimensions
    figure: Final[Figure] = create_figure(
        width=best[width_i], height=best[height_i],  # type: ignore
        dpi=dpi, **kwargs)
    if (n_plots <= 1) and (plot_config is None):
        # if there is only one plot, we are done here
        return figure, ((figure, 0, items, 0, 0, 0), )

    # if there are multiple plots, we need to generate them
    allfigs: list[tuple[Axes | Figure,
                        int, int, int, int, int]] = []
    index: int = 0
    chunk_start: int = 0
    nrows: Final[int] = best[rows_i]  # type: ignore
    ncols: Final[int] = best[cols_i]  # type: ignore
    chunks: Final[list[int]] = best[chunks_i]  # type: ignore
    plots_per_row: Final[list[int]] = best[plots_per_row_i]  # type: ignore
    for i in range(nrows):
        for j in range(plots_per_row[i]):
            chunk_next = chunk_start + chunks[index]
            if plot_config is None:
                sp = figure.add_subplot(nrows, ncols, (i * ncols) + j + 1)
            else:
                sp = figure.add_subplot(nrows, ncols, (i * ncols) + j + 1,
                                        **plot_config)
            allfigs.append((sp, chunk_start, chunk_next, i, j, index))
            chunk_start = chunk_next
            index += 1
    return figure, tuple(allfigs)


def save_figure(fig: Figure,
                file_name: str = "figure",
                dir_name: str = ".",
                formats: str | Iterable[str] = "svg") -> list[Path]:
    """
    Store the given figure in files of the given formats and dispose it.

    :param fig: the figure to save
    :param file_name: the basic file name
    :param dir_name: the directory name
    :param formats: the file format(s)
    :return: a list of files
    """
    if not isinstance(fig, Figure):
        raise type_error(fig, "figure", Figure)
    if not isinstance(file_name, str):
        raise type_error(file_name, "file_name", str)
    if len(file_name) <= 0:
        raise ValueError(f"Invalid filename {file_name!r}.")
    if not isinstance(dir_name, str):
        raise type_error(dir_name, "dir_name", str)
    if len(dir_name) <= 0:
        raise ValueError(f"Invalid dirname {dir_name!r}.")
    if isinstance(formats, str):
        formats = [formats]
    if not isinstance(formats, Iterable):
        raise type_error(formats, "formats", Iterable)

    size = fig.get_size_inches()
    orientation: Final[str] = \
        "landscape" if size[0] >= size[1] else "portrait"

    # set minimal margins to the axes to avoid wasting space
    for ax in fig.axes:  # consider the dimension of the axes
        margins: list[int] = [0] * len(ax.margins())
        ax.margins(*margins)

    use_dir = directory_path(dir_name)
    files = []
    for fmt in formats:
        if not isinstance(fmt, str):
            raise type_error(fmt, "element of formats", str)
        dest_file = Path(os.path.join(use_dir, f"{file_name}.{fmt}"))
        dest_file.ensure_file_exists()
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            # UserWarning: There are no gridspecs with layoutgrids.
            # Possibly did not call parent GridSpec with the "figure" keyword
            fig.savefig(dest_file, transparent=True, format=fmt,
                        orientation=orientation,
                        dpi="figure",
                        bbox_inches="tight",
                        pad_inches=1.0 / 72.0)
        dest_file.enforce_file()
        files.append(dest_file)

    fig.clf(False)
    plt.close(fig)
    del fig

    if len(files) <= 0:
        raise ValueError("No formats were specified.")

    return files


def label_box(axes: Axes,
              text: str,
              x: float | None = None,
              y: float | None = None,
              font_size: float = pd.importance_to_font_size(0),
              may_rotate_text: bool = False,
              z_order: float | None = None,
              font: None | str | Callable =
              lambda: Lang.current().font()) -> None:
    """
    Put a label text box near an axis.

    :param axes: the axes to add the label to
    :param text: the text to place
    :param x: the location along the x-axis: `0` means left,
        `0.5` means centered, `1` means right
    :param y: the location along the x-axis: `0` means bottom,
        `0.5` means centered, `1` means top
    :param font_size: the font size
    :param may_rotate_text: should we rotate the text by 90° if that
        makes sense (`True`) or always keep it horizontally (`False`)
    :param z_order: an optional z-order value
    :param font: the font to use
    """
    if x is None:
        if y is None:
            raise ValueError("At least one of x or y must not be None.")
        x = 0
    elif y is None:
        y = 0

    spacing: Final[float] = max(4.0, font_size / 2.0)
    xtext: float = 0.0
    ytext: float = 0.0
    xalign: str = "center"
    yalign: str = "center"
    if x >= 0.85:
        xtext = -spacing
        xalign = "right"
    elif x <= 0.15:
        xtext = spacing
        xalign = "left"
    if y >= 0.85:
        ytext = -spacing
        yalign = "top"
    elif y <= 0.15:
        ytext = spacing
        yalign = "bottom"

    args = {"text": text,
            "xy": (x, y),
            "xytext": (xtext, ytext),
            "verticalalignment": yalign,
            "horizontalalignment": xalign,
            "xycoords": "axes fraction",
            "textcoords": "offset points",
            "fontsize": font_size,
            "bbox": {"boxstyle": "round",
                     "color": "white",
                     "fill": True,
                     "linewidth": 0,
                     "alpha": 0.9}}
    if z_order is not None:
        args["zorder"] = z_order

    if may_rotate_text and (len(text) > 2):
        args["rotation"] = 90

    if callable(font):
        font = font()
    if font is not None:
        if not isinstance(font, str):
            raise type_error(font, "font", str)
        args["fontname"] = font

    axes.annotate(**args)  # type: ignore


def label_axes(axes: Axes,
               x_label: str | None = None,
               x_label_inside: bool = True,
               x_label_location: float = 0.5,
               y_label: str | None = None,
               y_label_inside: bool = True,
               y_label_location: float = 1,
               font_size: float = pd.importance_to_font_size(0),
               z_order: float | None = None) -> None:
    """
    Put labels on a figure.

    :param axes: the axes to add the label to
    :param x_label: a callable returning the label for
        the x-axis, a label string, or `None` if no label should be put
    :param x_label_inside: put the x-axis label inside the plot (so that
        it does not consume additional vertical space)
    :param x_label_location: the location of the x-axis label if it is
        placed inside the plot area
    :param y_label: a callable returning the label for
        the y-axis, a label string, or `None` if no label should be put
    :param y_label_inside: put the xyaxis label inside the plot (so that
        it does not consume additional horizontal space)nal vertical space)
    :param y_label_location: the location of the y-axis label if it is
        placed inside the plot area
    :param font_size: the font size to use
    :param z_order: an optional z-order value
    """
    # put the label on the x-axis, if any
    if x_label is not None:
        if not isinstance(x_label, str):
            raise type_error(x_label, "x_label", str)
        if len(x_label) > 0:
            if x_label_inside:
                label_box(axes, text=x_label, x=x_label_location, y=0,
                          font_size=font_size, z_order=z_order)
            else:
                axes.set_xlabel(x_label, fontsize=font_size)

    # put the label on the y-axis, if any
    if y_label is not None:
        if not isinstance(y_label, str):
            raise type_error(y_label, "y_label", str)
        if len(y_label) > 0:
            if y_label_inside:
                label_box(axes, text=y_label, x=0, y=y_label_location,
                          font_size=font_size, may_rotate_text=True,
                          z_order=z_order)
            else:
                axes.set_ylabel(y_label, fontsize=font_size)


def get_axes(figure: Axes | Figure) -> Axes:
    """
    Obtain the axes from a figure or axes object.

    :param figure: the figure
    :return: the Axes
    """
    if isinstance(figure, Figure):
        return figure.add_axes((0.005, 0.005, 0.99, 0.99))
    if hasattr(figure, "axes") \
            or isinstance(getattr(type(figure), "axes", None), property):
        try:
            if isinstance(figure.axes, Axes):
                return cast(Axes, figure.axes)
            if isinstance(figure.axes, Sequence):
                ax = figure.axes[0]
                if isinstance(ax, Axes):
                    return cast(Axes, ax)
            elif isinstance(figure.axes, Iterable):
                for k in figure.axes:
                    if isinstance(k, Axes):
                        return cast(Axes, k)
                    break
        except TypeError:
            pass
        except IndexError:
            pass
    if isinstance(figure, Axes):
        return cast(Axes, figure)
    raise TypeError(
        f"Cannot get Axes of object of type {type_name_of(figure)}.")


def get_renderer(figure: Axes | Figure | SubFigure) -> RendererBase:
    """
    Get a renderer that can be used for determining figure element sizes.

    :param figure: the figure element
    :return: the renderer
    """
    if isinstance(figure, Axes):
        figure = figure.figure
    if not isinstance(figure, Figure):
        raise type_error(figure, "figure", Figure)
    canvas = figure.canvas
    if hasattr(canvas, "renderer"):
        return canvas.renderer
    if hasattr(canvas, "get_renderer"):
        return canvas.get_renderer()
    return RendererAgg(width=figure.get_figwidth(),
                       height=figure.get_figheight(),
                       dpi=figure.get_dpi())


def cm_to_inch(cm: int | float) -> float:
    """
    Convert cm to inch.

    :param cm: the cm value
    :return: the value in inch
    """
    if not isinstance(cm, int):
        if not isinstance(cm, float):
            raise type_error(cm, "cm", (int, float))
        if not isfinite(cm):
            raise ValueError(f"cm must be finite, but is {cm}.")
    res: float = cm / 2.54
    if not isfinite(res):
        raise ValueError(f"Conversation {cm} cm to inch "
                         f"must be finite, but is {res}.")
    return res


#: the color attributes
__COLOR_ATTRS: Final[tuple[tuple[bool, bool, str], ...]] = \
    ((True, True, "get_label"), (False, True, "label"),
     (False, True, "_label"), (True, False, "get_color"),
     (False, False, "color"), (False, False, "_color"),
     (False, False, "edgecolor"), (False, False, "_edgecolor"),
     (False, False, "markeredgecolor"), (False, False, "_markeredgecolor"))


def get_label_colors(
        handles: Iterable[Artist],
        color_map: dict[str, tuple[float, ...] | str] | None = None,
        default_color: tuple[float, ...] | str = pd.COLOR_BLACK) \
        -> list[tuple[float, ...] | str]:
    """
    Get a list with label colors from a set of artists.

    :param handles: the handles
    :param color_map: an optional color decode
    :param default_color: the default color
    :returns: a list of label colors
    """
    if not isinstance(handles, Iterable):
        raise type_error(handles, "handles", Iterable)

    def __get_color(a: Artist, colmap=color_map,
                    defcol=default_color) -> tuple[float, ...] | str:
        if not isinstance(a, Artist):
            raise type_error(a, "artist", Artist)

        for acall, astr, aname in __COLOR_ATTRS:
            if hasattr(a, aname):
                val = getattr(a, aname)
                if val is None:
                    continue
                if acall:
                    val = val()
                if val is None:
                    continue
                if astr:
                    if colmap:
                        if val in colmap:
                            val = colmap[val]
                        else:
                            continue
                    else:
                        continue
                if val is None:
                    continue
                if val != defcol:
                    return val
        return defcol

    return [__get_color(aa) for aa in handles]
