# Copyright (c) 2013 OpenStack Foundation
# 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 neutron_lib.api.definitions import portbindings
from neutron_lib import constants
from neutron_lib.plugins.ml2 import api
from oslo_log import log
from oslo_serialization import jsonutils
import sqlalchemy

from neutron.db import segments_db

LOG = log.getLogger(__name__)


class InstanceSnapshot(object):
    """Used to avoid holding references to DB objects in PortContext."""
    def __init__(self, obj):
        self._model_class = obj.__class__
        self._identity_key = sqlalchemy.orm.util.identity_key(instance=obj)[1]
        self._cols = [col.key
                      for col in sqlalchemy.inspect(self._model_class).columns]
        for col in self._cols:
            setattr(self, col, getattr(obj, col))

    def persist_state_to_session(self, session):
        """Updates the state of the snapshot in the session.

        Finds the SQLA object in the session if it exists or creates a new
        object and updates the object with the column values stored in this
        snapshot.
        """
        db_obj = session.query(self._model_class).get(self._identity_key)
        if db_obj:
            for col in self._cols:
                setattr(db_obj, col, getattr(self, col))
        else:
            session.add(self._model_class(**{col: getattr(self, col)
                                             for col in self._cols}))


class MechanismDriverContext(object):
    """MechanismDriver context base class."""
    def __init__(self, plugin, plugin_context):
        self._plugin = plugin
        # This temporarily creates a reference loop, but the
        # lifetime of PortContext is limited to a single
        # method call of the plugin.
        self._plugin_context = plugin_context


class NetworkContext(MechanismDriverContext, api.NetworkContext):

    def __init__(self, plugin, plugin_context, network,
                 original_network=None, segments=None):
        super(NetworkContext, self).__init__(plugin, plugin_context)
        self._network = network
        self._original_network = original_network
        self._segments = segments_db.get_network_segments(
            plugin_context, network['id']) if segments is None else segments

    @property
    def current(self):
        return self._network

    @property
    def original(self):
        return self._original_network

    @property
    def network_segments(self):
        return self._segments


class SubnetContext(MechanismDriverContext, api.SubnetContext):

    def __init__(self, plugin, plugin_context, subnet, network,
                 original_subnet=None):
        super(SubnetContext, self).__init__(plugin, plugin_context)
        self._subnet = subnet
        self._original_subnet = original_subnet
        self._network_context = NetworkContext(plugin, plugin_context,
                                               network) if network else None

    @property
    def current(self):
        return self._subnet

    @property
    def original(self):
        return self._original_subnet

    @property
    def network(self):
        if self._network_context is None:
            network = self._plugin.get_network(
                self._plugin_context, self.current['network_id'])
            self._network_context = NetworkContext(
                self._plugin, self._plugin_context, network)
        return self._network_context


