# -*- coding: utf-8 -*-
"""
Low level tool for writing percent difference reports. Typically, this
is called via: :func:`cla.DR_Results.rptpct`.
"""
from io import StringIO
from types import SimpleNamespace
import warnings
import numpy as np
import matplotlib.pyplot as plt
from pyyeti import ytools, locate, writer, guitools
from ._utilities import _get_rpt_headers, _get_numform, _proc_filterval
from ._magpct import magpct


__all__ = ["rptpct1"]


# FIXME: We need the str/repr formatting used in Numpy < 1.14.
try:
    np.set_printoptions(legacy="1.13")
except TypeError:
    pass


def _apply_pv(value, pv, oldlen):
    # if value has a len that's > 1, try to partition it down;
    # otherwise, return it as is:
    try:
        n = len(value)
    except TypeError:
        return value
    else:
        if n == 1:
            return value

    # `value` is a vector with len > 1 ... ensure it is a true numpy
    # array:
    value = np.atleast_1d(value)

    # oldlen is either 0 (for `value` vectors that are expected to be
    # full size ... currently, only the `filterval` and
    # `magpct_filterval` vectors), or it is the length of the
    # dimension that the `value` index type of partition vector
    # (currently, only the `ignorepv` vector) was originally defined
    # to partition.
    if oldlen == 0:
        # `value` is `filterval` or `magpct_filterval` ... these just
        # need to be partitioned down:
        newvalue = value[pv]
    else:
        # `value` is `ignorepv` ... it needs to be redefined to
        # correspond to reduced size:
        truefalse = locate.index2bool(value, oldlen)
        newvalue = truefalse[pv].nonzero()[0]
    return newvalue


def _align_mxmn(mxmn1, mxmn2, labels2, row_number, infodct):
    if infodct["labels"] and infodct["labels"] != labels2:
        n = len(infodct["labels"])
        pv1, pv2 = locate.list_intersect(infodct["labels"], labels2)
        mxmn1 = mxmn1[pv1]
        mxmn2 = mxmn2[pv2]
        infodct["labels"] = [infodct["labels"][i] for i in pv1]
        row_number = row_number[pv1]
        infodct["filterval"] = _apply_pv(infodct["filterval"], pv1, 0)
        infodct["magpct_filterval"] = _apply_pv(infodct["magpct_filterval"], pv1, 0)
        infodct["ignorepv"] = _apply_pv(infodct["ignorepv"], pv1, n)
    return mxmn1, mxmn2, row_number


def _get_filtline(filterval):
    if len(filterval) > 1:
        filtline = "Filter:      <defined row-by-row>\n"
    else:
        filtline = f"Filter:      {filterval[0]}\n"
    return filtline


def _get_noteline(use_range, names, prtbads, flagbads):
    noteline = "Notes:       "
    tab = "             "
    if not use_range:
        noteline += "% Diff = +/- abs(({0}-{1})/{1})*100\n".format(*names)
    else:
        noteline += "% Diff = +/- abs({0}-{1})/max(abs({1}(max,min)))*100\n".format(
            *names
        )

    noteline += tab + "Sign set such that positive % differences indicate exceedances\n"
    prtbad, prtbadh, prtbadl = prtbads
    flagbad, flagbadh, flagbadl = flagbads
    if prtbad is not None or prtbadh is not None or prtbadl is not None:
        if prtbad is not None:
            prtbad = abs(prtbad)
            noteline += tab + f"Printing rows where abs(% Diff) > {prtbad}%\n"
        elif prtbadh is not None:
            noteline += tab + f"Printing rows where % Diff > {prtbadh}%\n"
        else:
            noteline += tab + f"Printing rows where % Diff < {prtbadl}%\n"

    if flagbad is not None or flagbadh is not None or flagbadl is not None:
        if flagbad is not None:
            flagbad = abs(flagbad)
            noteline += tab + f"Flagging (*) rows where abs(% Diff) > {flagbad}%\n"
        elif flagbadh is not None:
            noteline += tab + f"Flagging (*) rows where % Diff > {flagbadh}%\n"
        else:
            noteline += tab + f"Flagging (*) rows where % Diff < {flagbadl}%\n"
    return noteline


def _get_badpv(pct, pv, bad, badh, badl, defaultpv=False):
    if bad is not None or badh is not None or badl is not None:
        badpv = pv.copy()
        if bad is not None:
            badpv &= abs(pct) > bad
        elif badh is not None:
            badpv &= pct > badh
        else:
            badpv &= pct < badl
    else:
        badpv = np.empty(len(pct), bool)
        badpv[:] = defaultpv
    return badpv


