"""Various low level data validators."""

import calendar
from collections.abc import Mapping
from io import open

import fontTools.misc.filesystem as fs
from fontTools.ufoLib.utils import numberTypes

# -------
# Generic
# -------


def isDictEnough(value):
    """
    Some objects will likely come in that aren't
    dicts but are dict-ish enough.
    """
    if isinstance(value, Mapping):
        return True
    for attr in ("keys", "values", "items"):
        if not hasattr(value, attr):
            return False
    return True


def genericTypeValidator(value, typ):
    """
    Generic. (Added at version 2.)
    """
    return isinstance(value, typ)


def genericIntListValidator(values, validValues):
    """
    Generic. (Added at version 2.)
    """
    if not isinstance(values, (list, tuple)):
        return False
    valuesSet = set(values)
    validValuesSet = set(validValues)
    if valuesSet - validValuesSet:
        return False
    for value in values:
        if not isinstance(value, int):
            return False
    return True


def genericNonNegativeIntValidator(value):
    """
    Generic. (Added at version 3.)
    """
    if not isinstance(value, int):
        return False
    if value < 0:
        return False
    return True


def genericNonNegativeNumberValidator(value):
    """
    Generic. (Added at version 3.)
    """
    if not isinstance(value, numberTypes):
        return False
    if value < 0:
        return False
    return True


def genericDictValidator(value, prototype):
    """
    Generic. (Added at version 3.)
    """
    # not a dict
    if not isinstance(value, Mapping):
        return False
    # missing required keys
    for key, (typ, required) in prototype.items():
        if not required:
            continue
        if key not in value:
            return False
    # unknown keys
    for key in value.keys():
        if key not in prototype:
            return False
    # incorrect types
    for key, v in value.items():
        prototypeType, required = prototype[key]
        if v is None and not required:
            continue
        if not isinstance(v, prototypeType):
            return False
    return True


# --------------
# fontinfo.plist
# --------------

# Data Validators


def fontInfoStyleMapStyleNameValidator(value):
    """
    Version 2+.
    """
    options = ["regular", "italic", "bold", "bold italic"]
    return value in options


def fontInfoOpenTypeGaspRangeRecordsValidator(value):
    """
    Version 3+.
    """
    if not isinstance(value, list):
        return False
    if len(value) == 0:
        return True
    validBehaviors = [0, 1, 2, 3]
    dictPrototype = dict(rangeMaxPPEM=(int, True), rangeGaspBehavior=(list, True))
    ppemOrder = []
    for rangeRecord in value:
        if not genericDictValidator(rangeRecord, dictPrototype):
            return False
        ppem = rangeRecord["rangeMaxPPEM"]
        behavior = rangeRecord["rangeGaspBehavior"]
        ppemValidity = genericNonNegativeIntValidator(ppem)
        if not ppemValidity:
            return False
        behaviorValidity = genericIntListValidator(behavior, validBehaviors)
        if not behaviorValidity:
            return False
        ppemOrder.append(ppem)
    if ppemOrder != sorted(ppemOrder):
        return False
    return True


def fontInfoOpenTypeHeadCreatedValidator(value):
    """
    Version 2+.
    """
    # format: 0000/00/00 00:00:00
    if not isinstance(value, str):
        return False
    # basic formatting
    if not len(value) == 19:
        return False
    if value.count(" ") != 1:
        return False
    date, time = value.split(" ")
    if date.count("/") != 2:
        return False
    if time.count(":") != 2:
        return False
    # date
    year, month, day = date.split("/")
    if len(year) != 4:
        return False
    if len(month) != 2:
        return False
    if len(day) != 2:
        return False
    try:
        year = int(year)
        month = int(month)
        day = int(day)
    except ValueError:
        return False
    if month < 1 or month > 12:
        return False
    monthMaxDay = calendar.monthrange(year, month)[1]
    if day < 1 or day > monthMaxDay:
        return False
    # time
    hour, minute, second = time.split(":")
    if len(hour) != 2:
        return False
    if len(minute) != 2:
        return False
    if len(second) != 2:
        return False
    try:
        hour = int(hour)
        minute = int(minute)
        second = int(second)
    except ValueError:
        return False
    if hour < 0 or hour > 23:
        return False
    if minute < 0 or minute > 59:
        return False
    if second < 0 or second > 59:
        return False
    # fallback
    return True


