"""
Functionality to create an HTML report using a fitted GLM & contrasts.

Functions
---------

make_glm_report(model, contrasts):
    Creates an HTMLReport Object which can be viewed or saved as a report.

"""

import datetime
import inspect
import uuid
import warnings
from html import escape
from pathlib import Path
from string import Template
from typing import Union

import numpy as np
import pandas as pd

from nilearn import DEFAULT_DIVERGING_CMAP
from nilearn._utils import logger
from nilearn._utils.docs import fill_doc
from nilearn._utils.glm import coerce_to_dict, make_stat_maps
from nilearn._utils.helpers import is_matplotlib_installed
from nilearn._utils.html_document import HEIGHT_DEFAULT, WIDTH_DEFAULT
from nilearn._utils.logger import find_stack_level
from nilearn._utils.niimg import load_niimg, safe_get_data
from nilearn._utils.niimg_conversions import check_niimg
from nilearn._version import __version__
from nilearn.externals import tempita
from nilearn.glm.thresholding import (
    threshold_stats_img,
    warn_default_threshold,
)
from nilearn.maskers import NiftiMasker
from nilearn.reporting._utils import (
    dataframe_to_html,
)
from nilearn.reporting.get_clusters_table import (
    clustering_params_to_dataframe,
    get_clusters_table,
)
from nilearn.reporting.html_report import (
    HTMLReport,
    _render_warnings_partial,
    is_notebook,
)
from nilearn.reporting.utils import (
    CSS_PATH,
    HTML_TEMPLATE_PATH,
    JS_PATH,
    TEMPLATE_ROOT_PATH,
    figure_to_png_base64,
)
from nilearn.surface.surface import SurfaceImage
from nilearn.surface.surface import get_data as get_surface_data

MNI152TEMPLATE = None
if is_matplotlib_installed():
    from matplotlib import pyplot as plt

    from nilearn._utils.plotting import (
        generate_contrast_matrices_figures,
        generate_design_matrices_figures,
        resize_plot_inches,
    )
    from nilearn.plotting import (
        plot_glass_brain,
        plot_roi,
        plot_stat_map,
        plot_surf_stat_map,
    )
    from nilearn.plotting.image.utils import (  # type: ignore[assignment]
        MNI152TEMPLATE,
    )


