# Copyright 2014
# The Cloudscaling Group, Inc.
#
# 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 classes of GCE API conversion layer.

Classes in this layer aggregate functionality of OpenStack necessary
and sufficient to handle supported GCE API requests
"""

from oslo_config import cfg
from oslo_utils import timeutils

from gceapi import db
from gceapi import exception

FLAGS = cfg.CONF


class Singleton(type):
    """Singleton metaclass.

    KIND must be overridden in classes based on this type.
    """
    _instances = {}
    KIND = ""

    def __call__(self, *args, **kwargs):
        if not self.KIND:
            raise NotImplementedError
        if self.KIND not in self._instances:
            singleton = super(Singleton, self).__call__(*args, **kwargs)
            self._instances[self.KIND] = singleton
        return self._instances[self.KIND]

    @classmethod
    def get_instance(cls, kind):
        """Get singleton by name."""

        return cls._instances.get(kind)


class NetSingleton(Singleton):
    """Proxy loader for net depended API.

    NEUTRON_API_MODULE and NOVA_API_MODULE must be overridden in classes
    based on this type.
    """

    NEUTRON_API_MODULE = None
    NOVA_API_MODULE = None

    def __call__(self):
        net_api = FLAGS.get("network_api")
        # NOTE(Alex): Initializing proper network singleton
        if net_api is None or ("quantum" in net_api
                               or "neutron" in net_api):
            return self.NEUTRON_API_MODULE.API()
        else:
            return self.NOVA_API_MODULE.API()


class API(object):
    """Base GCE API abstraction class

    Inherited classes should implement one class of GCE API functionality.
    There should be enough public methods implemented to cover necessary
    methods of GCE API in the class. Other public methods can exist to be
    invoked from other APIs of this layer.
    Class in this layer should use each others functionality instead of
    calling corresponding low-level routines.
    Basic methods should be named including "item(s)" instead of specific
    functional names.

    Descendants are stateless singletons.
    Supports callbacks for interaction of APIs in this layer
    """
    # TODO(Alex): Now action methods get body of parameters straight from GCE
    # request while returning results in terms of Openstack to be converted
    # to GCE terms in controller. In next version this layer should be revised
    # to work symmetrically with incoming and outgoing data.

    __metaclass__ = Singleton

    def __init__(self, *args, **kwargs):
        super(API, self).__init__(*args, **kwargs)
        self._callbacks = []

    def _get_type(self):
        """GCE API object type method. Should be overridden."""

        raise NotImplementedError

    def _get_persistent_attributes(self):
        """Iterable of name of columns stored in GCE API database.

        Should be overridden.
        """

        raise NotImplementedError

    def get_item(self, context, name, scope=None):
        """Returns fully filled item for particular inherited API."""

        raise exception.NotFound

    def get_items(self, context, scope=None):
        """Returns list of items."""

        return []

    def delete_item(self, context, name, scope=None):
        """Deletes an item."""

        raise exception.NotFound

    def add_item(self, context, name, body, scope=None):
        """Creates an item. It returns created item."""

        raise exception.NotFound

    def get_scopes(self, context, item):
        """Returns which zones/regions the item belongs too."""

        return []

    def _process_callbacks(self, context, reason, item, **kwargs):
        for cb_reason, cb_func in self._callbacks:
            if cb_reason == reason:
                cb_func(context, item, **kwargs)

    def _register_callback(self, reason, func):
        """Callbacks registration

        Callbacks can be registered by one API to be called by another before
        some action for checking possibility of the action or to process
        pre-actions
        """

        self._callbacks.append((reason, func))

    @classmethod
    def _get_complex_operation_progress(cls, context, item_id):
        return None

    def _prepare_item(self, item, db_item):
        if db_item is not None:
            item.update(db_item)
        return item

    def _add_db_item(self, context, item):
        db_item = dict((key, item.get(key))
                   for key in self._get_persistent_attributes()
                   if key in item)
        if ("creationTimestamp" in self._get_persistent_attributes() and
                "creationTimestamp" not in db_item):
            # TODO(ft): Google doesn't return microseconds but returns
            # server time zone: 2013-12-06T03:34:31.340-08:00
            utcnow = timeutils.isotime(None, True)
            db_item["creationTimestamp"] = utcnow
            item["creationTimestamp"] = utcnow
        db.add_item(context, self._get_type(), db_item)
        return item

    def _delete_db_item(self, context, item):
        return db.delete_item(context, self._get_type(), item["id"])

    def _update_db_item(self, context, item):
        db_item = dict((key, item.get(key))
                   for key in self._get_persistent_attributes()
                   if key in item)
        db.update_item(context, self._get_type(), db_item)

    def _get_db_items(self, context):
        return db.get_items(context, self._get_type())

    def _get_db_items_dict(self, context):
        return dict((item["id"], item) for item in self._get_db_items(context))

    def _get_db_item_by_id(self, context, item_id):
        return db.get_item_by_id(context, self._get_type(), item_id)

    def _get_db_item_by_name(self, context, name):
        return db.get_item_by_name(context, self._get_type(), name)

    def _purge_db(self, context, os_items, db_items_dict):
        only_os_items = []
        existed_db_items = set()
        for item in os_items:
            db_item = db_items_dict.get(str(item["id"]))
            if db_item is None:
                only_os_items.append(item)
            else:
                existed_db_items.add(db_item["id"])
        for item in db_items_dict.itervalues():
            if item["id"] not in existed_db_items:
                self._delete_db_item(context, item)
        return only_os_items

    @staticmethod
    def _from_gce(name):
        return name.replace("-", ".")

    @staticmethod
    def _to_gce(name):
        return name.replace(".", "-")


class _CallbackReasons(object):
    check_delete = 1
    pre_delete = 2
    post_add = 3


_callback_reasons = _CallbackReasons()