# Copyright (c) 2014 Thales Services SAS
# 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.

from oslo.db import exception as db_exc

from neutron.common import exceptions as exc
from neutron.openstack.common import log
from neutron.plugins.ml2 import driver_api as api


# Number of retries to find a valid segment candidate and allocate it
DB_MAX_RETRIES = 10


LOG = log.getLogger(__name__)


class TypeDriverHelper(api.TypeDriver):
    """TypeDriver Helper for segment allocation.

    Provide methods helping to perform segment allocation fully or partially
    specified.
    """

    def __init__(self, model):
        self.model = model
        self.primary_keys = set(dict(model.__table__.columns))
        self.primary_keys.remove("allocated")

    def allocate_fully_specified_segment(self, session, **raw_segment):
        """Allocate segment fully specified by raw_segment.

        If segment exists, then try to allocate it and return db object
        If segment does not exists, then try to create it and return db object
        If allocation/creation failed, then return None
        """

        network_type = self.get_type()
        try:
            with session.begin(subtransactions=True):
                alloc = (session.query(self.model).filter_by(**raw_segment).
                         first())
                if alloc:
                    if alloc.allocated:
                        # Segment already allocated
                        return
                    else:
                        # Segment not allocated
                        LOG.debug("%(type)s segment %(segment)s allocate "
                                  "started ",
                                  {"type": network_type,
                                   "segment": raw_segment})
                        count = (session.query(self.model).
                                 filter_by(allocated=False, **raw_segment).
                                 update({"allocated": True}))
                        if count:
                            LOG.debug("%(type)s segment %(segment)s allocate "
                                      "done ",
                                  {"type": network_type,
                                   "segment": raw_segment})
                            return alloc

                        # Segment allocated or deleted since select
                        LOG.debug("%(type)s segment %(segment)s allocate "
                                  "failed: segment has been allocated or "
                                  "deleted",
                                  {"type": network_type,
                                   "segment": raw_segment})

                # Segment to create or already allocated
                LOG.debug("%(type)s segment %(segment)s create started",
                          {"type": network_type, "segment": raw_segment})
                alloc = self.model(allocated=True, **raw_segment)
                alloc.save(session)
                LOG.debug("%(type)s segment %(segment)s create done",
                          {"type": network_type, "segment": raw_segment})

        except db_exc.DBDuplicateEntry:
            # Segment already allocated (insert failure)
            alloc = None
            LOG.debug("%(type)s segment %(segment)s create failed",
                      {"type": network_type, "segment": raw_segment})

        return alloc

    def allocate_partially_specified_segment(self, session, **filters):
        """Allocate model segment from pool partially specified by filters.

        Return allocated db object or None.
        """

        network_type = self.get_type()
        with session.begin(subtransactions=True):
            select = (session.query(self.model).
                      filter_by(allocated=False, **filters))

            # Selected segment can be allocated before update by someone else,
            # We retry until update success or DB_MAX_RETRIES retries
            for attempt in range(1, DB_MAX_RETRIES + 1):
                alloc = select.first()

                if not alloc:
                    # No resource available
                    return

                raw_segment = dict((k, alloc[k]) for k in self.primary_keys)
                LOG.debug("%(type)s segment allocate from pool, attempt "
                          "%(attempt)s started with %(segment)s ",
                          {"type": network_type, "attempt": attempt,
                           "segment": raw_segment})
                count = (session.query(self.model).
                         filter_by(allocated=False, **raw_segment).
                         update({"allocated": True}))
                if count:
                    LOG.debug("%(type)s segment allocate from pool, attempt "
                              "%(attempt)s success with %(segment)s ",
                              {"type": network_type, "attempt": attempt,
                               "segment": raw_segment})
                    return alloc

                # Segment allocated since select
                LOG.debug("Allocate %(type)s segment from pool, "
                          "attempt %(attempt)s failed with segment "
                          "%(segment)s",
                          {"type": network_type, "attempt": attempt,
                           "segment": raw_segment})

        LOG.warning(_("Allocate %(type)s segment from pool failed "
                      "after %(number)s failed attempts"),
                    {"type": network_type, "number": DB_MAX_RETRIES})
        raise exc.NoNetworkFoundInMaximumAllowedAttempts()
