# Copyright 2012 OpenStack, Foundation
# Copyright 2013 IBM Corp.
# 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 copy
import functools
import uuid

from oslo_config import cfg
from oslo_log import log as logging
import six

from glance.common import exception
from glance.common import timeutils
from glance.common import utils
from glance.db import utils as db_utils
from glance.i18n import _, _LI, _LW

CONF = cfg.CONF
LOG = logging.getLogger(__name__)

DATA = {
    'images': {},
    'members': {},
    'metadef_namespace_resource_types': [],
    'metadef_namespaces': [],
    'metadef_objects': [],
    'metadef_properties': [],
    'metadef_resource_types': [],
    'metadef_tags': [],
    'tags': {},
    'locations': [],
    'tasks': {},
    'task_info': {},
}

INDEX = 0


def log_call(func):
    @functools.wraps(func)
    def wrapped(*args, **kwargs):
        LOG.info(_LI('Calling %(funcname)s: args=%(args)s, '
                     'kwargs=%(kwargs)s'),
                 {"funcname": func.__name__,
                  "args": args,
                  "kwargs": kwargs})
        output = func(*args, **kwargs)
        LOG.info(_LI('Returning %(funcname)s: %(output)s'),
                 {"funcname": func.__name__,
                  "output": output})
        return output
    return wrapped


def configure():
    if CONF.workers not in [0, 1]:
        msg = _('CONF.workers should be set to 0 or 1 when using the '
                'db.simple.api backend. Fore more info, see '
                'https://bugs.launchpad.net/glance/+bug/1619508')
        LOG.critical(msg)
        raise SystemExit(msg)


def reset():
    global DATA
    DATA = {
        'images': {},
        'members': [],
        'metadef_namespace_resource_types': [],
        'metadef_namespaces': [],
        'metadef_objects': [],
        'metadef_properties': [],
        'metadef_resource_types': [],
        'metadef_tags': [],
        'tags': {},
        'locations': [],
        'tasks': {},
        'task_info': {},
    }


def clear_db_env(*args, **kwargs):
    """
    Setup global environment configuration variables.

    We have no connection-oriented environment variables, so this is a NOOP.
    """
    pass


def _get_session():
    return DATA


@utils.no_4byte_params
def _image_location_format(image_id, value, meta_data, status, deleted=False):
    dt = timeutils.utcnow()
    return {
        'id': str(uuid.uuid4()),
        'image_id': image_id,
        'created_at': dt,
        'updated_at': dt,
        'deleted_at': dt if deleted else None,
        'deleted': deleted,
        'url': value,
        'metadata': meta_data,
        'status': status,
    }


def _image_property_format(image_id, name, value):
    return {
        'image_id': image_id,
        'name': name,
        'value': value,
        'deleted': False,
        'deleted_at': None,
    }


def _image_member_format(image_id, tenant_id, can_share, status='pending',
                         deleted=False):
    dt = timeutils.utcnow()
    return {
        'id': str(uuid.uuid4()),
        'image_id': image_id,
        'member': tenant_id,
        'can_share': can_share,
        'status': status,
        'created_at': dt,
        'updated_at': dt,
        'deleted': deleted,
    }


def _pop_task_info_values(values):
    task_info_values = {}
    for k, v in list(values.items()):
        if k in ['input', 'result', 'message']:
            values.pop(k)
            task_info_values[k] = v

    return task_info_values


def _format_task_from_db(task_ref, task_info_ref):
    task = copy.deepcopy(task_ref)
    if task_info_ref:
        task_info = copy.deepcopy(task_info_ref)
        task_info_values = _pop_task_info_values(task_info)
        task.update(task_info_values)
    return task


def _task_format(task_id, **values):
    dt = timeutils.utcnow()
    task = {
        'id': task_id,
        'type': 'import',
        'status': 'pending',
        'owner': None,
        'expires_at': None,
        'created_at': dt,
        'updated_at': dt,
        'deleted_at': None,
        'deleted': False,
    }
    task.update(values)
    return task


def _task_info_format(task_id, **values):
    task_info = {
        'task_id': task_id,
        'input': None,
        'result': None,
        'message': None,
    }
    task_info.update(values)
    return task_info


@utils.no_4byte_params
def _image_update(image, values, properties):
    # NOTE(bcwaldon): store properties as a list to match sqlalchemy driver
    properties = [{'name': k,
                   'value': v,
                   'image_id': image['id'],
                   'deleted': False} for k, v in properties.items()]
    if 'properties' not in image.keys():
        image['properties'] = []
    image['properties'].extend(properties)
    values = db_utils.ensure_image_dict_v2_compliant(values)
    image.update(values)
    return image


def _image_format(image_id, **values):
    dt = timeutils.utcnow()
    image = {
        'id': image_id,
        'name': None,
        'owner': None,
        'locations': [],
        'status': 'queued',
        'protected': False,
        'visibility': 'shared',
        'container_format': None,
        'disk_format': None,
        'min_ram': 0,
        'min_disk': 0,
        'size': None,
        'virtual_size': None,
        'checksum': None,
        'os_hash_algo': None,
        'os_hash_value': None,
        'tags': [],
        'created_at': dt,
        'updated_at': dt,
        'deleted_at': None,
        'deleted': False,
        'os_hidden': False
    }

    locations = values.pop('locations', None)
    if locations is not None:
        image['locations'] = []
        for location in locations:
            location_ref = _image_location_format(image_id,
                                                  location['url'],
                                                  location['metadata'],
                                                  location['status'])
            image['locations'].append(location_ref)
            DATA['locations'].append(location_ref)

    return _image_update(image, values, values.pop('properties', {}))


