###
# Copyright 2020 Hewlett Packard Enterprise, Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###

# -*- coding: utf-8 -*-
"""Handles schema and registry gathering as well as schema parsing and validation."""

# ---------Imports---------

import json
import logging
import re
import textwrap

import six

from redfish.rest.containers import RisObject

from .sharedtypes import JSONEncoder

# ---------End of imports---------


# ---------Debug logger---------

LOGGER = logging.getLogger(__name__)


# ---------End of debug logger---------


class InvalidPathsError(Exception):
    """Raised when requested path is not found"""

    pass


class RegistryValidationError(Exception):
    """Registration Validation Class Error"""

    def __init__(self, msg, regentry=None, selector=None):
        super(RegistryValidationError, self).__init__(msg)
        self.reg = regentry
        self.sel = selector
        self.message = msg


class UnknownValidatorError(Exception):
    """Raised when we find an attribute type that we don't know how to validate."""

    pass


class ValidationManager(object):
    """Keeps track of all the schemas and registries and provides helpers
    to simplify validation."""

    def __init__(self, monolith, defines=None):
        """init of ValidationManager"""
        super(ValidationManager, self).__init__()

        self._schemaid = Typepathforval.typepath.schemapath
        self._regid = Typepathforval.typepath.regpath

        self._classes = list()
        self._classpaths = list()
        # type and path defines object
        self.defines = defines
        self.monolith = monolith
        # error
        self._errors = list()
        self._warnings = list()
        self.updatevalidationdata()

    @property
    def errors(self):
        """All errors found by the last validation."""
        return self._errors

    @property
    def warnings(self):
        """All warnings found by the last validation."""
        return self._warnings

    def reset_errors_warnings(self):
        """Resets warnings and errors, getting ready for the next validation."""
        self._errors = list()
        self._warnings = list()

    def updatevalidationdata(self):
        """Loads the types into the validation manager from monolith."""
        monolith = self.monolith
        for instance in monolith.iter():
            if (
                (
                    x.lower() in instance.maj_type.lower()
                    for x in (
                        self.defines.defs.schemafilecollectiontype,
                        "Collection.",
                        self.defines.defs.regfilecollectiontype,
                    )
                )
                and any(x.lower() in instance.path.lower() for x in (self._schemaid, self._regid))
                and instance
                and instance.path not in self._classpaths
            ):
                self._classpaths.append(instance.path)
                self._classes.append(instance.resp.dict)

    def find_prop(self, propname, latestschema=False, proppath=None):
        """Searches through all locations and returns the schema
        found for the provided propname type.

        :param propname: String containing the schema name.
        :type propname: str
        :param proppath: String containing the schema path if you wish to use that instead.
        :type proppath: str
        :param latestschema: Flag to determine if we should drop the schema version when we try to
                             match schema information. If True, the version will be dropped.
        :type latestschema: bool
        """
        if proppath:
            self.monolith.load(path=proppath, crawl=False, loadtype="ref")
            return True
        for cls in self._classes:
            found = self.find_property(propname, cls=cls, latestschema=latestschema)
            if found:
                return found
        return None

    def itermems(self, membername=None):
        """Searches through all locations and yields each entry.

        :param membername: string containing the registry name. If not passed we use the typedefines
                           string by default.
        :type membername: str
        """
        if not membername:
            membername = self.defines.defs.collectionstring
        for items in self._classes:
            for item in items[membername]:
                yield item

    def iterregmems(self, membername=None):
        """Searches through all registries and yields each entry.

        :param membername: string containing the registry name. If not passed we use the typedefines
                           string by default.
        :type membername: str
        """
        if not membername:
            membername = self.defines.defs.collectionstring
        for items in self._classes:
            if "registr" in items["Name"].lower():  # For Gen9 type/name issue
                for item in items[membername]:
                    yield item

    def iterschemamems(self, membername=None):
        """Searches through all schemas and yields each entry

        :param membername: string containing the registry name. If not passed we use the typedefines
                           string by default.
        :type membername: str
        """
        if not membername:
            membername = self.defines.defs.collectionstring
        for items in self._classes:
            if "schema" in items["Name"].lower():  # For Gen9 type/name issue
                for item in items[membername]:
                    yield item

    def find_property(self, propname, cls=None, latestschema=False):
        """Returns iLO/BIOS registries/schemas

        :param propname: string containing the registry/schema name.
        :type propname: str
        :param cls: self._classes list of dictionaries.
        :type cls: list
        :param latestschema: flag to drop the versioning in the type string.
        :type latestschema: bool.

        :returns: iLO/BIOS registries/schemas that match the supplied name.
        """
        result = []
        dataloc = cls.get("Items", None)
        dataloc = cls.get("Members", None) if not dataloc else dataloc
        keyword = "Schema"
        if dataloc and isinstance(dataloc, list):
            _ = propname.split(".")[0].strip("#")
            propname = propname.split(".")[0].strip("#") if latestschema else propname
            for entry in dataloc:
                if entry:
                    if "Schema" in entry:
                        if propname.lower() in entry["Schema"].lower():
                            result.append(entry)
                    elif "Registry" in entry:
                        if (
                            propname.lower() in entry["@odata.id"].lower()
                            and propname.lower() in entry["Registry"].lower()
                        ):
                            result.append(entry)
                            keyword = "Registry"
                    else:
                        if "@odata.id" in entry:
                            reglink = entry["@odata.id"].split("/")
                            reglink = reglink[len(reglink) - 2]
                            if reglink.lower().startswith(propname.lower()):
                                self.monolith.load(path=entry["@odata.id"], crawl=False)
                                result.append(self.monolith.paths[entry["@odata.id"]].dict)

        if result:
            result = max(
                result,
                key=lambda res: res[Typepathforval.typepath.defs.hrefstring]
                if res.get(Typepathforval.typepath.defs.hrefstring, None)
                else res[keyword],
            )
            schemapath = self.geturidict(result["Location"][0])
            self.monolith.load(path=schemapath, crawl=False, loadtype="ref")
            return result

    def geturidict(self, locationobj):
        """Return the external reference link.

        :param locationobj: Dictionary to get the URI reference from.
        :type locationobj: dict
        """
        if Typepathforval.typepath.defs.isgen10:
            try:
                return locationobj["Uri"]
            except KeyError:
                raise InvalidPathsError("Error accessing Uri path!/n")
        elif Typepathforval.typepath.defs.isgen9:
            try:
                return locationobj["Uri"]["extref"]
            except KeyError:
                raise InvalidPathsError("Error accessing extref path!/n")

    def validatedict(
        self,
        tdict,
        currtype=None,
        proppath=None,
        latestschema=False,
        searchtype=None,
        monolith=None,
        reg=None,
        unique=None,
    ):
        """Load the schema file and validate tdict against it.

        :param tdict: the dictionary to test against.
        :type tdict: dict
        :param currtype: String containing the type the tdict dictionary is.
        :type currtype: str
        :param proppath: String containing the schema path of the tdict dictionary if you wish to
                         use that instead.
        :type proppath: str
        :param latestschema: Flag to determine if we should drop the schema version when we try to
                             match schema information. If True, the version will be dropped.
        :type latestschema: bool
        :param searchtype: Include the attribute registry of you are validating a bios registry.
        :type searchtype: str
        :param monolith: Full data model retrieved from server.
        :type monolith: dict
        :param unique: Flag to override for skipping unique properties.
        :type unique: bool
        :param reg: Registry entry of the given attribute. If this is not provided we will attempt
                    to search based on the searchtype and currtype/proppath arguments.
        :type reg: dict.
        :returns: returns an error list.
        """
        if not reg:
            reg = self.get_registry_model(
                currtype=currtype,
                searchtype=searchtype,
                proppath=proppath,
                latestschema=latestschema,
            )

        if reg:
            list(
                map(
                    lambda x: self.checkreadunique(
                        tdict,
                        x,
                        reg=reg,
                        warnings=self._warnings,
                        unique=unique,
                        searchtype=searchtype,
                    ),
                    list(tdict.keys()),
                )
            )
            orireg = reg.copy()
            ttdict = {key: val for key, val in list(tdict.items()) if not isinstance(val, (dict, list))}
            results = reg.validate_attribute_values(ttdict)
            self._errors.extend(results)

            for ki, val in list(tdict.items()):
                if ki in ttdict:
                    tdict[ki] = ttdict[ki]
                    continue
                reg = orireg.copy()
                valexists = False
                if val and isinstance(val, list):
                    valexists = True
                    # TODO: only validates if its a single dict within list
                    if len(val) == 1 and isinstance(val[0], dict):
                        treg = self.nestedreg(reg=reg, args=[ki])
                        self.validatedict(
                            val[0],
                            unique=unique,
                            monolith=monolith,
                            reg=treg,
                            currtype=currtype,
                            searchtype=searchtype,
                        )
                    else:
                        continue
                elif val and isinstance(val, dict):
                    valexists = True
                    treg = self.nestedreg(reg=reg, args=[ki])
                    self.validatedict(
                        val,
                        monolith=monolith,
                        reg=treg,
                        unique=unique,
                        searchtype=searchtype,
                    )
                if not val and valexists:
                    del tdict[ki]

        else:
            self._errors.append(RegistryValidationError("Unable to locate registry model"))
        return self._errors, self._warnings

    def checkreadunique(self, tdict, tkey, reg=None, warnings=None, unique=None, searchtype=None):
        """Check for and remove the readonly and unique attributes if required.

        :param tdict: the dictionary to test against.
        :type tdict: dict.
        :param tkey: The attribute key value to be tested.
        :type tkey: str.
        :param warnings: list containing found warnings.
        :type warnings: list.
        :param unique: flag to determine override for unique properties.
        :type unique: str.
        :param reg: Registry entry of the given attribute.
        :type reg: dict.
        :returns: returns boolean.
        """
        if "Attributes" in tdict:
            return False
            # key = list(tdict[tkey])[0]
            # reg = reg["Attributes"][key]
        else:
            if tkey in reg:
                reg = reg[tkey]

        if not unique and reg.get("IsSystemUniqueProperty", None):
            if tdict[tkey] and not isinstance(tdict[tkey], bool):
                self._warnings.append("Property '%s' is unique and override not authorized. Skipping...\n" % str(tkey))
                del tdict[tkey]
                return True
        if not reg.get("ReadOnly") or (reg.get(tkey, None) and not reg[tkey].get("readonly")):
            if unique and reg.get("IsSystemUniqueProperty", None):
                self._warnings.append("Property '%s' is unique, but override authorized...Patching..\n" % str(tkey))
                return False
        # if not searchtype or (reg.get("ReadOnly") or (reg.get(tkey)
        #                                                and reg[tkey].get("readonly"))):
        #    self._warnings.append("Property is read-only. skipping... '%s'" % str(tkey))
        #    del tdict[tkey]
        #    return True
        elif reg.get("ReadOnly") or (reg.get(tkey) and reg[tkey].get("readonly")):
            if tdict[tkey]:
                self._warnings.append("Property '%s' is read-only. Skipping...\n" % str(tkey))
                del tdict[tkey]
                return True
        else:
            return False

    def get_registry_model(
        self,
        currtype=None,
        proppath=None,
        getmsg=False,
        searchtype=None,
        newarg=None,
        latestschema=False,
    ):
        """Loads the schema file and find the registry model if available. A registry model is a
        object built for schema/bios registry data.

        :param currtype: Type selection string.
        :type currtype: dict.
        :param proppath: String containing the schema path if you wish to use that instead.
        :type proppath: str
        :param getmsg: Flag to determine if commit should be skipped.
        :type getmsg: bool
        :param searchtype: Include the attribute registry of you are validating a bios registry.
        :type searchtype: str
        :param newarg: List of multi level properties to be modified.
        :type newarg: list
        :param latestschema: Flag to determine if we should drop the schema version when we try to
                             match schema information. If True, the version will be dropped.
        :type latestschema: bool
        :returns: Schema in object form called a registry object.
        """
        regdict = None
        monolith = self.monolith
        currtype = currtype.split("#")[-1].split(".")[0] + "." if currtype and latestschema else currtype
        if (
            not currtype
            or not self.find_prop(
                currtype,
                latestschema=latestschema,
                proppath=proppath if not searchtype else None,
            )
        ) and (not searchtype):
            self._errors.append(RegistryValidationError("Location info is missing.\n"))
            return None
        if not searchtype:
            searchtype = "object"

        try:
            for instance in monolith.iter(searchtype):
                if (
                    (searchtype == Typepathforval.typepath.defs.attributeregtype)
                    or (
                        searchtype == "object"
                        and any(
                            currtype in xtitle
                            for xtitle in (
                                instance.resp.dict.get("title", ""),
                                instance.resp.dict.get("oldtitle", ""),
                            )
                        )
                    )
                    or (
                        searchtype != "object"
                        and currtype.split("#")[-1].split(".")[0] == instance.dict.get("RegistryPrefix", "")
                    )
                ):
                    regdict = instance.resp.dict
                    break
        except BaseException:
            pass

        if not regdict:
            self._errors.append(RegistryValidationError("Location data is empty.\n"))
            return None

        jsonreg = json.loads(json.dumps(regdict, indent=2, cls=JSONEncoder))

        if getmsg:
            return {jsonreg["RegistryPrefix"]: jsonreg["Messages"]}

        # This was done for bios registry model compatibility
        if "RegistryEntries" in jsonreg:
            regitem = jsonreg["RegistryEntries"]
            if "Attributes" in regitem:
                newitem = {item[Typepathforval.typepath.defs.attributenametype]: item for item in regitem["Attributes"]}
                regitem["Attributes"] = newitem
                if not Typepathforval.typepath.flagiften:
                    del regitem["Attributes"]
                    newitem.update(regitem)
                    regitem = newitem
                reg = HpPropertiesRegistry.parse(regitem)
            return self.nestedreg(reg=reg, args=newarg) if newarg else reg

        if "properties" in jsonreg:
            regitem = jsonreg["properties"]
            if "Properties" in regitem:
                regitem.update(regitem["Properties"])
                del regitem["Properties"]
            reg = HpPropertiesRegistry.parse(regitem)

            return self.nestedreg(reg=reg, args=newarg) if newarg else reg

    def nestedreg(self, reg=None, args=None):
        """Go through the registry entry to find the required nested attribute.

        :param reg: Registry entry of the given attribute.
        :type reg: dict
        :param args: List of multi level properties to be modified.
        :type args: list
        :returns: dict of Registry entry
        """
        for arg in args:
            try:
                arg = next((key for key in list(reg.keys()) if key.lower() == arg.lower()), None)
                if not arg:
                    return None
                if ("properties" in reg[arg].keys()) and ("patternProperties" in reg[arg].keys()):
                    reg[arg]["properties"].update(reg[arg]["patternProperties"])
                    reg = reg[arg]["properties"]
                elif "oneOf" in reg[arg]:
                    oneof = reg[arg]["oneOf"]
                    for item in oneof:
                        reg = item["properties"]
                elif (
                    "type" in reg[arg]
                    and reg[arg]["type"] == "array"
                    and "items" in reg[arg]
                    and "properties" in reg[arg]["items"]
                ):
                    reg = reg[arg]["items"]["properties"]
                elif ("properties" not in reg[arg].keys()) or ("patternProperties" in reg[arg].keys()):
                    reg = reg[arg]
                else:
                    reg = reg[arg]["properties"]
            except:
                try:
                    reg = reg[arg]["patternProperties"]
                except:
                    return None
        return reg