class PortContext(MechanismDriverContext, api.PortContext):

    def __init__(self, plugin, plugin_context, port, network, binding,
                 binding_levels, original_port=None):
        super(PortContext, self).__init__(plugin, plugin_context)
        self._port = port
        self._original_port = original_port
        if isinstance(network, NetworkContext):
            self._network_context = network
        else:
            self._network_context = NetworkContext(
                plugin, plugin_context, network) if network else None
        # NOTE(kevinbenton): InstanceSnapshot can go away once we are working
        # with OVO objects instead of native SQLA objects.
        self._binding = InstanceSnapshot(binding)
        self._binding_levels = [InstanceSnapshot(l)
                                for l in (binding_levels or [])]
        self._segments_to_bind = None
        self._new_bound_segment = None
        self._next_segments_to_bind = None
        if original_port:
            self._original_vif_type = binding.vif_type
            self._original_vif_details = self._plugin._get_vif_details(binding)
            self._original_binding_levels = self._binding_levels
        else:
            self._original_vif_type = None
            self._original_vif_details = None
            self._original_binding_levels = None
        self._new_port_status = None

    # The following methods are for use by the ML2 plugin and are not
    # part of the driver API.

    def _prepare_to_bind(self, segments_to_bind):
        self._segments_to_bind = segments_to_bind
        self._new_bound_segment = None
        self._next_segments_to_bind = None

    def _clear_binding_levels(self):
        self._binding_levels = []

    def _push_binding_level(self, binding_level):
        self._binding_levels.append(InstanceSnapshot(binding_level))

    def _pop_binding_level(self):
        return self._binding_levels.pop()

    # The following implement the abstract methods and properties of
    # the driver API.

    @property
    def current(self):
        return self._port

    @property
    def original(self):
        return self._original_port

    @property
    def status(self):
        # REVISIT(rkukura): Eliminate special DVR case as part of
        # resolving bug 1367391?
        if self._port['device_owner'] == constants.DEVICE_OWNER_DVR_INTERFACE:
            return self._binding.status

        return self._port['status']

    @property
    def original_status(self):
        # REVISIT(rkukura): Should return host-specific status for DVR
        # ports. Fix as part of resolving bug 1367391.
        if self._original_port:
            return self._original_port['status']

    @property
    def network(self):
        if not self._network_context:
            network = self._plugin.get_network(
                self._plugin_context, self.current['network_id'])
            self._network_context = NetworkContext(
                self._plugin, self._plugin_context, network)
        return self._network_context

    @property
    def binding_levels(self):
        if self._binding_levels:
            return [{
                api.BOUND_DRIVER: level.driver,
                api.BOUND_SEGMENT: self._expand_segment(level.segment_id)
            } for level in self._binding_levels]

    @property
    def original_binding_levels(self):
        if self._original_binding_levels:
            return [{
                api.BOUND_DRIVER: level.driver,
                api.BOUND_SEGMENT: self._expand_segment(level.segment_id)
            } for level in self._original_binding_levels]

    @property
    def top_bound_segment(self):
        if self._binding_levels:
            return self._expand_segment(self._binding_levels[0].segment_id)

    @property
    def original_top_bound_segment(self):
        if self._original_binding_levels:
            return self._expand_segment(
                self._original_binding_levels[0].segment_id)

    @property
    def bottom_bound_segment(self):
        if self._binding_levels:
            return self._expand_segment(self._binding_levels[-1].segment_id)

    @property
    def original_bottom_bound_segment(self):
        if self._original_binding_levels:
            return self._expand_segment(
                self._original_binding_levels[-1].segment_id)

    def _expand_segment(self, segment_id):
        for s in self.network.network_segments:
            if s['id'] == segment_id:
                return s
        # TODO(kevinbenton): eliminate the query below. The above should
        # always return since the port is bound to a network segment. Leaving
        # in for now for minimally invasive change for back-port.
        segment = segments_db.get_segment_by_id(self._plugin_context,
                                                segment_id)
        if not segment:
            LOG.warning("Could not expand segment %s", segment_id)
        return segment

    @property
    def host(self):
        # REVISIT(rkukura): Eliminate special DVR case as part of
        # resolving bug 1367391?
        if self._port['device_owner'] == constants.DEVICE_OWNER_DVR_INTERFACE:
            return self._binding.host

        return self._port.get(portbindings.HOST_ID)

    @property
    def original_host(self):
        # REVISIT(rkukura): Eliminate special DVR case as part of
        # resolving bug 1367391?
        if self._port['device_owner'] == constants.DEVICE_OWNER_DVR_INTERFACE:
            return self._original_port and self._binding.host
        else:
            return (self._original_port and
                    self._original_port.get(portbindings.HOST_ID))

    @property
    def vif_type(self):
        return self._binding.vif_type

    @property
    def original_vif_type(self):
        return self._original_vif_type

    @property
    def vif_details(self):
        return self._plugin._get_vif_details(self._binding)

    @property
    def original_vif_details(self):
        return self._original_vif_details

    @property
    def segments_to_bind(self):
        return self._segments_to_bind

    def host_agents(self, agent_type):
        return self._plugin.get_agents(self._plugin_context,
                                       filters={'agent_type': [agent_type],
                                                'host': [self._binding.host]})

    def set_binding(self, segment_id, vif_type, vif_details,
                    status=None):
        # TODO(rkukura) Verify binding allowed, segment in network
        self._new_bound_segment = segment_id
        self._binding.vif_type = vif_type
        self._binding.vif_details = jsonutils.dumps(vif_details)
        self._new_port_status = status

    def continue_binding(self, segment_id, next_segments_to_bind):
        # TODO(rkukura) Verify binding allowed, segment in network
        self._new_bound_segment = segment_id
        self._next_segments_to_bind = next_segments_to_bind

    def allocate_dynamic_segment(self, segment):
        network_id = self._network_context.current['id']

        return self._plugin.type_manager.allocate_dynamic_segment(
                self._plugin_context, network_id, segment)

    def release_dynamic_segment(self, segment_id):
        return self._plugin.type_manager.release_dynamic_segment(
                self._plugin_context, segment_id)