def _get_pct_diff(a, b, filt, pv, nastring, mxmn_b=None, ismax=True, flagbads=None):
    # either can pass filter to be kept:
    pv &= (abs(a) > filt) | (abs(b) > filt)

    if mxmn_b is not None:
        denom = np.nanmax(abs(mxmn_b), axis=1)
    else:
        denom = abs(b)

    # put 1's in for filtered values ... this is temporary
    a = a.copy()
    b = b.copy()
    a[~pv] = 1.0
    b[~pv] = 1.0

    z = denom == 0.0
    denom[z] = 1.0
    pct = 100 * abs(a - b) / denom
    pct[z] = 100.0  # np.inf

    # make less extreme values negative
    neg = a < b if ismax else a > b
    pct[neg] *= -1.0

    # put nan's in for the filtered or n/a rows:
    pct[~pv] = np.nan

    # make 7 char version:
    spct = [f"{p:7.2f}" for p in pct]
    badpv = _get_badpv(pct, pv, *flagbads, False)
    for j in badpv.nonzero()[0]:
        spct[j] += "*"
    for j in (~pv).nonzero()[0]:
        spct[j] = nastring

    return pct, spct


def _get_histogram_str(desc, hdr, pctinfo):
    pctcount = pctinfo["hsto"]
    s = [
        (f"\n\n    {desc} - {hdr} Comparison Histogram\n\n"),
        ("      % Diff      Count    Percent\n     --------   --------   -------\n"),
    ]
    with StringIO() as f:
        writer.vecwrite(f, "     {:8.2f}   {:8.0f}   {:7.2f}\n", pctcount)
        s.append(f.getvalue())
        s.append("\n")

    # total_percent_10 will either be 0 or 1000:
    #  - 0 if all % diffs are "n/a"
    #  - 1000 otherwise
    total_percent_10 = np.round(pctcount[:, 2].sum() * 10)
    last = -1.0
    for pdiff in [1, 2, 5, 10, 15, 20, 25, 50, 100, 500]:
        pvdiff = abs(pctcount[:, 0]) <= pdiff
        num = pctcount[pvdiff, 2].sum()
        if num > last:
            s.append(f"    {num:.1f}% of values are within {pdiff}%\n")
        if np.round(num * 10) == total_percent_10:
            break
        last = num

    pct = pctinfo["pct"]
    n = len(pct)
    if n == 0:
        s.append(
            "\n    % Diff Statistics: [Min, Max, Mean, StdDev]"
            " = [n/a, n/a, n/a, n/a]\n"
        )
    else:
        stddev = 0.0 if n <= 1 else pct.std(ddof=1)
        s.append(
            "\n    % Diff Statistics: [Min, Max, Mean, StdDev]"
            f" = [{pct.min():.2f}, {pct.max():.2f}, {pct.mean():.4f}, {stddev:.4f}]\n"
        )
    return "".join(s)


def _proc_pct(
    ext1,
    ext2,
    filterval,
    magpct_filterval,
    *,
    names,
    mxmn1,
    comppv,
    mxmn_b,
    ismax,
    histogram_inc,
    prtbads,
    flagbads,
    numform,
    valhdr,
    maxhdr,
    minhdr,
    absmhdr,
    pdhdr,
    nastring,
    doabsmax,
    shortabsmax,
    print_info,
):
    # handle magpct stuff here:
    mag = ext1[comppv], ext2[comppv]
    if magpct_filterval is not None and len(magpct_filterval) > 1:
        magfilt = magpct_filterval[comppv]
    else:
        magfilt = magpct_filterval

    pv = comppv.copy()
    pct, spct = _get_pct_diff(
        ext1,
        ext2,
        filterval,
        pv,
        nastring,
        mxmn_b=mxmn_b,
        ismax=ismax,
        flagbads=flagbads,
    )
    pct_ret = pct[pv]
    hsto = ytools.histogram(pct_ret, histogram_inc)

    # for trimming down if prtbad set:
    prtpv = _get_badpv(pct, pv, *prtbads, True)
    pctlen = max(len(pdhdr), len(max(spct, key=len)))
    sformatpd = f"{{:{pctlen}}}"

    # for writer.formheader:
    numlen = max(13, len(max(names, key=len)), len(numform.format(np.pi)))
    if not doabsmax:
        print_info.headers1.extend([*names, ""])
        print_info.headers2.extend([valhdr, valhdr, pdhdr])
        print_info.formats.extend([numform, numform, sformatpd])
        print_info.printargs.extend([ext1, ext2, spct])
        print_info.widths.extend([numlen, numlen, pctlen])
        print_info.seps.extend([4, 2, 2])
        print_info.justs.extend(["c", "c", "c"])
    elif shortabsmax:
        print_info.headers1.extend([*names, ""])
        print_info.headers2.extend([absmhdr, absmhdr, pdhdr])
        print_info.formats.extend([numform, numform, sformatpd])
        print_info.printargs.extend([ext1, ext2, spct])
        print_info.widths.extend([numlen, numlen, pctlen])
        print_info.seps.extend([4, 2, 2])
        print_info.justs.extend(["c", "c", "c"])
    else:
        print_info.headers1.extend([names[0], names[0], names[0], names[1], ""])
        print_info.headers2.extend([maxhdr, minhdr, absmhdr, absmhdr, pdhdr])
        print_info.formats.extend([numform, numform, numform, numform, sformatpd])
        print_info.printargs.extend([mxmn1[:, 0], mxmn1[:, 1], ext1, ext2, spct])
        print_info.widths.extend([numlen, numlen, numlen, numlen, pctlen])
        print_info.seps.extend([4, 2, 2, 2, 2])
        print_info.justs.extend(["c", "c", "c", "c", "c"])
    return dict(
        pct=pct_ret, spct=spct, hsto=hsto, prtpv=prtpv, mag=mag, magfilt=magfilt
    )