class HpPropertiesRegistry(RisObject):
    """Models a schema or bios attribute registry. Registry model."""

    def __init__(self, d):
        super(HpPropertiesRegistry, self).__init__(d)

    def validate_attribute_values(self, tdict):
        """Look for tdict in the attribute list and attempt to validate its value.

        :param tdict: the dictionary to test against.
        :type tdict: dict
        :returns: A validated list
        """
        result = list()

        for tkey in tdict:
            if tkey not in self:
                # Added for Gen 9 Bios properties not in registry
                continue
            elif self[tkey] and (checkattr(self[tkey], "type") or checkattr(self[tkey], "Type")):
                keyval = list()
                keyval.append(tdict[tkey])
                temp = self.validate_attribute(self[tkey], keyval, tkey)
                tdict[tkey] = keyval[0]

                for err in temp:
                    if isinstance(err, RegistryValidationError):
                        if err.reg:
                            err.sel = tkey

                result.extend(temp)

        return result

    def get_validator(self, attrname, newargs=None, oneof=None):
        """Returns attribute validator type.

        :param attrname: attribute name to validate. Ex: In A/B/C, this will be A.
        :type attrname: str
        :param newargs: List of multi level properties to be modified. Ex: In A/B/C this will be
                        a list of B and C.
        :type newargs: list
        :param oneof: Special string for "oneof" options within validation.
        :type oneof: string
        :returns: The validator type class for the property passed.
        """
        if oneof:
            self = oneof

        if newargs:
            for arg in newargs:
                try:
                    self = self["properties"]
                except Exception:
                    pass

                if not checkattr(self, arg):
                    return None
                elif not arg == newargs[-1]:
                    self = self[arg]

        if not checkattr(self, attrname):
            return None

        validator = None
        if EnumValidator.is_type(self[attrname]):
            validator = EnumValidator.parse(self[attrname])
        elif StringValidator.is_type(self[attrname]):
            validator = StringValidator.parse(self[attrname])
        elif ObjectValidator.is_type(self[attrname]):
            validator = ObjectValidator.parse(self[attrname])
        elif IntegerValidator.is_type(self[attrname]):
            validator = IntegerValidator.parse(self[attrname])
        elif BoolValidator.is_type(self[attrname]):
            validator = BoolValidator.parse(self[attrname])
        elif PasswordValidator.is_type(self[attrname]):
            validator = PasswordValidator.parse(self[attrname])
        elif "oneOf" in list(self[attrname].keys()):
            for item in self[attrname]["oneOf"]:
                validator = self.get_validator(attrname, newargs, HpPropertiesRegistry({attrname: item}))
                if validator:
                    break
        return validator

    def validate_attribute(self, attrentry, attrvallist, name):
        """Function to validate attribute against its schema.

        :param attrentry: Key of property to validate.
        :type attrentry: str
        :param attrval: Value of Key to validate.
        :type attrval: str
        :param name: Clean name for outputting information to users.
        :type name: str
        :returns: returns list with validated attributes
        """
        result = list()
        validator = None
        if self.nulltypevalidationcheck(attrval=attrvallist[0], attrentry=attrentry):
            return result

        if EnumValidator.is_type(attrentry):
            validator = EnumValidator.parse(attrentry)
        elif StringValidator.is_type(attrentry):
            validator = StringValidator.parse(attrentry)
        elif IntegerValidator.is_type(attrentry):
            validator = IntegerValidator.parse(attrentry)
        elif BoolValidator.is_type(attrentry):
            validator = BoolValidator.parse(attrentry)
        elif ObjectValidator.is_type(attrentry):
            validator = ObjectValidator.parse(attrentry)
        elif PasswordValidator.is_type(attrentry):
            validator = PasswordValidator.parse(attrentry)
        else:
            raise UnknownValidatorError(attrentry)

        if validator:
            result.extend(validator.is_array(attrentry, attrvallist, name))
            result.extend(validator.validate(attrvallist, name))
        return result

    def nulltypevalidationcheck(self, attrval=None, attrentry=None):
        """Function to validate null attributes against iLO schema

        :param attrentry: Key of property to validate.
        :type attrentry: str
        :param attrval: Value of Key to validate.
        :type attrval: str
        :returns: True if entry is null and valid.
        """
        if "type" in attrentry and attrval is None:
            if isinstance(attrentry["type"], list):
                for item in attrentry["type"]:
                    if item.lower() == "null":
                        return True
        return False


