# Copyright 2014 Adobe. All rights reserved.

"""
Auto-hinting program for PostScript, OpenType/CFF and UFO fonts.
"""

import argparse
import logging
import os
import re
import subprocess
import sys
import textwrap

from . import __version__, get_font_format
from .autohint import ACOptions, hintFiles


FONTINFO_FILE_NAME = 'fontinfo'

GENERAL_INFO = """
psautohint is a tool for hinting fonts. It supports the following formats:
Type 1 (PFA and PFB), CFF (a.k.a. Type 2), OTF (name- and CID-keyed), and UFO.

By default, psautohint will hint all glyphs in the input font. Options are
available to specify a subset of the glyphs for hinting and other settings.

Note that the hinting results are better if the font's alignment zones are set
properly. For a font supporting a bicameral script (e.g. Latin, Greek, or
Cyrillic) the zones should capture the baseline, the x-height and the capital
height, and respective overshoots of the glyphs, at the very least.
Additionally, ascender, descender, small cap, and figures heights may also be
considered.

The reports provided by the stemHist tool can be useful for choosing alignment
zone and stem width values.
"""

FDDICT_DOC = r"""
By default, psautohint uses the font's global alignment zones and stem widths
when hinting each glyph. However, if there is a file named 'fontinfo' in the
same directory as the input font file, psautohint will search it for
definitions of sets of alignment zones (a.k.a 'FDDict'), and for matching
lists of glyphs to which each FDDict should be applied to. This approach
allows a group of glyphs to be hinted using a different set of zones and stem
widths than other glyphs. This solution isn't as robust as having multiple
hint dictionaries --supported in CID fonts-- in the font, as the final
name-keyed font can only have one set of alignment zones, but it does allow
for improved hinting when different sets of glyphs need different alignment
zones.

If FDDict definitions are used, then the global alignment zones and stem widths
in the source font will be ignored. For any glyphs not covered by an explicit
FDDict definition, psautohint will synthesize a dummy FDDict, where the zones
are set outside of the font's bounding box, so not to influence the hinting.
This is desirable for glyphs that have no features that need to be aligned.

If psautohint finds an FDDict named 'FinalFont', it will write that set of
values to the output font. Otherwise, it will merge all the alignment zones
and stem widths as the union of all FDDict definitions. If this merging fails
because some of the alignment zones and stem widths overlap, then the font
developer will have to provide a 'FinalFont' FDDict that explicitly defines
which stems and zones to use in the hinted output font.

To use a FDDict, one must define both the values of alignment zones and stem
widths, and the set of glyphs to apply it to. The FDDict must be defined in
the file before the set of glyphs which belong to it. Both the FDDict and the
glyph set define a name; an FDDict is applied to the glyph set with the same
name.

Running psautohint with the option --print-dflt-fddict will provide the list
of default FDDict values for the source font. These can be used as a starting
point for alternate FDDict definitions.

Running psautohint with the option --print-list-fddict will provide all the
user-defined FDDict defintions, as well as the list of glyphs associated with
each of them. One can use these to confirm the values, and to check which
glyphs are assigned to which FDDict. In particular, one should check the glyph
list of the FDDict named 'No Alignment Zones'; this list contains the glyphs
that did not match any of the search terms of the user-defined FDDicts.

The definitions use the following syntax:

begin FDDict <name>
    <key-1> <value-1>
    <key-2> <value-2>
    ...
    <key-n> <value-n>
end FDDict <name>

begin GlyphSet <name>
    <glyphname-1> <glyphname-2> ...
    <glyphname-n>
end GlyphSet <name>

The glyph names may be either a real glyph name, or a regular expression
designed to match several names.

An abbreviated regex primer:
^ ..... Matches at the start of the glyph name
$ ..... Matches at the end
[aABb]  Matches any one character in the set a, A, b, B
[A-Z]   Matches any one character in the set comprising the range from A-Z
[abA-Z] Matches any one character in the set a, b, and the characters
          in the range from A-Z
. ..... Matches any single character
+ ..... Maches whatever preceded it one or more times
* ..... Matches whatever preceded it none or more times.
\ ..... An escape character that includes the following character without
          the second one being interpreted as a regex special character

Examples:
^[A-Z]$    Matches names with one character in the range from A-Z.
^[A-Z].+   Matches any name where the first character is in the range A-Z,
             and it is followed by one or more characters.
[A-Z].+    Matches any name with a character that is in the range A-Z and
             which is followed by one or more characters.
^[A-Z].*   Matches any name with one or more characters, and
             the first character is in the range A-Z
^.+\.smallcaps   Matches any name that contains ".smallcaps"
^.+\.smallcaps$  Matches any name that ends with ".smallcaps"
^.+\.s[0-24]0$   Matches any name that ends with ".s00",".s10",".s20" or ".s40"


Example FDDict and GlyphSet definitions
=======================================

begin FDDict ST_Smallcaps
    # It's good practice to put the non-hint stuff first
    OrigEmSqUnits 1000
    FlexOK true
    # This gets used as the hint dict name if the font
    # is eventually built as a CID font.
    FontName MyFont-Bold

    # Alignment zones
    # The first is a bottom zone, the rest are top zones
    BaselineOvershoot -20
    BaselineYCoord 0
    CapHeight 900
    CapOvershoot 20
    LcHeight 700
    LcOvershoot 15

    # Stem widths
    DominantV [236 267]
    DominantH [141 152]
end FDDict ST_Smallcaps


begin FDDict LM_Smallcaps
    OrigEmSqUnits 1000
    FlexOK true
    FontName MyFont-Bold
    BaselineOvershoot -25
    BaselineYCoord 0
    CapHeight 950
    CapOvershoot 25
    LcHeight 750
    LcOvershoot 21
    DominantV [236 267]
    DominantH [141 152]
end FDDict LM_Smallcaps


begin GlyphSet LM_Smallcaps
    [Ll]\S+\.smallcap  [Mm]\S+\.smallcap
end GlyphSet LM_Smallcaps


begin GlyphSet ST_Smallcaps
    [Tt]\S+\.smallcap  [Ss]\S+\.smallcap
end GlyphSet ST_Smallcaps

***************************************

Note that whitespace must exist between keywords and values, but is otherwise
ignored. '#' marks the start of a comment; all the text after this character
on a line is ignored. GlyphSet and FDDict definitions may be intermixed,
as long as any FDDict is defined before the GlyphSet which refers to it.

At least two BlueValue pairs (the 'BaselineYCoord' bottom zone and any top
zone), and DominantH and DominantV values must be provided. All other keywords
are optional.

The full set of recognized FDDict keywords is:

BlueValue pairs:
    # BaselineOvershoot is a bottom zone, the rest are top zones.
    BaselineYCoord
    BaselineOvershoot

    CapHeight
    CapOvershoot

    LcHeight
    LcOvershoot

    AscenderHeight
    AscenderOvershoot

    FigHeight
    FigOvershoot

    Height5
    Height5Overshoot

    Height6
    Height6Overshoot

OtherBlues pairs:
    Baseline5Overshoot
    Baseline5

    Baseline6Overshoot
    Baseline6

    SuperiorOvershoot
    SuperiorBaseline

    OrdinalOvershoot
    OrdinalBaseline

    DescenderOvershoot
    DescenderHeight

For zones which capture the bottom of a feature in a glyph --BaselineYCoord
and all OtherBlues values-- the value specifies the top of the zone, and the
'Overshoot' is a negative value which specifes the offset to the bottom of
the zone, e.g.
    BaselineYCoord 0
    BaselineOvershoot -12

For zones which capture the top of a feature in a glyph --all BlueValue values
except BaselineYCoord-- the value specifies the bottom of the zone, and the
'Overshoot' is a positive value which specifes the offset to the top of the
zone, e.g.
    Height6 800
    Height6Overshoot 20

Note also that there is no implied sequential order of values. Height6 may have
a value less than or equal to CapHeight.

The values for keywords in one FontDict definiton are completely independent
from the values used in another FontDict. There is no inheritance from one
definition to the next.

Miscellaneous values:
  FontName ..... PostScript font name.
                 Only used by makeotf when building a CID font.
  OrigEmSqUnits  Single value: size of em-square.
                 Only used by makeotf when building a CID font.
  LanguageGroup  0 or 1. Specifies whether counter hints for ideographic
                 glyphs should be applied.
                 Only used by makeotf when building a CID font.
  DominantV .... List of dominant vertical stems, in the form
                 [<stem-value-1> <stem-value-2> ...]
  DominantH .... List of dominant horizontal stems, in the form
                 [<stem-value-1> <stem-value-2> ...]
  FlexOK ....... true or false.
  VCounterChars  List of glyphs to which counter hints may be applied,
                 in the form [<glyph-name-1> <glyph-name-2> ...]
  HCounterChars  List of glyphs to which counter hints may be applied,
                 in the form [<glyph-name-1> <glyph-name-2> ...]

Counter hints help to keep the space between stems open and equal in size.
psautohint will try and add counter hints to only a short hard-coded list
of glyphs,
    V-counters: m, M, T, ellipsis
    H-counters: element, equivalence, notelement, divide

To extend this list, use the VCounterChars and HCounterChars keywords.
A maximum of 64 glyph names can be added to either the vertical or
horizontal list.

---
Note for cognoscenti: the psautohint program code ignores StdHW and StdVW
entries if DominantV and DominantH entries are present, so it omits writing the
Std[HV]W keywords to fontinfo file. Also, psautohint will add any non-duplicate
stem width values for StemSnap[HV] to the Dominant[HV] stem width list, but the
StemSnap[HV] entries are not necessary if the full list of stem widths are
supplied as values for the Dominant[HV] keywords. Hence it also writes the full
stem list for the Dominant[HV] keywords, and does not write the StemSnap[HV]
keywords, to the fontinfo file. This is technically not right, as Dominant[HV]
array is supposed to hold only two values, but the psautohint program is not
affected, and it can write fewer entries this way.
"""


