# Source code for fontTools.designspaceLib.statNames

    """Compute name information for a given location in user-space coordinates
    using STAT data. This can be used to fill-in automatically the names of an
    instance:

    .. code:: python

        instance = doc.instances[0]
        names = getStatNames(doc, instance.getFullUserLocation(doc))
        print(names.styleNames)
    """
    from __future__ import annotations

    from dataclasses import dataclass
    from typing import Dict, Optional, Tuple, Union
    import logging

    from fontTools.designspaceLib import (
        AxisDescriptor,
        AxisLabelDescriptor,
        DesignSpaceDocument,
        DesignSpaceDocumentError,
        DiscreteAxisDescriptor,
        SimpleLocationDict,
        SourceDescriptor,
    )

    LOGGER = logging.getLogger(__name__)

    # TODO(Python 3.8): use Literal
    # RibbiStyleName = Union[Literal["regular"], Literal["bold"], Literal["italic"], Literal["bold italic"]]
    RibbiStyle = str
    BOLD_ITALIC_TO_RIBBI_STYLE = {
        (False, False): "regular",
        (False, True): "italic",
        (True, False): "bold",
        (True, True): "bold italic",
    }

    @dataclass
    class StatNames:
        """Name data generated from the STAT table information."""

        familyNames: Dict[str, str]
        styleNames: Dict[str, str]
        postScriptFontName: Optional[str]
        styleMapFamilyNames: Dict[str, str]
        styleMapStyleName: Optional[RibbiStyle]

    def getStatNames(
        doc: DesignSpaceDocument, userLocation: SimpleLocationDict
    ) -> StatNames:
        """Compute the family, style, PostScript names of the given ``userLocation``
        using the document's STAT information.

        Also computes localizations.

        If not enough STAT data is available for a given name, either its dict of
        localized names will be empty (family and style names), or the name will be
        None (PostScript name).

        .. versionadded:: 5.0
        """
        familyNames: Dict[str, str] = {}
        defaultSource: Optional[SourceDescriptor] = doc.findDefault()
        if defaultSource is None:
            LOGGER.warning("Cannot determine default source to look up family name.")
        elif defaultSource.familyName is None:
            LOGGER.warning(
                "Cannot look up family name, assign the 'familyname' attribute to the default source."
            )
        else:
            familyNames = {
                "en": defaultSource.familyName,
                **defaultSource.localisedFamilyName,
            }

        styleNames: Dict[str, str] = {}
        # If a free-standing label matches the location, use it for name generation.
        label = doc.labelForUserLocation(userLocation)
        if label is not None:
            styleNames = {"en": label.name, **label.labelNames}
        # Otherwise, scour the axis labels for matches.
        else:
            # Gather all languages in which at least one translation is provided
            # Then build names for all these languages, but fallback to English
            # whenever a translation is missing.
            labels = _getAxisLabelsForUserLocation(doc.axes, userLocation)
            if labels:
                languages = set(language for label in labels for language in label.labelNames)
                languages.add("en")
                for language in languages:
                    styleName = " ".join(
                        label.labelNames.get(language, label.defaultName)
                        for label in labels
                        if not label.elidable
                    )
                    if not styleName and doc.elidedFallbackName is not None:
                        styleName = doc.elidedFallbackName
                    styleNames[language] = styleName

        if "en" not in familyNames or "en" not in styleNames:
            # Not enough information to compute PS names of styleMap names
            return StatNames(
                familyNames=familyNames,
                styleNames=styleNames,
                postScriptFontName=None,
                styleMapFamilyNames={},
                styleMapStyleName=None,
            )

        postScriptFontName = f"{familyNames['en']}-{styleNames['en']}".replace(" ", "")

        styleMapStyleName, regularUserLocation = _getRibbiStyle(doc, userLocation)

        styleNamesForStyleMap = styleNames
        if regularUserLocation != userLocation:
            regularStatNames = getStatNames(doc, regularUserLocation)
            styleNamesForStyleMap = regularStatNames.styleNames

        styleMapFamilyNames = {}
        for language in set(familyNames).union(styleNames.keys()):
            familyName = familyNames.get(language, familyNames["en"])
            styleName = styleNamesForStyleMap.get(language, styleNamesForStyleMap["en"])
            styleMapFamilyNames[language] = (familyName + " " + styleName).strip()

        return StatNames(
            familyNames=familyNames,
            styleNames=styleNames,
            postScriptFontName=postScriptFontName,
            styleMapFamilyNames=styleMapFamilyNames,
            styleMapStyleName=styleMapStyleName,
        )

    def _getSortedAxisLabels(
        axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
    ) -> Dict[str, list[AxisLabelDescriptor]]:
        """Returns axis labels sorted by their ordering, with unordered ones appended as
        they are listed."""

        # First, get the axis labels with explicit ordering...
        sortedAxes = sorted(
            (axis for axis in axes if axis.axisOrdering is not None),
            key=lambda a: a.axisOrdering,
        )
        sortedLabels: Dict[str, list[AxisLabelDescriptor]] = {
            axis.name: axis.axisLabels for axis in sortedAxes
        }

        # ... then append the others in the order they appear.
        # NOTE: This relies on Python 3.7+ dict's preserved insertion order.
        for axis in axes:
            if axis.axisOrdering is None:
                sortedLabels[axis.name] = axis.axisLabels

        return sortedLabels

    def _getAxisLabelsForUserLocation(
        axes: list[Union[AxisDescriptor, DiscreteAxisDescriptor]],
        userLocation: SimpleLocationDict,
    ) -> list[AxisLabelDescriptor]:
        labels: list[AxisLabelDescriptor] = []

        allAxisLabels = _getSortedAxisLabels(axes)
        if allAxisLabels.keys() != userLocation.keys():
            LOGGER.warning(
                f"Mismatch between user location '{userLocation.keys()}' and available "
                f"labels for '{allAxisLabels.keys()}'."
            )

        for axisName, axisLabels in allAxisLabels.items():
            userValue = userLocation[axisName]
            label: Optional[AxisLabelDescriptor] = next(
                (
                    l
                    for l in axisLabels
                    if l.userValue == userValue
                    or (
                        l.userMinimum is not None
                        and l.userMaximum is not None
                        and l.userMinimum <= userValue <= l.userMaximum
                    )
                ),
                None,
            )
            if label is None:
                LOGGER.debug(
                    f"Document needs a label for axis '{axisName}', user value '{userValue}'."
                )
            else:
                labels.append(label)

        return labels

    def _getRibbiStyle(
        self: DesignSpaceDocument, userLocation: SimpleLocationDict
    ) -> Tuple[RibbiStyle, SimpleLocationDict]:
        """Compute the RIBBI style name of the given user location,
        return the location of the matching Regular in the RIBBI group.

        .. versionadded:: 5.0
        """
        regularUserLocation = {}
        axes_by_tag = {axis.tag: axis for axis in self.axes}

        bold: bool = False
        italic: bool = False

        axis = axes_by_tag.get("wght")
        if axis is not None:
            for regular_label in axis.axisLabels:
                if regular_label.linkedUserValue == userLocation[axis.name]:
                    regularUserLocation[axis.name] = regular_label.userValue
                    bold = True
                    break

        axis = axes_by_tag.get("ital") or axes_by_tag.get("slnt")
        if axis is not None:
            for urpright_label in axis.axisLabels:
                if urpright_label.linkedUserValue == userLocation[axis.name]:
                    regularUserLocation[axis.name] = urpright_label.userValue
                    italic = True
                    break

        return BOLD_ITALIC_TO_RIBBI_STYLE[bold, italic], {
            **userLocation,
            **regularUserLocation,
        }