class BaseValidator(RisObject):
    """Base class for all validators."""

    def __init__(self, d):
        super(BaseValidator, self).__init__(d)

    def validate(self):
        """Overridable function for validation"""
        raise RuntimeError("You must override this method in your derived class")

    def common_print_help(self, name):
        """Common human readable schema data.

        :param name: clean name for outputting.
        :type name: str
        :returns: A human readable string of schema data.
        """
        outdata = ""
        wrapper = textwrap.TextWrapper()
        wrapper.initial_indent = " " * 4
        wrapper.subsequent_indent = " " * 4

        outdata += "\nNAME\n"
        outdata += "%s\n" % wrapper.fill("%s" % name)
        outdata += "\n"

        if "DisplayName" in self:
            outdata += "\nDISPLAY NAME\n"
            outdata += "%s\n" % wrapper.fill("%(DisplayName)s" % self)
            outdata += "\n"

        if "description" in self:
            outdata += "\nDESCRIPTION\n"
            outdata += "%s\n" % wrapper.fill("%(description)s" % self)
            outdata += "\n"

        if "HelpText" in self:
            outdata += "\nHELP TEXT\n"
            outdata += "%s\n" % wrapper.fill("%(HelpText)s" % self)
            outdata += "\n"

        if "WarningText" in self:
            outdata += "\n************************************************\n"
            outdata += "\nWARNING\n"
            outdata += "%s\n" % wrapper.fill("%(WarningText)s" % self)
            outdata += "\n\n**********************************************\n"
            outdata += "\n"

        if "type" in self and isinstance(self["type"], list):
            outdata += "\nTYPE\n"
            for item in self["type"]:
                outdata += "%s\n" % wrapper.fill("%s" % item)
            outdata += "\n"
        elif "type" in self:
            outdata += "\nTYPE\n"
            outdata += "%s\n" % wrapper.fill("%(type)s" % self)
            outdata += "\n"
        elif "Type" in self:
            outdata += "\nTYPE\n"
            outdata += "%s\n" % wrapper.fill("%(Type)s" % self)
            outdata += "\n"

        if "ReadOnly" in self:
            outdata += "\nREAD-ONLY\n"
            outdata += "%s\n" % wrapper.fill("%(ReadOnly)s" % self)
            outdata += "\n"
        elif "readonly" in self:
            outdata += "\nREAD-ONLY\n"
            outdata += "%s\n" % wrapper.fill("%(readonly)s" % self)
            outdata += "\n"
        return outdata

    def is_arrtype(self, attrentry):
        """Validate that the type is an array.

        :param attrentry: Registry model entry used for validation.
        :type attrentry: dict
        :returns: A boolean based on whether type is an array.
        """
        if "type" in attrentry and attrentry["type"] == "array":
            return True
        return False

    def is_array(self, attrentry, arrval, name):
        """Validate that the given value is an array type.

        :param attrentry: Registry model entry used for validation.
        :type attrentry: dict
        :param attrval: Value of Key to validate.
        :type attrval: str
        :returns: A boolean based on whether type is array and the value is valid for array type.
        """
        result = []

        if self.is_arrtype(attrentry):
            if isinstance(
                arrval[0],
                (
                    frozenset,
                    list,
                    set,
                    tuple,
                ),
            ):
                return []
            else:
                result.append(
                    RegistryValidationError(
                        "'%s' is not a valid setting " "for '%s', expecting an array" % (arrval[0], name),
                        regentry=self,
                    )
                )
        return result