def _figure_on(name, doabsmax, show_figures):
    figsize = [8.5, 11.0]
    if doabsmax:
        figsize[1] /= 3.0
    if show_figures:
        plt.figure(name, figsize=figsize)
        plt.clf()
    else:
        plt.figure(figsize=figsize)


def _figure_off(show_figures):
    if not show_figures:
        plt.close()


def _prep_subplot(pctinfo, sp):
    if "mx" in pctinfo:
        # if not just doing absmax
        if sp > 311:
            plt.subplot(sp, sharex=plt.gca())
        else:
            plt.subplot(sp)


def _plot_magpct(
    pctinfo,
    names,
    desc,
    doabsmax,
    filename,
    magpct_options,
    use_range,
    maxhdr,
    minhdr,
    absmhdr,
    show_figures,
    tight_layout_args,
):
    ptitle = f"{desc} - {{}} Comparison vs Magnitude"
    xl = f"{names[1]} Magnitude"
    yl = f"% Diff of {names[0]} vs {names[1]}"
    _figure_on("Magpct - " + desc, doabsmax, show_figures)
    try:
        for lbl, hdr, sp, ismax in (
            ("mx", maxhdr, 311, True),
            ("mn", minhdr, 312, False),
            ("amx", absmhdr, 313, True),
        ):
            _prep_subplot(pctinfo, sp)
            if lbl in pctinfo:
                if use_range:
                    ref = pctinfo["amx"]["mag"][1]
                else:
                    ref = None
                magpct(
                    pctinfo[lbl]["mag"][0],
                    pctinfo[lbl]["mag"][1],
                    Ref=ref,
                    ismax=ismax,
                    filterval=pctinfo[lbl]["magfilt"],
                    **magpct_options,
                )
                plt.title(ptitle.format(hdr))
                plt.xlabel(xl)
                plt.ylabel(yl)
            plt.grid(True)
        plt.tight_layout(**tight_layout_args)
        if isinstance(filename, str):
            plt.savefig(filename + ".magpct.png")
    finally:
        _figure_off(show_figures)


def _plot_histogram(
    pctinfo,
    names,
    desc,
    doabsmax,
    filename,
    histogram_inc,
    maxhdr,
    minhdr,
    absmhdr,
    show_figures,
    tight_layout_args,
):
    ptitle = f"{desc} - {{}} Comparison Histogram"
    xl = f"% Diff of {names[0]} vs {names[1]}"
    yl = "Percent Occurrence (%)"
    _figure_on("Histogram - " + desc, doabsmax, show_figures)
    try:
        for lbl, hdr, sp in (
            ("mx", maxhdr, 311),
            ("mn", minhdr, 312),
            ("amx", absmhdr, 313),
        ):
            _prep_subplot(pctinfo, sp)
            if lbl in pctinfo:
                width = histogram_inc
                x = pctinfo[lbl]["hsto"][:, 0]
                y = pctinfo[lbl]["hsto"][:, 2]
                colors = ["b"] * len(x)
                ax = abs(x)
                pv1 = ((ax > 5) & (ax <= 10)).nonzero()[0]
                pv2 = (ax > 10).nonzero()[0]
                for pv, c in ((pv1, "m"), (pv2, "r")):
                    for i in pv:
                        colors[i] = c
                plt.bar(x, y, width=width, color=colors, align="center")
                plt.title(ptitle.format(hdr))
                plt.xlabel(xl)
                plt.ylabel(yl)
                x = abs(max(plt.xlim(), key=abs))
                if x < 5:
                    plt.xlim(-5, 5)
            plt.grid(True)
        plt.tight_layout(**tight_layout_args)
        if isinstance(filename, str):
            plt.savefig(filename + ".histogram.png")
    finally:
        _figure_off(show_figures)


