#!/usr/bin/env python3
#
# Copyright (C) 2019 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
#

import os
import sys
import copy
import subprocess

import vyos.configinterface as VyIfconfig

from vyos.config import Config
from vyos import ConfigError

default_config_data = {
    'address': [],
    'address_remove': [],
    'aging': '300',
    'br_name': '',
    'description': '',
    'deleted': False,
    'dhcp_client_id': '',
    'dhcp_hostname': '',
    'dhcpv6_parameters_only': False,
    'dhcpv6_temporary': False,
    'disable': False,
    'disable_link_detect': False,
    'forwarding_delay': '15',
    'hello_time': '2',
    'igmp_querier': 0,
    'arp_cache_timeout_ms': '30000',
    'mac' : '',
    'max_age': '20',
    'member': [],
    'member_remove': [],
    'priority': '32768',
    'stp': 'off'
}

def subprocess_cmd(command):
    process = subprocess.Popen(command,stdout=subprocess.PIPE, shell=True)
    proc_stdout = process.communicate()[0].strip()
    pass

def diff(first, second):
    second = set(second)
    return [item for item in first if item not in second]

def get_config():
    bridge = copy.deepcopy(default_config_data)
    conf = Config()

    # determine tagNode instance
    try:
        bridge['br_name'] = os.environ['VYOS_TAGNODE_VALUE']
    except KeyError as E:
        print("Interface not specified")

    # Check if bridge has been removed
    if not conf.exists('interfaces bridge ' + bridge['br_name']):
        bridge['deleted'] = True
        return bridge

    # set new configuration level
    conf.set_level('interfaces bridge ' + bridge['br_name'])

    # retrieve configured interface addresses
    if conf.exists('address'):
        bridge['address'] = conf.return_values('address')

    # retrieve aging - how long addresses are retained
    if conf.exists('aging'):
        bridge['aging'] = conf.return_value('aging')

    # retrieve interface description
    if conf.exists('description'):
        bridge['description'] = conf.return_value('description')

    # DHCP client identifier
    if conf.exists('dhcp-options client-id'):
        bridge['dhcp_client_id'] = conf.return_value('dhcp-options client-id')

    # DHCP client hostname
    if conf.exists('dhcp-options host-name'):
        bridge['dhcp_hostname'] = conf.return_value('dhcp-options host-name')

    # DHCPv6 acquire only config parameters, no address
    if conf.exists('dhcpv6-options parameters-only'):
        bridge['dhcpv6_parameters_only'] = True

    # DHCPv6 IPv6 "temporary" address
    if conf.exists('dhcpv6-options temporary'):
        bridge['dhcpv6_temporary'] = True

    # Disable this bridge interface
    if conf.exists('disable'):
        bridge['disable'] = True

    # Ignore link state changes
    if conf.exists('disable-link-detect'):
        bridge['disable_link_detect'] = True

    # Forwarding delay
    if conf.exists('forwarding-delay'):
        bridge['forwarding_delay'] = conf.return_value('forwarding-delay')

    # Hello packet advertisment interval
    if conf.exists('hello-time'):
        bridge['hello_time'] = conf.return_value('hello-time')

    # Enable Internet Group Management Protocol (IGMP) querier
    if conf.exists('igmp querier'):
        bridge['igmp_querier'] = 1

    # ARP cache entry timeout in seconds
    if conf.exists('ip arp-cache-timeout'):
        tmp = 1000 * int(conf.return_value('ip arp-cache-timeout'))
        bridge['arp_cache_timeout_ms'] = str(tmp)

    # Media Access Control (MAC) address
    if conf.exists('mac'):
        bridge['mac'] = conf.return_value('mac')

    # Interval at which neighbor bridges are removed
    if conf.exists('max-age'):
        bridge['max_age'] = conf.return_value('max-age')

    # Determine bridge member interface (currently configured)
    for intf in conf.list_nodes('member interface'):
        iface = {
            'name': intf,
            'cost': '',
            'priority': ''
        }

        if conf.exists('member interface {} cost'.format(intf)):
            iface['cost'] = conf.return_value('member interface {} cost'.format(intf))

        if conf.exists('member interface {} priority'.format(intf)):
            iface['priority'] = conf.return_value('member interface {} priority'.format(intf))

        bridge['member'].append(iface)

    # Determine bridge member interface (currently effective) - to determine which
    # interfaces is no longer assigend to the bridge and thus can be removed
    eff_intf = conf.list_effective_nodes('member interface')
    act_intf = conf.list_nodes('member interface')
    bridge['member_remove'] = diff(eff_intf, act_intf)

    # Determine interface addresses (currently effective) - to determine which
    # address is no longer valid and needs to be removed from the bridge
    eff_addr = conf.return_effective_values('address')
    act_addr = conf.return_values('address')
    bridge['address_remove'] = diff(eff_addr, act_addr)

    # Priority for this bridge
    if conf.exists('priority'):
        bridge['priority'] = conf.return_value('priority')

    # Enable spanning tree protocol
    if conf.exists('stp'):
        bridge['stp'] = 'on'

    return bridge