def _read_txt_file(file_path):
    with open(file_path, encoding='utf-8') as f:
        return f.read()


def _expand_cid_name(glyph_name, name_aliases):
    glyphRange = glyph_name.split("-")
    if len(glyphRange) > 1:
        g1 = _expand_cid_name(glyphRange[0], name_aliases)
        g2 = _expand_cid_name(glyphRange[1], name_aliases)
        glyph_name = "%s-%s" % (g1, g2)

    elif glyph_name[0] == "/":
        glyph_name = "cid" + glyph_name[1:].zfill(5)
        if glyph_name == "cid00000":
            glyph_name = ".notdef"
            name_aliases[glyph_name] = "cid00000"

    elif glyph_name.startswith("cid") and (len(glyph_name) < 8):
        glyph_name = "cid" + glyph_name[3:].zfill(5)
        if glyph_name == "cid00000":
            glyph_name = ".notdef"
            name_aliases[glyph_name] = "cid00000"

    return glyph_name


def _process_glyph_list_arg(glyph_list, name_aliases):
    return [_expand_cid_name(n, name_aliases) for n in glyph_list]


class HintOptions(ACOptions):
    def __init__(self, pargs):
        super(HintOptions, self).__init__()
        self.inputPaths = pargs.font_paths
        self.outputPaths = pargs.output_paths
        self.reference_font = pargs.reference_font
        self.hintAll = pargs.hint_all_ufo
        self.allowChanges = pargs.allow_changes
        self.noFlex = pargs.no_flex
        self.noHintSub = pargs.no_hint_sub
        self.allow_no_blues = pargs.no_zones_stems
        self.logOnly = pargs.report_only
        self.printDefaultFDDict = pargs.print_dflt_fddict
        self.printFDDictList = pargs.print_list_fddict
        self.round_coords = not pargs.decimal
        self.writeToDefaultLayer = pargs.write_to_default_layer


