# Copyright (c) 2014 Rackspace Hosting
#
# 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 ryu.lib import dpid as dpidlib
from ryu.services.protocols.ovsdb import event as ovsdb_event

import six

import uuid


def _get_table_row(table, attr_name, attr_value, tables):
    sentinel = object()

    for row in tables[table].rows.values():
        if getattr(row, attr_name, sentinel) == attr_value:
            return row


def _get_controller(tables, attr_val, attr_name='target'):
    return _get_table_row('Controller', attr_name, attr_val, tables=tables)


def _get_bridge(tables, attr_val, attr_name='name'):
    return _get_table_row('Bridge', attr_name, attr_val, tables=tables)


def _get_port(tables, attr_val, attr_name='name'):
    return _get_table_row('Port', attr_name, attr_val, tables=tables)


def _get_iface(tables, attr_val, attr_name='name'):
    return _get_table_row('Interface', attr_name, attr_val, tables=tables)


def match_row(manager, system_id, table, fn):
    def _match_row(tables):
        return next((r for r in tables[table].rows.values()
                     if fn(r)), None)

    request_to_get_tables = ovsdb_event.EventReadRequest(system_id,
                                                         _match_row)
    reply_to_get_tables = manager.send_request(request_to_get_tables)
    return reply_to_get_tables.result


def match_rows(manager, system_id, table, fn):
    def _match_rows(tables):
        return (r for r in tables[table].rows.values() if fn(r))

    request = ovsdb_event.EventReadRequest(system_id, _match_rows)
    reply = manager.send_request(request)
    return reply.result


def row_by_name(manager, system_id, name, table='Bridge', fn=None):
    matched_row = match_row(manager, system_id, table,
                            lambda row: row.name == name)

    if fn is not None:
        return fn(matched_row)

    return matched_row


def rows_by_external_id(manager, system_id, key, value,
                        table='Bridge', fn=None):
    matched_rows = match_rows(manager, system_id, table,
                              lambda r: (key in r.external_ids and
                                         r.external_ids.get(key) == value))

    if matched_rows and fn is not None:
        return [fn(row) for row in matched_rows]

    return matched_rows


def rows_by_other_config(manager, system_id, key, value,
                         table='Bridge', fn=None):
    matched_rows = match_rows(manager, system_id, table,
                              lambda r: (key in r.other_config and
                                         r.other_config.get(key) == value))

    if matched_rows and fn is not None:
        return [fn(row) for row in matched_rows]

    return matched_rows


def get_column_value(manager, table, record, column):
    """
    Example : To get datapath_id from Bridge table
    get_column_value('Bridge', <bridge name>, 'datapath_id').strip('"')
    """
    row = row_by_name(manager, record, table)
    value = getattr(row, column, "")

    if isinstance(value, list) and len(value) == 1:
        value = value[0]

    return str(value)


def get_iface_by_name(manager, system_id, name, fn=None):
    iface = row_by_name(manager, system_id, name, 'Interface')

    if fn is not None:
        return fn(iface)

    return iface


def get_ifaces_by_external_id(manager, system_id, key, value, fn=None):
    return rows_by_external_id(manager, system_id, key, value,
                               'Interface', fn)


def get_ifaces_by_other_config(manager, system_id, key, value, fn=None):
    return rows_by_other_config(manager, system_id, key, value,
                                'Interface', fn)


def get_port_by_name(manager, system_id, name, fn=None):
    port = row_by_name(manager, system_id, name, 'Port')

    if fn is not None:
        return fn(port)

    return port


def get_bridge_for_iface_name(manager, system_id, iface_name, fn=None):
    iface = row_by_name(manager, system_id, iface_name, 'Interface')
    port = match_row(manager, system_id, 'Port',
                     lambda x: iface in x.interfaces)
    bridge = match_row(manager, system_id, 'Bridge',
                       lambda x: port in x.ports)

    if fn is not None:
        return fn(bridge)

    return bridge


def get_table(manager, system_id, name):
    def _get_table(tables):
        return tables[name]

    request_to_get_tables = ovsdb_event.EventReadRequest(system_id,
                                                         _get_table)
    reply_to_get_tables = manager.send_request(request_to_get_tables)
    return reply_to_get_tables.result


def get_bridge_by_datapath_id(manager, system_id, datapath_id, fn=None):
    def _match_fn(row):
        row_dpid = dpidlib.str_to_dpid(str(row.datapath_id[0]))
        return row_dpid == datapath_id

    bridge = match_row(manager, system_id, 'Bridge', _match_fn)

    if fn is not None:
        return fn(bridge)

    return bridge


def get_datapath_ids_for_systemd_id(manager, system_id):
    def _get_dp_ids(tables):
        dp_ids = []

        bridges = tables.get('Bridge')

        if not bridges:
            return dp_ids

        for bridge in bridges.rows.values():
            datapath_ids = bridge.datapath_id
            dp_ids.extend(dpidlib.str_to_dpid(dp_id) for dp_id in datapath_ids)

        return dp_ids

    request = ovsdb_event.EventReadRequest(system_id, _get_dp_ids)
    reply = manager.send_request(request)
    return reply.result


def get_system_id_for_datapath_id(manager, datapath_id):
    def _get_dp_ids(tables):
        bridges = tables.get('Bridge')

        if not bridges:
            return None

        for bridge in bridges.rows.values():
            datapath_ids = [dpidlib.str_to_dpid(dp_id)
                            for dp_id in bridge.datapath_id]

            if datapath_id in datapath_ids:
                openvswitch = tables['Open_vSwitch'].rows

                if openvswitch:
                    row = openvswitch.get(list(openvswitch.keys())[0])
                    return row.external_ids.get('system-id')

        return None

    request = ovsdb_event.EventReadRequest(None, _get_dp_ids)
    reply = manager.send_request(request)

    # NOTE(jkoelker) Bulk reads return a tuple of (system_id, result)
    for result in reply.result:
        if result[1]:
            return result[0]

    return None