def fontInfoOpenTypeNameRecordsValidator(value):
    """
    Version 3+.
    """
    if not isinstance(value, list):
        return False
    dictPrototype = dict(
        nameID=(int, True),
        platformID=(int, True),
        encodingID=(int, True),
        languageID=(int, True),
        string=(str, True),
    )
    for nameRecord in value:
        if not genericDictValidator(nameRecord, dictPrototype):
            return False
    return True


def fontInfoOpenTypeOS2WeightClassValidator(value):
    """
    Version 2+.
    """
    if not isinstance(value, int):
        return False
    if value < 0:
        return False
    return True


def fontInfoOpenTypeOS2WidthClassValidator(value):
    """
    Version 2+.
    """
    if not isinstance(value, int):
        return False
    if value < 1:
        return False
    if value > 9:
        return False
    return True


def fontInfoVersion2OpenTypeOS2PanoseValidator(values):
    """
    Version 2.
    """
    if not isinstance(values, (list, tuple)):
        return False
    if len(values) != 10:
        return False
    for value in values:
        if not isinstance(value, int):
            return False
    # XXX further validation?
    return True


def fontInfoVersion3OpenTypeOS2PanoseValidator(values):
    """
    Version 3+.
    """
    if not isinstance(values, (list, tuple)):
        return False
    if len(values) != 10:
        return False
    for value in values:
        if not isinstance(value, int):
            return False
        if value < 0:
            return False
    # XXX further validation?
    return True


def fontInfoOpenTypeOS2FamilyClassValidator(values):
    """
    Version 2+.
    """
    if not isinstance(values, (list, tuple)):
        return False
    if len(values) != 2:
        return False
    for value in values:
        if not isinstance(value, int):
            return False
    classID, subclassID = values
    if classID < 0 or classID > 14:
        return False
    if subclassID < 0 or subclassID > 15:
        return False
    return True


def fontInfoPostscriptBluesValidator(values):
    """
    Version 2+.
    """
    if not isinstance(values, (list, tuple)):
        return False
    if len(values) > 14:
        return False
    if len(values) % 2:
        return False
    for value in values:
        if not isinstance(value, numberTypes):
            return False
    return True


def fontInfoPostscriptOtherBluesValidator(values):
    """
    Version 2+.
    """
    if not isinstance(values, (list, tuple)):
        return False
    if len(values) > 10:
        return False
    if len(values) % 2:
        return False
    for value in values:
        if not isinstance(value, numberTypes):
            return False
    return True


def fontInfoPostscriptStemsValidator(values):
    """
    Version 2+.
    """
    if not isinstance(values, (list, tuple)):
        return False
    if len(values) > 12:
        return False
    for value in values:
        if not isinstance(value, numberTypes):
            return False
    return True


def fontInfoPostscriptWindowsCharacterSetValidator(value):
    """
    Version 2+.
    """
    validValues = list(range(1, 21))
    if value not in validValues:
        return False
    return True


def fontInfoWOFFMetadataUniqueIDValidator(value):
    """
    Version 3+.
    """
    dictPrototype = dict(id=(str, True))
    if not genericDictValidator(value, dictPrototype):
        return False
    return True


def fontInfoWOFFMetadataVendorValidator(value):
    """
    Version 3+.
    """
    dictPrototype = {
        "name": (str, True),
        "url": (str, False),
        "dir": (str, False),
        "class": (str, False),
    }
    if not genericDictValidator(value, dictPrototype):
        return False
    if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
        return False
    return True