class _CustomHelpFormatter(argparse.RawDescriptionHelpFormatter):
    """
    Adds extra line between options
    """
    @staticmethod
    def __add_whitespace(i, i_wtsp, arg):
        if i == 0:
            return arg
        return (" " * i_wtsp) + arg

    def _split_lines(self, arg, width):
        arg_rows = arg.splitlines()
        for i, line in enumerate(arg_rows):
            search = re.search(r'\s*[0-9\-]{0,}\.?\s*', line)
            if line.strip() == "":
                arg_rows[i] = " "
            elif search:
                line_wtsp = search.end()
                lines = [self.__add_whitespace(j, line_wtsp, x)
                         for j, x in enumerate(textwrap.wrap(line, width))]
                arg_rows[i] = lines

        # [''] adds the extra line between args
        return [item for sublist in arg_rows for item in sublist] + ['']


class _AdditionalHelpAction(argparse.Action):
    """
    Based on argparse's _HelpAction and _VersionAction.
    """

    def __init__(self,
                 option_strings,
                 addl_help=None,
                 dest=argparse.SUPPRESS,
                 default=argparse.SUPPRESS,
                 help=None):
        super(_AdditionalHelpAction, self).__init__(
            option_strings=option_strings,
            dest=dest,
            default=default,
            nargs=0,
            help=help)
        self.addl_help = addl_help

    def __call__(self, parser, namespace, values, option_string=None):
        formatter = parser._get_formatter()
        formatter.add_text(self.addl_help)
        parser.exit(message=formatter.format_help())