@fill_doc
def make_glm_report(
    model,
    contrasts=None,
    first_level_contrast=None,
    title=None,
    bg_img="MNI152TEMPLATE",
    threshold=3.09,
    alpha=0.001,
    cluster_threshold=0,
    height_control="fpr",
    two_sided=False,
    min_distance=8.0,
    plot_type="slice",
    cut_coords=None,
    display_mode=None,
    report_dims=(WIDTH_DEFAULT, HEIGHT_DEFAULT),
):
    """Return HTMLReport object \
    for a report which shows all important aspects of a fitted GLM.

    The object can be opened in a browser, displayed in a notebook,
    or saved to disk as a standalone HTML file.

    Examples
    --------
    report = make_glm_report(model, contrasts)
    report.open_in_browser()
    report.save_as_html(destination_path)

    Parameters
    ----------
    model : FirstLevelModel or SecondLevelModel object
        A fitted first or second level model object.
        Must have the computed design matrix(ces).

    contrasts : :obj:`dict` with :obj:`str` - ndarray key-value pairs \
        or :obj:`str` \
        or :obj:`list` of :obj:`str` \
        or ndarray or \
        :obj:`list` of ndarray, Default=None

        Contrasts information for a first or second level model.

        Example:

            Dict of :term:`contrast` names and coefficients,
            or list of :term:`contrast` names
            or list of :term:`contrast` coefficients
            or :term:`contrast` name
            or :term:`contrast` coefficient

            Each :term:`contrast` name must be a string.
            Each :term:`contrast` coefficient must be a list
            or numpy array of ints.

        Contrasts are passed to ``contrast_def`` for FirstLevelModel
        (:func:`nilearn.glm.first_level.FirstLevelModel.compute_contrast`)
        & second_level_contrast for SecondLevelModel
        (:func:`nilearn.glm.second_level.SecondLevelModel.compute_contrast`)

    %(first_level_contrast)s

        .. versionadded:: 0.12.0

    title : :obj:`str`, default=None
        If string, represents the web page's title and primary heading,
        model type is sub-heading.
        If None, page titles and headings are autogenerated
        using :term:`contrast` names.

    bg_img : Niimg-like object, default='MNI152TEMPLATE'
        See :ref:`extracting_data`.
        The background image for mask and stat maps to be plotted on upon.
        To turn off background image, just pass "bg_img=None".

    threshold : :obj:`float`, default=3.09
        Cluster forming threshold in same scale as `stat_img` (either a
        t-scale or z-scale value). Used only if height_control is None.

        .. note::

            - When ``two_sided`` is True:

              ``'threshold'`` cannot be negative.

              The given value should be within the range of minimum and maximum
              intensity of the input image.
              All intensities in the interval ``[-threshold, threshold]``
              will be set to zero.

            - When ``two_sided`` is False:

              - If the threshold is negative:

                It should be greater than the minimum intensity
                of the input data.
                All intensities greater than or equal
                to the specified threshold will be set to zero.
                All other intensities keep their original values.

              - If the threshold is positive:

                It should be less than the maximum intensity
                of the input data.
                All intensities less than or equal
                to the specified threshold will be set to zero.
                All other intensities keep their original values.

    alpha : :obj:`float`, default=0.001
        Number controlling the thresholding (either a p-value or q-value).
        Its actual meaning depends on the height_control parameter.
        This function translates alpha to a z-scale threshold.

    cluster_threshold : :obj:`int`, default=0
        Cluster size threshold, in voxels.

    height_control :  :obj:`str`, default='fpr'
        false positive control meaning of cluster forming
        threshold: 'fpr' or 'fdr' or 'bonferroni' or None.

    two_sided : :obj:`bool`, default=False
        Whether to employ two-sided thresholding or to evaluate positive values
        only.

    min_distance : :obj:`float`, default=8.0
        For display purposes only.
        Minimum distance between subpeaks in mm.

    plot_type : :obj:`str`, {'slice', 'glass'}, default='slice'
        Specifies the type of plot to be drawn for the statistical maps.

    %(cut_coords)s

    display_mode :  :obj:`str`, default=None
        Default is 'z' if plot_type is 'slice'; '
        ortho' if plot_type is 'glass'.

        Choose the direction of the cuts:
        'x' - sagittal, 'y' - coronal, 'z' - axial,
        'l' - sagittal left hemisphere only,
        'r' - sagittal right hemisphere only,
        'ortho' - three cuts are performed in orthogonal directions.

        Possible values are:
        'ortho', 'x', 'y', 'z', 'xz', 'yx', 'yz',
        'l', 'r', 'lr', 'lzr', 'lyr', 'lzry', 'lyrz'.

    report_dims : Sequence[:obj:`int`, :obj:`int`], default=(1600, 800)
        Specifies width, height (in pixels) of report window within a notebook.
        Only applicable when inserting the report into a Jupyter notebook.
        Can be set after report creation using report.width, report.height.

    Returns
    -------
    report_text : HTMLReport Object
        Contains the HTML code for the :term:`GLM` Report.

    """
    if not is_matplotlib_installed():
        warnings.warn(
            ("No plotting back-end detected. Output will be missing figures."),
            UserWarning,
            stacklevel=find_stack_level(),
        )

    parameters = dict(**inspect.signature(make_glm_report).parameters)
    if height_control is not None and float(threshold) != float(
        parameters["threshold"].default
    ):
        warnings.warn(
            (
                f"'{threshold=}' will not be used with '{height_control=}'. "
                "'threshold' is only used when 'height_control=None'. "
                f"Set 'threshold' to '{parameters['threshold'].default}' "
                "to avoid this warning."
            ),
            UserWarning,
            stacklevel=find_stack_level(),
        )
    warn_default_threshold(
        threshold,
        parameters["threshold"].default,
        3.0,
        height_control=height_control,
    )

    unique_id = str(uuid.uuid4()).replace("-", "")

    title = f"<br>{title}" if title else ""
    title = f"Statistical Report - {model.__str__()}{title}"

    docstring = model.__doc__
    snippet = docstring.partition("Parameters\n    ----------\n")[0]

    date = datetime.datetime.now().replace(microsecond=0).isoformat()

    smoothing_fwhm = getattr(model, "smoothing_fwhm", 0)
    if smoothing_fwhm == 0:
        smoothing_fwhm = None

    model_attributes = _glm_model_attributes_to_dataframe(model)
    with pd.option_context("display.max_colwidth", 100):
        model_attributes_html = dataframe_to_html(
            model_attributes,
            precision=2,
            header=True,
            sparsify=False,
        )

    contrasts = coerce_to_dict(contrasts)

    # If some contrasts are passed
    # we do not rely on filenames stored in the model.
    output = None
    if contrasts is None:
        output = model._reporting_data.get("filenames", None)
        if output is not None and output["use_absolute_path"]:
            output = _turn_into_full_path(output, output["dir"])

    design_matrices = None
    mask_plot = None
    mask_info = {"n_elements": 0, "coverage": 0}
    results = None
    warning_messages = ["The model has not been fit yet."]
    if model.__sklearn_is_fitted__():
        warning_messages = []

        if model.__str__() == "Second Level Model":
            design_matrices = [model.design_matrix_]
        else:
            design_matrices = model.design_matrices_

        if bg_img == "MNI152TEMPLATE":
            bg_img = MNI152TEMPLATE if model._is_volume_glm() else None
        if (
            not model._is_volume_glm()
            and bg_img
            and not isinstance(bg_img, SurfaceImage)
        ):
            raise TypeError(
                f"'bg_img' must a SurfaceImage instance. Got {type(bg_img)=}"
            )

        mask_plot = _mask_to_plot(model, bg_img, cut_coords)

        # We try to rely on the content of glm object only
        # by reading images from disk rarther than recomputing them
        mask_info = {
            k: v
            for k, v in model.masker_._report_content.items()
            if k in ["n_elements", "coverage"]
        }
        if "coverage" in mask_info:
            mask_info["coverage"] = f"{mask_info['coverage']:0.1f}"

        statistical_maps = {}
        if model._is_volume_glm() and output is not None:
            try:
                statistical_maps = {
                    contrast_name: output["dir"]
                    / output["statistical_maps"][contrast_name]["z_score"]
                    for contrast_name in output["statistical_maps"]
                }
            except KeyError:  # pragma: no cover
                if contrasts is not None:
                    statistical_maps = make_stat_maps(
                        model,
                        contrasts,
                        output_type="z_score",
                        first_level_contrast=first_level_contrast,
                    )
        elif contrasts is not None:
            statistical_maps = make_stat_maps(
                model,
                contrasts,
                output_type="z_score",
                first_level_contrast=first_level_contrast,
            )

        logger.log(
            "Generating contrast-level figures...", verbose=model.verbose
        )
        results = _make_stat_maps_contrast_clusters(
            stat_img=statistical_maps,
            threshold_orig=threshold,
            alpha=alpha,
            cluster_threshold=cluster_threshold,
            height_control=height_control,
            two_sided=two_sided,
            min_distance=min_distance,
            bg_img=bg_img,
            cut_coords=cut_coords,
            display_mode=display_mode,
            plot_type=plot_type,
        )

    design_matrices_dict = tempita.bunch()
    contrasts_dict = tempita.bunch()
    if output is not None:
        design_matrices_dict = output["design_matrices_dict"]
        contrasts_dict = output["contrasts_dict"]

    if is_matplotlib_installed():
        logger.log(
            "Generating design matrices figures...", verbose=model.verbose
        )
        design_matrices_dict = generate_design_matrices_figures(
            design_matrices,
            design_matrices_dict=design_matrices_dict,
            output=output,
        )

        logger.log(
            "Generating contrast matrices figures...", verbose=model.verbose
        )
        contrasts_dict = generate_contrast_matrices_figures(
            design_matrices,
            contrasts,
            contrasts_dict=contrasts_dict,
            output=output,
        )

    run_wise_dict = tempita.bunch()
    for i_run in design_matrices_dict:
        tmp = tempita.bunch()
        tmp["design_matrix_png"] = design_matrices_dict[i_run][
            "design_matrix_png"
        ]
        tmp["correlation_matrix_png"] = design_matrices_dict[i_run][
            "correlation_matrix_png"
        ]
        tmp["all_contrasts"] = None
        if i_run in contrasts_dict:
            tmp["all_contrasts"] = contrasts_dict[i_run]
        run_wise_dict[i_run] = tmp

    # for methods writing, only keep the contrast expressed as strings
    if contrasts is not None:
        contrasts = [x for x in contrasts.values() if isinstance(x, str)]
    method_section_template_path = HTML_TEMPLATE_PATH / "method_section.html"
    method_tpl = tempita.HTMLTemplate.from_filename(
        str(method_section_template_path),
        encoding="utf-8",
    )
    method_section = method_tpl.substitute(
        version=__version__,
        model_type=model.__str__(),
        reporting_data=tempita.bunch(**model._reporting_data),
        smoothing_fwhm=smoothing_fwhm,
        contrasts=contrasts,
    )

    body_template_path = HTML_TEMPLATE_PATH / "glm_report.html"
    tpl = tempita.HTMLTemplate.from_filename(
        str(body_template_path),
        encoding="utf-8",
    )

    css_file_path = CSS_PATH / "masker_report.css"
    with css_file_path.open(encoding="utf-8") as css_file:
        css = css_file.read()

    with (JS_PATH / "carousel.js").open(encoding="utf-8") as js_file:
        js_carousel = js_file.read()

    body = tpl.substitute(
        css=css,
        title=title,
        docstring=snippet,
        warning_messages=_render_warnings_partial(warning_messages),
        parameters=model_attributes_html,
        mask_plot=mask_plot,
        results=results,
        run_wise_dict=run_wise_dict,
        unique_id=unique_id,
        date=date,
        show_navbar="style='display: none;'" if is_notebook() else "",
        method_section=method_section,
        js_carousel=js_carousel,
        displayed_runs=list(range(len(run_wise_dict))),
        **mask_info,
    )

    # revert HTML safe substitutions in CSS sections
    body = body.replace(".pure-g &gt; div", ".pure-g > div")

    head_template_path = (
        TEMPLATE_ROOT_PATH / "html" / "report_head_template.html"
    )
    with head_template_path.open() as head_file:
        head_tpl = Template(head_file.read())

    head_css_file_path = CSS_PATH / "head.css"
    with head_css_file_path.open(encoding="utf-8") as head_css_file:
        head_css = head_css_file.read()

    report = HTMLReport(
        body=body,
        head_tpl=head_tpl,
        head_values={
            "head_css": head_css,
            "version": __version__,
            "page_title": title,
            "display_footer": "style='display: none'" if is_notebook() else "",
        },
    )

    report.resize(*report_dims)

    return report