def fontInfoWOFFMetadataCreditsValidator(value):
    """
    Version 3+.
    """
    dictPrototype = dict(credits=(list, True))
    if not genericDictValidator(value, dictPrototype):
        return False
    if not len(value["credits"]):
        return False
    dictPrototype = {
        "name": (str, True),
        "url": (str, False),
        "role": (str, False),
        "dir": (str, False),
        "class": (str, False),
    }
    for credit in value["credits"]:
        if not genericDictValidator(credit, dictPrototype):
            return False
        if "dir" in credit and credit.get("dir") not in ("ltr", "rtl"):
            return False
    return True


def fontInfoWOFFMetadataDescriptionValidator(value):
    """
    Version 3+.
    """
    dictPrototype = dict(url=(str, False), text=(list, True))
    if not genericDictValidator(value, dictPrototype):
        return False
    for text in value["text"]:
        if not fontInfoWOFFMetadataTextValue(text):
            return False
    return True


def fontInfoWOFFMetadataLicenseValidator(value):
    """
    Version 3+.
    """
    dictPrototype = dict(url=(str, False), text=(list, False), id=(str, False))
    if not genericDictValidator(value, dictPrototype):
        return False
    if "text" in value:
        for text in value["text"]:
            if not fontInfoWOFFMetadataTextValue(text):
                return False
    return True


def fontInfoWOFFMetadataTrademarkValidator(value):
    """
    Version 3+.
    """
    dictPrototype = dict(text=(list, True))
    if not genericDictValidator(value, dictPrototype):
        return False
    for text in value["text"]:
        if not fontInfoWOFFMetadataTextValue(text):
            return False
    return True


def fontInfoWOFFMetadataCopyrightValidator(value):
    """
    Version 3+.
    """
    dictPrototype = dict(text=(list, True))
    if not genericDictValidator(value, dictPrototype):
        return False
    for text in value["text"]:
        if not fontInfoWOFFMetadataTextValue(text):
            return False
    return True


def fontInfoWOFFMetadataLicenseeValidator(value):
    """
    Version 3+.
    """
    dictPrototype = {"name": (str, True), "dir": (str, False), "class": (str, False)}
    if not genericDictValidator(value, dictPrototype):
        return False
    if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
        return False
    return True


def fontInfoWOFFMetadataTextValue(value):
    """
    Version 3+.
    """
    dictPrototype = {
        "text": (str, True),
        "language": (str, False),
        "dir": (str, False),
        "class": (str, False),
    }
    if not genericDictValidator(value, dictPrototype):
        return False
    if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
        return False
    return True


def fontInfoWOFFMetadataExtensionsValidator(value):
    """
    Version 3+.
    """
    if not isinstance(value, list):
        return False
    if not value:
        return False
    for extension in value:
        if not fontInfoWOFFMetadataExtensionValidator(extension):
            return False
    return True


def fontInfoWOFFMetadataExtensionValidator(value):
    """
    Version 3+.
    """
    dictPrototype = dict(names=(list, False), items=(list, True), id=(str, False))
    if not genericDictValidator(value, dictPrototype):
        return False
    if "names" in value:
        for name in value["names"]:
            if not fontInfoWOFFMetadataExtensionNameValidator(name):
                return False
    for item in value["items"]:
        if not fontInfoWOFFMetadataExtensionItemValidator(item):
            return False
    return True


def fontInfoWOFFMetadataExtensionItemValidator(value):
    """
    Version 3+.
    """
    dictPrototype = dict(id=(str, False), names=(list, True), values=(list, True))
    if not genericDictValidator(value, dictPrototype):
        return False
    for name in value["names"]:
        if not fontInfoWOFFMetadataExtensionNameValidator(name):
            return False
    for val in value["values"]:
        if not fontInfoWOFFMetadataExtensionValueValidator(val):
            return False
    return True


def fontInfoWOFFMetadataExtensionNameValidator(value):
    """
    Version 3+.
    """
    dictPrototype = {
        "text": (str, True),
        "language": (str, False),
        "dir": (str, False),
        "class": (str, False),
    }
    if not genericDictValidator(value, dictPrototype):
        return False
    if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
        return False
    return True


