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

from oslo_config import cfg
from oslo_log import log
import six

from neutron._i18n import _
from neutron.db import api as db_api
from neutron.quota import resource

LOG = log.getLogger(__name__)


# Wrappers for easing access to the ResourceRegistry singleton


def register_resource(resource):
    ResourceRegistry.get_instance().register_resource(resource)


def register_resource_by_name(resource_name, plural_name=None):
    ResourceRegistry.get_instance().register_resource_by_name(
        resource_name, plural_name)


def get_all_resources():
    return ResourceRegistry.get_instance().resources


def unregister_all_resources():
    if not ResourceRegistry._instance:
        return
    return ResourceRegistry.get_instance().unregister_resources()


def get_resource(resource_name):
    return ResourceRegistry.get_instance().get_resource(resource_name)


def is_tracked(resource_name):
    return ResourceRegistry.get_instance().is_tracked(resource_name)


# auxiliary functions and decorators


def set_resources_dirty(context):
    """Sets the dirty bit for resources with usage changes.

    This routine scans all registered resources, and, for those whose
    dirty status is True, sets the dirty bit to True in the database
    for the appropriate tenants.

    Please note that this routine begins a nested transaction, and it
    is not recommended that this transaction begins within another
    transaction. For this reason the function will raise a SqlAlchemy
    exception if such an attempt is made.

    :param context: a Neutron request context with a DB session
    """
    if not cfg.CONF.QUOTAS.track_quota_usage:
        return

    for res in get_all_resources().values():
        with db_api.context_manager.writer.using(context):
            if is_tracked(res.name) and res.dirty:
                res.mark_dirty(context)


def resync_resource(context, resource_name, tenant_id):
    if not cfg.CONF.QUOTAS.track_quota_usage:
        return

    if is_tracked(resource_name):
        res = get_resource(resource_name)
        # If the resource is tracked count supports the resync_usage parameter
        res.resync(context, tenant_id)


def mark_resources_dirty(f):
    """Decorator for functions which alter resource usage.

    This decorator ensures set_resource_dirty is invoked after completion
    of the decorated function.
    """

    @six.wraps(f)
    def wrapper(_self, context, *args, **kwargs):
        ret_val = f(_self, context, *args, **kwargs)
        set_resources_dirty(context)
        return ret_val

    return wrapper


class tracked_resources(object):
    """Decorator for specifying resources for which usage should be tracked.

    A plugin class can use this decorator to specify for which resources
    usage info should be tracked into an appropriate table rather than being
    explicitly counted.
    """

    def __init__(self, override=False, **kwargs):
        self._tracked_resources = kwargs
        self._override = override

    def __call__(self, f):

        @six.wraps(f)
        def wrapper(*args, **kwargs):
            registry = ResourceRegistry.get_instance()
            for resource_name in self._tracked_resources:
                registry.set_tracked_resource(
                    resource_name,
                    self._tracked_resources[resource_name],
                    self._override)
            return f(*args, **kwargs)

        return wrapper


class ResourceRegistry(object):
    """Registry for resource subject to quota limits.

    This class keeps track of Neutron resources for which quota limits are
    enforced, regardless of whether their usage is being tracked or counted.

    For tracked-usage resources, that is to say those resources for which
    there are usage counters which are kept in sync with the actual number
    of rows in the database, this class allows the plugin to register their
    names either explicitly or through the @tracked_resources decorator,
    which should preferably be applied to the __init__ method of the class.
    """

    _instance = None

    @classmethod
    def get_instance(cls):
        if cls._instance is None:
            cls._instance = cls()
        return cls._instance

    def __init__(self):
        self._resources = {}
        # Map usage tracked resources to the correspondent db model class
        self._tracked_resource_mappings = {}

    def __contains__(self, resource):
        return resource in self._resources

    def _create_resource_instance(self, resource_name, plural_name):
        """Factory function for quota Resource.

        This routine returns a resource instance of the appropriate type
        according to system configuration.

        If QUOTAS.track_quota_usage is True, and there is a model mapping for
        the current resource, this function will return an instance of
        AccountedResource; otherwise an instance of CountableResource.
        """

        if (not cfg.CONF.QUOTAS.track_quota_usage or
            resource_name not in self._tracked_resource_mappings):
            LOG.info("Creating instance of CountableResource for "
                     "resource:%s", resource_name)
            return resource.CountableResource(
                resource_name, resource._count_resource,
                'quota_%s' % resource_name)
        else:
            LOG.info("Creating instance of TrackedResource for "
                     "resource:%s", resource_name)
            return resource.TrackedResource(
                resource_name,
                self._tracked_resource_mappings[resource_name],
                'quota_%s' % resource_name)

    def set_tracked_resource(self, resource_name, model_class, override=False):
        # Do not do anything if tracking is disabled by config
        if not cfg.CONF.QUOTAS.track_quota_usage:
            return

        if isinstance(self._resources.get(resource_name),
                      resource.CountableResource):
            raise RuntimeError(_("Resource %s is already registered as a "
                                 "countable resource.") % resource_name)
        current_model_class = self._tracked_resource_mappings.setdefault(
            resource_name, model_class)

        # Check whether setdefault also set the entry in the dict
        if current_model_class != model_class:
            LOG.debug("A model class is already defined for %(resource)s: "
                      "%(current_model_class)s. Override:%(override)s",
                      {'resource': resource_name,
                       'current_model_class': current_model_class,
                       'override': override})
            if override:
                self._tracked_resource_mappings[resource_name] = model_class
        LOG.debug("Tracking information for resource: %s configured",
                  resource_name)

    def is_tracked(self, resource_name):
        """Find out if a resource if tracked or not.

        :param resource_name: name of the resource.
        :returns: True if resource_name is registered and tracked, otherwise
                 False. Please note that here when False it returned it
                 simply means that resource_name is not a TrackedResource
                 instance, it does not necessarily mean that the resource
                 is not registered.
        """
        return resource_name in self._tracked_resource_mappings

    def register_resource(self, resource):
        if resource.name in self._resources:
            LOG.warning('%s is already registered', resource.name)
        if resource.name in self._tracked_resource_mappings:
            resource.register_events()
        self._resources[resource.name] = resource

    def register_resources(self, resources):
        for res in resources:
            self.register_resource(res)

    def register_resource_by_name(self, resource_name,
                                  plural_name=None):
        """Register a resource by name."""
        resource = self._create_resource_instance(
            resource_name, plural_name)
        self.register_resource(resource)

    def unregister_resources(self):
        """Unregister all resources."""
        for (res_name, res) in self._resources.items():
            if res_name in self._tracked_resource_mappings:
                res.unregister_events()
        self._resources.clear()
        self._tracked_resource_mappings.clear()

    def get_resource(self, resource_name):
        """Return a resource given its name.

        :returns: The resource instance or None if the resource is not found
        """
        return self._resources.get(resource_name)

    @property
    def resources(self):
        return self._resources
