# 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.

import operator

import pecan
from pecan import rest
from webob import exc
from zun.api.controllers import versions
from zun.api import versioned_method
from zun.common import exception
from zun.common.i18n import _


# name of attribute to keep version method information
VER_METHOD_ATTR = 'versioned_methods'


class APIBase(object):

    def __init__(self, **kwargs):
        for field in self.fields:
            if field in kwargs:
                value = kwargs[field]
                setattr(self, field, value)

    def __setattr__(self, field, value):
        super(APIBase, self).__setattr__(field, value)

    def as_dict(self):
        """Render this object as a dict of its fields."""
        return {f: getattr(self, f)
                for f in self.fields
                if hasattr(self, f)}

    def __json__(self):
        return self.as_dict()

    def unset_fields_except(self, except_list=None):
        """Unset fields so they don't appear in the message body.

        :param except_list: A list of fields that won't be touched.

        """
        if except_list is None:
            except_list = []

        for k in self.as_dict():
            if k not in except_list:
                setattr(self, k, None)


class ControllerMetaclass(type):
    """Controller metaclass.

    This metaclass automates the task of assembling a dictionary
    mapping action keys to method names.
    """

    def __new__(mcs, name, bases, cls_dict):
        """Adds version function dictionary to the class."""

        versioned_methods = None

        for base in bases:
            if base.__name__ == "Controller":
                # NOTE(cyeoh): This resets the VER_METHOD_ATTR attribute
                # between API controller class creations. This allows us
                # to use a class decorator on the API methods that doesn't
                # require naming explicitly what method is being versioned as
                # it can be implicit based on the method decorated. It is a bit
                # ugly.
                if VER_METHOD_ATTR in base.__dict__:
                    versioned_methods = getattr(base, VER_METHOD_ATTR)
                    delattr(base, VER_METHOD_ATTR)

        if versioned_methods:
            cls_dict[VER_METHOD_ATTR] = versioned_methods

        return super(ControllerMetaclass, mcs).__new__(mcs, name, bases,
                                                       cls_dict)


class Controller(rest.RestController, metaclass=ControllerMetaclass):
    """Base Rest Controller"""

    @pecan.expose('json')
    def _no_version_match(self, *args, **kwargs):
        from pecan import request

        raise exc.HTTPNotAcceptable(_(
            "Version %(ver)s was requested but the requested API is not "
            "supported for this version.") % {'ver': request.version})

    def __getattribute__(self, key):

        def version_select():
            """Select the correct method based on version

            @return: Returns the correct versioned method
            @raises: HTTPNotAcceptable if there is no method which
                 matches the name and version constraints
            """

            from pecan import request
            ver = request.version

            func_list = self.versioned_methods[key]
            for func in func_list:
                if ver.matches(func.start_version, func.end_version):
                    return func.func

            return self._no_version_match

        try:
            version_meth_dict = object.__getattribute__(self, VER_METHOD_ATTR)
        except AttributeError:
            # No versioning on this class
            return object.__getattribute__(self, key)
        if version_meth_dict and key in version_meth_dict:
            return version_select().__get__(self, self.__class__)

        return object.__getattribute__(self, key)

    # NOTE: This decorator MUST appear first (the outermost
    # decorator) on an API method for it to work correctly
    @classmethod
    def api_version(cls, min_ver, max_ver=None):
        """Decorator for versioning api methods.

        Add the decorator to any pecan method that has been exposed.
        This decorator will store the method, min version, and max
        version in a list for each api. It will check that there is no
        overlap between versions and methods. When the api is called the
        controller will use the list for each api to determine which
        method to call.

        Example:
            @base.Controller.api_version("1.1", "1.2")
            def get_one(self, container_id):
            {...code for versions 1.1 to 1.2...}

            @base.Controller.api_version("1.3")
            def get_one(self, container_id):
            {...code for versions 1.3 to latest}

        @min_ver: string representing minimum version
        @max_ver: optional string representing maximum version
        @raises: ApiVersionsIntersect if an version overlap is found between
        method versions.
        """

        def decorator(f):
            obj_min_ver = versions.Version('', '', '', min_ver)
            if max_ver:
                obj_max_ver = versions.Version('', '', '', max_ver)
            else:
                obj_max_ver = versions.Version('', '', '',
                                               versions.CURRENT_MAX_VER)

            # Add to list of versioned methods registered
            func_name = f.__name__
            new_func = versioned_method.VersionedMethod(
                func_name, obj_min_ver, obj_max_ver, f)

            func_dict = getattr(cls, VER_METHOD_ATTR, {})
            if not func_dict:
                setattr(cls, VER_METHOD_ATTR, func_dict)

            func_list = func_dict.get(func_name, [])
            if not func_list:
                func_dict[func_name] = func_list
            func_list.append(new_func)

            is_intersect = Controller.check_for_versions_intersection(
                func_list)

            if is_intersect:
                raise exception.ApiVersionsIntersect(
                    name=new_func.name,
                    min_ver=new_func.start_version,
                    max_ver=new_func.end_version
                )

            # Ensure the list is sorted by minimum version (reversed)
            # so later when we work through the list in order we find
            # the method which has the latest version which supports
            # the version requested.
            func_list.sort(key=lambda f: f.start_version, reverse=True)

            return f

        return decorator

    @staticmethod
    def check_for_versions_intersection(func_list):
        """Determines whether function list intersections

        General algorithm:
        https://en.wikipedia.org/wiki/Intersection_algorithm

        :param func_list: list of VersionedMethod objects
        :return: boolean
        """

        pairs = []
        counter = 0

        for f in func_list:
            pairs.append((f.start_version, 1))
            pairs.append((f.end_version, -1))

        pairs.sort(key=operator.itemgetter(1), reverse=True)
        pairs.sort(key=operator.itemgetter(0))

        for p in pairs:
            counter += p[1]

            if counter > 1:
                return True

        return False
