###
# 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 -*-
"""Monolith database implementation. Crawls Redfish and Legacy REST implementations
   and holds all data retrieved. The created database is called the **monolith** and referenced as
   such in the code documentation."""

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

import logging
import re
import sys
import threading
from re import error as regexerr

# Added for py3 compatibility
import six

try:
    from collections import OrderedDict, defaultdict
except ImportError:
    from collections.abc import OrderedDict, defaultdict

from queue import Queue

import jsonpath_rw
import jsonpointer
from jsonpointer import set_pointer
from six.moves.urllib.parse import urlparse, urlunparse
from six.moves.urllib.parse import ParseResult
from six.moves.urllib.parse import quote

from redfish.rest.containers import RestRequest, StaticRestResponse
from redfish.ris.ris_threaded import LoadWorker
from redfish.ris.sharedtypes import Dictable

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

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

LOGGER = logging.getLogger(__name__)


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


class BiosUnregisteredError(Exception):
    """Raised when BIOS has not been registered correctly in iLO"""

    pass


class SchemaValidationError(Exception):
    """Schema Validation Class Error"""

    pass


class SessionExpired(Exception):
    """Raised when session has expired"""

    pass


class RisMonolithMemberBase(Dictable):
    """RIS monolith member base class"""

    pass


class RisInstanceNotFoundError(Exception):
    """Raised when attempting to select an instance that does not exist"""

    pass


class RisMonolithMemberv100(RisMonolithMemberBase):
    """Class used by :class:`RisMonolith` for holding information on a response and adds extra data
    for monolith usage. A member can be marked as *modified* which means another operation may have
    rendered this member out of date. It should be reloaded before continuing to ensure data is
    up to date.

    :param restresp: `RestResponse` to create a member from.
    :type restresp: :class:`redfish.rest.containers.RestResponse`
    :param isredfish: Flag if the response is redfish or not
    :type isredfish: bool
    """

    def __init__(self, restresp=None, isredfish=True):
        self._resp = restresp
        self._patches = list()
        # Check if typedef can be used here
        self._typestring = "@odata.type" if isredfish else "Type"
        self.modified = False
        self.defpath = self.deftype = self.defetag = self._type = None
        self.__bool__ = self.__nonzero__

    @property
    def type(self):
        """Get type of the monolith member's response"""
        try:
            if self and self._typestring in self.resp.dict:
                return self.resp.dict[self._typestring]
            # Added for object type
            elif self and "type" in self.resp.dict:
                return self.resp.dict["type"]
        except (AttributeError, ValueError, TypeError):
            return self.deftype  # data not yet fetched, probably empty dict, so assume deftype
        return None

    @property
    def maj_type(self):
        """Get major type of the monolith member's response"""
        if self.type:
            if "." in self.type:
                types = ".".join(self.type.split(".", 2)[:2])
                retval = types[1:] if types.startswith("#") else types
            else:
                retval = self.type
            return retval
        return self.deftype

    def __nonzero__(self):
        """Defining the bool value for the class"""
        return True if self.resp else False

    @property
    def resp(self):
        """Get the entire response of the monolith member"""
        return self._resp

    @property
    def path(self):
        """Get path of the monolith member's response"""

        try:
            if self:
                return self.resp.request.path
        except (AttributeError, ValueError):
            pass
        return self.defpath

    @property
    def patches(self):
        """Get patches for the monolith member"""
        return self._patches

    @patches.setter
    def patches(self, val):
        """Set patches for the monolith member"""
        self._patches = val

    @property
    def dict(self):
        """Get the dictionary of the monolith member's response"""
        return self._resp.dict

    @property
    def etag(self):
        """Get the etag of the response"""
        return self.defetag if not self.resp else self.resp.getheader("etag")

    def popdefs(self, typename, pathval, etagval):
        """Populate the default values in the class

        :param typename: The default **Type** string. Example: @odata.type
        :type typename: str
        :param pathval: The default **Path** string. Example: @odata.id
        :type pathval: str
        :param etagval: The default **ETag** value.
        :type etagval: str
        """
        self.defetag = etagval
        self.deftype = typename
        self.defpath = pathval

    def to_dict(self):
        """Converts Monolith Member to a dictionary. This is the reverse of :func:`load_from_dict`.

        :returns: returns the Monolith Member in dictionary form
        """
        result = OrderedDict()
        if self.maj_type:
            result["Type"] = self.type

            if self.resp:
                if self.maj_type == "Collection.1" and "MemberType" in self.resp.dict:
                    result["MemberType"] = self.resp.dict["MemberType"]

            result["links"] = OrderedDict()
            result["links"]["href"] = ""
            result["ETag"] = self.etag

            if self.resp:
                result["Content"] = self.resp.dict
                result["Status"] = self.resp.status
                result["Headers"] = self.resp.getheaders()
            result["OriginalUri"] = self.path
            result["Patches"] = self._patches
            result["modified"] = self.modified
            result["MajType"] = self.maj_type

        return result

    def load_from_dict(self, src):
        """Load variables to a monolith member from a dictionary.
        This is the reverse of :func:`to_dict`.

        :param src: Source to load member data from.
        :type src: dict
        """
        if "Type" in src:
            self._type = src["Type"]
            if "Content" in src:
                restreq = RestRequest(method="GET", path=src["OriginalUri"])
                src["restreq"] = restreq
                self._resp = StaticRestResponse(**src)
            self.deftype = src["MajType"]
            self.defpath = src["OriginalUri"]
            self.defetag = src["ETag"]
            self._patches = src["Patches"]
            self.modified = src["modified"]