class EnumValidator(BaseValidator):
    """Enum validator class"""

    def __init__(self, d):
        super(EnumValidator, self).__init__(d)

    @staticmethod
    def is_type(attrentry):
        """Validate that the type is enumeration.

        :param attrentry: Registry model entry used for validation.
        :type attrentry: dict
        :returns: A boolean based on whether type is eneumeration.
        """
        if "type" in attrentry:
            if isinstance(attrentry["type"], list):
                for item in attrentry["type"]:
                    if item.lower() == "enumeration":
                        return True
                    elif "enum" in attrentry and item.lower() == "string":
                        return True
            elif "enum" in attrentry and attrentry["type"] == "array":
                for key, value in attrentry["items"].items():
                    if key.lower() == "type" and value.lower() == "string":
                        return True
            else:
                if attrentry["type"].lower() == "enumeration":
                    return True
                elif "enum" in attrentry and attrentry["type"].lower() == "string":
                    return True
        elif "Type" in attrentry:
            if attrentry["Type"].lower() == "enumeration":
                return True

        return False

    def validate(self, keyval, name):
        """Validate against schemas.

        :param keyval: New value to be used for validation in a list
        :type keyval: list
        :param name: Clean name for outputting human readable info.
        :type name: str
        :returns: An error if validation fails.
        """
        result = list()
        newval = keyval[0]

        try:
            for possibleval in self.enum:
                if (
                    possibleval
                    and (
                        isinstance(possibleval, type(newval))
                        or (isinstance(possibleval, six.string_types) and isinstance(newval, six.string_types))
                    )
                    and possibleval.lower() == str(newval).lower()
                ):
                    keyval[0] = possibleval
                    return result
        except Exception:
            for possibleval in self.Value:
                if possibleval.ValueName.lower() == str(newval).lower():
                    keyval[0] = possibleval.ValueName
                    return result

        result.append(
            RegistryValidationError("'%s' is not a valid setting " "for '%s'" % (newval, name), regentry=self)
        )

        return result

    def print_help(self, name):
        """Human readable schema information specific to Enum data.

        :param name: Clean name for outputting human readable info.
        :type name: str
        :returns: A human readable string of schema data.
        """
        outdata = self.common_print_help(name)
        outdata += "\nPOSSIBLE VALUES\n"
        try:
            for possibleval in self.enum:
                outdata += "    %s\n" % possibleval
        except Exception:
            for possibleval in self.Value:
                outdata += "    %(ValueName)s\n" % possibleval
        outdata += "\n"
        return outdata