def fontInfoWOFFMetadataExtensionValueValidator(value):
    """
    Version 3+.
    """
    dictPrototype = {
        "text": (str, True),
        "language": (str, False),
        "dir": (str, False),
        "class": (str, False),
    }
    if not genericDictValidator(value, dictPrototype):
        return False
    if "dir" in value and value.get("dir") not in ("ltr", "rtl"):
        return False
    return True


# ----------
# Guidelines
# ----------


def guidelinesValidator(value, identifiers=None):
    """
    Version 3+.
    """
    if not isinstance(value, list):
        return False
    if identifiers is None:
        identifiers = set()
    for guide in value:
        if not guidelineValidator(guide):
            return False
        identifier = guide.get("identifier")
        if identifier is not None:
            if identifier in identifiers:
                return False
            identifiers.add(identifier)
    return True


_guidelineDictPrototype = dict(
    x=((int, float), False),
    y=((int, float), False),
    angle=((int, float), False),
    name=(str, False),
    color=(str, False),
    identifier=(str, False),
)


def guidelineValidator(value):
    """
    Version 3+.
    """
    if not genericDictValidator(value, _guidelineDictPrototype):
        return False
    x = value.get("x")
    y = value.get("y")
    angle = value.get("angle")
    # x or y must be present
    if x is None and y is None:
        return False
    # if x or y are None, angle must not be present
    if x is None or y is None:
        if angle is not None:
            return False
    # if x and y are defined, angle must be defined
    if x is not None and y is not None and angle is None:
        return False
    # angle must be between 0 and 360
    if angle is not None:
        if angle < 0:
            return False
        if angle > 360:
            return False
    # identifier must be 1 or more characters
    identifier = value.get("identifier")
    if identifier is not None and not identifierValidator(identifier):
        return False
    # color must follow the proper format
    color = value.get("color")
    if color is not None and not colorValidator(color):
        return False
    return True


# -------
# Anchors
# -------


def anchorsValidator(value, identifiers=None):
    """
    Version 3+.
    """
    if not isinstance(value, list):
        return False
    if identifiers is None:
        identifiers = set()
    for anchor in value:
        if not anchorValidator(anchor):
            return False
        identifier = anchor.get("identifier")
        if identifier is not None:
            if identifier in identifiers:
                return False
            identifiers.add(identifier)
    return True


_anchorDictPrototype = dict(
    x=((int, float), False),
    y=((int, float), False),
    name=(str, False),
    color=(str, False),
    identifier=(str, False),
)


def anchorValidator(value):
    """
    Version 3+.
    """
    if not genericDictValidator(value, _anchorDictPrototype):
        return False
    x = value.get("x")
    y = value.get("y")
    # x and y must be present
    if x is None or y is None:
        return False
    # identifier must be 1 or more characters
    identifier = value.get("identifier")
    if identifier is not None and not identifierValidator(identifier):
        return False
    # color must follow the proper format
    color = value.get("color")
    if color is not None and not colorValidator(color):
        return False
    return True


# ----------
# Identifier
# ----------


def identifierValidator(value):
    """
    Version 3+.

    >>> identifierValidator("a")
    True
    >>> identifierValidator("")
    False
    >>> identifierValidator("a" * 101)
    False
    """
    validCharactersMin = 0x20
    validCharactersMax = 0x7E
    if not isinstance(value, str):
        return False
    if not value:
        return False
    if len(value) > 100:
        return False
    for c in value:
        c = ord(c)
        if c < validCharactersMin or c > validCharactersMax:
            return False
    return True


# -----
# Color
# -----