def _filter_images(images, filters, context,
                   status='accepted', is_public=None,
                   admin_as_user=False):
    filtered_images = []
    if 'properties' in filters:
        prop_filter = filters.pop('properties')
        filters.update(prop_filter)

    if status == 'all':
        status = None

    visibility = filters.pop('visibility', None)
    os_hidden = filters.pop('os_hidden', False)

    for image in images:
        member = image_member_find(context, image_id=image['id'],
                                   member=context.owner, status=status)
        is_member = len(member) > 0
        has_ownership = context.owner and image['owner'] == context.owner
        image_is_public = image['visibility'] == 'public'
        image_is_community = image['visibility'] == 'community'
        image_is_shared = image['visibility'] == 'shared'
        image_is_hidden = image['os_hidden'] == True
        acts_as_admin = context.is_admin and not admin_as_user
        can_see = (image_is_public
                   or image_is_community
                   or has_ownership
                   or (is_member and image_is_shared)
                   or acts_as_admin)
        if not can_see:
            continue

        if visibility:
            if visibility == 'public':
                if not image_is_public:
                    continue
            elif visibility == 'private':
                if not (image['visibility'] == 'private'):
                    continue
                if not (has_ownership or acts_as_admin):
                    continue
            elif visibility == 'shared':
                if not image_is_shared:
                    continue
            elif visibility == 'community':
                if not image_is_community:
                    continue
        else:
            if (not has_ownership) and image_is_community:
                continue

        if is_public is not None:
            if not image_is_public == is_public:
                continue

        if os_hidden:
            if image_is_hidden:
                continue

        to_add = True
        for k, value in six.iteritems(filters):
            key = k
            if k.endswith('_min') or k.endswith('_max'):
                key = key[0:-4]
                try:
                    value = int(value)
                except ValueError:
                    msg = _("Unable to filter on a range "
                            "with a non-numeric value.")
                    raise exception.InvalidFilterRangeValue(msg)
            if k.endswith('_min'):
                to_add = image.get(key) >= value
            elif k.endswith('_max'):
                to_add = image.get(key) <= value
            elif k in ['created_at', 'updated_at']:
                attr_value = image.get(key)
                operator, isotime = utils.split_filter_op(value)
                parsed_time = timeutils.parse_isotime(isotime)
                threshold = timeutils.normalize_time(parsed_time)
                to_add = utils.evaluate_filter_op(attr_value, operator,
                                                  threshold)
            elif k in ['name', 'id', 'status',
                       'container_format', 'disk_format']:
                attr_value = image.get(key)
                operator, list_value = utils.split_filter_op(value)
                if operator == 'in':
                    threshold = utils.split_filter_value_for_quotes(list_value)
                    to_add = attr_value in threshold
                elif operator == 'eq':
                    to_add = (attr_value == list_value)
                else:
                    msg = (_("Unable to filter by unknown operator '%s'.")
                           % operator)
                    raise exception.InvalidFilterOperatorValue(msg)

            elif k != 'is_public' and image.get(k) is not None:
                to_add = image.get(key) == value
            elif k == 'tags':
                filter_tags = value
                image_tags = image_tag_get_all(context, image['id'])
                for tag in filter_tags:
                    if tag not in image_tags:
                        to_add = False
                        break
            else:
                to_add = False
                for p in image['properties']:
                    properties = {p['name']: p['value'],
                                  'deleted': p['deleted']}
                    to_add |= (properties.get(key) == value and
                               properties.get('deleted') is False)

            if not to_add:
                break

        if to_add:
            filtered_images.append(image)

    return filtered_images


def _do_pagination(context, images, marker, limit, show_deleted,
                   status='accepted'):
    start = 0
    end = -1
    if marker is None:
        start = 0
    else:
        # Check that the image is accessible
        _image_get(context, marker, force_show_deleted=show_deleted,
                   status=status)

        for i, image in enumerate(images):
            if image['id'] == marker:
                start = i + 1
                break
        else:
            raise exception.ImageNotFound()

    end = start + limit if limit is not None else None
    return images[start:end]


def _sort_images(images, sort_key, sort_dir):
    sort_key = ['created_at'] if not sort_key else sort_key

    default_sort_dir = 'desc'

    if not sort_dir:
        sort_dir = [default_sort_dir] * len(sort_key)
    elif len(sort_dir) == 1:
        default_sort_dir = sort_dir[0]
        sort_dir *= len(sort_key)

    for key in ['created_at', 'id']:
        if key not in sort_key:
            sort_key.append(key)
            sort_dir.append(default_sort_dir)

    for key in sort_key:
        if images and not (key in images[0]):
            raise exception.InvalidSortKey()

    if any(dir for dir in sort_dir if dir not in ['asc', 'desc']):
        raise exception.InvalidSortDir()

    if len(sort_key) != len(sort_dir):
        raise exception.Invalid(message='Number of sort dirs does not match '
                                        'the number of sort keys')

    for key, dir in reversed(list(zip(sort_key, sort_dir))):
        reverse = dir == 'desc'
        images.sort(key=lambda x: x[key] or '', reverse=reverse)

    return images


def _image_get(context, image_id, force_show_deleted=False, status=None):
    try:
        image = DATA['images'][image_id]
    except KeyError:
        LOG.warn(_LW('Could not find image %s'), image_id)
        raise exception.ImageNotFound()

    if image['deleted'] and not (force_show_deleted
                                 or context.can_see_deleted):
        LOG.warn(_LW('Unable to get deleted image'))
        raise exception.ImageNotFound()

    if not is_image_visible(context, image):
        LOG.warn(_LW('Unable to get unowned image'))
        raise exception.Forbidden("Image not visible to you")

    return image


@log_call
def image_get(context, image_id, session=None, force_show_deleted=False,
              v1_mode=False):
    image = copy.deepcopy(_image_get(context, image_id, force_show_deleted))
    image = _normalize_locations(context, image,
                                 force_show_deleted=force_show_deleted)
    if v1_mode:
        image = db_utils.mutate_image_dict_to_v1(image)
    return image


@log_call
def image_get_all(context, filters=None, marker=None, limit=None,
                  sort_key=None, sort_dir=None,
                  member_status='accepted', is_public=None,
                  admin_as_user=False, return_tag=False, v1_mode=False):
    filters = filters or {}
    images = DATA['images'].values()
    images = _filter_images(images, filters, context, member_status,
                            is_public, admin_as_user)
    images = _sort_images(images, sort_key, sort_dir)
    images = _do_pagination(context, images, marker, limit,
                            filters.get('deleted'))

    force_show_deleted = True if filters.get('deleted') else False
    res = []
    for image in images:
        img = _normalize_locations(context, copy.deepcopy(image),
                                   force_show_deleted=force_show_deleted)
        if return_tag:
            img['tags'] = image_tag_get_all(context, img['id'])

        if v1_mode:
            img = db_utils.mutate_image_dict_to_v1(img)
        res.append(img)
    return res


def image_restore(context, image_id):
    """Restore the pending-delete image to active."""
    image = _image_get(context, image_id)
    if image['status'] != 'pending_delete':
        msg = (_('cannot restore the image from %s to active (wanted '
                 'from_state=pending_delete)') % image['status'])
        raise exception.Conflict(msg)
    values = {'status': 'active', 'deleted': 0}
    image_update(context, image_id, values)


@log_call
def image_property_create(context, values):
    image = _image_get(context, values['image_id'])
    prop = _image_property_format(values['image_id'],
                                  values['name'],
                                  values['value'])
    image['properties'].append(prop)
    return prop


@log_call
def image_property_delete(context, prop_ref, image_ref):
    prop = None
    for p in DATA['images'][image_ref]['properties']:
        if p['name'] == prop_ref:
            prop = p
    if not prop:
        raise exception.NotFound()
    prop['deleted_at'] = timeutils.utcnow()
    prop['deleted'] = True
    return prop


@log_call
def image_member_find(context, image_id=None, member=None,
                      status=None, include_deleted=False):
    filters = []
    images = DATA['images']
    members = DATA['members']

    def is_visible(member):
        return (member['member'] == context.owner or
                images[member['image_id']]['owner'] == context.owner)

    if not context.is_admin:
        filters.append(is_visible)

    if image_id is not None:
        filters.append(lambda m: m['image_id'] == image_id)
    if member is not None:
        filters.append(lambda m: m['member'] == member)
    if status is not None:
        filters.append(lambda m: m['status'] == status)

    for f in filters:
        members = filter(f, members)
    return [copy.deepcopy(m) for m in members]


@log_call
def image_member_count(context, image_id):
    """Return the number of image members for this image

    :param image_id: identifier of image entity
    """
    if not image_id:
        msg = _("Image id is required.")
        raise exception.Invalid(msg)

    members = DATA['members']
    return len([x for x in members if x['image_id'] == image_id])