def _turn_into_full_path(bunch, dir: Path) -> Union[str, tempita.bunch]:
    """Recursively turns str values of a dict into path.

    Used to turn relative paths into full paths.
    """
    if isinstance(bunch, str) and not bunch.startswith(str(dir)):
        return str(dir / bunch)
    tmp = tempita.bunch()
    for k in bunch:
        if isinstance(bunch[k], (dict, str, tempita.bunch)):
            tmp[k] = _turn_into_full_path(bunch[k], dir)
        else:
            tmp[k] = bunch[k]
    return tmp


def _glm_model_attributes_to_dataframe(model):
    """Return a pandas dataframe with pertinent model attributes & information.

    Parameters
    ----------
    model : FirstLevelModel or SecondLevelModel object.

    Returns
    -------
    pandas.DataFrame
        DataFrame with the pertinent attributes of the model.
    """
    model_attributes = pd.DataFrame.from_dict(
        model._attributes_to_dict(),
        orient="index",
    )

    if len(model_attributes) == 0:
        return model_attributes

    attribute_units = {
        "t_r": "seconds",
        "high_pass": "Hertz",
        "smoothing_fwhm": "mm",
    }
    attribute_names_with_units = {
        attribute_name_: attribute_name_ + f" ({attribute_unit_})"
        for attribute_name_, attribute_unit_ in attribute_units.items()
    }
    model_attributes = model_attributes.rename(
        index=attribute_names_with_units
    )
    model_attributes.index.names = ["Parameter"]
    model_attributes.columns = ["Value"]

    return model_attributes


