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

"""Options specific to resources managed by Keystone (Domain, User, etc)."""

import six

from keystone.common import validation
from keystone.i18n import _


def _validator(value):
    return


def boolean_validator(value):
    if value not in (True, False):
        raise TypeError(_('Expected boolean value, got %r') % type(value))


def ref_mapper_to_dict_options(ref):
    """Convert the values in _resource_option_mapper to options dict.

    NOTE: this is to be called from the relevant `to_dict` methods or
          similar and must be called from within the active session context.

    :param ref: the DB model ref to extract options from
    :returns: Dict of options as expected to be returned out of to_dict in
              the `options` key.
    """
    options = {}
    for opt in ref._resource_option_mapper.values():
        if opt.option_id in ref.resource_options_registry.option_ids:
            r_opt = ref.resource_options_registry.get_option_by_id(
                opt.option_id)
            if r_opt is not None:
                options[r_opt.option_name] = opt.option_value
    return options


def resource_options_ref_to_mapper(ref, option_class):
    """Convert the _resource_options property-dict to options attr map.

    The model must have the resource option mapper located in the
    ``_resource_option_mapper`` attribute.

    The model must have the resource option registry located in the
    ``resource_options_registry`` attribute.

    The option dict with key(opt_id), value(opt_value) will be pulled from
    ``ref._resource_options``.

    NOTE: This function MUST be called within the active writer session
          context!

    :param ref: The DB model reference that is actually stored to the
                backend.
    :param option_class: Class that is used to store the resource option
                         in the DB.
    """
    options = getattr(ref, '_resource_options', None)
    if options is not None:
        # To ensure everything is clean, no lingering refs.
        delattr(ref, '_resource_options')
    else:
        # _resource_options didn't exist. Work from an empty set.
        options = {}

    # NOTE(notmorgan): explicitly use .keys() here as the attribute mapper
    # has some oddities at times. This guarantees we are working with keys.
    set_options = set(ref._resource_option_mapper.keys())
    # Get any options that are not registered and slate them for removal from
    # the DB. This will delete unregistered options.
    clear_options = set_options.difference(
        ref.resource_options_registry.option_ids)
    options.update({x: None for x in clear_options})

    # Set the resource options for user in the Attribute Mapping.
    for r_opt_id, r_opt_value in options.items():
        if r_opt_value is None:
            # Delete any option set explicitly to None, ignore unset
            # options.
            ref._resource_option_mapper.pop(r_opt_id, None)
        else:
            # Set any options on the user_ref itself.
            opt_obj = option_class(
                option_id=r_opt_id,
                option_value=r_opt_value)
            ref._resource_option_mapper[r_opt_id] = opt_obj


class ResourceOptionRegistry(object):
    def __init__(self, registry_name):
        self._registered_options = {}
        self._registry_type = registry_name

    @property
    def option_names(self):
        return set([opt.option_name for opt in self.options])

    @property
    def options_by_name(self):
        return {opt.option_name: opt
                for opt in self._registered_options.values()}

    @property
    def options(self):
        return self._registered_options.values()

    @property
    def option_ids(self):
        return set(self._registered_options.keys())

    def get_option_by_id(self, opt_id):
        return self._registered_options.get(opt_id, None)

    def get_option_by_name(self, name):
        for option in self._registered_options.values():
            if name == option.option_name:
                return option
        return None

    @property
    def json_schema(self):
        schema = {'type': 'object',
                  'properties': {},
                  'additionalProperties': False}
        for opt in self.options:
            if opt.json_schema is not None:
                # NOTE(notmorgan): All options are nullable. Null indicates
                # the option should be reset and removed from the DB store.
                schema['properties'][opt.option_name] = validation.nullable(
                    opt.json_schema)
            else:
                # NOTE(notmorgan): without 'type' being specified, this
                # can be of any-type. We are simply specifying no interesting
                # values beyond that the property may exist here.
                schema['properties'][opt.option_name] = {}
        return schema

    def register_option(self, option):
        if option in self.options:
            # Re-registering the exact same option does nothing.
            return

        if option.option_id in self._registered_options:
            raise ValueError(_('Option %(option_id)s already defined in '
                               '%(registry)s.') %
                             {'option_id': option.option_id,
                              'registry': self._registry_type})
        if option.option_name in self.option_names:
            raise ValueError(_('Option %(option_name)s already defined in '
                               '%(registry)s') %
                             {'option_name': option.option_name,
                              'registry': self._registry_type})
        self._registered_options[option.option_id] = option


class ResourceOption(object):

    def __init__(self, option_id, option_name, validator=_validator,