class DuplicateMessageFilter(logging.Filter):
    """
    Suppresses any log message that was reported before in the same module and
    for the same logging level. We check for module and level number in
    addition to the message just in case, though checking the message only is
    probably enough.
    """

    def __init__(self):
        super(DuplicateMessageFilter, self).__init__()
        self.logs = set()

    def filter(self, record):
        current = (record.module, record.levelno, record.getMessage())
        if current in self.logs:
            return False
        self.logs.add(current)
        return True


def _split_comma_sequence(comma_str):
    return [item.strip() for item in comma_str.split(',')]


def _check_save_path(path_str):
    test_path = os.path.abspath(os.path.realpath(path_str))
    del_test_file = True
    check_path = test_path
    if os.path.isdir(test_path):
        check_path = os.path.join(test_path, "dummy")
    try:
        if os.path.exists(check_path):
            del_test_file = False
        open(check_path, 'a').close()
        if del_test_file:
            os.remove(check_path)
    except (IOError, OSError):
        raise argparse.ArgumentTypeError(
            f"{test_path} is not a valid path to write to.")
    return test_path


def _check_tx():
    try:
        subprocess.check_output(["tx", "-h"], stderr=subprocess.STDOUT)
        return True
    except (subprocess.CalledProcessError, OSError):
        return False


def _validate_font_paths(path_lst, parser):
    """
    Checks that all input paths are fonts, and that all are the same format.
    Returns the font format string.
    """
    has_tx = None
    format_set = set()
    for path in path_lst:
        font_format = get_font_format(path)
        base_name = os.path.basename(path)
        if font_format is None:
            parser.error(f"{base_name} is not a supported font format")
        if font_format in ("PFA", "PFB", "PFC"):
            if has_tx is None:
                has_tx = _check_tx()
            if not has_tx:
                parser.error(f"{base_name} font format requires 'tx'. "
                             "Please install 'afdko'.")
        format_set.add(font_format)
    if len(format_set) > 1:
        parser.error("the input fonts must be all of the same format")
    # get the set's single-item via tuple unpacking
    ft_format_str, = format_set
    return ft_format_str