def get_bridges_by_system_id(manager, system_id, fn=None):
    bridges = get_table(manager, system_id, 'Bridge').rows.values()

    if fn is not None:
        return fn(bridges)

    return bridges


def bridge_exists(manager, system_id, bridge_name):
    return bool(row_by_name(manager, system_id, bridge_name))


def port_exists(manager, system_id, port_name):
    return bool(row_by_name(manager, system_id, port_name, 'Port'))


def set_external_id(manager, system_id, key, val, fn):
    val = str(val)

    def _set_iface_external_id(tables, *_):
        row = fn(tables)

        if not row:
            return None

        external_ids = row.external_ids
        external_ids[key] = val
        row.external_ids = external_ids

    req = ovsdb_event.EventModifyRequest(system_id, _set_iface_external_id)
    return manager.send_request(req)


def set_iface_external_id(manager, system_id, iface_name, key, val):
    return set_external_id(manager, system_id, key, val,
                           lambda tables: _get_iface(tables, iface_name))


def set_other_config(manager, system_id, key, val, fn):
    val = str(val)

    def _set_iface_other_config(tables, *_):
        row = fn(tables)

        if not row:
            return None

        other_config = row.other_config
        other_config[key] = val
        row.other_config = other_config

    req = ovsdb_event.EventModifyRequest(system_id, _set_iface_other_config)
    return manager.send_request(req)


def set_iface_other_config(manager, system_id, iface_name, key, val):
    return set_other_config(manager, system_id, key, val,
                            lambda tables: _get_iface(tables, iface_name))


def del_external_id(manager, system_id, key, fn):
    def _del_iface_external_id(tables, *_):
        row = fn(tables)

        if not row:
            return None

        external_ids = row.external_ids
        if key in external_ids:
            external_ids.pop(key)
            row.external_ids = external_ids

    req = ovsdb_event.EventModifyRequest(system_id, _del_iface_external_id)
    return manager.send_request(req)


def del_iface_external_id(manager, system_id, iface_name, key):
    return del_external_id(manager, system_id, key,
                           lambda tables: _get_iface(tables, iface_name))


def del_other_config(manager, system_id, key, fn):
    def _del_iface_other_config(tables, *_):
        row = fn(tables)

        if not row:
            return None

        other_config = row.other_config
        if key in other_config:
            other_config.pop(key)
            row.other_config = other_config

    req = ovsdb_event.EventModifyRequest(system_id, _del_iface_other_config)
    return manager.send_request(req)


def del_iface_other_config(manager, system_id, iface_name, key):
    return del_other_config(manager, system_id, key,
                            lambda tables: _get_iface(tables, iface_name))


def del_port(manager, system_id, bridge_name, fn):
    def _delete_port(tables, *_):
        bridge = _get_bridge(tables, bridge_name)

        if not bridge:
            return

        port = fn(tables)

        if not port:
            return

        ports = bridge.ports
        ports.remove(port)
        bridge.ports = ports

    req = ovsdb_event.EventModifyRequest(system_id, _delete_port)

    return manager.send_request(req)


def del_port_by_uuid(manager, system_id, bridge_name, port_uuid):
    return del_port(manager, system_id, bridge_name,
                    lambda tables: _get_port(tables, port_uuid,
                                             attr_name='uuid'))


def del_port_by_name(manager, system_id, bridge_name, port_name):
    return del_port(manager, system_id, bridge_name,
                    lambda tables, _: _get_port(tables, port_name))


def set_controller(manager, system_id, bridge_name,
                   target, controller_info=None):
    def _set_controller(tables, insert):
        bridge = _get_bridge(tables, bridge_name)

        controller = _get_controller(tables, target)
        _uuid = None
        if not controller:
            _uuid = controller_info.get('uuid', uuid.uuid4())
            controller = insert(tables['Controller'], _uuid)
            controller.target = target
            controller.connection_mode = ['out-of-band']

        elif 'out-of-band' not in controller.connection_mode:
            controller.connection_mode = ['out-of-band']

        if controller_info:
            for key, val in six.iteritems(controller_info):
                setattr(controller, key, val)

        bridge.controller = [controller]

        return _uuid

    req = ovsdb_event.EventModifyRequest(system_id, _set_controller)
    return manager.send_request(req)


def create_port(manager, system_id, bridge_name, port_info, iface_info=None,
                port_insert_uuid=None, iface_insert_uuid=None):
    if iface_info is None:
        iface_info = {}

    if not port_insert_uuid:
        port_insert_uuid = uuid.uuid4()

    if not iface_insert_uuid:
        iface_insert_uuid = uuid.uuid4()

    def _create_port(tables, insert):
        bridge = _get_bridge(tables, bridge_name)

        if not bridge:
            return

        default_port_name = 'port' + str(port_insert_uuid)

        if 'name' not in iface_info:
            iface_info['name'] = port_info.get('name', default_port_name)

        if 'type' not in iface_info:
            iface_info['type'] = 'internal'

        if 'name' not in port_info:
            port_info['name'] = default_port_name

        iface = insert(tables['Interface'], iface_insert_uuid)
        for key, val in six.iteritems(iface_info):
            setattr(iface, key, val)

        port = insert(tables['Port'], port_insert_uuid)
        for key, val in six.iteritems(port_info):
            setattr(port, key, val)

        port.interfaces = [iface]

        bridge.ports = bridge.ports + [port]

        return port_insert_uuid, iface_insert_uuid

    req = ovsdb_event.EventModifyRequest(system_id, _create_port)

    return manager.send_request(req)