class BoolValidator(BaseValidator):
    """Bool validator class"""

    def __init__(self, d):
        super(BoolValidator, self).__init__(d)

    @staticmethod
    def is_type(attrentry):
        """Validate that the type is boolean.

        :param attrentry: Registry model entry used for validation.
        :type attrentry: dict
        :returns: A boolean based on whether type is boolean.
        """
        if "type" in attrentry:
            if isinstance(attrentry["type"], list):
                for item in attrentry["type"]:
                    if item.lower() == "boolean":
                        return True
            elif attrentry["type"] == "array":
                for key, value in attrentry["items"].items():
                    if key.lower() == "type" and value.lower() == "boolean":
                        return True
            else:
                if attrentry["type"].lower() == "boolean":
                    return True
        elif "Type" in attrentry:
            if attrentry["Type"].lower() == "boolean":
                return True

        return False

    def validate(self, newval, name):
        """Validate against schemas.

        :param newval: New value to be used for validation in a list
        :type newval: list
        :param name: Clean name for outputting human readable info.
        :type name: str
        :returns: An error if value is invalid.
        """
        result = list()
        if newval[0] is False or newval[0] is True:
            return result

        result.append(
            RegistryValidationError("'%s' is not a valid setting for '%s'" % (newval[0], name), regentry=self)
        )

        return result

    def print_help(self, name):
        """Human readable schema information specific to Boolean data.

        :param name: Clean name for outputting human readable info.
        :type name: str
        :returns: A human readable string of schema data.
        """
        outdata = self.common_print_help(name)
        outdata += "\nPOSSIBLE VALUES\n"
        outdata += "    True or False\n"
        outdata += "\n"
        return outdata