def _mask_to_plot(model, bg_img, cut_coords):
    """Plot a mask image and creates PNG code of it.

    Parameters
    ----------
    model

    bg_img : Niimg-like object
        See :ref:`extracting_data`.
        The background image that the mask will be plotted on top of.
        To turn off background image, just pass "bg_img=None".

    cut_coords


    Returns
    -------
    mask_plot : str
        PNG Image for the mask plot.

    """
    if not is_matplotlib_installed():
        return None
    # Select mask_img to use for plotting
    if not model._is_volume_glm():
        model.masker_._create_figure_for_report()
        fig = plt.gcf()
        mask_plot = figure_to_png_base64(fig)
        # prevents sphinx-gallery & jupyter from scraping & inserting plots
        plt.close()
        return mask_plot

    if isinstance(model.mask_img, NiftiMasker):
        mask_img = model.masker_.mask_img_
    else:
        try:
            # check that mask_img is a niiimg-like object
            check_niimg(model.mask_img)
            mask_img = model.mask_img
        except Exception:
            mask_img = model.masker_.mask_img_

    plot_roi(
        roi_img=mask_img,
        bg_img=bg_img,
        display_mode="z",
        cmap="Set1",
        cut_coords=cut_coords,
        colorbar=False,
    )
    mask_plot = figure_to_png_base64(plt.gcf())
    # prevents sphinx-gallery & jupyter from scraping & inserting plots
    plt.close()

    return mask_plot