@log_call
@utils.no_4byte_params
def image_member_create(context, values):
    member = _image_member_format(values['image_id'],
                                  values['member'],
                                  values.get('can_share', False),
                                  values.get('status', 'pending'),
                                  values.get('deleted', False))
    global DATA
    DATA['members'].append(member)
    return copy.deepcopy(member)


@log_call
def image_member_update(context, member_id, values):
    global DATA
    for member in DATA['members']:
        if member['id'] == member_id:
            member.update(values)
            member['updated_at'] = timeutils.utcnow()
            return copy.deepcopy(member)
    else:
        raise exception.NotFound()


@log_call
def image_member_delete(context, member_id):
    global DATA
    for i, member in enumerate(DATA['members']):
        if member['id'] == member_id:
            del DATA['members'][i]
            break
    else:
        raise exception.NotFound()


@log_call
@utils.no_4byte_params
def image_location_add(context, image_id, location):
    deleted = location['status'] in ('deleted', 'pending_delete')
    location_ref = _image_location_format(image_id,
                                          value=location['url'],
                                          meta_data=location['metadata'],
                                          status=location['status'],
                                          deleted=deleted)
    DATA['locations'].append(location_ref)
    image = DATA['images'][image_id]
    image.setdefault('locations', []).append(location_ref)


@log_call
@utils.no_4byte_params
def image_location_update(context, image_id, location):
    loc_id = location.get('id')
    if loc_id is None:
        msg = _("The location data has an invalid ID: %d") % loc_id
        raise exception.Invalid(msg)

    deleted = location['status'] in ('deleted', 'pending_delete')
    updated_time = timeutils.utcnow()
    delete_time = updated_time if deleted else None

    updated = False
    for loc in DATA['locations']:
        if loc['id'] == loc_id and loc['image_id'] == image_id:
            loc.update({"value": location['url'],
                        "meta_data": location['metadata'],
                        "status": location['status'],
                        "deleted": deleted,
                        "updated_at": updated_time,
                        "deleted_at": delete_time})
            updated = True
            break

    if not updated:
        msg = (_("No location found with ID %(loc)s from image %(img)s") %
               dict(loc=loc_id, img=image_id))
        LOG.warn(msg)
        raise exception.NotFound(msg)


@log_call
def image_location_delete(context, image_id, location_id, status,
                          delete_time=None):
    if status not in ('deleted', 'pending_delete'):
        msg = _("The status of deleted image location can only be set to "
                "'pending_delete' or 'deleted'.")
        raise exception.Invalid(msg)

    deleted = False
    for loc in DATA['locations']:
        if loc['id'] == location_id and loc['image_id'] == image_id:
            deleted = True
            delete_time = delete_time or timeutils.utcnow()
            loc.update({"deleted": deleted,
                        "status": status,
                        "updated_at": delete_time,
                        "deleted_at": delete_time})
            break

    if not deleted:
        msg = (_("No location found with ID %(loc)s from image %(img)s") %
               dict(loc=location_id, img=image_id))
        LOG.warn(msg)
        raise exception.NotFound(msg)


def _image_locations_set(context, image_id, locations):
    # NOTE(zhiyan): 1. Remove records from DB for deleted locations
    used_loc_ids = [loc['id'] for loc in locations if loc.get('id')]
    image = DATA['images'][image_id]
    for loc in image['locations']:
        if loc['id'] not in used_loc_ids and not loc['deleted']:
            image_location_delete(context, image_id, loc['id'], 'deleted')
    for i, loc in enumerate(DATA['locations']):
        if (loc['image_id'] == image_id and loc['id'] not in used_loc_ids and
                not loc['deleted']):
            del DATA['locations'][i]

    # NOTE(zhiyan): 2. Adding or update locations
    for loc in locations:
        if loc.get('id') is None:
            image_location_add(context, image_id, loc)
        else:
            image_location_update(context, image_id, loc)


def _image_locations_delete_all(context, image_id, delete_time=None):
    image = DATA['images'][image_id]
    for loc in image['locations']:
        if not loc['deleted']:
            image_location_delete(context, image_id, loc['id'], 'deleted',
                                  delete_time=delete_time)

    for i, loc in enumerate(DATA['locations']):
        if image_id == loc['image_id'] and loc['deleted'] == False:
            del DATA['locations'][i]


def _normalize_locations(context, image, force_show_deleted=False):
    """
    Generate suitable dictionary list for locations field of image.

    We don't need to set other data fields of location record which return
    from image query.
    """

    if image['status'] == 'deactivated' and not context.is_admin:
        # Locations are not returned for a deactivated image for non-admin user
        image['locations'] = []
        return image

    if force_show_deleted:
        locations = image['locations']
    else:
        locations = [x for x in image['locations'] if not x['deleted']]
    image['locations'] = [{'id': loc['id'],
                           'url': loc['url'],
                           'metadata': loc['metadata'],
                           'status': loc['status']}
                          for loc in locations]
    return image


@log_call
def image_create(context, image_values, v1_mode=False):
    global DATA
    image_id = image_values.get('id', str(uuid.uuid4()))

    if image_id in DATA['images']:
        raise exception.Duplicate()

    if 'status' not in image_values:
        raise exception.Invalid('status is a required attribute')

    allowed_keys = set(['id', 'name', 'status', 'min_ram', 'min_disk', 'size',
                        'virtual_size', 'checksum', 'locations', 'owner',
                        'protected', 'is_public', 'container_format',
                        'disk_format', 'created_at', 'updated_at', 'deleted',
                        'deleted_at', 'properties', 'tags', 'visibility',
                        'os_hidden', 'os_hash_algo', 'os_hash_value'])

    incorrect_keys = set(image_values.keys()) - allowed_keys
    if incorrect_keys:
        raise exception.Invalid(
            'The keys %s are not valid' % str(incorrect_keys))

    image = _image_format(image_id, **image_values)
    DATA['images'][image_id] = image
    DATA['tags'][image_id] = image.pop('tags', [])

    image = _normalize_locations(context, copy.deepcopy(image))
    if v1_mode:
        image = db_utils.mutate_image_dict_to_v1(image)
    return image


@log_call
def image_update(context, image_id, image_values, purge_props=False,
                 from_state=None, v1_mode=False):
    global DATA
    try:
        image = DATA['images'][image_id]
    except KeyError:
        raise exception.ImageNotFound()

    location_data = image_values.pop('locations', None)
    if location_data is not None:
        _image_locations_set(context, image_id, location_data)

    # replace values for properties that already exist
    new_properties = image_values.pop('properties', {})
    for prop in image['properties']:
        if prop['name'] in new_properties:
            prop['value'] = new_properties.pop(prop['name'])
        elif purge_props:
            # this matches weirdness in the sqlalchemy api
            prop['deleted'] = True

    image['updated_at'] = timeutils.utcnow()
    _image_update(image, image_values, new_properties)
    DATA['images'][image_id] = image

    image = _normalize_locations(context, copy.deepcopy(image))
    if v1_mode:
        image = db_utils.mutate_image_dict_to_v1(image)
    return image


