"""This submodule contains a JSON reference translator."""

__author__ = 'Štěpán Tomsa'
__copyright__ = 'Copyright © 2021 Štěpán Tomsa'
__license__ = 'MIT'
__all__ = ()

import prance.util.url as _url


def _reference_key(ref_url, item_path):
    """
    Return a portion of the dereferenced URL.

    format - ref-url_obj-path
    """
    return ref_url.path.split('/')[-1] + '_' + '_'.join(item_path[1:])


def _local_ref(path):
    url = '#/' + '/'.join(path)
    return {'$ref': url}


# Underscored to allow some time for the public API to be stabilized.
class _RefTranslator:
    """
    Resolve JSON pointers/references in a spec by translation.

    References to objects in other files are copied to the /components/schemas
    object of the root document, while being translated to point to the the new
    object locations.
    """

    def __init__(self, specs, url):
        """
        Construct a JSON reference translator.

        The translated specs are in the `specs` member after a call to
        `translate_references` has been made.

        If a URL is given, it is used as a base for calculating the absolute
        URL of relative file references.

        :param dict specs: The parsed specs in which to translate any references.
        :param str url: [optional] The URL to base relative references on.
        """
        import copy
        self.specs = copy.deepcopy(specs)

        self.__strict = True
        self.__reference_cache = {}
        self.__collected_references = {}

        if url:
            self.url = _url.absurl(url)
            url_key = (_url.urlresource(self.url), self.__strict)

            # If we have a url, we want to add ourselves to the reference cache
            # - that creates a reference loop, but prevents child resolvers from
            # creating a new resolver for this url.
            self.__reference_cache[url_key] = self.specs
        else:
            self.url = None

    def translate_references(self):
        """
        Iterate over the specification document, performing the translation.

        Traverses over the whole document, adding the referenced object from
        external files to the /components/schemas object in the root document
        and translating the references to the new location.
        """
        self.specs = self._translate_partial(self.url, self.specs)

        # Add collected references to the root document.
        if self.__collected_references:
          if 'components' not in self.specs:
            self.specs['components'] = {}
          if 'schemas' not in self.specs['components']:
            self.specs['components'].update({'schemas': {}})

          self.specs['components']['schemas'].update(self.__collected_references)

    def _dereference(self, ref_url, obj_path):
        """
        Dereference the URL and object path.

        Returns the dereferenced object.

        :param mixed ref_url: The URL at which the reference is located.
        :param list obj_path: The object path within the URL resource.
        :param tuple recursions: A recursion stack for resolving references.
        :return: A copy of the dereferenced value, with all internal references
            resolved.
        """
        # In order to start dereferencing anything in the referenced URL, we have
        # to read and parse it, of course.
        contents = _url.fetch_url(ref_url, self.__reference_cache, strict=self.__strict)

        # In this inner parser's specification, we can now look for the referenced
        # object.
        value = contents
        if len(obj_path) != 0:
            from prance.util.path import path_get
            try:
                value = path_get(value, obj_path)
            except (KeyError, IndexError, TypeError) as ex:
                raise _url.ResolutionError('Cannot resolve reference "%s": %s'
                                           % (ref_url.geturl(), str(ex)))

        # Deep copy value; we don't want to create recursive structures
        import copy
        value = copy.deepcopy(value)

        # Now resolve partial specs
        value = self._translate_partial(ref_url, value)

        # That's it!
        return value

    def _translate_partial(self, base_url, partial):
        changes = dict(tuple(self._translating_iterator(base_url, partial, ())))

        paths = sorted(changes.keys(), key = len)

        from prance.util.path import path_set
        for path in paths:
            value = changes[path]
            if len(path) == 0:
                partial = value
            else:
                path_set(partial, list(path), value, create = True)

        return partial

    def _translating_iterator(self, base_url, partial, path):
        from prance.util.iterators import reference_iterator
        for _, ref_string, item_path in reference_iterator(partial):
            ref_url, obj_path = _url.split_url_reference(base_url, ref_string)
            full_path = path + item_path

            if ref_url.path == self.url.path:
                # Reference to the root document.
                ref_path = obj_path
            else:
                # Reference to a non-root document.
                ref_key = _reference_key(ref_url, obj_path)
                if ref_key not in self.__collected_references:
                    self.__collected_references[ref_key] = None
                    ref_value = self._dereference(ref_url, obj_path)
                    self.__collected_references[ref_key] = ref_value
                ref_path = ['components', 'schemas', ref_key]

            ref_obj = _local_ref(ref_path)
            yield full_path, ref_obj

[(49, 'copy.deepcopy', 'copy.deepcopy', False, 'import copy\n'), (99, 'prance.util.url.fetch_url', '_url.fetch_url', True, 'import prance.util.url as _url\n'), (114, 'copy.deepcopy', 'copy.deepcopy', False, 'import copy\n'), (139, 'prance.util.iterators.reference_iterator', 'reference_iterator', False, 'from prance.util.iterators import reference_iterator\n'), (56, 'prance.util.url.absurl', '_url.absurl', True, 'import prance.util.url as _url\n'), (140, 'prance.util.url.split_url_reference', '_url.split_url_reference', True, 'import prance.util.url as _url\n'), (57, 'prance.util.url.urlresource', '_url.urlresource', True, 'import prance.util.url as _url\n'), (107, 'prance.util.path.path_get', 'path_get', False, 'from prance.util.path import path_get\n')]