def _validate_path(path_str):
    valid_path = os.path.abspath(os.path.realpath(path_str))
    if not os.path.exists(valid_path):
        raise argparse.ArgumentTypeError(
            f"{path_str} is not a valid path.")
    return valid_path


RE_COUNTER_CHARS = re.compile(
    r"([VH])CounterChars\s+[\(\[]\s*([^\)\r\n]+)[\)\]]")
RE_COMMENT_LINE = re.compile(r"#[^\r\n]+")


def _parse_fontinfo_file(options, fontinfo_path):
    """
    Collects VCounterChars and HCounterChars values from a fontinfo file,
    and updates the ACOptions class.
    """
    data = _read_txt_file(fontinfo_path)
    data = re.sub(RE_COMMENT_LINE, "", data)
    counterGlyphLists = RE_COUNTER_CHARS.findall(data)
    for entry in counterGlyphLists:
        glyphList = entry[1].split()
        if glyphList:
            if entry[0] == "V":
                options.vCounterGlyphs.extend(glyphList)
            else:
                options.hCounterGlyphs.extend(glyphList)


def get_options(args):
    parser = argparse.ArgumentParser(
        formatter_class=_CustomHelpFormatter,
        description=__doc__
    )
    parser_fonts = parser.add_argument(
        'font_paths',
        metavar='FONT',
        nargs='*',
        type=_validate_path,
        help='Type1/CFF/OTF/UFO font files'
    )
    parser.add_argument(
        '-v',
        '--verbose',
        action='count',
        default=0,
        help='verbose mode\n'
             'Use -vv for extra-verbose mode.'
    )
    parser.add_argument(
        '-o',
        '--output',
        metavar='PATH',
        nargs='+',
        dest='output_paths',
        type=_check_save_path,
        help='specify a file to write the hints to\n'
             'The hints are written to a new font instead of modifying the '
             'original one.'
    )
    parser.add_argument(
        '-r',
        '--reference-font',
        metavar='PATH',
        type=_validate_path,
        help='reference font\n'
             'Font to be used as reference, when hinting multiple fonts '
             'compatibily.'
    )
    parser.add_argument(
        '-a',
        '--all',
        action='store_true',
        dest='hint_all_ufo',
        help='hint all glyphs\n'
             'All glyphs get hinted even if:\n'
             '- they have been hinted before, or\n'
             '- the outlines in the default layer have not changed\n'
             'This is a UFO-only option.'
    )
    parser.add_argument(
        '-w',
        '--write-to-default-layer',
        action='store_true',
        help='write hints to default layer. This is a UFO-only option.'
    )
    glyphs_parser = parser.add_mutually_exclusive_group()
    glyphs_parser.add_argument(
        '-g',
        '--glyphs',
        metavar='GLYPH_LIST',
        dest='glyphs_to_hint',
        type=_split_comma_sequence,
        default=[],
        help='comma-separated sequence of glyphs to hint\n'
             'The glyph identifiers may be glyph indexes, glyph names, or '
             'glyph CIDs. CID values must be prefixed with a forward slash.\n'
             'Examples:\n'
             '    psautohint -g A,B,C,69 MyFont.ufo\n'
             '    psautohint -g /103,/434,68 MyCIDFont'
    )
    glyphs_parser.add_argument(
        '--glyphs-file',
        metavar='PATH',
        dest='glyphs_to_hint_file',
        type=_validate_path,
        help='file containing a list of glyphs to hint\n'
             'The file must contain a comma-separated sequence of glyph '
             'identifiers.'
    )
    glyphs_parser.add_argument(
        '-x',
        '--exclude-glyphs',
        metavar='GLYPH_LIST',
        dest='glyphs_to_not_hint',
        type=_split_comma_sequence,
        default=[],
        help='comma-separated sequence of glyphs to NOT hint\n'
             "Counterpart to '--glyphs' option."
    )
    glyphs_parser.add_argument(
        '--exclude-glyphs-file',
        metavar='PATH',
        dest='glyphs_to_not_hint_file',
        type=_validate_path,
        help='file containing a list of glyphs to NOT hint\n'
             "Counterpart to '--glyphs-file' option."
    )
    report_parser = parser.add_mutually_exclusive_group()
    report_parser.add_argument(
        '-c',
        '--allow-changes',
        action='store_true',
        help='allow changes to the glyph outlines\n'
             'Paths are reordered to reduce hint substitution, and nearly '
             'straight curves are flattened.'
    )
    report_parser.add_argument(
        '--report-only',
        action='store_true',
        help='process the font without modifying it'
    )
    parser.add_argument(
        '--log',
        metavar='PATH',
        dest='log_path',
        type=_check_save_path,
        help='write output messages to a file'
    )
    parser.add_argument(
        '-d',
        '--decimal',
        action='store_true',
        help='use decimal coordinates'
    )
    parser.add_argument(
        '--no-flex',
        action='store_true',
        help='suppress generation of flex commands'
    )
    parser.add_argument(
        '--no-hint-sub',
        action='store_true',
        help='suppress hint substitution'
    )
    parser.add_argument(
        '--no-zones-stems',
        action='store_true',
        help='allow the font to have no alignment zones nor stem widths'
    )
    parser.add_argument(
        '--fontinfo-file',
        metavar='PATH',
        type=_validate_path,
        help='file containing hinting parameters\n'
             f"Default: '{FONTINFO_FILE_NAME}'"
    )
    parser.add_argument(
        '--print-dflt-fddict',
        action='store_true',
        help="print the font's default FDDict set\n"
             'This can be used as a starting point for building FDDict '
             "sets. Use '--doc-fddict' option for more info."
    )
    parser.add_argument(
        '--print-list-fddict',
        action='store_true',
        help='print the list of user-defined FDDict sets, and the glyphs '
             'associated with each of them\n'
             'This is useful for checking the FDDict sets and their glyph-'
             "matching definitions. Use '--doc-fddict' option for more info."
    )
    parser.add_argument(
        '--doc-fddict',
        action=_AdditionalHelpAction,
        help="show a description of the format for defining sets of "
             "alternate alignment zones in a 'fontinfo' file and exit",
        addl_help=FDDICT_DOC
    )
    parser.add_argument(
        '--info',
        action=_AdditionalHelpAction,
        help="show program's general info and exit",
        addl_help=GENERAL_INFO
    )
    parser.add_argument(
        '--version',
        action='version',
        version=__version__
    )
    parser.add_argument(
        '--traceback',
        action='store_true',
        help="show traceback for exceptions.",
    )
    parser.add_argument(
        '--test',
        action='store_true',
        help="toggle settings needed for testing",
    )

    parsed_args = parser.parse_args(args)

    if parsed_args.verbose == 0:
        log_level = logging.WARNING
    elif parsed_args.verbose == 1:
        log_level = logging.INFO
    else:
        log_level = logging.DEBUG

    logging.basicConfig(format="%(levelname)s: %(message)s", level=log_level,
                        filename=parsed_args.log_path)

    if parsed_args.test is False:
        # Filter duplicate logging messages only when not running the tests
        for handler in logging.root.handlers:
            handler.addFilter(DuplicateMessageFilter())

    if (not len(parsed_args.font_paths or []) and
            len(parsed_args.output_paths or [])):
        # allow "psautohint -o outputpath inputpath"
        # see https://github.com/adobe-type-tools/psautohint/issues/129
        half = len(parsed_args.output_paths) // 2
        parsed_args.font_paths = [
            parsed_args.output_paths.pop(half) for _ in range(half)]

    if not len(parsed_args.font_paths):
        parser.error(
            f"the following arguments are required: {parser_fonts.metavar}")

    if (parsed_args.output_paths and
            len(parsed_args.font_paths) != len(parsed_args.output_paths)):
        parser.error("number of input and output fonts differ")

    if parsed_args.reference_font in parsed_args.font_paths:
        parser.error("the reference font cannot also be a font input")

    if len(parsed_args.font_paths) != len(set(parsed_args.font_paths)):
        parser.error("found duplicate font inputs")

    if parsed_args.reference_font:
        all_font_paths = parsed_args.font_paths + [parsed_args.reference_font]
    else:
        all_font_paths = parsed_args.font_paths

    options = HintOptions(parsed_args)

    options.font_format = _validate_font_paths(all_font_paths, parser)

    if parsed_args.glyphs_to_hint:
        options.glyphList = _process_glyph_list_arg(
            parsed_args.glyphs_to_hint, options.nameAliases)
    elif parsed_args.glyphs_to_not_hint:
        options.excludeGlyphList = True
        options.glyphList = _process_glyph_list_arg(
            parsed_args.glyphs_to_not_hint, options.nameAliases)
    elif parsed_args.glyphs_to_hint_file:
        options.glyphList = _process_glyph_list_arg(
            _read_txt_file(parsed_args.glyphs_to_hint_file),
            options.nameAliases)
    elif parsed_args.glyphs_to_not_hint_file:
        options.excludeGlyphList = True
        options.glyphList = _process_glyph_list_arg(
            _read_txt_file(parsed_args.glyphs_to_not_hint_file),
            options.nameAliases)

    if not parsed_args.fontinfo_file:
        fontinfo_path = os.path.join(os.path.dirname(all_font_paths[0]),
                                     FONTINFO_FILE_NAME)
        if os.path.isfile(fontinfo_path):
            _parse_fontinfo_file(options, fontinfo_path)
    else:
        _parse_fontinfo_file(options, parsed_args.fontinfo_file)

    return options, parsed_args