@log_call
def image_destroy(context, image_id):
    global DATA
    try:
        delete_time = timeutils.utcnow()
        DATA['images'][image_id]['deleted'] = True
        DATA['images'][image_id]['deleted_at'] = delete_time

        # NOTE(flaper87): Move the image to one of the deleted statuses
        # if it hasn't been done yet.
        if (DATA['images'][image_id]['status'] not in
                ['deleted', 'pending_delete']):
            DATA['images'][image_id]['status'] = 'deleted'

        _image_locations_delete_all(context, image_id,
                                    delete_time=delete_time)

        for prop in DATA['images'][image_id]['properties']:
            image_property_delete(context, prop['name'], image_id)

        members = image_member_find(context, image_id=image_id)
        for member in members:
            image_member_delete(context, member['id'])

        tags = image_tag_get_all(context, image_id)
        for tag in tags:
            image_tag_delete(context, image_id, tag)

        return _normalize_locations(context,
                                    copy.deepcopy(DATA['images'][image_id]))
    except KeyError:
        raise exception.ImageNotFound()


@log_call
def image_tag_get_all(context, image_id):
    return DATA['tags'].get(image_id, [])


@log_call
def image_tag_get(context, image_id, value):
    tags = image_tag_get_all(context, image_id)
    if value in tags:
        return value
    else:
        raise exception.NotFound()


@log_call
def image_tag_set_all(context, image_id, values):
    global DATA
    DATA['tags'][image_id] = list(values)


@log_call
@utils.no_4byte_params
def image_tag_create(context, image_id, value):
    global DATA
    DATA['tags'][image_id].append(value)
    return value


@log_call
def image_tag_delete(context, image_id, value):
    global DATA
    try:
        DATA['tags'][image_id].remove(value)
    except ValueError:
        raise exception.NotFound()


def is_image_visible(context, image, status=None):
    if status == 'all':
        status = None
    return db_utils.is_image_visible(context, image, image_member_find, status)


def user_get_storage_usage(context, owner_id, image_id=None, session=None):
    images = image_get_all(context, filters={'owner': owner_id})
    total = 0
    for image in images:
        if image['status'] in ['killed', 'deleted']:
            continue

        if image['id'] != image_id:
            locations = [loc for loc in image['locations']
                         if loc.get('status') != 'deleted']
            total += (image['size'] * len(locations))
    return total


@log_call
def task_create(context, values):
    """Create a task object"""
    global DATA

    task_values = copy.deepcopy(values)
    task_id = task_values.get('id', str(uuid.uuid4()))
    required_attributes = ['type', 'status', 'input']
    allowed_attributes = ['id', 'type', 'status', 'input', 'result', 'owner',
                          'message', 'expires_at', 'created_at',
                          'updated_at', 'deleted_at', 'deleted']

    if task_id in DATA['tasks']:
        raise exception.Duplicate()

    for key in required_attributes:
        if key not in task_values:
            raise exception.Invalid('%s is a required attribute' % key)

    incorrect_keys = set(task_values.keys()) - set(allowed_attributes)
    if incorrect_keys:
        raise exception.Invalid(
            'The keys %s are not valid' % str(incorrect_keys))

    task_info_values = _pop_task_info_values(task_values)
    task = _task_format(task_id, **task_values)
    DATA['tasks'][task_id] = task
    task_info = _task_info_create(task['id'], task_info_values)

    return _format_task_from_db(task, task_info)


@log_call
def task_update(context, task_id, values):
    """Update a task object"""
    global DATA
    task_values = copy.deepcopy(values)
    task_info_values = _pop_task_info_values(task_values)
    try:
        task = DATA['tasks'][task_id]
    except KeyError:
        LOG.debug("No task found with ID %s", task_id)
        raise exception.TaskNotFound(task_id=task_id)

    task.update(task_values)
    task['updated_at'] = timeutils.utcnow()
    DATA['tasks'][task_id] = task
    task_info = _task_info_update(task['id'], task_info_values)

    return _format_task_from_db(task, task_info)


@log_call
def task_get(context, task_id, force_show_deleted=False):
    task, task_info = _task_get(context, task_id, force_show_deleted)
    return _format_task_from_db(task, task_info)


def _task_get(context, task_id, force_show_deleted=False):
    try:
        task = DATA['tasks'][task_id]
    except KeyError:
        msg = _LW('Could not find task %s') % task_id
        LOG.warn(msg)
        raise exception.TaskNotFound(task_id=task_id)

    if task['deleted'] and not (force_show_deleted or context.can_see_deleted):
        msg = _LW('Unable to get deleted task %s') % task_id
        LOG.warn(msg)
        raise exception.TaskNotFound(task_id=task_id)

    if not _is_task_visible(context, task):
        LOG.debug("Forbidding request, task %s is not visible", task_id)
        msg = _("Forbidding request, task %s is not visible") % task_id
        raise exception.Forbidden(msg)

    task_info = _task_info_get(task_id)

    return task, task_info


@log_call
def task_delete(context, task_id):
    global DATA
    try:
        DATA['tasks'][task_id]['deleted'] = True
        DATA['tasks'][task_id]['deleted_at'] = timeutils.utcnow()
        DATA['tasks'][task_id]['updated_at'] = timeutils.utcnow()
        return copy.deepcopy(DATA['tasks'][task_id])
    except KeyError:
        LOG.debug("No task found with ID %s", task_id)
        raise exception.TaskNotFound(task_id=task_id)


def _task_soft_delete(context):
    """Scrub task entities which are expired """
    global DATA
    now = timeutils.utcnow()
    tasks = DATA['tasks'].values()

    for task in tasks:
        if(task['owner'] == context.owner and task['deleted'] == False
           and task['expires_at'] <= now):

            task['deleted'] = True
            task['deleted_at'] = timeutils.utcnow()


@log_call
def task_get_all(context, filters=None, marker=None, limit=None,
                 sort_key='created_at', sort_dir='desc'):
    """
    Get all tasks that match zero or more filters.

    :param filters: dict of filter keys and values.
    :param marker: task id after which to start page
    :param limit: maximum number of tasks to return
    :param sort_key: task attribute by which results should be sorted
    :param sort_dir: direction in which results should be sorted (asc, desc)
    :returns: tasks set
    """
    _task_soft_delete(context)
    filters = filters or {}
    tasks = DATA['tasks'].values()
    tasks = _filter_tasks(tasks, filters, context)
    tasks = _sort_tasks(tasks, sort_key, sort_dir)
    tasks = _paginate_tasks(context, tasks, marker, limit,
                            filters.get('deleted'))

    filtered_tasks = []
    for task in tasks:
        filtered_tasks.append(_format_task_from_db(task, task_info_ref=None))

    return filtered_tasks


def _is_task_visible(context, task):
    """Return True if the task is visible in this context."""
    # Is admin == task visible
    if context.is_admin:
        return True

    # No owner == task visible
    if task['owner'] is None:
        return True

    # Perform tests based on whether we have an owner
    if context.owner is not None:
        if context.owner == task['owner']:
            return True

    return False


def _filter_tasks(tasks, filters, context, admin_as_user=False):
    filtered_tasks = []

    for task in tasks:
        has_ownership = context.owner and task['owner'] == context.owner
        can_see = (has_ownership or (context.is_admin and not admin_as_user))
        if not can_see:
            continue

        add = True
        for k, value in six.iteritems(filters):
            add = task[k] == value and task['deleted'] is False
            if not add:
                break

        if add:
            filtered_tasks.append(task)

    return filtered_tasks


