# Copyright 2011 OpenStack Foundation
# Copyright 2012 RedHat Inc.
# Copyright 2018 Verizon Wireless
# 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.

"""Base class for all storage backends"""

from functools import wraps
import logging

from oslo_config import cfg
from oslo_utils import encodeutils
from oslo_utils import importutils
from oslo_utils import units

from glance_store import capabilities
from glance_store import exceptions
from glance_store.i18n import _

LOG = logging.getLogger(__name__)


_MULTI_BACKEND_OPTS = [
    cfg.StrOpt('store_description',
               help=_("""
This option will be used to provide a constructive information about
the store backend to end users. Using /v2/stores-info call user can
seek more information on all available backends.

""")),
    cfg.IntOpt('weight',
               help=_("""
This option is used to define a relative weight for this store over
any others that are configured. The actual value of the weight is meaningless
and only serves to provide a "sort order" compared to others. Any stores
with the same weight will be treated as equivalent.
"""),
               default=0),
]


class Store(capabilities.StoreCapability):

    OPTIONS = None
    MULTI_BACKEND_OPTIONS = _MULTI_BACKEND_OPTS
    READ_CHUNKSIZE = 4 * units.Mi  # 4M
    WRITE_CHUNKSIZE = READ_CHUNKSIZE

    def __init__(self, conf, backend=None):
        """
        Initialize the Store
        """

        super(Store, self).__init__()

        self.conf = conf
        self.backend_group = backend
        self.store_location_class = None
        self._url_prefix = None

        try:
            if self.OPTIONS is not None:
                group = 'glance_store'
                if self.backend_group:
                    group = self.backend_group
                    if self.MULTI_BACKEND_OPTIONS is not None:
                        self.conf.register_opts(
                            self.MULTI_BACKEND_OPTIONS, group=group)

                self.conf.register_opts(self.OPTIONS, group=group)
        except cfg.DuplicateOptError:
            pass

    @property
    def url_prefix(self):
        return self._url_prefix

    @property
    def weight(self):
        if self.backend_group is None:
            # NOTE(danms): A backend with no config group can not have a
            # weight set, so just return the default
            return 0
        else:
            return getattr(self.conf, self.backend_group).weight

    def configure(self, re_raise_bsc=False):
        """
        Configure the store to use the stored configuration options
        and initialize capabilities based on current configuration.

        Any store that needs special configuration should implement
        this method.
        """

        try:
            self.configure_add()
        except exceptions.BadStoreConfiguration as e:
            self.unset_capabilities(capabilities.BitMasks.WRITE_ACCESS)
            msg = (_("Failed to configure store correctly: %s "
                     "Disabling add method.")
                   % encodeutils.exception_to_unicode(e))
            LOG.warning(msg)
            if re_raise_bsc:
                raise
        finally:
            self.update_capabilities()

    def get_schemes(self):
        """
        Returns a tuple of schemes which this store can handle.
        """
        raise NotImplementedError

    def get_store_location_class(self):
        """
        Returns the store location class that is used by this store.
        """
        if not self.store_location_class:
            class_name = "%s.StoreLocation" % (self.__module__)
            LOG.debug("Late loading location class %s", class_name)
            self.store_location_class = importutils.import_class(class_name)
        return self.store_location_class

    def configure_add(self):
        """
        This is like `configure` except that it's specifically for
        configuring the store to accept objects.

        If the store was not able to successfully configure
        itself, it should raise `exceptions.BadStoreConfiguration`.
        """
        # NOTE(flaper87): This should probably go away

    @capabilities.check
    def get(self, location, offset=0, chunk_size=None, context=None):
        """
        Takes a `glance_store.location.Location` object that indicates
        where to find the image file, and returns a tuple of generator
        (for reading the image file) and image_size

        :param location: `glance_store.location.Location` object, supplied
                        from glance_store.location.get_location_from_uri()
        :raises: `glance.exceptions.NotFound` if image does not exist
        """
        raise NotImplementedError

    def get_size(self, location, context=None):
        """
        Takes a `glance_store.location.Location` object that indicates
        where to find the image file, and returns the size

        :param location: `glance_store.location.Location` object, supplied
                        from glance_store.location.get_location_from_uri()
        :raises: `glance_store.exceptions.NotFound` if image does not exist
        """
        raise NotImplementedError

    # NOTE(rosmaita): use the @glance_store.driver.back_compat_add
    # annotation on implementions for backward compatibility with
    # pre-0.26.0 add().  Need backcompat because pre-0.26.0 returned
    # a 4 tuple, this returns a 5-tuple
    @capabilities.check
    def add(self, image_id, image_file, image_size, hashing_algo,
            context=None, verifier=None):
        """
        Stores an image file with supplied identifier to the backend
        storage system and returns a tuple containing information
        about the stored image.

        :param image_id: The opaque image identifier
        :param image_file: The image data to write, as a file-like object
        :param image_size: The size of the image data to write, in bytes
        :param hashing_algo: A hashlib algorithm identifier (string)
        :param context: A context object
        :param verifier: An object used to verify signatures for images

        :returns: tuple of: (1) URL in backing store, (2) bytes written,
                  (3) checksum, (4) multihash value, and (5) a dictionary
                  with storage system specific information
        :raises: `glance_store.exceptions.Duplicate` if the image already
                 exists
        """
        raise NotImplementedError

    @capabilities.check
    def delete(self, location, context=None):
        """
        Takes a `glance_store.location.Location` object that indicates
        where to find the image file to delete

        :param location: `glance_store.location.Location` object, supplied
                  from glance_store.location.get_location_from_uri()
        :raises: `glance_store.exceptions.NotFound` if image does not exist
        """
        raise NotImplementedError

    def set_acls(self, location, public=False, read_tenants=None,
                 write_tenants=None, context=None):
        """
        Sets the read and write access control list for an image in the
        backend store.

        :param location: `glance_store.location.Location` object, supplied
                  from glance_store.location.get_location_from_uri()
        :param public: A boolean indicating whether the image should be public.
        :param read_tenants: A list of tenant strings which should be granted
                      read access for an image.
        :param write_tenants: A list of tenant strings which should be granted
                      write access for an image.
        """
        raise NotImplementedError