def colorValidator(value):
    """
    Version 3+.

    >>> colorValidator("0,0,0,0")
    True
    >>> colorValidator(".5,.5,.5,.5")
    True
    >>> colorValidator("0.5,0.5,0.5,0.5")
    True
    >>> colorValidator("1,1,1,1")
    True

    >>> colorValidator("2,0,0,0")
    False
    >>> colorValidator("0,2,0,0")
    False
    >>> colorValidator("0,0,2,0")
    False
    >>> colorValidator("0,0,0,2")
    False

    >>> colorValidator("1r,1,1,1")
    False
    >>> colorValidator("1,1g,1,1")
    False
    >>> colorValidator("1,1,1b,1")
    False
    >>> colorValidator("1,1,1,1a")
    False

    >>> colorValidator("1 1 1 1")
    False
    >>> colorValidator("1 1,1,1")
    False
    >>> colorValidator("1,1 1,1")
    False
    >>> colorValidator("1,1,1 1")
    False

    >>> colorValidator("1, 1, 1, 1")
    True
    """
    if not isinstance(value, str):
        return False
    parts = value.split(",")
    if len(parts) != 4:
        return False
    for part in parts:
        part = part.strip()
        converted = False
        try:
            part = int(part)
            converted = True
        except ValueError:
            pass
        if not converted:
            try:
                part = float(part)
                converted = True
            except ValueError:
                pass
        if not converted:
            return False
        if part < 0:
            return False
        if part > 1:
            return False
    return True


# -----
# image
# -----

pngSignature = b"\x89PNG\r\n\x1a\n"

_imageDictPrototype = dict(
    fileName=(str, True),
    xScale=((int, float), False),
    xyScale=((int, float), False),
    yxScale=((int, float), False),
    yScale=((int, float), False),
    xOffset=((int, float), False),
    yOffset=((int, float), False),
    color=(str, False),
)


def imageValidator(value):
    """
    Version 3+.
    """
    if not genericDictValidator(value, _imageDictPrototype):
        return False
    # fileName must be one or more characters
    if not value["fileName"]:
        return False
    # color must follow the proper format
    color = value.get("color")
    if color is not None and not colorValidator(color):
        return False
    return True


def pngValidator(path=None, data=None, fileObj=None):
    """
    Version 3+.

    This checks the signature of the image data.
    """
    assert path is not None or data is not None or fileObj is not None
    if path is not None:
        with open(path, "rb") as f:
            signature = f.read(8)
    elif data is not None:
        signature = data[:8]
    elif fileObj is not None:
        pos = fileObj.tell()
        signature = fileObj.read(8)
        fileObj.seek(pos)
    if signature != pngSignature:
        return False, "Image does not begin with the PNG signature."
    return True, None


# -------------------
# layercontents.plist
# -------------------


def layerContentsValidator(value, ufoPathOrFileSystem):
    """
    Check the validity of layercontents.plist.
    Version 3+.
    """
    if isinstance(ufoPathOrFileSystem, fs.base.FS):
        fileSystem = ufoPathOrFileSystem
    else:
        fileSystem = fs.osfs.OSFS(ufoPathOrFileSystem)

    bogusFileMessage = "layercontents.plist in not in the correct format."
    # file isn't in the right format
    if not isinstance(value, list):
        return False, bogusFileMessage
    # work through each entry
    usedLayerNames = set()
    usedDirectories = set()
    contents = {}
    for entry in value:
        # layer entry in the incorrect format
        if not isinstance(entry, list):
            return False, bogusFileMessage
        if not len(entry) == 2:
            return False, bogusFileMessage
        for i in entry:
            if not isinstance(i, str):
                return False, bogusFileMessage
        layerName, directoryName = entry
        # check directory naming
        if directoryName != "glyphs":
            if not directoryName.startswith("glyphs."):
                return (
                    False,
                    "Invalid directory name (%s) in layercontents.plist."
                    % directoryName,
                )
        if len(layerName) == 0:
            return False, "Empty layer name in layercontents.plist."
        # directory doesn't exist
        if not fileSystem.exists(directoryName):
            return False, "A glyphset does not exist at %s." % directoryName
        # default layer name
        if layerName == "public.default" and directoryName != "glyphs":
            return (
                False,
                "The name public.default is being used by a layer that is not the default.",
            )
        # check usage
        if layerName in usedLayerNames:
            return (
                False,
                "The layer name %s is used by more than one layer." % layerName,
            )
        usedLayerNames.add(layerName)
        if directoryName in usedDirectories:
            return (
                False,
                "The directory %s is used by more than one layer." % directoryName,
            )
        usedDirectories.add(directoryName)
        # store
        contents[layerName] = directoryName
    # missing default layer
    foundDefault = "glyphs" in contents.values()
    if not foundDefault:
        return False, "The required default glyph set is not in the UFO."
    return True, None