def _sort_tasks(tasks, sort_key, sort_dir):
    reverse = False
    if tasks and not (sort_key in tasks[0]):
        raise exception.InvalidSortKey()

    def keyfn(x):
        return (x[sort_key] if x[sort_key] is not None else '',
                x['created_at'], x['id'])

    reverse = sort_dir == 'desc'
    tasks.sort(key=keyfn, reverse=reverse)
    return tasks


def _paginate_tasks(context, tasks, marker, limit, show_deleted):
    start = 0
    end = -1
    if marker is None:
        start = 0
    else:
        # Check that the task is accessible
        _task_get(context, marker, force_show_deleted=show_deleted)

        for i, task in enumerate(tasks):
            if task['id'] == marker:
                start = i + 1
                break
        else:
            if task:
                raise exception.TaskNotFound(task_id=task['id'])
            else:
                msg = _("Task does not exist")
                raise exception.NotFound(message=msg)

    end = start + limit if limit is not None else None
    return tasks[start:end]


def _task_info_create(task_id, values):
    """Create a Task Info for Task with given task ID"""
    global DATA
    task_info = _task_info_format(task_id, **values)
    DATA['task_info'][task_id] = task_info

    return task_info


def _task_info_update(task_id, values):
    """Update Task Info for Task with given task ID and updated values"""
    global DATA
    try:
        task_info = DATA['task_info'][task_id]
    except KeyError:
        LOG.debug("No task info found with task id %s", task_id)
        raise exception.TaskNotFound(task_id=task_id)

    task_info.update(values)
    DATA['task_info'][task_id] = task_info

    return task_info


def _task_info_get(task_id):
    """Get Task Info for Task with given task ID"""
    global DATA
    try:
        task_info = DATA['task_info'][task_id]
    except KeyError:
        msg = _LW('Could not find task info %s') % task_id
        LOG.warn(msg)
        raise exception.TaskNotFound(task_id=task_id)

    return task_info


def _metadef_delete_namespace_content(get_func, key, context, namespace_name):
    global DATA
    metadefs = get_func(context, namespace_name)
    data = DATA[key]
    for metadef in metadefs:
        data.remove(metadef)
    return metadefs


@log_call
@utils.no_4byte_params
def metadef_namespace_create(context, values):
    """Create a namespace object"""
    global DATA

    namespace_values = copy.deepcopy(values)
    namespace_name = namespace_values.get('namespace')
    required_attributes = ['namespace', 'owner']
    allowed_attributes = ['namespace', 'owner', 'display_name', 'description',
                          'visibility', 'protected']

    for namespace in DATA['metadef_namespaces']:
        if namespace['namespace'] == namespace_name:
            LOG.debug("Can not create the metadata definition namespace. "
                      "Namespace=%s already exists.", namespace_name)
            raise exception.MetadefDuplicateNamespace(
                namespace_name=namespace_name)

    for key in required_attributes:
        if key not in namespace_values:
            raise exception.Invalid('%s is a required attribute' % key)

    incorrect_keys = set(namespace_values.keys()) - set(allowed_attributes)
    if incorrect_keys:
        raise exception.Invalid(
            'The keys %s are not valid' % str(incorrect_keys))

    namespace = _format_namespace(namespace_values)
    DATA['metadef_namespaces'].append(namespace)

    return namespace


@log_call
@utils.no_4byte_params
def metadef_namespace_update(context, namespace_id, values):
    """Update a namespace object"""
    global DATA
    namespace_values = copy.deepcopy(values)

    namespace = metadef_namespace_get_by_id(context, namespace_id)
    if namespace['namespace'] != values['namespace']:
        for db_namespace in DATA['metadef_namespaces']:
            if db_namespace['namespace'] == values['namespace']:
                LOG.debug("Invalid update. It would result in a duplicate "
                          "metadata definition namespace with the same "
                          "name of %s", values['namespace'])
                emsg = (_("Invalid update. It would result in a duplicate"
                          " metadata definition namespace with the same"
                          " name of %s")
                        % values['namespace'])
                raise exception.MetadefDuplicateNamespace(emsg)
    DATA['metadef_namespaces'].remove(namespace)

    namespace.update(namespace_values)
    namespace['updated_at'] = timeutils.utcnow()
    DATA['metadef_namespaces'].append(namespace)

    return namespace


@log_call
def metadef_namespace_get_by_id(context, namespace_id):
    """Get a namespace object"""
    try:
        namespace = next(namespace for namespace in DATA['metadef_namespaces']
                         if namespace['id'] == namespace_id)
    except StopIteration:
        msg = (_("Metadata definition namespace not found for id=%s")
               % namespace_id)
        LOG.warn(msg)
        raise exception.MetadefNamespaceNotFound(msg)

    if not _is_namespace_visible(context, namespace):
        LOG.debug("Forbidding request, metadata definition namespace=%s "
                  "is not visible.", namespace.namespace)
        emsg = _("Forbidding request, metadata definition namespace=%s "
                 "is not visible.") % namespace.namespace
        raise exception.MetadefForbidden(emsg)

    return namespace


