# -*- 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
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': 'filterval'}

        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
        "filterval" as in the default case shown above. In that case,
        ``magpct_options['filterval']`` gets internally reset to the
        initial value of `filterval` (described above).

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

        .. note::
           The two filter value options (`filterval` and
           ``magpct_options['filterval']``) have different defaults:
           None and 'filterval`, respectively. They also differ on how
           the ``None`` setting is used: for `filterval`, None is
           replaced by 1.e-6 while for `magpct_filterval`, None means
           that the "magpct" plot will not have any filters applied at
           all.

    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": "filterval"}
    else:
        magpct_options = magpct_options.copy()

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

    if isinstance(magpct_filterval, str):
        if magpct_filterval != "filterval":
            raise ValueError(
                "``magpct_options['filterval']`` is an invalid "
                f"string: {magpct_filterval!r} (can only "
                "be 'filterval' if a string)"
            )
        # copy the initial `filterval` setting:
        magpct_filterval = filterval

    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")
    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
    @ytools.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