class StringValidator(BaseValidator):
    """Constructor"""

    def __init__(self, d):
        super(StringValidator, self).__init__(d)

    @staticmethod
    def is_type(attrentry):
        """Validate that the type is string.

        :param attrentry: Registry model entry used for validation.
        :type attrentry: dict
        :returns: A boolean based on whether type is string.
        """
        if "type" in attrentry:
            if isinstance(attrentry["type"], list):
                for item in attrentry["type"]:
                    if item.lower() == "string":
                        return True
            elif attrentry["type"] == "array":
                for key, value in attrentry["items"].items():
                    if key.lower() == "type" and "string" in value:
                        return True
            else:
                if attrentry["type"].lower() == "string":
                    return True
        elif "Type" in attrentry:
            if attrentry["Type"].lower() == "string":
                return True

        return False

    def validate(self, newvallist, _):
        """Validate against schemas.

        :param newvallist: New value to be used for validation in a list
        :type newvallist: list
        :returns: An error if validation fails criteria.
        """
        newval = newvallist[0]
        result = list()
        namestr = Typepathforval.typepath.defs.attributenametype
        if not isinstance(newval, str):
            result.append(RegistryValidationError("Given value must be a string"))
            return result
        if "MinLength" in self:
            if len(newval) < int(self["MinLength"]):
                result.append(
                    RegistryValidationError(
                        "'%s' must be at least '%s' characters long" % (self[namestr], int(self["MinLength"])),
                        regentry=self,
                    )
                )

        if "MaxLength" in self:
            if len(newval) > int(self["MaxLength"]):
                result.append(
                    RegistryValidationError(
                        "'%s' must be less than '%s' characters long" % (self[namestr], int(self["MaxLength"])),
                        regentry=self,
                    )
                )

        if "ValueExpression" in self:
            if self["ValueExpression"]:
                pat = re.compile(self["ValueExpression"])
                if newval and not pat.match(newval):
                    result.append(
                        RegistryValidationError(
                            "'%s' must match the regular expression " "'%s'" % (self[namestr], self["ValueExpression"]),
                            regentry=self,
                        )
                    )

        return result

    def print_help(self, name):
        """Human readable schema information specific to String data.

        :param name: Clean name for outputting human readable info.
        :type name: str
        :returns: A human readable string of schema data.
        """
        wrapper = textwrap.TextWrapper()
        wrapper.initial_indent = " " * 4
        wrapper.subsequent_indent = " " * 4
        outdata = self.common_print_help(name)
        if "MinLength" in self:
            outdata += "\nMIN LENGTH\n"
            outdata += "%s" % wrapper.fill("%(MinLength)s" % self)
            outdata += "\n"

        if "MaxLength" in self:
            outdata += "\nMAX LENGTH\n"
            outdata += "%s" % wrapper.fill("%(MaxLength)s" % self)
            outdata += "\n"
        return outdata