@log_call
def metadef_namespace_get(context, namespace_name):
    """Get a namespace object"""
    try:
        namespace = next(namespace for namespace in DATA['metadef_namespaces']
                         if namespace['namespace'] == namespace_name)
    except StopIteration:
        LOG.debug("No namespace found with name %s", namespace_name)
        raise exception.MetadefNamespaceNotFound(
            namespace_name=namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    return namespace


@log_call
def metadef_namespace_get_all(context,
                              marker=None,
                              limit=None,
                              sort_key='created_at',
                              sort_dir='desc',
                              filters=None):
    """Get a namespaces list"""
    resource_types = filters.get('resource_types', []) if filters else []
    visibility = filters.get('visibility') if filters else None

    namespaces = []
    for namespace in DATA['metadef_namespaces']:
        if not _is_namespace_visible(context, namespace):
            continue

        if visibility and namespace['visibility'] != visibility:
            continue

        if resource_types:
            for association in DATA['metadef_namespace_resource_types']:
                if association['namespace_id'] == namespace['id']:
                    if association['name'] in resource_types:
                        break
            else:
                continue

        namespaces.append(namespace)

    return namespaces


@log_call
def metadef_namespace_delete(context, namespace_name):
    """Delete a namespace object"""
    global DATA

    namespace = metadef_namespace_get(context, namespace_name)
    DATA['metadef_namespaces'].remove(namespace)

    return namespace


@log_call
def metadef_namespace_delete_content(context, namespace_name):
    """Delete a namespace content"""
    global DATA
    namespace = metadef_namespace_get(context, namespace_name)
    namespace_id = namespace['id']

    objects = []

    for object in DATA['metadef_objects']:
        if object['namespace_id'] != namespace_id:
            objects.append(object)

    DATA['metadef_objects'] = objects

    properties = []

    for property in DATA['metadef_objects']:
        if property['namespace_id'] != namespace_id:
            properties.append(object)

    DATA['metadef_objects'] = properties

    return namespace


@log_call
def metadef_object_get(context, namespace_name, object_name):
    """Get a metadef object"""
    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    for object in DATA['metadef_objects']:
        if (object['namespace_id'] == namespace['id'] and
                object['name'] == object_name):
            return object
    else:
        LOG.debug("The metadata definition object with name=%(name)s"
                  " was not found in namespace=%(namespace_name)s.",
                  {'name': object_name, 'namespace_name': namespace_name})
        raise exception.MetadefObjectNotFound(namespace_name=namespace_name,
                                              object_name=object_name)


@log_call
def metadef_object_get_by_id(context, namespace_name, object_id):
    """Get a metadef object"""
    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    for object in DATA['metadef_objects']:
        if (object['namespace_id'] == namespace['id'] and
                object['id'] == object_id):
            return object
    else:
        msg = (_("Metadata definition object not found for id=%s")
               % object_id)
        LOG.warn(msg)
        raise exception.MetadefObjectNotFound(msg)


@log_call
def metadef_object_get_all(context, namespace_name):
    """Get a metadef objects list"""
    namespace = metadef_namespace_get(context, namespace_name)

    objects = []

    _check_namespace_visibility(context, namespace, namespace_name)

    for object in DATA['metadef_objects']:
        if object['namespace_id'] == namespace['id']:
            objects.append(object)

    return objects


@log_call
@utils.no_4byte_params
def metadef_object_create(context, namespace_name, values):
    """Create a metadef object"""
    global DATA

    object_values = copy.deepcopy(values)
    object_name = object_values['name']
    required_attributes = ['name']
    allowed_attributes = ['name', 'description', 'json_schema', 'required']

    namespace = metadef_namespace_get(context, namespace_name)

    for object in DATA['metadef_objects']:
        if (object['name'] == object_name and
                object['namespace_id'] == namespace['id']):
            LOG.debug("A metadata definition object with name=%(name)s "
                      "in namespace=%(namespace_name)s already exists.",
                      {'name': object_name, 'namespace_name': namespace_name})
            raise exception.MetadefDuplicateObject(
                object_name=object_name, namespace_name=namespace_name)

    for key in required_attributes:
        if key not in object_values:
            raise exception.Invalid('%s is a required attribute' % key)

    incorrect_keys = set(object_values.keys()) - set(allowed_attributes)
    if incorrect_keys:
        raise exception.Invalid(
            'The keys %s are not valid' % str(incorrect_keys))

    object_values['namespace_id'] = namespace['id']

    _check_namespace_visibility(context, namespace, namespace_name)

    object = _format_object(object_values)
    DATA['metadef_objects'].append(object)

    return object


@log_call
@utils.no_4byte_params
def metadef_object_update(context, namespace_name, object_id, values):
    """Update a metadef object"""
    global DATA

    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    object = metadef_object_get_by_id(context, namespace_name, object_id)
    if object['name'] != values['name']:
        for db_object in DATA['metadef_objects']:
            if (db_object['name'] == values['name'] and
                    db_object['namespace_id'] == namespace['id']):
                LOG.debug("Invalid update. It would result in a duplicate "
                          "metadata definition object with same name=%(name)s "
                          "in namespace=%(namespace_name)s.",
                          {'name': object['name'],
                           'namespace_name': namespace_name})
                emsg = (_("Invalid update. It would result in a duplicate"
                          " metadata definition object with the same"
                          " name=%(name)s "
                          " in namespace=%(namespace_name)s.")
                        % {'name': object['name'],
                           'namespace_name': namespace_name})
                raise exception.MetadefDuplicateObject(emsg)
    DATA['metadef_objects'].remove(object)

    object.update(values)
    object['updated_at'] = timeutils.utcnow()
    DATA['metadef_objects'].append(object)

    return object


@log_call
def metadef_object_delete(context, namespace_name, object_name):
    """Delete a metadef object"""
    global DATA

    object = metadef_object_get(context, namespace_name, object_name)
    DATA['metadef_objects'].remove(object)

    return object


def metadef_object_delete_namespace_content(context, namespace_name,
                                            session=None):
    """Delete an object or raise if namespace or object doesn't exist."""
    return _metadef_delete_namespace_content(
        metadef_object_get_all, 'metadef_objects', context, namespace_name)


@log_call
def metadef_object_count(context, namespace_name):
    """Get metadef object count in a namespace"""
    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    count = 0
    for object in DATA['metadef_objects']:
        if object['namespace_id'] == namespace['id']:
            count = count + 1

    return count


@log_call
def metadef_property_count(context, namespace_name):
    """Get properties count in a namespace"""
    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    count = 0
    for property in DATA['metadef_properties']:
        if property['namespace_id'] == namespace['id']:
            count = count + 1

    return count


@log_call
@utils.no_4byte_params
def metadef_property_create(context, namespace_name, values):
    """Create a metadef property"""
    global DATA

    property_values = copy.deepcopy(values)
    property_name = property_values['name']
    required_attributes = ['name']
    allowed_attributes = ['name', 'description', 'json_schema', 'required']

    namespace = metadef_namespace_get(context, namespace_name)

    for property in DATA['metadef_properties']:
        if (property['name'] == property_name and
                property['namespace_id'] == namespace['id']):
            LOG.debug("Can not create metadata definition property. A property"
                      " with name=%(name)s already exists in"
                      " namespace=%(namespace_name)s.",
                      {'name': property_name,
                       'namespace_name': namespace_name})
            raise exception.MetadefDuplicateProperty(
                property_name=property_name,
                namespace_name=namespace_name)

    for key in required_attributes:
        if key not in property_values:
            raise exception.Invalid('%s is a required attribute' % key)

    incorrect_keys = set(property_values.keys()) - set(allowed_attributes)
    if incorrect_keys:
        raise exception.Invalid(
            'The keys %s are not valid' % str(incorrect_keys))

    property_values['namespace_id'] = namespace['id']

    _check_namespace_visibility(context, namespace, namespace_name)

    property = _format_property(property_values)
    DATA['metadef_properties'].append(property)

    return property


@log_call
@utils.no_4byte_params
def metadef_property_update(context, namespace_name, property_id, values):
    """Update a metadef property"""
    global DATA

    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    property = metadef_property_get_by_id(context, namespace_name, property_id)
    if property['name'] != values['name']:
        for db_property in DATA['metadef_properties']:
            if (db_property['name'] == values['name'] and
                    db_property['namespace_id'] == namespace['id']):
                LOG.debug("Invalid update. It would result in a duplicate"
                          " metadata definition property with the same"
                          " name=%(name)s"
                          " in namespace=%(namespace_name)s.",
                          {'name': property['name'],
                           'namespace_name': namespace_name})
                emsg = (_("Invalid update. It would result in a duplicate"
                          " metadata definition property with the same"
                          " name=%(name)s"
                          " in namespace=%(namespace_name)s.")
                        % {'name': property['name'],
                           'namespace_name': namespace_name})
                raise exception.MetadefDuplicateProperty(emsg)
    DATA['metadef_properties'].remove(property)

    property.update(values)
    property['updated_at'] = timeutils.utcnow()
    DATA['metadef_properties'].append(property)

    return property


@log_call
def metadef_property_get_all(context, namespace_name):
    """Get a metadef properties list"""
    namespace = metadef_namespace_get(context, namespace_name)

    properties = []

    _check_namespace_visibility(context, namespace, namespace_name)

    for property in DATA['metadef_properties']:
        if property['namespace_id'] == namespace['id']:
            properties.append(property)

    return properties


@log_call
def metadef_property_get_by_id(context, namespace_name, property_id):
    """Get a metadef property"""
    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    for property in DATA['metadef_properties']:
        if (property['namespace_id'] == namespace['id'] and
                property['id'] == property_id):
            return property
    else:
        msg = (_("Metadata definition property not found for id=%s")
               % property_id)
        LOG.warn(msg)
        raise exception.MetadefPropertyNotFound(msg)


@log_call
def metadef_property_get(context, namespace_name, property_name):
    """Get a metadef property"""
    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    for property in DATA['metadef_properties']:
        if (property['namespace_id'] == namespace['id'] and
                property['name'] == property_name):
            return property
    else:
        LOG.debug("No property found with name=%(name)s in"
                  " namespace=%(namespace_name)s ",
                  {'name': property_name, 'namespace_name': namespace_name})
        raise exception.MetadefPropertyNotFound(namespace_name=namespace_name,
                                                property_name=property_name)


@log_call
def metadef_property_delete(context, namespace_name, property_name):
    """Delete a metadef property"""
    global DATA

    property = metadef_property_get(context, namespace_name, property_name)
    DATA['metadef_properties'].remove(property)

    return property


def metadef_property_delete_namespace_content(context, namespace_name,
                                              session=None):
    """Delete a property or raise if it or namespace doesn't exist."""
    return _metadef_delete_namespace_content(
        metadef_property_get_all, 'metadef_properties', context,
        namespace_name)


@log_call
def metadef_resource_type_create(context, values):
    """Create a metadef resource type"""
    global DATA

    resource_type_values = copy.deepcopy(values)
    resource_type_name = resource_type_values['name']

    allowed_attrubites = ['name', 'protected']

    for resource_type in DATA['metadef_resource_types']:
        if resource_type['name'] == resource_type_name:
            raise exception.Duplicate()

    incorrect_keys = set(resource_type_values.keys()) - set(allowed_attrubites)
    if incorrect_keys:
        raise exception.Invalid(
            'The keys %s are not valid' % str(incorrect_keys))

    resource_type = _format_resource_type(resource_type_values)
    DATA['metadef_resource_types'].append(resource_type)

    return resource_type


@log_call
def metadef_resource_type_get_all(context):
    """List all resource types"""
    return DATA['metadef_resource_types']


@log_call
def metadef_resource_type_get(context, resource_type_name):
    """Get a resource type"""
    try:
        resource_type = next(resource_type for resource_type in
                             DATA['metadef_resource_types']
                             if resource_type['name'] ==
                             resource_type_name)
    except StopIteration:
        LOG.debug("No resource type found with name %s", resource_type_name)
        raise exception.MetadefResourceTypeNotFound(
            resource_type_name=resource_type_name)

    return resource_type


@log_call
def metadef_resource_type_association_create(context, namespace_name,
                                             values):
    global DATA

    association_values = copy.deepcopy(values)

    namespace = metadef_namespace_get(context, namespace_name)
    resource_type_name = association_values['name']
    resource_type = metadef_resource_type_get(context,
                                              resource_type_name)

    required_attributes = ['name', 'properties_target', 'prefix']
    allowed_attributes = copy.deepcopy(required_attributes)

    for association in DATA['metadef_namespace_resource_types']:
        if (association['namespace_id'] == namespace['id'] and
                association['resource_type'] == resource_type['id']):
            LOG.debug("The metadata definition resource-type association of"
                      " resource_type=%(resource_type_name)s to"
                      " namespace=%(namespace_name)s, already exists.",
                      {'resource_type_name': resource_type_name,
                       'namespace_name': namespace_name})
            raise exception.MetadefDuplicateResourceTypeAssociation(
                resource_type_name=resource_type_name,
                namespace_name=namespace_name)

    for key in required_attributes:
        if key not in association_values:
            raise exception.Invalid('%s is a required attribute' % key)

    incorrect_keys = set(association_values.keys()) - set(allowed_attributes)
    if incorrect_keys:
        raise exception.Invalid(
            'The keys %s are not valid' % str(incorrect_keys))

    association = _format_association(namespace, resource_type,
                                      association_values)
    DATA['metadef_namespace_resource_types'].append(association)

    return association


@log_call
def metadef_resource_type_association_get(context, namespace_name,
                                          resource_type_name):
    namespace = metadef_namespace_get(context, namespace_name)
    resource_type = metadef_resource_type_get(context, resource_type_name)

    for association in DATA['metadef_namespace_resource_types']:
        if (association['namespace_id'] == namespace['id'] and
                association['resource_type'] == resource_type['id']):
            return association
    else:
        LOG.debug("No resource type association found associated with "
                  "namespace %s and resource type %s", namespace_name,
                  resource_type_name)
        raise exception.MetadefResourceTypeAssociationNotFound(
            resource_type_name=resource_type_name,
            namespace_name=namespace_name)


@log_call
def metadef_resource_type_association_get_all_by_namespace(context,
                                                           namespace_name):
    namespace = metadef_namespace_get(context, namespace_name)

    namespace_resource_types = []
    for resource_type in DATA['metadef_namespace_resource_types']:
        if resource_type['namespace_id'] == namespace['id']:
            namespace_resource_types.append(resource_type)

    return namespace_resource_types


@log_call
def metadef_resource_type_association_delete(context, namespace_name,
                                             resource_type_name):
    global DATA

    resource_type = metadef_resource_type_association_get(context,
                                                          namespace_name,
                                                          resource_type_name)
    DATA['metadef_namespace_resource_types'].remove(resource_type)

    return resource_type


@log_call
def metadef_tag_get(context, namespace_name, name):
    """Get a metadef tag"""
    namespace = metadef_namespace_get(context, namespace_name)
    _check_namespace_visibility(context, namespace, namespace_name)

    for tag in DATA['metadef_tags']:
        if tag['namespace_id'] == namespace['id'] and tag['name'] == name:
            return tag
    else:
        LOG.debug("The metadata definition tag with name=%(name)s"
                  " was not found in namespace=%(namespace_name)s.",
                  {'name': name, 'namespace_name': namespace_name})
        raise exception.MetadefTagNotFound(name=name,
                                           namespace_name=namespace_name)


@log_call
def metadef_tag_get_by_id(context, namespace_name, id):
    """Get a metadef tag"""
    namespace = metadef_namespace_get(context, namespace_name)
    _check_namespace_visibility(context, namespace, namespace_name)

    for tag in DATA['metadef_tags']:
        if tag['namespace_id'] == namespace['id'] and tag['id'] == id:
            return tag
    else:
        msg = (_("Metadata definition tag not found for id=%s") % id)
        LOG.warn(msg)
        raise exception.MetadefTagNotFound(msg)


@log_call
def metadef_tag_get_all(context, namespace_name, filters=None, marker=None,
                        limit=None, sort_key='created_at', sort_dir=None,
                        session=None):
    """Get a metadef tags list"""

    namespace = metadef_namespace_get(context, namespace_name)
    _check_namespace_visibility(context, namespace, namespace_name)

    tags = []
    for tag in DATA['metadef_tags']:
        if tag['namespace_id'] == namespace['id']:
            tags.append(tag)

    return tags


@log_call
@utils.no_4byte_params
def metadef_tag_create(context, namespace_name, values):
    """Create a metadef tag"""
    global DATA

    tag_values = copy.deepcopy(values)
    tag_name = tag_values['name']
    required_attributes = ['name']
    allowed_attributes = ['name']

    namespace = metadef_namespace_get(context, namespace_name)

    for tag in DATA['metadef_tags']:
        if tag['name'] == tag_name and tag['namespace_id'] == namespace['id']:
            LOG.debug("A metadata definition tag with name=%(name)s"
                      " in namespace=%(namespace_name)s already exists.",
                      {'name': tag_name, 'namespace_name': namespace_name})
            raise exception.MetadefDuplicateTag(
                name=tag_name, namespace_name=namespace_name)

    for key in required_attributes:
        if key not in tag_values:
            raise exception.Invalid('%s is a required attribute' % key)

    incorrect_keys = set(tag_values.keys()) - set(allowed_attributes)
    if incorrect_keys:
        raise exception.Invalid(
            'The keys %s are not valid' % str(incorrect_keys))

    tag_values['namespace_id'] = namespace['id']

    _check_namespace_visibility(context, namespace, namespace_name)

    tag = _format_tag(tag_values)
    DATA['metadef_tags'].append(tag)
    return tag


@log_call
def metadef_tag_create_tags(context, namespace_name, tag_list):
    """Create a metadef tag"""
    global DATA

    namespace = metadef_namespace_get(context, namespace_name)
    _check_namespace_visibility(context, namespace, namespace_name)

    required_attributes = ['name']
    allowed_attributes = ['name']
    data_tag_list = []
    tag_name_list = []
    for tag_value in tag_list:
        tag_values = copy.deepcopy(tag_value)
        tag_name = tag_values['name']

        for key in required_attributes:
            if key not in tag_values:
                raise exception.Invalid('%s is a required attribute' % key)

        incorrect_keys = set(tag_values.keys()) - set(allowed_attributes)
        if incorrect_keys:
            raise exception.Invalid(
                'The keys %s are not valid' % str(incorrect_keys))

        if tag_name in tag_name_list:
            LOG.debug("A metadata definition tag with name=%(name)s"
                      " in namespace=%(namespace_name)s already exists.",
                      {'name': tag_name, 'namespace_name': namespace_name})
            raise exception.MetadefDuplicateTag(
                name=tag_name, namespace_name=namespace_name)
        else:
            tag_name_list.append(tag_name)

        tag_values['namespace_id'] = namespace['id']
        data_tag_list.append(_format_tag(tag_values))

    DATA['metadef_tags'] = []
    for tag in data_tag_list:
        DATA['metadef_tags'].append(tag)

    return data_tag_list


@log_call
@utils.no_4byte_params
def metadef_tag_update(context, namespace_name, id, values):
    """Update a metadef tag"""
    global DATA

    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    tag = metadef_tag_get_by_id(context, namespace_name, id)
    if tag['name'] != values['name']:
        for db_tag in DATA['metadef_tags']:
            if (db_tag['name'] == values['name'] and
                    db_tag['namespace_id'] == namespace['id']):
                LOG.debug("Invalid update. It would result in a duplicate"
                          " metadata definition tag with same name=%(name)s "
                          " in namespace=%(namespace_name)s.",
                          {'name': tag['name'],
                           'namespace_name': namespace_name})
                raise exception.MetadefDuplicateTag(
                    name=tag['name'], namespace_name=namespace_name)

    DATA['metadef_tags'].remove(tag)

    tag.update(values)
    tag['updated_at'] = timeutils.utcnow()
    DATA['metadef_tags'].append(tag)
    return tag


@log_call
def metadef_tag_delete(context, namespace_name, name):
    """Delete a metadef tag"""
    global DATA

    tags = metadef_tag_get(context, namespace_name, name)
    DATA['metadef_tags'].remove(tags)

    return tags


def metadef_tag_delete_namespace_content(context, namespace_name,
                                         session=None):
    """Delete an tag or raise if namespace or tag doesn't exist."""
    return _metadef_delete_namespace_content(
        metadef_tag_get_all, 'metadef_tags', context, namespace_name)


@log_call
def metadef_tag_count(context, namespace_name):
    """Get metadef tag count in a namespace"""
    namespace = metadef_namespace_get(context, namespace_name)

    _check_namespace_visibility(context, namespace, namespace_name)

    count = 0
    for tag in DATA['metadef_tags']:
        if tag['namespace_id'] == namespace['id']:
            count = count + 1

    return count


def _format_association(namespace, resource_type, association_values):
    association = {
        'namespace_id': namespace['id'],
        'resource_type': resource_type['id'],
        'properties_target': None,
        'prefix': None,
        'created_at': timeutils.utcnow(),
        'updated_at': timeutils.utcnow()

    }
    association.update(association_values)
    return association


def _format_resource_type(values):
    dt = timeutils.utcnow()
    resource_type = {
        'id': _get_metadef_id(),
        'name': values['name'],
        'protected': True,
        'created_at': dt,
        'updated_at': dt
    }
    resource_type.update(values)
    return resource_type


def _format_property(values):
    property = {
        'id': _get_metadef_id(),
        'namespace_id': None,
        'name': None,
        'json_schema': None
    }
    property.update(values)
    return property


def _format_namespace(values):
    dt = timeutils.utcnow()
    namespace = {
        'id': _get_metadef_id(),
        'namespace': None,
        'display_name': None,
        'description': None,
        'visibility': 'private',
        'protected': False,
        'owner': None,
        'created_at': dt,
        'updated_at': dt
    }
    namespace.update(values)
    return namespace


def _format_object(values):
    dt = timeutils.utcnow()
    object = {
        'id': _get_metadef_id(),
        'namespace_id': None,
        'name': None,
        'description': None,
        'json_schema': None,
        'required': None,
        'created_at': dt,
        'updated_at': dt
    }
    object.update(values)
    return object


def _format_tag(values):
    dt = timeutils.utcnow()
    tag = {
        'id': _get_metadef_id(),
        'namespace_id': None,
        'name': None,
        'created_at': dt,
        'updated_at': dt
    }
    tag.update(values)
    return tag


def _is_namespace_visible(context, namespace):
    """Return true if namespace is visible in this context"""
    if context.is_admin:
        return True

    if namespace.get('visibility', '') == 'public':
        return True

    if namespace['owner'] is None:
        return True

    if context.owner is not None:
        if context.owner == namespace['owner']:
            return True

    return False


def _check_namespace_visibility(context, namespace, namespace_name):
    if not _is_namespace_visible(context, namespace):
        LOG.debug("Forbidding request, metadata definition namespace=%s "
                  "is not visible.", namespace_name)
        emsg = _("Forbidding request, metadata definition namespace=%s"
                 " is not visible.") % namespace_name
        raise exception.MetadefForbidden(emsg)


def _get_metadef_id():
    global INDEX
    INDEX += 1
    return INDEX