def back_compat_add(store_add_fun):
    """
    Provides backward compatibility for the 0.26.0+ Store.add() function.
    In 0.26.0, the 'hashing_algo' parameter is introduced and Store.add()
    returns a 5-tuple containing a computed 'multihash' value.

    This wrapper behaves as follows:

    If no hashing_algo identifier is supplied as an argument, the response
    is the pre-0.26.0 4-tuple of::

    (backend_url, bytes_written, checksum, metadata_dict)

    If a hashing_algo is supplied, the response is a 5-tuple::

    (backend_url, bytes_written, checksum, multihash, metadata_dict)

    The wrapper detects the presence of a 'hashing_algo' argument both
    by examining named arguments and positionally.
    """

    @wraps(store_add_fun)
    def add_adapter(*args, **kwargs):
        """
        Wrapper for the store 'add' function.  If no hashing_algo identifier
        is supplied, the response is the pre-0.25.0 4-tuple of::

        (backend_url, bytes_written, checksum, metadata_dict)

        If a hashing_algo is supplied, the response is a 5-tuple::

        (backend_url, bytes_written, checksum, multihash, metadata_dict)
        """
        # strategy: assume this until we determine otherwise
        back_compat_required = True

        # specify info about 0.26.0 Store.add() call (can't introspect
        # this because the add method is wrapped by the capabilities
        # check)
        p_algo = 4
        max_args = 7

        num_args = len(args)
        num_kwargs = len(kwargs)

        if num_args + num_kwargs == max_args:
            # everything is present, including hashing_algo
            back_compat_required = False
        elif ('hashing_algo' in kwargs or
              (num_args >= p_algo + 1 and isinstance(args[p_algo], str))):
            # there is a hashing_algo argument present
            back_compat_required = False
        else:
            # this is a pre-0.26.0-style call, so let's figure out
            # whether to insert the hashing_algo in the args or kwargs
            if kwargs and 'image_' in ''.join(kwargs):
                # if any of the image_* is named, everything after it
                # must be named as well, so slap the algo into kwargs
                kwargs['hashing_algo'] = 'md5'
            else:
                args = args[:p_algo] + ('md5',) + args[p_algo:]

        # business time
        (backend_url,
         bytes_written,
         checksum,
         multihash,
         metadata_dict) = store_add_fun(*args, **kwargs)

        if back_compat_required:
            return (backend_url, bytes_written, checksum, metadata_dict)

        return (backend_url, bytes_written, checksum, multihash,
                metadata_dict)

    return add_adapter