# ------------
# groups.plist
# ------------


def groupsValidator(value):
    """
    Check the validity of the groups.
    Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).

    >>> groups = {"A" : ["A", "A"], "A2" : ["A"]}
    >>> groupsValidator(groups)
    (True, None)

    >>> groups = {"" : ["A"]}
    >>> valid, msg = groupsValidator(groups)
    >>> valid
    False
    >>> print(msg)
    A group has an empty name.

    >>> groups = {"public.awesome" : ["A"]}
    >>> groupsValidator(groups)
    (True, None)

    >>> groups = {"public.kern1." : ["A"]}
    >>> valid, msg = groupsValidator(groups)
    >>> valid
    False
    >>> print(msg)
    The group data contains a kerning group with an incomplete name.
    >>> groups = {"public.kern2." : ["A"]}
    >>> valid, msg = groupsValidator(groups)
    >>> valid
    False
    >>> print(msg)
    The group data contains a kerning group with an incomplete name.

    >>> groups = {"public.kern1.A" : ["A"], "public.kern2.A" : ["A"]}
    >>> groupsValidator(groups)
    (True, None)

    >>> groups = {"public.kern1.A1" : ["A"], "public.kern1.A2" : ["A"]}
    >>> valid, msg = groupsValidator(groups)
    >>> valid
    False
    >>> print(msg)
    The glyph "A" occurs in too many kerning groups.
    """
    bogusFormatMessage = "The group data is not in the correct format."
    if not isDictEnough(value):
        return False, bogusFormatMessage
    firstSideMapping = {}
    secondSideMapping = {}
    for groupName, glyphList in value.items():
        if not isinstance(groupName, (str)):
            return False, bogusFormatMessage
        if not isinstance(glyphList, (list, tuple)):
            return False, bogusFormatMessage
        if not groupName:
            return False, "A group has an empty name."
        if groupName.startswith("public."):
            if not groupName.startswith("public.kern1.") and not groupName.startswith(
                "public.kern2."
            ):
                # unknown public.* name. silently skip.
                continue
            else:
                if len("public.kernN.") == len(groupName):
                    return (
                        False,
                        "The group data contains a kerning group with an incomplete name.",
                    )
            if groupName.startswith("public.kern1."):
                d = firstSideMapping
            else:
                d = secondSideMapping
            for glyphName in glyphList:
                if not isinstance(glyphName, str):
                    return (
                        False,
                        "The group data %s contains an invalid member." % groupName,
                    )
                if glyphName in d:
                    return (
                        False,
                        'The glyph "%s" occurs in too many kerning groups.' % glyphName,
                    )
                d[glyphName] = groupName
    return True, None


# -------------
# kerning.plist
# -------------


def kerningValidator(data):
    """
    Check the validity of the kerning data structure.
    Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).

    >>> kerning = {"A" : {"B" : 100}}
    >>> kerningValidator(kerning)
    (True, None)

    >>> kerning = {"A" : ["B"]}
    >>> valid, msg = kerningValidator(kerning)
    >>> valid
    False
    >>> print(msg)
    The kerning data is not in the correct format.

    >>> kerning = {"A" : {"B" : "100"}}
    >>> valid, msg = kerningValidator(kerning)
    >>> valid
    False
    >>> print(msg)
    The kerning data is not in the correct format.
    """
    bogusFormatMessage = "The kerning data is not in the correct format."
    if not isinstance(data, Mapping):
        return False, bogusFormatMessage
    for first, secondDict in data.items():
        if not isinstance(first, str):
            return False, bogusFormatMessage
        elif not isinstance(secondDict, Mapping):
            return False, bogusFormatMessage
        for second, value in secondDict.items():
            if not isinstance(second, str):
                return False, bogusFormatMessage
            elif not isinstance(value, numberTypes):
                return False, bogusFormatMessage
    return True, None