def rptpct1(
    mxmn1,
    mxmn2,
    filename,
    *,
    title="PERCENT DIFFERENCE REPORT",
    names=("Self", "Reference"),
    desc=None,
    filterval=None,
    labels=None,
    units=None,
    ignorepv=None,
    uf_reds=None,
    use_range=True,
    numform=None,
    prtbad=None,
    prtbadh=None,
    prtbadl=None,
    flagbad=None,
    flagbadh=None,
    flagbadl=None,
    dohistogram=True,
    histogram_inc=1.0,
    domagpct=True,
    magpct_options=None,
    doabsmax=False,
    shortabsmax=False,
    roundvals=-1,
    rowhdr="Row",
    deschdr="Description",
    maxhdr="Maximum",
    minhdr="Minimum",
    absmhdr="Abs-Max",
    perpage=-1,
    tight_layout_args=None,
    show_figures=False,
    align_by_label=True,
):
    """
    Write a percent difference report between 2 sets of max/min data

    Parameters
    ----------
    mxmn1 : 2d array_like or SimpleNamespace
        The max/min data to compare to the `mxmn2` set. If 2-column
        array_like, its columns are: [max, min]. If SimpleNamespace,
        it must be as defined in :class:`DR_Results` and have these
        members:

        .. code-block:: none

            .ext = [max, min]
            .drminfo = SimpleNamespace which has (at least):
               .desc      = one line description of category
               .filterval = the filter value; (see `filterval`
                            description below)
               .labels    = a list of descriptions; one per row
               .ignorepv  = these rows will get 'n/a' for % diff
               .units     = string with units
               .uf_reds   = uncertainty factors

        Note that the inputs `desc`, `labels`, etc, override the
        values above.
    mxmn2 : 2d array_like or SimpleNamespace
        The reference set of max/min data. Format is the same as
        `mxmn1`.

        .. note::
            If both `mxmn1` and `mxmn2` are SimpleNamespaces and have
            the ``.drminfo.labels`` attribute, this routine will, by
            default, use the labels to align the data sets for
            comparison. To prevent this, set the `align_by_label`
            parameter to False.

    filename : string or file_like or 1 or None
        Either a name of a file, or is a file_like object as returned
        by :func:`open` or :class:`io.StringIO`. Input as integer 1 to
        write to stdout. Can also be the name of a directory or None;
        in these cases, a GUI is opened for file selection.
    title : string; must be named; optional
        Title for the report
    names : list/tuple; must be named; optional
        Two (short) strings identifying the two sets of data
    desc : string or None; must be named; optional
        A one line description of the table. Overrides
        `mxmn1.drminfo.desc`. If neither are input,
        'No description provided' is used.
    filterval : scalar, 1d array_like or None; must be named; optional
        Numbers with absolute value <= than `filterval` will get a
        'n/a' % diff. If vector, length must match number of rows in
        `mxmn1` and `mxmn2` data. Overrides `mxmn1.drminfo.filterval`.
        If neither are input, `filterval` is set to 1.e-6.
    labels : list or None; must be named; optional
        A list of strings briefly describing each row. Overrides
        `mxmn1.drminfo.labels`. If neither are input,
        ``['Row 1','Row 2',...]`` is used.
    units : string or None; must be named; optional
        Specifies the units. Overrides `mxmn1.drminfo.units`. If
        neither are input, 'Not specified' is used.
    ignorepv : 1d array or None; must be named; optional
        0-offset index vector specifying which rows of `mxmn1` to
        ignore (they get the 'n/a' % diff). Overrides
        `mxmn1.drminfo.ignorepv`. If neither are input, no rows are
        ignored (though `filterval` is still used).

        .. note::
            `ignorepv` applies *before* any alignment by labels is
            done (when `align_by_label` is True, which is the
            default).

    uf_reds : 1d array or None; must be named; optional
        Uncertainty factors: [rigid, elastic, dynamic, static].
        Overrides `mxmn1.drminfo.uf_reds`. If neither is input,
        'Not specified' is used.
    use_range : bool; must be named, optional
        If True, the denominator of the % diff calc for both the max
        & min for each row is the absolute maximum of the reference
        max & min for that row. If False, the denominator is the
        applicable reference max or min. A quick example shows why
        ``use_range=True`` might be useful:

        .. code-block:: none

            If [max1, min1] = [12345, -10] and
               [max2, min2] = [12300,  50]
            Then:
               % diff = [0.37%,   0.49%]  if use_range is True
               % diff = [0.37%, 120.00%]  if use_range is False

        Note that the sign of the % diff is defined such that a
        positive % diff means an exceedance: where ``max1 > max2`` or
        ``min1 < min2``.

        `use_range` is ignored if `doabsmax` is True.

    numform : string or None; must be named; optional
        Format of the max & min numbers. If None, it is set internally
        to be 13 chars wide and depends on the range of numbers to
        print:

           - if range is "small", numform='{:13.xf}' where "x" ranges
             from 0 to 7
           - if range is "large", numform='{:13.6e}'

    prtbad : scalar or None; must be named; optional
        Only print rows where ``abs(%diff) > prtbad``. For example, to
        print rows off by more than 5%, use ``prtbad=5``. `prtbad`
        takes precedence over `prtbadh` and `prtbadl`.
    prtbadh : scalar or None; must be named; optional
        Only print rows where ``%diff > prtbadh``. Handy for showing
        just the exceedances. `prtbadh` takes precedence over
        `prtbadl`.
    prtbadl : scalar or None; must be named; optional
        Only print rows where ``%diff < prtbadl``. Handy for showing
        where reference rows are higher.
    flagbad : scalar or None; must be named; optional
        Flag % diffs where ``abs(%diff) > flagbad``. Works similar to
        `prtbad`. The flag is an asterisk (*).
    flagbadh : scalar or None; must be named; optional
        Flag % diffs where ``%diff > flagbadh``. Works similar to
        `prtbadh`. Handy for flagging exceedances. `flagbadh` takes
        precedence over `flagbadl`.
    flagbadl : scalar or None; must be named; optional
        Flag % diffs where ``%diff < flagbadl``. Works similar to
        `prtbadl`.
    dohistogram : bool; must be named; optional
        If True, plot the histograms. Plots will be written to
        "`filename`.histogram.png".
    histogram_inc : scalar; must be named; optional
        The histogram increment; defaults to 1.0 (for 1%).
    domagpct : bool; must be named; optional
        If True, plot the percent differences versus magnitude via
        :func:`magpct`. Plots will be written to
        "`filename`.magpct.png". Filtering for the "magpct" plot is
        controlled by the ``magpct_options['filterval']`` and
        ``magpct_options['symlogy']`` options. By default, all percent
        differences are shown, but the larger values (according to the
        `filterval` filter) are emphasized by using a mixed linear/log
        y-axis. The percent differences for the `ignorepv` rows are
        not plotted.
    magpct_options : None or dict; must be named; optional
        If None, it is internally reset to::

            magpct_options = {'filterval': 'same'}

        Use this parameter to provide any options to :func:`magpct`
        but note that the `filterval` option for :func:`magpct` is
        treated specially. Here, in addition to any of the values that
        :func:`magpct` accepts, it can also be set to the string
        "same" as in the default case shown above. If set to "same",
        ``magpct_options['filterval']`` gets internally reset to the
        final value of `filterval` so that the comparison table, the
        histogram, and the magpct plot all use the same filter value.
        For backward compatibility, the string "filterval" is accepted
        as well and works like "same".

        .. note::
            The call to :func:`magpct` is *after* applying `ignorepv`
            and doing any data aligning by labels.

        .. note::
           Unless the :func:`magpct` option `plot_all` is set to
           False, all values (even those smaller than `filterval`) are
           compared and shown on the :func:`magpct` plot in the shaded
           region.

    doabsmax : bool; must be named; optional
        If True, compare only absolute maximums.
    shortabsmax : bool; must be named; optional
        If True, set ``doabsmax=True`` and do not print the max1 and
        min1 columns.
    roundvals : integer; must be named; optional
        Round max & min numbers at specified decimal. If negative, no
        rounding.
    rowhdr : string; must be named; optional
        Header for row number column
    deschdr : string; must be named; optional
        Header for description column
    maxhdr : string; must be named; optional
        Header for the column 1 data
    minhdr : string; must be named; optional
        Header for the column 2 data
    absmhdr : string; must be named; optional
        Header for abs-max column
    perpage : integer; must be named; optional
        The number of lines to write perpage. If < 1, there is no
        limit (one page).
    tight_layout_args : dict or None; must be named; optional
        Arguments for :func:`matplotlib.pyplot.tight_layout`. If None,
        defaults to ``{'pad': 3.0}``.
    show_figures : bool; must be named; optional
        If True, plot figures will be displayed on the screen for
        interactive viewing. Warning: there may be many figures.
    align_by_label : bool; must be named; optional
        If True, use labels to align the two sets of data for
        comparison. See note above under the `mxmn2` option.

    Returns
    -------
    pdiff_info : dict
        Dictionary with 'amx' (abs-max), 'mx' (max), and 'mn' keys:

        .. code-block:: none

            <class 'dict'>[n=3]
                'amx': <class 'dict'>[n=5]
                    'hsto' : float64 ndarray 33 elems: (11, 3)
                    'mag'  : [n=2]: (float64 ndarray: (100,), ...
                    'pct'  : float64 ndarray 100 elems: (100,)
                    'prtpv': bool ndarray 100 elems: (100,)
                    'spct' : [n=100]: ['  -2.46', '  -1.50', ...
                'mn' : <class 'dict'>[n=5]
                    'hsto' : float64 ndarray 33 elems: (11, 3)
                    'mag'  : [n=2]: (float64 ndarray: (100,), ...
                    'pct'  : float64 ndarray 100 elems: (100,)
                    'prtpv': bool ndarray 100 elems: (100,)
                    'spct' : [n=100]: ['   1.55', '   1.53', ...
                'mx' : <class 'dict'>[n=5]
                    'hsto' : float64 ndarray 27 elems: (9, 3)
                    'mag'  : [n=2]: (float64 ndarray: (100,), ...
                    'pct'  : float64 ndarray 100 elems: (100,)
                    'prtpv': bool ndarray 100 elems: (100,)
                    'spct' : [n=100]: ['  -2.46', '  -1.50', ...

        Where:

        .. code-block:: none

            'hsto'  : output of :func:`histogram`: [center, count, %]
            'mag'   : inputs to :func:`magpct`
            'pct'   : percent differences
            'prtpv' : rows to print partition vector
            'spct'  : string version of 'pct'

    Examples
    --------
    >>> import numpy as np
    >>> from pyyeti import cla
    >>> ext1 = [[120.0, -8.0],
    ...         [8.0, -120.0]]
    >>> ext2 = [[115.0, -5.0],
    ...         [10.0, -125.0]]

    Run :func:`rptpct1` multiple times to get a more complete picture
    of all the output (the table is very wide). Also, the plots will
    be turned off for this example.

    First, the header:

    >>> opts = {'domagpct': False, 'dohistogram': False}
    >>> dct = cla.rptpct1(ext1, ext2, 1, **opts)  # doctest: +ELLIPSIS
    PERCENT DIFFERENCE REPORT
    <BLANKLINE>
    Description: No description provided
    Uncertainty: Not specified
    Units:       Not specified
    Filter:      1e-06
    Notes:       % Diff = +/- abs(Self-Reference)/max(abs(Reference...
                 Sign set such that positive % differences indicate...
    Date:        ...
    ...

    Then, the max/min/absmax percent difference table in 3 calls:

    >>> dct = cla.rptpct1(ext1, ext2, 1, **opts)  # doctest: +ELLIPSIS
    PERCENT DIFFERENCE REPORT
    ...
                                 Self        Reference             ...
      Row    Description       Maximum        Maximum      % Diff  ...
    -------  -----------    -------------  -------------  -------  ...
          1  Row      1         120.00000      115.00000     4.35  ...
          2  Row      2           8.00000       10.00000    -1.60  ...
    ...
    >>> dct = cla.rptpct1(ext1, ext2, 1, **opts)  # doctest: +ELLIPSIS
    PERCENT DIFFERENCE REPORT
    ...
                         ...     Self        Reference             ...
      Row    Description ...   Minimum        Minimum      % Diff  ...
    -------  ----------- ...-------------  -------------  -------  ...
          1  Row      1  ...     -8.00000       -5.00000     2.61  ...
          2  Row      2  ...   -120.00000     -125.00000    -4.00  ...
    ...
    >>> dct = cla.rptpct1(ext1, ext2, 1, **opts)  # doctest: +ELLIPSIS
    PERCENT DIFFERENCE REPORT
    ...
                         ...     Self        Reference
      Row    Description ...   Abs-Max        Abs-Max      % Diff
    -------  ----------- ...-------------  -------------  -------
          1  Row      1  ...    120.00000      115.00000     4.35
          2  Row      2  ...    120.00000      125.00000    -4.00
    ...

    Finally, the histogram summaries:

    >>> dct = cla.rptpct1(ext1, ext2, 1, **opts)  # doctest: +ELLIPSIS
    PERCENT DIFFERENCE REPORT
    ...
        No description provided - Maximum Comparison Histogram
    <BLANKLINE>
          % Diff      Count    Percent
         --------   --------   -------
            -2.00          1     50.00
             4.00          1     50.00
    <BLANKLINE>
        0.0% of values are within 1%
        50.0% of values are within 2%
        100.0% of values are within 5%
    <BLANKLINE>
        % Diff Statistics: [Min, Max, Mean, StdDev] = [-1.60, 4.35,...
    <BLANKLINE>
    <BLANKLINE>
        No description provided - Minimum Comparison Histogram
    <BLANKLINE>
          % Diff      Count    Percent
         --------   --------   -------
            -4.00          1     50.00
             3.00          1     50.00
    <BLANKLINE>
        0.0% of values are within 1%
        100.0% of values are within 5%
    <BLANKLINE>
        % Diff Statistics: [Min, Max, Mean, StdDev] = [-4.00, 2.61,...
    <BLANKLINE>
    <BLANKLINE>
        No description provided - Abs-Max Comparison Histogram
    <BLANKLINE>
          % Diff      Count    Percent
         --------   --------   -------
            -4.00          1     50.00
             4.00          1     50.00
    <BLANKLINE>
        0.0% of values are within 1%
        100.0% of values are within 5%
    <BLANKLINE>
        % Diff Statistics: [Min, Max, Mean, StdDev] = [-4.00, 4.35,...
    """
    if tight_layout_args is None:
        tight_layout_args = {"pad": 3.0}

    if magpct_options is None:
        magpct_options = {"filterval": "same"}
    else:
        magpct_options = magpct_options.copy()

    # magpct_options['filterval'] gets special treatment:
    magpct_filterval = magpct_options["filterval"]
    del magpct_options["filterval"]

    reset_magpct_filterval = False
    if isinstance(magpct_filterval, str):
        if magpct_filterval in ("same", "filterval"):
            reset_magpct_filterval = True
            magpct_filterval = None
        else:
            raise ValueError(
                "``magpct_options['filterval']`` is an invalid "
                f"string: {magpct_filterval!r} (can only "
                "be 'filterval' if a string)"
            )

    infovars = (
        "desc",
        "filterval",
        "magpct_filterval",
        "labels",
        "units",
        "ignorepv",
        "uf_reds",
    )
    dct = locals()
    infodct = {n: dct[n] for n in infovars}
    del dct

    # check mxmn1:
    if isinstance(mxmn1, SimpleNamespace):
        sns = mxmn1.drminfo
        for key, value in infodct.items():
            if value is None:
                infodct[key] = getattr(sns, key, None)
        del sns
        mxmn1 = mxmn1.ext
    else:
        mxmn1 = np.atleast_2d(mxmn1)
    row_number = np.arange(1, mxmn1.shape[0] + 1)

    # check mxmn2:
    if isinstance(mxmn2, SimpleNamespace) and getattr(mxmn2, "drminfo", None):
        labels2 = mxmn2.drminfo.labels
        mxmn2 = mxmn2.ext
        if align_by_label:
            # use labels and labels2 to align data; this is in case
            # the two sets of results recover some of the same items,
            # but not all
            mxmn1, mxmn2, row_number = _align_mxmn(
                mxmn1, mxmn2, labels2, row_number, infodct
            )
    else:
        mxmn2 = np.atleast_2d(mxmn2)

    desc = infodct["desc"]
    if desc is None:
        desc = "No description provided"

    R = mxmn1.shape[0]
    if R != mxmn2.shape[0]:
        raise ValueError(
            f"`mxmn1` and `mxmn2` have a different number of rows: "
            f"{R} vs {mxmn2.shape[0]} for category with `desc` = {desc}"
        )

    filterval = infodct["filterval"]
    magpct_filterval = infodct["magpct_filterval"]
    labels = infodct["labels"]
    units = infodct["units"]
    ignorepv = infodct["ignorepv"]
    uf_reds = infodct["uf_reds"]
    del infodct

    if filterval is None:
        filterval = 1.0e-6
    filterval = _proc_filterval(filterval, R, "filterval")
    if reset_magpct_filterval:
        magpct_filterval = filterval
    else:
        magpct_filterval = _proc_filterval(
            magpct_filterval, R, "magpct_options['filterval']"
        )

    if labels is None:
        labels = [f"Row {i + 1:6d}" for i in range(R)]
    elif len(labels) != R:
        raise ValueError(
            "length of `labels` does not match number"
            f" of rows in `mxmn1`: {len(labels)} vs {R} for "
            f"category with `desc` = {desc}"
        )
    if units is None:
        units = "Not specified"
    if numform is None:
        numform = _get_numform(mxmn1)

    pdhdr = "% Diff"
    nastring = "n/a "
    comppv = np.ones(R, bool)
    if ignorepv is not None:
        comppv[ignorepv] = False

    # for row labels:
    w = max(11, len(max(labels, key=len)))
    frm = f"{{:{w}}}"

    # start preparing for writer.formheader:
    print_info = SimpleNamespace(
        headers1=["", ""],
        headers2=[rowhdr, deschdr],
        formats=["{:7d}", frm],
        printargs=[row_number, labels],
        widths=[7, w],
        seps=[0, 2],
        justs=["c", "l"],
    )

    if shortabsmax:
        doabsmax = True
    if doabsmax:
        use_range = False
    if roundvals > -1:
        mxmn1 = np.round(mxmn1, roundvals)
        mxmn2 = np.round(mxmn2, roundvals)

    prtbads = (prtbad, prtbadh, prtbadl)
    flagbads = (flagbad, flagbadh, flagbadl)

    # compute percent differences
    pctinfo = {}
    kwargs = dict(
        names=names,
        mxmn1=mxmn1,
        comppv=comppv,
        histogram_inc=histogram_inc,
        numform=numform,
        prtbads=prtbads,
        flagbads=flagbads,
        maxhdr=maxhdr,
        minhdr=minhdr,
        absmhdr=absmhdr,
        pdhdr=pdhdr,
        nastring=nastring,
        doabsmax=doabsmax,
        shortabsmax=shortabsmax,
        print_info=print_info,
    )
    with warnings.catch_warnings():
        warnings.filterwarnings("ignore", r"All-NaN (slice|axis) encountered")
        mx1 = np.nanmax(abs(mxmn1), axis=1)
        mx2 = np.nanmax(abs(mxmn2), axis=1)
    if not doabsmax:
        max1, min1 = mxmn1[:, 0], mxmn1[:, 1]
        max2, min2 = mxmn2[:, 0], mxmn2[:, 1]
        mxmn_b = mxmn2 if use_range else None
        prtpv = np.zeros(R, bool)
        for i in zip(
            ("mx", "mn", "amx"),
            (max1, min1, mx1),
            (max2, min2, mx2),
            (True, False, True),
            (maxhdr, minhdr, absmhdr),
        ):
            lbl, ext1, ext2, ismax, valhdr = i
            pctinfo[lbl] = _proc_pct(
                ext1,
                ext2,
                filterval,
                magpct_filterval,
                mxmn_b=mxmn_b,
                ismax=ismax,
                valhdr=valhdr,
                **kwargs,
            )
            prtpv |= pctinfo[lbl]["prtpv"]
        prtpv &= comppv
    else:
        pctinfo["amx"] = _proc_pct(
            mx1,
            mx2,
            filterval,
            magpct_filterval,
            mxmn_b=None,
            ismax=True,
            valhdr=absmhdr,
            **kwargs,
        )
        prtpv = pctinfo["amx"]["prtpv"]
    hu, frm = writer.formheader(
        [print_info.headers1, print_info.headers2],
        print_info.widths,
        print_info.formats,
        sep=print_info.seps,
        just=print_info.justs,
    )

    # format page header:
    misc = _get_filtline(filterval) + _get_noteline(use_range, names, prtbads, flagbads)
    hdrs = _get_rpt_headers(desc=desc, uf_reds=uf_reds, units=units, misc=misc)
    header = title + "\n\n" + hdrs + "\n"

    imode = plt.isinteractive()
    plt.interactive(show_figures)
    try:
        if domagpct:
            _plot_magpct(
                pctinfo,
                names,
                desc,
                doabsmax,
                filename,
                magpct_options,
                use_range,
                maxhdr,
                minhdr,
                absmhdr,
                show_figures,
                tight_layout_args,
            )
        if dohistogram:
            _plot_histogram(
                pctinfo,
                names,
                desc,
                doabsmax,
                filename,
                histogram_inc,
                maxhdr,
                minhdr,
                absmhdr,
                show_figures,
                tight_layout_args,
            )
    finally:
        plt.interactive(imode)

    # write results
    @guitools.write_text_file
    def _wtcmp(f, header, hu, frm, printargs, perpage, prtpv, pctinfo, desc):
        prtpv = prtpv.nonzero()[0]
        if perpage < 1:
            # one additional in case size is zero
            perpage = prtpv.size + 1
        pages = (prtpv.size + perpage - 1) // perpage
        if prtpv.size < len(printargs[0]):
            for i, item in enumerate(printargs):
                printargs[i] = [item[j] for j in prtpv]
        tabhead = header + hu
        pager = "\n"  # + chr(12)
        for p in range(pages):
            if p > 0:
                f.write(pager)
            f.write(tabhead)
            b = p * perpage
            e = b + perpage
            writer.vecwrite(f, frm, *printargs, so=slice(b, e))
        f.write(pager)
        for lbl, hdr in zip(("mx", "mn", "amx"), (maxhdr, minhdr, absmhdr)):
            if lbl in pctinfo:
                f.write(_get_histogram_str(desc, hdr, pctinfo[lbl]))

    _wtcmp(
        filename, header, hu, frm, print_info.printargs, perpage, prtpv, pctinfo, desc
    )
    return pctinfo