def verify(bridge):
    if bridge is None:
        return None

    conf = Config()
    for br in conf.list_nodes('interfaces bridge'):
        # it makes no sense to verify ourself in this case
        if br == bridge['br_name']:
            continue

        for intf in bridge['member']:
            tmp = conf.list_nodes('interfaces bridge {} member interface'.format(br))
            if intf['name'] in tmp:
                raise ConfigError('{} can be assigned to any one bridge only'.format(intf['name']))

    return None

def generate(bridge):
    if bridge is None:
        return None

    return None

def apply(bridge):
    if bridge is None:
        return None

    cmd = ''
    if bridge['deleted']:
        # bridges need to be shutdown first
        cmd += 'ip link set dev "{}" down'.format(bridge['br_name'])
        cmd += ' && '
        # delete bridge
        cmd += 'brctl delbr "{}"'.format(bridge['br_name'])
        subprocess_cmd(cmd)

    else:
        # create bridge if it does not exist
        if not os.path.exists("/sys/class/net/" + bridge['br_name']):
            # create bridge interface
            cmd += 'brctl addbr "{}"'.format(bridge['br_name'])
            cmd += ' && '
            # activate "UP" the interface
            cmd += 'ip link set dev "{}" up'.format(bridge['br_name'])
            cmd += ' && '

        # set ageing time
        cmd += 'brctl setageing "{}" "{}"'.format(bridge['br_name'], bridge['aging'])
        cmd += ' && '

        # set bridge forward delay
        cmd += 'brctl setfd "{}" "{}"'.format(bridge['br_name'], bridge['forwarding_delay'])
        cmd += ' && '

        # set hello time
        cmd += 'brctl sethello "{}" "{}"'.format(bridge['br_name'], bridge['hello_time'])
        cmd += ' && '

        # set max message age
        cmd += 'brctl setmaxage "{}" "{}"'.format(bridge['br_name'], bridge['max_age'])
        cmd += ' && '

        # set bridge priority
        cmd += 'brctl setbridgeprio "{}" "{}"'.format(bridge['br_name'], bridge['priority'])
        cmd += ' && '

        # turn stp on/off
        cmd += 'brctl stp "{}" "{}"'.format(bridge['br_name'], bridge['stp'])

        for intf in bridge['member_remove']:
            # remove interface from bridge
            cmd += ' && '
            cmd += 'brctl delif "{}" "{}"'.format(bridge['br_name'], intf)

        for intf in bridge['member']:
            # add interface to bridge
            # but only if it is not yet member of this bridge
            if not os.path.exists('/sys/devices/virtual/net/' + bridge['br_name'] + '/brif/' + intf['name']):
                cmd += ' && '
                cmd += 'brctl addif "{}" "{}"'.format(bridge['br_name'], intf['name'])

            # set bridge port cost
            if intf['cost']:
                cmd += ' && '
                cmd += 'brctl setpathcost "{}" "{}" "{}"'.format(bridge['br_name'], intf['name'], intf['cost'])

            # set bridge port priority
            if intf['priority']:
                cmd += ' && '
                cmd += 'brctl setportprio "{}" "{}" "{}"'.format(bridge['br_name'], intf['name'], intf['priority'])

        subprocess_cmd(cmd)

        # Change interface MAC address
        if bridge['mac']:
            VyIfconfig.set_mac_address(bridge['br_name'], bridge['mac'])

        # update interface description used e.g. within SNMP
        VyIfconfig.set_description(bridge['br_name'], bridge['description'])

        # Ignore link state changes?
        VyIfconfig.set_link_detect(bridge['br_name'], bridge['disable_link_detect'])

        # enable or disable IGMP querier
        VyIfconfig.set_multicast_querier(bridge['br_name'], bridge['igmp_querier'])

        # ARP cache entry timeout in seconds
        VyIfconfig.set_arp_cache_timeout(bridge['br_name'], bridge['arp_cache_timeout_ms'])

        # Configure interface address(es)
        for addr in bridge['address_remove']:
            VyIfconfig.remove_interface_address(bridge['br_name'], addr)

        for addr in bridge['address']:
            VyIfconfig.add_interface_address(bridge['br_name'], addr)

    return None

if __name__ == '__main__':
    try:
        c = get_config()
        verify(c)
        generate(c)
        apply(c)
    except ConfigError as e:
        print(e)
        sys.exit(1)