@fill_doc
def _make_stat_maps_contrast_clusters(
    stat_img,
    threshold_orig,
    alpha,
    cluster_threshold,
    height_control,
    two_sided,
    min_distance,
    bg_img,
    cut_coords,
    display_mode,
    plot_type,
    # clusters_tsvs,
):
    """Populate a smaller HTML sub-template with the proper values, \
    make a list containing one or more of such components \
    & return the list to be inserted into the HTML Report Template.

    Each component contains the HTML code for
    a contrast & its corresponding statistical maps & cluster table;

    Parameters
    ----------
    stat_img : dictionary of Niimg-like object or SurfaceImage, or None
       Statistical image (presumably in z scale)
       whenever height_control is 'fpr' or None,
       stat_img=None is acceptable.
       If it is 'fdr' or 'bonferroni',
       an error is raised if stat_img is None.

    contrasts_plots : Dict[str, str]
        Contains contrast names & HTML code of the contrast's PNG plot.

    threshold_orig : float
       Desired threshold in z-scale.
       This is used only if height_control is None

    alpha : float
        Number controlling the thresholding (either a p-value or q-value).
        Its actual meaning depends on the height_control parameter.
        This function translates alpha to a z-scale threshold.

    cluster_threshold : float
        Cluster size threshold. In the returned thresholded map,
        sets of connected voxels (`clusters`) with size smaller
        than this number will be removed.

    height_control : string
        False positive control meaning of cluster forming
        threshold: 'fpr' or 'fdr' or 'bonferroni' or None.

    two_sided : `bool`, default=False
        Whether to employ two-sided thresholding or to evaluate positive values
        only.

    min_distance : float, default=8
        For display purposes only.
        Minimum distance between subpeaks in mm.

    bg_img : Niimg-like object
        Only used when plot_type is 'slice'.
        See :ref:`extracting_data`.
        The background image for stat maps to be plotted on upon.
        If nothing is specified, the MNI152 template will be used.
        To turn off background image, just pass "bg_img=False".

    %(cut_coords)s

    display_mode : string
        Choose the direction of the cuts:
        'x' - sagittal, 'y' - coronal, 'z' - axial,
        'l' - sagittal left hemisphere only,
        'r' - sagittal right hemisphere only,
        'ortho' - three cuts are performed in orthogonal directions.

        Possible values are:
        'ortho', 'x', 'y', 'z', 'xz', 'yx', 'yz',
        'l', 'r', 'lr', 'lzr', 'lyr', 'lzry', 'lyrz'.

    plot_type : string {'slice', 'glass'}
        The type of plot to be drawn.

    clusters_tsvs : dictionary of path of to tsv files

    Returns
    -------
    results : dict
        Each key contains
        contrast name, contrast plot, statistical map, cluster table.

    """
    if not display_mode:
        display_mode_selector = {"slice": "z", "glass": "lzry"}
        display_mode = display_mode_selector[plot_type]

    results = {}
    for contrast_name, stat_map_img in stat_img.items():
        # Only use threshold_stats_img to adjust the threshold
        # that we will pass to clustering_params_to_dataframe
        # and _stat_map_to_png
        # Necessary to avoid :
        # https://github.com/nilearn/nilearn/issues/4192

        # We silence further warnings about threshold:
        #   it would throw one per contrast and
        #   and also because threshold_stats_img and make_glm_report
        #   have different defaults.
        # TODO (nilearn>=0.15)
        # remove
        warnings.filterwarnings(
            "ignore",
            category=FutureWarning,
            message=".*default 'threshold' will be set.*",
        )

        thresholded_img, threshold = threshold_stats_img(
            stat_img=stat_map_img,
            threshold=threshold_orig,
            alpha=alpha,
            cluster_threshold=cluster_threshold,
            height_control=height_control,
            two_sided=two_sided,
        )

        table_details = clustering_params_to_dataframe(
            threshold,
            cluster_threshold,
            min_distance,
            height_control,
            alpha,
            is_volume_glm=not isinstance(stat_map_img, SurfaceImage),
        )
        table_details_html = dataframe_to_html(
            table_details,
            precision=3,
            header=False,
        )

        cluster_table_html = """
        <p style="text-align: center; font-size: 200%; color: grey"
            >
            Results table not available for surface data.
        </p>
        """
        if not isinstance(thresholded_img, SurfaceImage):
            # FIXME
            # The commented code below was there to reuse
            # cluster tables generated by save_glm_to_bids
            # to save time.
            # However cluster tables may have been computed
            # with different threshold, cluster_threshol...
            # by save_glm_to_bids than those requested in
            # generate_report.
            # So we are skipping this for now.

            # if clusters_tsvs:
            #     # try to reuse results saved to disk by
            #     # save_glm_to_bids
            #     try:
            #         cluster_table = pd.read_csv(
            #             clusters_tsvs[contrast_name], sep="\t"
            #         )
            #     except Exception:
            #         cluster_table = get_clusters_table(
            #             thresholded_img,
            #             stat_threshold=threshold,
            #             cluster_threshold=cluster_threshold,
            #             min_distance=min_distance,
            #             two_sided=two_sided,
            #         )
            # else:

            cluster_table = get_clusters_table(
                thresholded_img,
                stat_threshold=threshold,
                cluster_threshold=cluster_threshold,
                min_distance=min_distance,
                two_sided=two_sided,
            )
            cluster_table_html = dataframe_to_html(
                cluster_table,
                precision=2,
                index=False,
            )

        stat_map_png, _ = _stat_map_to_png(
            stat_img=thresholded_img,
            threshold=threshold,
            bg_img=bg_img,
            cut_coords=cut_coords,
            display_mode=display_mode,
            plot_type=plot_type,
            table_details=table_details,
            two_sided=two_sided,
        )

        if (
            not isinstance(thresholded_img, SurfaceImage)
            and len(cluster_table) < 2
        ):
            # do not pass anything when nothing survives thresholding
            cluster_table_html = None
            stat_map_png = None

        results[escape(contrast_name)] = tempita.bunch(
            stat_map_img=stat_map_png,
            cluster_table_details=table_details_html,
            cluster_table=cluster_table_html,
        )

    return results