class RisMonolith(Dictable):
    """Monolithic cache of RIS data. This takes a :class:`redfish.rest.v1.RestClient` and uses it to
    gather data from a server and saves it in a modifiable database called monolith.

    :param client: client to use for data retrieval.
    :type client: :class:`redfish.rest.v1.RestClient`
    :param typepath: The compatibility class to use for differentiating between Redfish/LegacyRest.
    :type typepath: :class:`redfish.rest.ris.Typesandpathdefines`
    :param directory_load: The flag to quick load using resource directory if possible.
           When set to True this will load paths, etags, and types, but not create responses for
           every monolith member. When responses are needed, they will need to be loaded separately.
    :type directory_load: bool
    """

    def __init__(self, client, typepath, directory_load=True):
        self._client = client
        self.name = "Monolithic output of RIS Service"
        self._visited_urls = list()
        self._type = None
        self._name = None
        self.progress = 0
        self._directory_load = directory_load
        self.is_redfish = self.client.is_redfish
        self.typesadded = defaultdict(set)
        self.paths = dict()
        self.ctree = defaultdict(set)
        self.colltypes = defaultdict(set)

        self.typepath = typepath
        self.collstr = self.typepath.defs.collectionstring
        self.etagstr = "ETag"
        if self.is_redfish:
            self._resourcedir = "/redfish/v1/ResourceDirectory/"
        else:
            self._resourcedir = "/rest/v1/ResourceDirectory"

        # MultiThreading
        self.get_queue = Queue()
        self.threads = []

    @property
    def directory_load(self):
        """The flag to gather information about a tree without downloading every path. Only usable
        on HPE systems with a ResourceDirectory. type"""
        return self._directory_load

    @directory_load.setter
    def directory_load(self, dir_load):
        """Set the directory_load flag"""
        self._directory_load = dir_load

    @property
    def type(self):
        """Return monolith version type"""
        return "Monolith.1.0.0"

    @property
    def visited_urls(self):
        """The urls visited by the monolith"""
        return list(set(self._visited_urls) | set(self.paths.keys()))

    @visited_urls.setter
    def visited_urls(self, visited_urls):
        """Set visited URLS."""
        self._visited_urls = visited_urls

    @property
    def types(self):
        """Returns list of types for members in the monolith

        :rtype: list
        """
        return list(self.typesadded.keys())

    @types.setter
    def types(self, member):
        """Adds a member to monolith

        :param member: Member created based on response.
        :type member: RisMonolithMemberv100
        """
        self.typesadded[member.maj_type].add(member.path)
        patches = []
        if member.path in list(self.paths.keys()):
            patches = self.paths[member.path].patches
        self.paths[member.path] = member
        self.paths[member.path].patches.extend([patch for patch in patches])

    @property
    def client(self):
        """Returns the current client object reference

        :rtype: class object
        """
        return self._client

    @client.setter
    def client(self, curr_client):
        """Set the current client

        :param curr_client: current client object
        :type curr_client: class object
        """
        self._client = curr_client

    def path(self, path):
        """Provides the member corresponding to the path specified. Case sensitive.

        :param path: path of the monolith member to return
        :type path: str
        :rtype: RisMonolithMemberv100
        """
        try:
            return self.paths[path]
        except:
            return None

    def iter(self, typeval=None):
        """An iterator that yields each member of monolith associated with a specific type. In the
        case that no type is included this will yield all members in the monolith.

        :rtype: RisMonolithMemberv100
        """
        if not typeval:
            for _, val in list(self.paths.items()):
                yield val
        else:
            for typename in self.gettypename(typeval):
                for item in self.typesadded[typename]:
                    yield self.paths[item]

    #             types = next(self.gettypename(typeval), None)
    #             if types in self.typesadded:
    #                 for item in self.typesadded[types]:
    #                     yield self.paths[item]
    #             else:
    #                 raise RisInstanceNotFoundError("Unable to locate instance for" \
    #                                                             " '%s'\n" % typeval)

    def itertype(self, typeval):
        """Iterator that yields member(s) of given type in the monolith and raises an error if no
        member of that type is found.

        :param typeval: type name of the requested member.
        :type typeval: str
        :rtype: RisMonolithMemberv100
        """
        typeiter = self.gettypename(typeval)
        types = next(typeiter, None)
        if types:
            while types:
                for item in self.typesadded[types]:
                    yield self.paths[item]
                types = next(typeiter, None)
        else:
            raise RisInstanceNotFoundError("Unable to locate instance for '%s'\n" % typeval)

    def typecheck(self, types):
        """Check if a member of given type exists in the monolith

        :param types: type to check.
        :type types: str
        :rtype: bool
        """
        if any(types in val for val in self.types):
            return True
        return False

    def gettypename(self, types):
        """Takes a full type response and returns all major types associated.
        Example: #Type.v1_0_0.Type will return iter(Type.1)

        :param types: The type of the requested response.
        :type types: str
        :rtype: iter of major types
        """
        types = types[1:] if types[0] in ("#", "#") else types
        return iter((xt for xt in self.types if xt and types.lower() in xt.lower()))

    def update_member(self, member=None, resp=None, path=None, init=True):
        """Adds member to the monolith. If the member already exists the
        data is updated in place. Takes either a RisMonolithMemberv100 instance or a
        :class:`redfish.rest.containers.RestResponse` along with that responses path.

        :param member: The monolith member to add to the monolith.
        :type member: RisMonolithMemberv100
        :param resp: The rest response to add to the monolith.
        :type resp: :class:`redfish.rest.containers.RestResponse`
        :param path: The path correlating to the response.
        :type path: str
        :param init: Flag if addition is part of the initial load. Set this to false if you are
                     calling this by itself.
        :type init: bool
        """
        if not member and resp and path:
            self._visited_urls.append(path.lower())

            member = RisMonolithMemberv100(resp, self.is_redfish)
            if not member:  # Assuming for lack of member and not member.type
                return
            if not member.type:
                member.deftype = "object"  # Hack for general schema with no type

        self.types = member

        if init:
            self.progress += 1
            if LOGGER.getEffectiveLevel() == 40:
                self._update_progress()

    def load(
        self,
        path=None,
        includelogs=False,
        init=False,
        crawl=True,
        loadtype="href",
        loadcomplete=False,
        path_refresh=False,
        json_out=False,
    ):
        """Walks the entire data model and caches all responses or loads an individual path into
        the monolith. Supports both threaded and sequential crawling.

        :param path: The path to start the crawl from the provided path if crawling or
                     loads the path into monolith. If path is not included, crawl will start with
                     the default. The default is */redfish/v1/* or */rest/v1* depending on if the
                     system is Redfish or LegacyRest.
        :type path: str.
        :param includelogs: Flag to determine if logs should be downloaded as well in the crawl.
        :type includelogs: bool
        :param init: Flag to determine if this is the initial load.
        :type init: bool
        :param crawl: Flag to determine if load should crawl through found links.
        :type crawl: bool
        :param loadtype: Flag to determine if loading standard links: *href* or schema links: *ref*.
        :type loadtype: str.
        :param loadcomplete: Flag to download the entire data model including registries and
                             schemas.
        :type loadcomplete: bool
        :param path_refresh: Flag to reload the path specified, clearing any patches and overwriting the
                    current data in the monolith.
        :type path_refresh: bool
        """
        if init:
            if LOGGER.getEffectiveLevel() == 40 and not json_out:
                sys.stdout.write("Discovering data...")
            else:
                LOGGER.info("Discovering data...")
            self.name = self.name + " at %s" % self.client.base_url

        selectivepath = path
        if not selectivepath:
            selectivepath = self.client.default_prefix
        if loadtype == "href" and not self.client.base_url.startswith("blobstore://."):
            if not self.threads:
                for _ in range(6):
                    workhand = LoadWorker(self.get_queue)
                    workhand.setDaemon(True)
                    workhand.start()
                    self.threads.append(workhand)

            self.get_queue.put(
                (
                    selectivepath,
                    includelogs,
                    loadcomplete,
                    crawl,
                    path_refresh,
                    init,
                    None,
                    None,
                    self,
                )
            )
            self.get_queue.join()

            # Raise any errors from threads, and set them back to None after
            excp = None
            for thread in self.threads:
                if excp is None:
                    excp = thread.get_exception()
                thread.exception = None

            if excp:
                raise excp

            # self.member_queue.join()
        else:
            # We can't load ref or local client in a threaded manner
            self._load(
                selectivepath,
                originaluri=None,
                crawl=crawl,
                includelogs=includelogs,
                init=init,
                loadtype=loadtype,
                loadcomplete=loadcomplete,
                path_refresh=path_refresh,
                prevpath=None,
            )

        if init:
            if LOGGER.getEffectiveLevel() == 40 and not json_out:
                sys.stdout.write("Done\n")
            else:
                LOGGER.info("Done\n")
        if self.directory_load and init:
            self._populatecollections()

    def _load(
        self,
        path,
        crawl=True,
        originaluri=None,
        includelogs=False,
        init=True,
        loadtype="href",
        loadcomplete=False,
        path_refresh=False,
        prevpath=None,
    ):
        """Sequential version of loading monolith and parsing schemas.

        :param path: path to start load from.
        :type path: str
        :param crawl: flag to determine if load should traverse found links.
        :type crawl: bool
        :param originaluri: variable to assist in determining originating path.
        :type originaluri: str
        :param includelogs: flag to determine if logs should be downloaded also.
        :type includelogs: bool
        :param init: flag to determine if first run of load.
        :type init: bool
        :param loadtype: flag to determine if load is meant for only href items.
        :type loadtype: str.
        :param loadcomplete: flag to download the entire monolith
        :type loadcomplete: bool
        :param path_refresh: flag to reload the members in the monolith instead of skip if they exist.
        :type path_refresh: bool
        """

        if (path.endswith("?page=1") or path.endswith(".json")) and not loadcomplete:
            # Don't download schemas in crawl unless we are loading absolutely everything
            return
        elif not includelogs and crawl:
            # Only include logs when asked as there can be an extreme amount of entries
            if "/log" in path.lower():
                return

        # remove fragments and quote characters in path
        p = urlparse(path)
        p = ParseResult(
            scheme=p.scheme, netloc=p.netloc, path=quote(p.path), params=p.params, query=p.query, fragment=""
        )
        path = urlunparse(p)

        if prevpath and prevpath != path:
            self.ctree[prevpath].update([path])
        if not path_refresh:
            if path.lower() in self.visited_urls:
                return
        LOGGER.debug("_loading %s", path)

        resp = self.client.get(path)
        if resp.status != 200 and path.lower() == self.typepath.defs.biospath:
            raise BiosUnregisteredError()
        elif resp.status == 401:
            raise SessionExpired("Invalid session. Please logout and " "log back in or include credentials.")
        elif resp.status not in (201, 200):
            self.removepath(path)
            return

        if loadtype == "ref":
            try:
                if resp.status in (201, 200):
                    self.update_member(resp=resp, path=path, init=init)
                self._parse_schema(resp)
            except jsonpointer.JsonPointerException:
                raise SchemaValidationError()

        self.update_member(resp=resp, path=path, init=init)

        fpath = lambda pa, path: (
            path if pa.endswith(self.typepath.defs.hrefstring) and pa.startswith((self.collstr, "Entries")) else None
        )

        if loadtype == "href":
            # follow all the href attributes
            if self.is_redfish:
                jsonpath_expr = jsonpath_rw.parse("$..'@odata.id'")
            else:
                jsonpath_expr = jsonpath_rw.parse("$..href")
            matches = jsonpath_expr.find(resp.dict)

            if "links" in resp.dict and "NextPage" in resp.dict["links"]:
                if originaluri:
                    next_link_uri = originaluri + "?page=" + str(resp.dict["links"]["NextPage"]["page"])
                    href = "%s" % next_link_uri

                    self._load(
                        href,
                        originaluri=originaluri,
                        includelogs=includelogs,
                        crawl=crawl,
                        init=init,
                        prevpath=None,
                        loadcomplete=loadcomplete,
                    )
                else:
                    next_link_uri = path + "?page=" + str(resp.dict["links"]["NextPage"]["page"])

                    href = "%s" % next_link_uri
                    self._load(
                        href,
                        originaluri=path,
                        includelogs=includelogs,
                        crawl=crawl,
                        init=init,
                        prevpath=None,
                        loadcomplete=loadcomplete,
                    )

            # Only use monolith if we are set to
            matchrdirpath = (
                next((match for match in matches if match.value == self._resourcedir), None)
                if self.directory_load
                else None
            )
            if not matchrdirpath and crawl:
                for match in matches:
                    if path == "/rest/v1" and not loadcomplete:
                        if (
                            str(match.full_path) == "links.Schemas.href"
                            or str(match.full_path) == "links.Registries.href"
                        ):
                            continue
                    elif not loadcomplete:
                        if (
                            str(match.full_path) == "Registries.@odata.id"
                            or str(match.full_path) == "JsonSchemas.@odata.id"
                        ):
                            continue

                    if match.value == path:
                        continue
                    elif not isinstance(match.value, six.string_types):
                        continue

                    href = "%s" % match.value
                    self._load(
                        href,
                        crawl=crawl,
                        originaluri=originaluri,
                        includelogs=includelogs,
                        init=init,
                        prevpath=fpath(str(match.full_path), path),
                        loadcomplete=loadcomplete,
                    )
            elif crawl:
                href = "%s" % matchrdirpath.value
                self._load(
                    href,
                    crawl=crawl,
                    originaluri=originaluri,
                    includelogs=includelogs,
                    init=init,
                    prevpath=path,
                    loadcomplete=loadcomplete,
                )
            if loadcomplete:
                if path == "/rest/v1":
                    schemamatch = jsonpath_rw.parse("$..extref")
                else:
                    schemamatch = jsonpath_rw.parse("$..Uri")
                smatches = schemamatch.find(resp.dict)
                matches = matches + smatches
                for match in matches:
                    if isinstance(match.value, six.string_types):
                        self._load(
                            match.value,
                            crawl=crawl,
                            originaluri=originaluri,
                            includelogs=includelogs,
                            init=init,
                            loadcomplete=loadcomplete,
                            prevpath=fpath(str(match.full_path), path),
                        )

    def _parse_schema(self, resp):
        """Function to get and replace schema $ref with data

        :param resp: response data containing ref items.
        :type resp: str
        """
        # pylint: disable=maybe-no-member
        if not self.typepath.gencompany:
            return self._parse_schema_gen(resp)
        jsonpath_expr = jsonpath_rw.parse('$.."$ref"')
        matches = jsonpath_expr.find(resp.dict)
        respcopy = resp.dict
        typeregex = "([#,@].*?\.)"
        if matches:
            for match in matches:
                fullpath = str(match.full_path)
                jsonfile = match.value.split("#")[0]
                jsonpath = match.value.split("#")[1]
                listmatch = None
                found = None

                if "redfish.dmtf.org" in jsonfile:
                    if "odata" in jsonfile:
                        jsonpath = jsonpath.replace(jsonpath.split("/")[-1], "odata" + jsonpath.split("/")[-1])
                    jsonfile = "Resource.json"

                found = re.search(typeregex, fullpath)
                if found:
                    repitem = fullpath[found.regs[0][0] : found.regs[0][1]]
                    schemapath = "/" + fullpath.replace(repitem, "~").replace(".", "/").replace("~", repitem)
                else:
                    schemapath = "/" + fullpath.replace(".", "/")

                if ".json" in jsonfile:
                    itempath = schemapath

                    if self.is_redfish:
                        if resp.request.path[-1] == "/":
                            newpath = "/".join(resp.request.path.split("/")[:-2]) + "/" + jsonfile + "/"
                        else:
                            newpath = "/".join(resp.request.path.split("/")[:-1]) + "/" + jsonfile + "/"
                    else:
                        newpath = "/".join(resp.request.path.split("/")[:-1]) + "/" + jsonfile

                    if "href.json" in newpath:
                        continue

                    if newpath.lower() not in self.visited_urls:
                        self.load(
                            newpath,
                            crawl=False,
                            includelogs=False,
                            init=False,
                            loadtype="ref",
                        )

                    instance = list()

                    # deprecated type "string" for Type.json
                    if "string" in self.types:
                        for item in self.iter("string"):
                            instance.append(item)
                    if "object" in self.types:
                        for item in self.iter("object"):
                            instance.append(item)

                    for item in instance:
                        if jsonfile in item.path:
                            if "anyOf" in fullpath:
                                break

                            dictcopy = item.dict
                            try:
                                # TODO may need to really verify this is acceptable regex
                                listmatch = re.search("[][0-9]+[]", itempath)
                            except regexerr:
                                pass
                                # LOGGER.info("An error occurred with regex match on path: %s\n%s\n"\
                                #            % (itempath, str(excp)))

                            if listmatch:
                                start = listmatch.regs[0][0]
                                end = listmatch.regs[0][1]

                                newitempath = [itempath[:start], itempath[end:]]
                                start = jsonpointer.JsonPointer(newitempath[0])
                                end = jsonpointer.JsonPointer(newitempath[1])

                                del start.parts[-1], end.parts[-1]
                                vals = start.resolve(respcopy)

                                count = 0

                                for val in vals:
                                    try:
                                        if "$ref" in six.iterkeys(end.resolve(val)):
                                            end.resolve(val).pop("$ref")
                                            end.resolve(val).update(dictcopy)
                                            replace_pointer = jsonpointer.JsonPointer(end.path + jsonpath)

                                            data = replace_pointer.resolve(val)
                                            set_pointer(val, end.path, data)
                                            start.resolve(respcopy)[count].update(val)

                                            break
                                    except:
                                        count += 1
                            else:
                                itempath = jsonpointer.JsonPointer(itempath)
                                del itempath.parts[-1]

                                try:
                                    if "$ref" in six.iterkeys(itempath.resolve(respcopy)):
                                        itempath.resolve(respcopy).pop("$ref")
                                        itempath.resolve(respcopy).update(dictcopy)
                                        break
                                except jsonpointer.JsonPointerException:
                                    pass

                if jsonpath:
                    if "anyOf" in fullpath:
                        continue

                    if not jsonfile:
                        replacepath = jsonpointer.JsonPointer(jsonpath)
                        schemapath = schemapath.replace("/$ref", "")
                        if re.search("\[\d]", schemapath):
                            schemapath = schemapath.translate(str.maketrans("", "", "[]"))
                        schemapath = jsonpointer.JsonPointer(schemapath)
                        data = replacepath.resolve(respcopy)

                        if "$ref" in schemapath.resolve(respcopy):
                            schemapath.resolve(respcopy).pop("$ref")
                            schemapath.resolve(respcopy).update(data)

                    else:
                        if not listmatch:
                            schemapath = schemapath.replace("/$ref", "")
                            replacepath = schemapath + jsonpath
                            replace_pointer = jsonpointer.JsonPointer(replacepath)
                            try:
                                data = replace_pointer.resolve(respcopy)
                                set_pointer(respcopy, schemapath, data)
                            except jsonpointer.JsonPointerException:
                                # TODO
                                pass

            resp.loaddict(respcopy)
        else:
            resp.loaddict(respcopy)

    def _parse_schema_gen(self, resp):
        """Redfish general function to get and replace schema $ref with data

        :param resp: response data containing ref items.
        :type resp: str

        """
        # pylint: disable=maybe-no-member
        getval = lambda inmat: getval(inmat.left) + "/" + str(inmat.right) if hasattr(inmat, "left") else str(inmat)
        respcopy = resp.dict
        jsonpath_expr = jsonpath_rw.parse('$.."anyOf"')
        while True:
            matches = jsonpath_expr.find(respcopy)
            if not matches:
                break
            match = matches[0]
            newval = None
            schlist = match.value
            schlist = [ele for ele in list(schlist) if ele != {"type": "null"}]
            norefsch = [ele for ele in list(schlist) if isinstance(ele, dict) and len(ele.keys()) > 1]
            if norefsch:
                newval = norefsch[0]
            else:
                newsc = [ele for ele in list(schlist) if not ele["$ref"].split("#")[0]]
                newval = newsc[0] if newsc else None
                if not newval:
                    schlist = [
                        ele["$ref"]
                        for ele in list(schlist)
                        if "$ref" in ele.keys()
                        and (ele["$ref"].split("#")[0].endswith(".json") and "odata" not in ele["$ref"].split("#")[0])
                    ]
                    maxsch = max(schlist)
                    newval = {"$ref": maxsch}

            itempath = "/" + getval(match.full_path)
            if re.search("\[\d+]", itempath):
                itempath = itempath.translate(str.maketrans("", "", "[]"))
            itempath = jsonpointer.JsonPointer(itempath)
            del itempath.parts[-1]
            if "anyOf" in six.iterkeys(itempath.resolve(respcopy)):
                itempath.resolve(respcopy).pop("anyOf")
                itempath.resolve(respcopy).update(newval)

        jsonpath_expr = jsonpath_rw.parse('$.."$ref"')
        matches = jsonpath_expr.find(respcopy)
        if matches:
            for _, match in enumerate(matches):
                jsonfile = match.value.split("#")[0]
                jsonfile = "" if jsonfile.lower() == resp.request.path.lower() else jsonfile
                jsonpath = match.value.split("#")[1]

                schemapath = "/" + getval(match.full_path)
                if jsonfile:
                    itempath = schemapath
                    if "/" not in jsonfile:
                        inds = -2 if resp.request.path[-1] == "/" else -1
                        jsonfile = "/".join(resp.request.path.split("/")[:inds]) + "/" + jsonfile + "/"
                    if jsonfile not in self.paths:
                        self.load(
                            jsonfile,
                            crawl=False,
                            includelogs=False,
                            init=False,
                            loadtype="ref",
                        )
                    item = self.paths[jsonfile] if jsonfile in self.paths else None

                    if not item:
                        if "anyOf" not in schemapath:
                            raise SchemaValidationError()
                        continue
                    if re.search("\[\d+]", itempath):
                        itempath = itempath.translate(str.maketrans("", "", "[]"))
                    itempath = jsonpointer.JsonPointer(itempath)
                    del itempath.parts[-1]
                    try:
                        if "$ref" in six.iterkeys(itempath.resolve(respcopy)):
                            itempath.resolve(respcopy).pop("$ref")
                            itempath.resolve(respcopy).update(item.dict)
                    except jsonpointer.JsonPointerException:
                        pass

                if jsonpath:
                    schemapath = schemapath.replace("/$ref", "")
                    if re.search("\[\d+]", schemapath):
                        schemapath = schemapath.translate(str.maketrans("", "", "[]"))
                    if not jsonfile:
                        replacepath = jsonpointer.JsonPointer(jsonpath)
                        schemapath = jsonpointer.JsonPointer(schemapath)
                        data = replacepath.resolve(respcopy)
                        if "$ref" in schemapath.resolve(respcopy):
                            schemapath.resolve(respcopy).pop("$ref")
                            schemapath.resolve(respcopy).update(data)
                    else:
                        replacepath = schemapath + jsonpath
                        replace_pointer = jsonpointer.JsonPointer(replacepath)
                        data = replace_pointer.resolve(respcopy)
                        set_pointer(respcopy, schemapath, data)

            resp.loaddict(respcopy)
        else:
            resp.loaddict(respcopy)

    def load_from_dict(self, src):
        """Load data to monolith from a dict. This is the reverse of :func:`to_dict`.

        :param src: data receive from rest operation.
        :type src: str

        """
        self._type = src["Type"]
        self._name = src["Name"]
        self.typesadded = defaultdict(set, {ki: set(val) for ki, val in src["typepath"].items()})
        self.ctree = defaultdict(set, {ki: set(val) for ki, val in src["ctree"].items()})
        self.colltypes = defaultdict(set, {ki: set(val) for ki, val in src["colls"].items()})
        for _, resp in list(src["resps"].items()):
            member = RisMonolithMemberv100(None, self.is_redfish)
            member.load_from_dict(resp)
            self.update_member(member=member, init=False)

    def to_dict(self):
        """Convert data to a dict from monolith. This is the reverse of :func:`load_from_dict`."""
        result = OrderedDict()
        result["Type"] = self.type
        result["Name"] = self.name
        result["typepath"] = self.typesadded
        result["ctree"] = self.ctree
        result["colls"] = self.colltypes
        result["resps"] = {x: v.to_dict() for x, v in list(self.paths.items())}
        return result

    def markmodified(self, opath, path=None, modpaths=None):
        """Mark the paths to be modifed which are connected to current path. When calling this
        function you only need to include `opath`.

        :param opath: original path which has been modified
        :type opath: str
        """
        modpaths = set() if modpaths is None else modpaths
        path = path if path else opath
        if not path:
            return
        modpaths.update(self.ctree[path] if path in self.ctree else set())
        self.paths[path].modified = True
        for npath in [
            unmodpath for unmodpath in modpaths if unmodpath in self.paths and not self.paths[unmodpath].modified
        ]:
            self.markmodified(opath, path=npath, modpaths=modpaths)
        return modpaths

    def checkmodified(self, opath, path=None, modpaths=None):
        """Check if the path or its children are modified. When calling this
        function you only need to include `opath`.

        :param opath: original path which has been modified
        :type opath: str
        """
        # return [paths for paths in self.ctree[path] if self.paths[paths].modified]
        modpaths = set() if modpaths is None else modpaths
        path = path if path else opath
        newpaths = set()
        if not path:
            return
        if path in self.paths and self.paths[path].modified:
            newpaths = (
                set([conn for conn in self.ctree[path] if conn in self.paths and self.paths[path].modified]) - modpaths
            )
            modpaths.update(newpaths | set([path]))
        for npath in [unmodpath for unmodpath in newpaths]:
            self.checkmodified(opath, path=npath, modpaths=modpaths)
        return modpaths

    def removepath(self, path):
        """Remove a given path from the cache

        :param path: path which is to be checked if modified
        :type path: str
        """
        if path in self._visited_urls:
            self._visited_urls.remove(path)
        if path not in self.paths:
            return
        if path in self.typesadded[self.paths[path].maj_type]:
            self.typesadded[self.paths[path].maj_type].remove(path)
        if not self.typesadded[self.paths[path].maj_type]:
            del self.typesadded[self.paths[path].maj_type]
        del self.paths[path]
        if path in self.ctree:
            del self.ctree[path]
        _ = [self.ctree[paths].remove(path) for paths in self.ctree if path in self.ctree[paths]]

    def _populatecollections(self):
        """Populate the collections type and types depending on resourcedirectory"""
        if self._resourcedir not in self.paths:
            return
        self.colltypes = defaultdict(set)
        alltypes = []
        colls = []
        for item in self.paths[self._resourcedir].dict["Instances"]:
            # Fix for incorrect RDir instances.
            if self.typepath.defs.typestring not in item or item[self.typepath.defs.hrefstring] in self.paths:
                continue
            typename = ".".join(item[self.typepath.defs.typestring].split(".", 2)[:2]).split("#")[-1]
            _ = [alltypes.append(typename) if "Collection" not in typename else None]
            _ = [colls.append(typename) if "Collection" in typename else None]
            member = RisMonolithMemberv100(None, self.is_redfish)
            member.popdefs(typename, item[self.typepath.defs.hrefstring], item[self.etagstr])
            self.update_member(member=member, init=False)
        for coll in colls:
            collname = coll.split("Collection")[0].split("#")[-1]
            typename = next((name for name in alltypes if name.startswith(collname)), None)
            colltype = ".".join(coll.split(".", 2)[:2]).split("#")[-1]
            self.colltypes[typename].add(colltype)

    def capture(self, redmono=False):
        """Crawls the server specified by the client and returns the entire monolith.

        :param redmono: Flag to return only the headers and responses instead of the entire monolith
                        member data.
        :type redmono: bool
        :rtype: dict
        """
        self.load(includelogs=True, crawl=True, loadcomplete=True, path_refresh=True, init=True)
        return (
            self.to_dict()
            if not redmono
            else {
                x: {"Headers": v.resp.getheaders(), "Response": v.resp.dict} for x, v in list(self.paths.items()) if v
            }
        )

    def killthreads(self):
        """Function to kill threads on logout"""
        threads = []
        for thread in threading.enumerate():
            if isinstance(thread, LoadWorker):
                self.get_queue.put(
                    (
                        "KILL",
                        "KILL",
                        "KILL",
                        "KILL",
                        "KILL",
                        "KILL",
                        "KILL",
                        "KILL",
                        "KILL",
                        "KILL",
                    )
                )
                threads.append(thread)

        for thread in threads:
            thread.join()

    def _update_progress(self):
        """Simple function to increment the dot progress"""
        if self.progress % 6 == 0:
            sys.stdout.write(".")