# -------------
# lib.plist/lib
# -------------

_bogusLibFormatMessage = "The lib data is not in the correct format: %s"


def fontLibValidator(value):
    """
    Check the validity of the lib.
    Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).

    >>> lib = {"foo" : "bar"}
    >>> fontLibValidator(lib)
    (True, None)

    >>> lib = {"public.awesome" : "hello"}
    >>> fontLibValidator(lib)
    (True, None)

    >>> lib = {"public.glyphOrder" : ["A", "C", "B"]}
    >>> fontLibValidator(lib)
    (True, None)

    >>> lib = "hello"
    >>> valid, msg = fontLibValidator(lib)
    >>> valid
    False
    >>> print(msg)  # doctest: +ELLIPSIS
    The lib data is not in the correct format: expected a dictionary, ...

    >>> lib = {1: "hello"}
    >>> valid, msg = fontLibValidator(lib)
    >>> valid
    False
    >>> print(msg)
    The lib key is not properly formatted: expected str, found int: 1

    >>> lib = {"public.glyphOrder" : "hello"}
    >>> valid, msg = fontLibValidator(lib)
    >>> valid
    False
    >>> print(msg)  # doctest: +ELLIPSIS
    public.glyphOrder is not properly formatted: expected list or tuple,...

    >>> lib = {"public.glyphOrder" : ["A", 1, "B"]}
    >>> valid, msg = fontLibValidator(lib)
    >>> valid
    False
    >>> print(msg)  # doctest: +ELLIPSIS
    public.glyphOrder is not properly formatted: expected str,...
    """
    if not isDictEnough(value):
        reason = "expected a dictionary, found %s" % type(value).__name__
        return False, _bogusLibFormatMessage % reason
    for key, value in value.items():
        if not isinstance(key, str):
            return False, (
                "The lib key is not properly formatted: expected str, found %s: %r"
                % (type(key).__name__, key)
            )
        # public.glyphOrder
        if key == "public.glyphOrder":
            bogusGlyphOrderMessage = "public.glyphOrder is not properly formatted: %s"
            if not isinstance(value, (list, tuple)):
                reason = "expected list or tuple, found %s" % type(value).__name__
                return False, bogusGlyphOrderMessage % reason
            for glyphName in value:
                if not isinstance(glyphName, str):
                    reason = "expected str, found %s" % type(glyphName).__name__
                    return False, bogusGlyphOrderMessage % reason
    return True, None


# --------
# GLIF lib
# --------


def glyphLibValidator(value):
    """
    Check the validity of the lib.
    Version 3+ (though it's backwards compatible with UFO 1 and UFO 2).

    >>> lib = {"foo" : "bar"}
    >>> glyphLibValidator(lib)
    (True, None)

    >>> lib = {"public.awesome" : "hello"}
    >>> glyphLibValidator(lib)
    (True, None)

    >>> lib = {"public.markColor" : "1,0,0,0.5"}
    >>> glyphLibValidator(lib)
    (True, None)

    >>> lib = {"public.markColor" : 1}
    >>> valid, msg = glyphLibValidator(lib)
    >>> valid
    False
    >>> print(msg)
    public.markColor is not properly formatted.
    """
    if not isDictEnough(value):
        reason = "expected a dictionary, found %s" % type(value).__name__
        return False, _bogusLibFormatMessage % reason
    for key, value in value.items():
        if not isinstance(key, str):
            reason = "key (%s) should be a string" % key
            return False, _bogusLibFormatMessage % reason
        # public.markColor
        if key == "public.markColor":
            if not colorValidator(value):
                return False, "public.markColor is not properly formatted."
    return True, None


if __name__ == "__main__":
    import doctest

    doctest.testmod()