@fill_doc
def _stat_map_to_png(
    stat_img,
    threshold,
    bg_img,
    cut_coords,
    display_mode,
    plot_type,
    table_details,
    two_sided,
):
    """Generate PNG code for a statistical map, \
    including its clustering parameters.

    Parameters
    ----------
    stat_img : Niimg-like object or None
       Statistical image (presumably in z scale),
       to be plotted as slices or glass brain.

    threshold : float
       Desired threshold in z-scale.

    bg_img : Niimg-like object
        Only used when plot_type is 'slice'.
        See :ref:`extracting_data`.
        The background image for stat maps to be plotted on upon.
        If nothing is specified, the MNI152 template will be used.
        To turn off background image, just pass "bg_img=False".

    %(cut_coords)s

    display_mode : string
        Choose the direction of the cuts:
        'x' - sagittal, 'y' - coronal, 'z' - axial,
        'l' - sagittal left hemisphere only,
        'r' - sagittal right hemisphere only,
        'ortho' - three cuts are performed in orthogonal directions.

        Possible values are:
        'ortho', 'x', 'y', 'z', 'xz', 'yx', 'yz',
        'l', 'r', 'lr', 'lzr', 'lyr', 'lzry', 'lyrz'.

    plot_type : string {'slice', 'glass'}
        The type of plot to be drawn.

    table_details : pandas.Dataframe
        Dataframe listing the parameters used for clustering,
        to be included in the plot.

    two_sided : `bool`, default=False
        Whether to employ two-sided thresholding or to evaluate positive values
        only.

    Returns
    -------
    stat_map_png : string
        PNG Image Data representing a statistical map.

    fig : matplotlib figure
        only used for testing

    """
    if not is_matplotlib_installed():
        return None, None

    cmap = DEFAULT_DIVERGING_CMAP

    if two_sided:
        symmetric_cbar = True
        vmin = vmax = None

    else:
        symmetric_cbar = False

        if isinstance(stat_img, SurfaceImage):
            data = get_surface_data(stat_img)
        else:
            stat_img = load_niimg(stat_img)
            data = safe_get_data(stat_img, ensure_finite=True)

        vmin = np.nanmin(data)
        vmax = np.nanmax(data)
        if vmin >= 0.0:
            vmin = 0
            cmap = "Reds"
        elif vmax <= 0.0:
            vmax = 0
            cmap = "Blues_r"

    if isinstance(stat_img, SurfaceImage):
        if not two_sided and threshold < 0:
            # we cannot use negative threshold in plot_surf_stat_map
            # so we flip the sign of the image, the colormap
            # and we relabel the colorbar later
            for k, v in stat_img.data.parts.items():
                stat_img.data.parts[k] = -v
            cmap = "Blues"

        surf_mesh = bg_img.mesh if bg_img else None
        stat_map_plot = plot_surf_stat_map(
            stat_map=stat_img,
            hemi="left",
            bg_map=bg_img,
            surf_mesh=surf_mesh,
            cmap=cmap,
            darkness=None,
            symmetric_cbar=symmetric_cbar,
            threshold=abs(threshold),
        )

        x_label_color = "black"

    else:
        if plot_type == "slice":
            stat_map_plot = plot_stat_map(
                stat_img,
                bg_img=bg_img,
                cut_coords=cut_coords,
                display_mode=display_mode,
                cmap=cmap,
                symmetric_cbar=symmetric_cbar,
                draw_cross=False,
                threshold=abs(threshold),
            )
        elif plot_type == "glass":
            stat_map_plot = plot_glass_brain(
                stat_img,
                display_mode=display_mode,
                plot_abs=False,
                symmetric_cbar=symmetric_cbar,
                cmap=cmap,
                threshold=abs(threshold),
            )
        else:
            raise ValueError(
                "Invalid plot type provided. "
                "Acceptable options are 'slice' or 'glass'."
            )

        x_label_color = "white" if plot_type == "slice" else "black"

    if hasattr(stat_map_plot, "_cbar"):
        cbar_ax = stat_map_plot._cbar.ax
        cbar_ax.set_xlabel(
            "Z score",
            labelpad=5,
            fontweight="bold",
            loc="right",
            color=x_label_color,
        )

        if (
            isinstance(stat_img, SurfaceImage)
            and not two_sided
            and threshold < 0
        ):
            # Because the image has been flipped
            # replace labels with their negative
            ticks = stat_map_plot._cbar.get_ticks()
            stat_map_plot._cbar.set_ticks(ticks)
            stat_map_plot._cbar.set_ticklabels([f"{-t:.2g}" for t in ticks])

    with pd.option_context("display.precision", 2):
        _add_params_to_plot(table_details, stat_map_plot)

    fig = plt.gcf()
    stat_map_png = figure_to_png_base64(fig)
    # prevents sphinx-gallery & jupyter from scraping & inserting plots
    plt.close()

    # the fig is returned for testing
    return stat_map_png, fig


def _add_params_to_plot(table_details, stat_map_plot):
    """Insert thresholding parameters into the stat map plot \
    as figure suptitle.

    Parameters
    ----------
    table_details : Dict[String, Any]
        Dict of parameters and values used in thresholding.

    stat_map_plot : matplotlib.Axes
        Axes object of the stat map plot.

    Returns
    -------
    stat_map_plot : matplotlib.Axes
        Axes object of the stat map plot, with the added suptitle.

    """
    thresholding_params = [
        ":".join([name, str(val)]) for name, val in table_details[0].items()
    ]
    thresholding_params = "  ".join(thresholding_params)
    suptitle_text = plt.suptitle(
        thresholding_params,
        fontsize=11,
        x=0.45,
        wrap=True,
    )
    fig = plt.gcf()
    resize_plot_inches(
        plot=fig,
        width_change=0.2,
        height_change=1,
    )

    if hasattr(stat_map_plot, "_black_bg") and stat_map_plot._black_bg:
        suptitle_text.set_color("w")

    return stat_map_plot
