# Copyright (c) 2018, Red Hat, 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.

"""Oslo Versioned Objects helper file.

These methods help with the serialization of Cinderlib objects that uses the
OVO serialization mechanism, so we remove circular references when doing the
JSON serialization of objects (for example in a Volume OVO it has a 'snapshot'
field which is a Snapshot OVO that has a 'volume' back reference), piggy back
on the OVO's serialization mechanism to add/get additional data we want.
"""

import functools
import json as json_lib

from cinder.objects import base as cinder_base_ovo
from oslo_versionedobjects import base as base_ovo
from oslo_versionedobjects import fields as ovo_fields

from cinderlib import objects


# Variable used to avoid circular references
BACKEND_CLASS = None


def setup(backend_class):
    global BACKEND_CLASS
    BACKEND_CLASS = backend_class

    # Use custom dehydration methods that prevent maximum recursion errors
    # due to circular references:
    #   ie: snapshot -> volume -> snapshots -> snapshot
    base_ovo.VersionedObject.obj_to_primitive = obj_to_primitive
    cinder_base_ovo.CinderObject.obj_from_primitive = classmethod(
        obj_from_primitive)

    fields = base_ovo.obj_fields
    fields.Object.to_primitive = staticmethod(field_ovo_to_primitive)
    fields.Field.to_primitive = field_to_primitive
    fields.List.to_primitive = iterable_to_primitive
    fields.Set.to_primitive = iterable_to_primitive
    fields.Dict.to_primitive = dict_to_primitive
    fields.DateTime.to_primitive = staticmethod(datetime_to_primitive)
    wrap_to_primitive(fields.FieldType)
    wrap_to_primitive(fields.IPAddress)


def wrap_to_primitive(cls):
    method = getattr(cls, 'to_primitive')

    @functools.wraps(method)
    def to_primitive(obj, attr, value, visited=None):
        return method(obj, attr, value)
    setattr(cls, 'to_primitive', staticmethod(to_primitive))


def _set_visited(element, visited):
    # visited keeps track of elements visited to prevent loops
    if visited is None:
        visited = set()
    # We only care about complex object that can have loops, others are ignored
    # to prevent us from not serializing simple objects, such as booleans, that
    # can have the same instance used for multiple fields.
    if isinstance(element,
                  (ovo_fields.ObjectField, cinder_base_ovo.CinderObject)):
        visited.add(id(element))
    return visited


def obj_to_primitive(self, target_version=None,
                     version_manifest=None, visited=None):
    # No target_version, version_manifest, or changes support
    visited = _set_visited(self, visited)
    primitive = {}
    for name, field in self.fields.items():
        if self.obj_attr_is_set(name):
            value = getattr(self, name)
            # Skip cycles
            if id(value) in visited:
                continue
            primitive[name] = field.to_primitive(self, name, value,
                                                 visited)

    obj_name = self.obj_name()
    obj = {
        self._obj_primitive_key('name'): obj_name,
        self._obj_primitive_key('namespace'): self.OBJ_PROJECT_NAMESPACE,
        self._obj_primitive_key('version'): self.VERSION,
        self._obj_primitive_key('data'): primitive
    }

    # Piggyback to store our own data
    cl_obj = getattr(self, '_cl_obj', None)
    clib_data = cl_obj and cl_obj._to_primitive()
    if clib_data:
        obj['cinderlib.data'] = clib_data

    return obj


def obj_from_primitive(
        cls, primitive, context=None,
        original_method=cinder_base_ovo.CinderObject.obj_from_primitive):
    result = original_method(primitive, context)
    result.cinderlib_data = primitive.get('cinderlib.data')
    return result


def field_ovo_to_primitive(obj, attr, value, visited=None):
    return value.obj_to_primitive(visited=visited)


def field_to_primitive(self, obj, attr, value, visited=None):
    if value is None:
        return None
    return self._type.to_primitive(obj, attr, value, visited)


def iterable_to_primitive(self, obj, attr, value, visited=None):
    visited = _set_visited(self, visited)
    result = []
    for elem in value:
        if id(elem) in visited:
            continue
        _set_visited(elem, visited)
        r = self._element_type.to_primitive(obj, attr, elem, visited)
        result.append(r)
    return result


def dict_to_primitive(self, obj, attr, value, visited=None):
    visited = _set_visited(self, visited)
    primitive = {}
    for key, elem in value.items():
        if id(elem) in visited:
            continue
        _set_visited(elem, visited)
        primitive[key] = self._element_type.to_primitive(
            obj, '%s["%s"]' % (attr, key), elem, visited)
    return primitive


def datetime_to_primitive(obj, attr, value, visited=None):
    """Stringify time in ISO 8601 with subsecond format.

    This is the same code as the one used by the OVO DateTime to_primitive
    but adding the subsecond resolution with the '.%f' part in strftime call.

    This is backward compatible with cinderlib using code that didn't generate
    subsecond resolution, because the from_primitive code of the OVO field uses
    oslo_utils.timeutils.parse_isotime which in the end uses
    iso8601.parse_date, and since the subsecond format is also ISO8601 it is
    properly parsed.
    """
    st = value.strftime('%Y-%m-%dT%H:%M:%S.%f')
    tz = value.tzinfo.tzname(None) if value.tzinfo else 'UTC'
    # Need to handle either iso8601 or python UTC format
    st += ('Z' if tz in ['UTC', 'UTC+00:00'] else tz)
    return st


def load(json_src, save=False):
    """Load any json serialized cinderlib object."""
    if isinstance(json_src, str):
        json_src = json_lib.loads(json_src)

    if isinstance(json_src, list):
        return [getattr(objects, obj['class']).load(obj, save)
                for obj in json_src]

    return getattr(objects, json_src['class']).load(json_src, save)


def json():
    """Convert to Json everything we have in this system."""
    return [backend.json for backend in BACKEND_CLASS.backends.values()]


def jsons():
    """Convert to a Json string everything we have in this system."""
    return json_lib.dumps(json(), separators=(',', ':'))


def dump():
    """Convert to Json everything we have in this system."""
    return [backend.dump for backend in BACKEND_CLASS.backends.values()]


def dumps():
    """Convert to a Json string everything we have in this system."""
    return json_lib.dumps(dump(), separators=(',', ':'))