class IntegerValidator(BaseValidator):
    """Interger validator class"""

    def __init__(self, d):
        super(IntegerValidator, self).__init__(d)

    @staticmethod
    def is_type(attrentry):
        """Validate that the type is integer.

        :param attrentry: Registry model entry used for validation.
        :type attrentry: dict
        :returns: A boolean based on whether type is integer.
        """
        if "type" in attrentry:
            if isinstance(attrentry["type"], list):
                for item in attrentry["type"]:
                    if item.lower() == "integer" or item.lower() == "number":
                        return True
            elif attrentry["type"] == "array":
                for key, value in attrentry["items"].items():
                    if key.lower() == "type":
                        if value.lower() == "integer" or value.lower() == "number":
                            return True
            else:
                if attrentry["type"].lower() == "integer" or attrentry["type"].lower().lower() == "number":
                    return True
        elif "Type" in attrentry:
            if attrentry["Type"].lower() == "integer":
                return True

        return False

    def validate(self, newvallist, _):
        """Validate against schemas.

        :param newvallist: New value to be used for validation in a list
        :type newvallist: list
        :returns: An error if validation fails criteria.
        """
        result = list()
        try:
            intval = int(newvallist[0])
            newvallist[0] = intval
        except:
            result.append(RegistryValidationError("'%(Name)s' must " "be an integer value'" % (self), regentry=self))
            return result

        if newvallist[0] and not str(intval).isdigit():
            result.append(RegistryValidationError("'%(Name)s' must " "be an integer value'" % (self), regentry=self))
            return result

        if "LowerBound" in self:
            if intval < int(self["LowerBound"]):
                result.append(
                    RegistryValidationError(
                        "'%s' must be greater" " than or equal to '%s'" % (self.Name, int(self["LowerBound"])),
                        regentry=self,
                    )
                )

        if "UpperBound" in self:
            if intval > int(self["UpperBound"]):
                result.append(
                    RegistryValidationError(
                        "'%s' must be less " "than or equal to '%s'" % (self.Name, int(self["LowerBound"])),
                        regentry=self,
                    )
                )

        return result

    def print_help(self, name):
        """Human readable schema information specific to Integer data.

        :param name: Clean name for outputting human readable info.
        :type name: str
        :returns: A human readable string of schema data.
        """
        outdata = self.common_print_help(name)
        return outdata