def main(args=None):
    options, pargs = get_options(args)

    try:
        hintFiles(options)
    except Exception as ex:
        if pargs.traceback:
            raise
        logging.error(ex)
        return 1


class ReportOptions(ACOptions):
    def __init__(self, pargs):
        super(ReportOptions, self).__init__()
        self.hintAll = True
        self.noFlex = True
        self.allow_no_blues = True
        self.logOnly = True
        self.inputPaths = pargs.font_paths
        self.outputPaths = pargs.output_paths
        self.report_stems = not pargs.alignment_zones
        self.report_zones = pargs.alignment_zones
        self.report_all_stems = pargs.all_stems


def get_stemhist_options(args):
    parser = argparse.ArgumentParser(
        formatter_class=_CustomHelpFormatter,
        description='Stem and Alignment zones report for PostScript, '
                    'OpenType/CFF and UFO fonts.'
    )
    parser.add_argument(
        'font_paths',
        metavar='FONT',
        nargs='+',
        type=_validate_path,
        help='Type1/CFF/OTF/UFO font files'
    )
    parser.add_argument(
        '-o',
        '--output',
        metavar='PATH',
        nargs='+',
        dest='output_paths',
        type=_check_save_path,
        help='When this is specified, the path argument is used as the base '
             'path for the reports. Otherwise, the font file path is used as '
             'the base path. Several reports are written, using the base path '
             'name plus an extension.\n'
             'Without the -z option, the report files are:\n'
             '   <base path>.hstm.txt  The horizontal stems.\n'
             '   <base path>.vstm.txt  The vertical stems.\n'
             'With the -z option, the report files are:\n'
             '   <base path>.top.txt   The top zones.\n'
             '   <base path>.bot.txt   The bottom zones.'
    )
    parser.add_argument(
        '-z',
        '--alignment-zones',
        action='store_true',
        dest='alignment_zones',
        help='Print alignment zone report rather than stem report.'
    )
    parser.add_argument(
        '-a',
        '--all',
        action='store_true',
        dest='all_stems',
        help='Include stems formed by curved line segments; by default, '
             'includes only stems formed by straight line segments.'
    )
    glyphs_parser = parser.add_mutually_exclusive_group()
    glyphs_parser.add_argument(
        '-g',
        '--glyphs',
        metavar='GLYPH_LIST',
        dest='glyphs_to_hint',
        type=_split_comma_sequence,
        default=[],
        help='comma-separated sequence of glyphs to process\n'
             'The glyph identifiers may be glyph indexes, glyph names, or '
             'glyph CIDs. CID values must be prefixed with a forward slash.\n'
             'Examples:\n'
             '    psautohint -g A,B,C,69 MyFont.ufo\n'
             '    psautohint -g /103,/434,68 MyCIDFont'
    )
    glyphs_parser.add_argument(
        '--glyphs-file',
        metavar='PATH',
        dest='glyphs_to_hint_file',
        type=_validate_path,
        help='file containing a list of glyphs to process\n'
             'The file must contain a comma-separated sequence of glyph '
             'identifiers.'
    )
    glyphs_parser.add_argument(
        '-x',
        '--exclude-glyphs',
        metavar='GLYPH_LIST',
        dest='glyphs_to_not_hint',
        type=_split_comma_sequence,
        default=[],
        help='comma-separated sequence of glyphs to NOT process\n'
             "Counterpart to '--glyphs' option."
    )
    glyphs_parser.add_argument(
        '--exclude-glyphs-file',
        metavar='PATH',
        dest='glyphs_to_not_hint_file',
        type=_validate_path,
        help='file containing a list of glyphs to NOT process\n'
             "Counterpart to '--glyphs-file' option."
    )
    parser.add_argument(
        '-v',
        '--verbose',
        action='count',
        default=0,
        help='verbose mode\n'
             'Use -vv for extra-verbose mode.'
    )
    parser.add_argument(
        '--version',
        action='version',
        version=__version__
    )
    parser.add_argument(
        '--traceback',
        action='store_true',
        help="show traceback for exceptions.",
    )
    parsed_args = parser.parse_args(args)

    if parsed_args.verbose == 0:
        log_level = logging.WARNING
    elif parsed_args.verbose == 1:
        log_level = logging.INFO
    else:
        log_level = logging.DEBUG

    logging.basicConfig(format="%(levelname)s: %(message)s", level=log_level)
    for handler in logging.root.handlers:
        handler.addFilter(DuplicateMessageFilter())

    if (parsed_args.output_paths and
            len(parsed_args.font_paths) != len(parsed_args.output_paths)):
        parser.error("number of input and output fonts differ")

    if len(parsed_args.font_paths) != len(set(parsed_args.font_paths)):
        parser.error("found duplicate font inputs")

    options = ReportOptions(parsed_args)

    options.font_format = _validate_font_paths(parsed_args.font_paths, parser)

    if parsed_args.glyphs_to_hint:
        options.glyphList = _process_glyph_list_arg(
            parsed_args.glyphs_to_hint, options.nameAliases)
    elif parsed_args.glyphs_to_not_hint:
        options.excludeGlyphList = True
        options.glyphList = _process_glyph_list_arg(
            parsed_args.glyphs_to_not_hint, options.nameAliases)
    elif parsed_args.glyphs_to_hint_file:
        options.glyphList = _process_glyph_list_arg(
            _read_txt_file(parsed_args.glyphs_to_hint_file),
            options.nameAliases)
    elif parsed_args.glyphs_to_not_hint_file:
        options.excludeGlyphList = True
        options.glyphList = _process_glyph_list_arg(
            _read_txt_file(parsed_args.glyphs_to_not_hint_file),
            options.nameAliases)

    return options, parsed_args


def stemhist(args=None):
    options, pargs = get_stemhist_options(args)

    try:
        hintFiles(options)
    except Exception as ex:
        if pargs.traceback:
            raise
        logging.error(ex)
        return 1


if __name__ == '__main__':
    sys.exit(main())