class ObjectValidator(BaseValidator):
    """Object validator class"""

    def __init__(self, d):
        super(ObjectValidator, self).__init__(d)

    @staticmethod
    def is_type(attrentry):
        """Validate that the type is object.

        :param attrentry: Registry model entry used for validation.
        :type attrentry: dict
        :returns: A boolean based on whether type is object.
        """
        if "type" in attrentry:
            if isinstance(attrentry["type"], list):
                for item in attrentry["type"]:
                    if item.lower() == "object":
                        return True
            elif attrentry["type"] == "array":
                for key, value in attrentry["items"].items():
                    if key.lower() == "type" and value.lower() == "object":
                        return True
                    elif key.lower() == "anyof":
                        try:
                            if value[0]["type"] == "object":
                                return True
                        except Exception:
                            continue
            else:
                if attrentry["type"].lower() == "object":
                    return True
        elif "Type" in attrentry:
            if attrentry["Type"].lower() == "object":
                return True

        return False

    def validate(self, newval, name):
        """Validate against schemas.

        :param newval: New value to be used for validation in a list
        :type newval: list
        :param name: Clean name for outputting human readable info.
        :type name: str
        :returns: An error if value is invalid.
        """
        # TODO: need to add logic for true postive and false negatives.
        result = list()
        if isinstance(newval[0], (dict, six.string_types, int)):
            result.append(
                RegistryValidationError(
                    "'%s' is not a valid setting for '%s'" % (newval[0], name),
                    regentry=self,
                )
            )
        return result

    def print_help(self, name):
        """Human readable schema information specific to Object data.

        :param name: Clean name for outputting human readable info.
        :type name: str
        :returns: A human readable string of schema data.
        """
        wrapper = textwrap.TextWrapper()
        wrapper.initial_indent = " " * 4
        wrapper.subsequent_indent = " " * 4
        outdata = self.common_print_help(name)
        if "properties" in self:
            outdata += "\nSUB-PROPERTIES\n"
            propdata = ", ".join(self.properties.keys())
            outdata += "%s" % wrapper.fill("%s" % propdata)
            outdata += "\n"
        elif "items" in self:
            outdata += "\nSUB-PROPERTIES\n"
            propdata = ", ".join(self["items"].properties.keys())
            outdata += "%s" % wrapper.fill("%s" % propdata)
            outdata += "\n"

        return outdata


class PasswordValidator(BaseValidator):
    """Password validator class"""

    def __init__(self, d):
        super(PasswordValidator, self).__init__(d)

    @staticmethod
    def is_type(attrentry):
        """Validate that the type is a password.

        :param attrentry: Registry model entry used for validation.
        :type attrentry: dict
        :returns: A boolean based on whether type is a password.
        """
        if "type" in attrentry:
            if isinstance(attrentry["type"], list):
                for item in attrentry["type"]:
                    if item.lower() == "password":
                        return True
            elif attrentry["type"] == "array":
                for key, value in attrentry["items"].items():
                    if key.lower() == "type" and value.lower() == "password":
                        return True
            else:
                if attrentry["type"].lower() == "password":
                    return True
        elif "Type" in attrentry:
            if attrentry["Type"].lower() == "password":
                return True

        return False

    def validate(self, newvallist, _):
        """Validate against schemas.

        :param newvallist: New value to be used for validation in a list
        :type newvallist: list
        :returns: An error if validation fails criteria.
        """
        result = list()
        newval = newvallist[0]

        if newval is None:
            return result

        if not isinstance(newval, str):
            result.append(RegistryValidationError("Given value must be a string"))
        if "MinLength" in self:
            if len(newval) < int(self["MinLength"]):
                result.append(
                    RegistryValidationError(
                        "'%s' must be at least" " '%s' characters long" % (self.Name, int(self["MinLength"])),
                        regentry=self,
                    )
                )

        if "MaxLength" in self:
            if len(newval) > int(self["MaxLength"]):
                result.append(
                    RegistryValidationError(
                        "'%s' must be less " "than '%s' characters long" % (self.Name, int(self["MaxLength"])),
                        regentry=self,
                    )
                )

        if "ValueExpression" in self:
            if self["ValueExpression"]:
                pat = re.compile(self["ValueExpression"])
                if newval and not pat.match(newval):
                    result.append(
                        RegistryValidationError(
                            "'%(Name)s' must " "match the regular expression '%(Value" "Expression)s'" % (self),
                            regentry=self,
                        )
                    )

        return result

    def print_help(self, name):
        """Human readable schema information specific to Password data.

        :param name: Clean name for outputting human readable info.
        :type name: str
        :returns: A human readable string of schema data.
        """
        wrapper = textwrap.TextWrapper()
        wrapper.initial_indent = " " * 4
        wrapper.subsequent_indent = " " * 4
        outdata = self.common_print_help(name)
        if "MinLength" in self:
            outdata += "\nMIN LENGTH\n"
            outdata += "%s" % wrapper.fill("%(MinLength)s" % self)
            outdata += "\n"

        if "MaxLength" in self:
            outdata += "\nMAX LENGTH\n"
            outdata += "%s" % wrapper.fill("%(MaxLength)s" % self)
            outdata += "\n"
        return outdata


class Typepathforval(object):
    """Way to store the typepath defines object."""

    typepath = None

    def __new__(cls, typepathobj):
        if typepathobj:
            Typepathforval.typepath = typepathobj


def checkattr(aobj, prop):
    """Check if an attribute exists"""
    try:
        if hasattr(aobj, prop):
            return True
    except:
        pass
    return False
