"""
ns3.py: defines classes for running emulations with ns-3 simulated networks.
"""

import subprocess
import threading
import time

import ns.core
import ns.internet
import ns.lte
import ns.mobility
import ns.network
import ns.tap_bridge
import ns.wifi
import ns.wimax

from core import constants
from core import logger
from core.coreobj import PyCoreNet
from core.enumerations import EventTypes
from core.enumerations import LinkTypes
from core.enumerations import NodeTypes
from core.misc.utils import make_tuple
from core.mobility import WayPointMobility
from core.netns.nodes import CoreNode
from core.session import Session

ns.core.GlobalValue.Bind(
    "SimulatorImplementationType",
    ns.core.StringValue("ns3::RealtimeSimulatorImpl")
)
ns.core.GlobalValue.Bind(
    "ChecksumEnabled",
    ns.core.BooleanValue("true")
)


class CoreNs3Node(CoreNode, ns.network.Node):
    """
    The CoreNs3Node is both a CoreNode backed by a network namespace and
    an ns-3 Node simulator object. When linked to simulated networks, the TunTap
    device will be used.
    """

    def __init__(self, *args, **kwds):
        ns.network.Node.__init__(self)
        # ns-3 ID starts at 0, CORE uses 1
        objid = self.GetId() + 1
        if 'objid' not in kwds:
            kwds['objid'] = objid
        CoreNode.__init__(self, *args, **kwds)

    def newnetif(self, net=None, addrlist=None, hwaddr=None, ifindex=None, ifname=None):
        """
        Add a network interface. If we are attaching to a CoreNs3Net, this
        will be a TunTap. Otherwise dispatch to CoreNode.newnetif().
        """
        if not addrlist:
            addrlist = []

        if not isinstance(net, CoreNs3Net):
            return CoreNode.newnetif(self, net, addrlist, hwaddr, ifindex, ifname)
        ifindex = self.newtuntap(ifindex=ifindex, ifname=ifname, net=net)
        self.attachnet(ifindex, net)
        netif = self.netif(ifindex)
        netif.sethwaddr(hwaddr)
        for addr in make_tuple(addrlist):
            netif.addaddr(addr)

        addrstr = netif.addrlist[0]
        (addr, mask) = addrstr.split('/')
        tap = net._tapdevs[netif]
        tap.SetAttribute(
            "IpAddress",
            ns.network.Ipv4AddressValue(ns.network.Ipv4Address(addr))
        )
        tap.SetAttribute(
            "Netmask",
            ns.network.Ipv4MaskValue(ns.network.Ipv4Mask("/" + mask))
        )
        ns.core.Simulator.Schedule(ns.core.Time('0'), netif.install)
        return ifindex

    def getns3position(self):
        """
        Return the ns-3 (x, y, z) position of a node.
        """
        try:
            mm = self.GetObject(ns.mobility.MobilityModel.GetTypeId())
            pos = mm.GetPosition()
            return pos.x, pos.y, pos.z
        except AttributeError:
            self.warn("ns-3 mobility model not found")
            return 0, 0, 0

    def setns3position(self, x, y, z):
        """
        Set the ns-3 (x, y, z) position of a node.
        """
        try:
            mm = self.GetObject(ns.mobility.MobilityModel.GetTypeId())
            if z is None:
                z = 0.0
            pos = mm.SetPosition(ns.core.Vector(x, y, z))
        except AttributeError:
            self.warn("ns-3 mobility model not found, not setting position")


class CoreNs3Net(PyCoreNet):
    """
    The CoreNs3Net is a helper PyCoreNet object. Networks are represented
    entirely in simulation with the TunTap device bridging the emulated and
    simulated worlds.
    """
    apitype = NodeTypes.WIRELESS_LAN.value
    linktype = LinkTypes.WIRELESS.value
    # icon used
    type = "wlan"

    def __init__(self, session, objid=None, name=None, start=True, policy=None):
        PyCoreNet.__init__(self, session, objid, name)
        self.tapbridge = ns.tap_bridge.TapBridgeHelper()
        self._ns3devs = {}
        self._tapdevs = {}

    def attach(self, netif):
        """
        Invoked from netif.attach(). Create a TAP device using the TapBridge
        object. Call getns3dev() to get model-specific device.
        """
        self._netif[netif] = netif
        self._linked[netif] = {}
        ns3dev = self.getns3dev(netif.node)
        tap = self.tapbridge.Install(netif.node, ns3dev)
        tap.SetMode(ns.tap_bridge.TapBridge.CONFIGURE_LOCAL)
        tap.SetAttribute(
            "DeviceName",
            ns.core.StringValue(netif.localname)
        )
        self._ns3devs[netif] = ns3dev
        self._tapdevs[netif] = tap

    def getns3dev(self, node):
        """
        Implement depending on network helper. Install this network onto
        the given node and return the device. Register the ns3 device into
        self._ns3devs
        """
        raise NotImplementedError

    def findns3dev(self, node):
        """
        Given a node, return the interface and ns3 device associated with
        this network.
        """
        for netif in node.netifs():
            if netif in self._ns3devs:
                return netif, self._ns3devs[netif]
        return None, None

    def shutdown(self):
        """
        Session.shutdown() will invoke this.
        """
        pass

    def usecorepositions(self):
        """
        Set position callbacks for interfaces on this net so the CORE GUI
        can update the ns-3 node position when moved with the mouse.
        """
        for netif in self.netifs():
            netif.poshook = self.setns3position

    def setns3position(self, netif, x, y, z):
        logger.info("setns3position: %s (%s, %s, %s)", netif.node.name, x, y, z)
        netif.node.setns3position(x, y, z)


class Ns3LteNet(CoreNs3Net):
    def __init__(self, *args, **kwds):
        """
        Uses a LteHelper to create an ns-3 based LTE network.
        """
        CoreNs3Net.__init__(self, *args, **kwds)
        self.lte = ns.lte.LteHelper()
        # enhanced NodeB node list
        self.enbnodes = []
        self.dlsubchannels = None
        self.ulsubchannels = None

    def setsubchannels(self, downlink, uplink):
        """
        Set the downlink/uplink subchannels, which are a list of ints.
        These should be set prior to using CoreNs3Node.newnetif().
        """
        self.dlsubchannels = downlink
        self.ulsubchannels = uplink

    def setnodeb(self, node):
        """
        Mark the given node as a nodeb (base transceiver station)
        """
        self.enbnodes.append(node)

    def linknodeb(self, node, nodeb, mob, mobb):
        """
        Register user equipment with a nodeb.
        Optionally install mobility model while we have the ns-3 devs handy.
        """
        tmp, nodebdev = self.findns3dev(nodeb)
        tmp, dev = self.findns3dev(node)
        if nodebdev is None or dev is None:
            raise KeyError, "ns-3 device for node not found"
        self.lte.RegisterUeToTheEnb(dev, nodebdev)
        if mob:
            self.lte.AddMobility(dev.GetPhy(), mob)
        if mobb:
            self.lte.AddDownlinkChannelRealization(mobb, mob, dev.GetPhy())

    def getns3dev(self, node):
        """
        Get the ns3 NetDevice using the LteHelper.
        """
        if node in self.enbnodes:
            devtype = ns.lte.LteHelper.DEVICE_TYPE_ENODEB
        else:
            devtype = ns.lte.LteHelper.DEVICE_TYPE_USER_EQUIPMENT
        nodes = ns.network.NodeContainer(node)
        devs = self.lte.Install(nodes, devtype)
        devs.Get(0).GetPhy().SetDownlinkSubChannels(self.dlsubchannels)
        devs.Get(0).GetPhy().SetUplinkSubChannels(self.ulsubchannels)
        return devs.Get(0)

    def attach(self, netif):
        """
        Invoked from netif.attach(). Create a TAP device using the TapBridge
        object. Call getns3dev() to get model-specific device.
        """
        self._netif[netif] = netif
        self._linked[netif] = {}
        ns3dev = self.getns3dev(netif.node)
        self.tapbridge.SetAttribute("Mode", ns.core.StringValue("UseLocal"))
        # self.tapbridge.SetAttribute("Mode",
        #                ns.core.IntegerValue(ns.tap_bridge.TapBridge.USE_LOCAL))
        tap = self.tapbridge.Install(netif.node, ns3dev)
        # tap.SetMode(ns.tap_bridge.TapBridge.USE_LOCAL)
        logger.info("using TAP device %s for %s/%s", netif.localname, netif.node.name, netif.name)
        subprocess.check_call(['tunctl', '-t', netif.localname, '-n'])
        # check_call([IP_BIN, 'link', 'set', 'dev', netif.localname, \
        #    'address', '%s' % netif.hwaddr])
        subprocess.check_call([constants.IP_BIN, 'link', 'set', netif.localname, 'up'])
        tap.SetAttribute("DeviceName", ns.core.StringValue(netif.localname))
        self._ns3devs[netif] = ns3dev
        self._tapdevs[netif] = tap


class Ns3WifiNet(CoreNs3Net):
    def __init__(self, *args, **kwds):
        """
        Uses a WifiHelper to create an ns-3 based Wifi network.
        """
        rate = kwds.pop('rate', 'OfdmRate54Mbps')
        CoreNs3Net.__init__(self, *args, **kwds)
        self.wifi = ns.wifi.WifiHelper().Default()
        self.wifi.SetStandard(ns.wifi.WIFI_PHY_STANDARD_80211a)
        self.wifi.SetRemoteStationManager(
            "ns3::ConstantRateWifiManager",
            "DataMode",
            ns.core.StringValue(rate),
            "NonUnicastMode",
            ns.core.StringValue(rate)
        )
        self.mac = ns.wifi.NqosWifiMacHelper.Default()
        self.mac.SetType("ns3::AdhocWifiMac")

        channel = ns.wifi.YansWifiChannelHelper.Default()
        self.phy = ns.wifi.YansWifiPhyHelper.Default()
        self.phy.SetChannel(channel.Create())

    def getns3dev(self, node):
        """
        Get the ns3 NetDevice using the WifiHelper.
        """
        devs = self.wifi.Install(self.phy, self.mac, node)
        return devs.Get(0)


class Ns3WimaxNet(CoreNs3Net):
    def __init__(self, *args, **kwds):
        CoreNs3Net.__init__(self, *args, **kwds)
        self.wimax = ns.wimax.WimaxHelper()
        self.scheduler = ns.wimax.WimaxHelper.SCHED_TYPE_SIMPLE
        self.phy = ns.wimax.WimaxHelper.SIMPLE_PHY_TYPE_OFDM
        # base station node list
        self.bsnodes = []

    def setbasestation(self, node):
        self.bsnodes.append(node)

    def getns3dev(self, node):
        if node in self.bsnodes:
            devtype = ns.wimax.WimaxHelper.DEVICE_TYPE_BASE_STATION
        else:
            devtype = ns.wimax.WimaxHelper.DEVICE_TYPE_SUBSCRIBER_STATION
        nodes = ns.network.NodeContainer(node)
        devs = self.wimax.Install(nodes, devtype, self.phy, self.scheduler)
        if node not in self.bsnodes:
            devs.Get(0).SetModulationType(ns.wimax.WimaxPhy.MODULATION_TYPE_QAM16_12)
        # debug
        self.wimax.EnableAscii("wimax-device-%s" % node.name, devs)
        return devs.Get(0)

    @staticmethod
    def ipv4netifaddr(netif):
        for addr in netif.addrlist:
            if ':' in addr:
                # skip ipv6
                continue
            ip = ns.network.Ipv4Address(addr.split('/')[0])
            mask = ns.network.Ipv4Mask('/' + addr.split('/')[1])
            return ip, mask
        return None, None

    def addflow(self, node1, node2, upclass, downclass):
        """
        Add a Wimax service flow between two nodes.
        """
        netif1, ns3dev1 = self.findns3dev(node1)
        netif2, ns3dev2 = self.findns3dev(node2)
        if not netif1 or not netif2:
            raise ValueError, "interface not found"
        addr1, mask1 = self.ipv4netifaddr(netif1)
        addr2, mask2 = self.ipv4netifaddr(netif2)
        clargs1 = (addr1, mask1, addr2, mask2) + downclass
        clargs2 = (addr2, mask2, addr1, mask1) + upclass
        clrec1 = ns.wimax.IpcsClassifierRecord(*clargs1)
        clrec2 = ns.wimax.IpcsClassifierRecord(*clargs2)
        ns3dev1.AddServiceFlow(self.wimax.CreateServiceFlow(
            ns.wimax.ServiceFlow.SF_DIRECTION_DOWN,
            ns.wimax.ServiceFlow.SF_TYPE_RTPS, clrec1)
        )
        ns3dev1.AddServiceFlow(self.wimax.CreateServiceFlow(
            ns.wimax.ServiceFlow.SF_DIRECTION_UP,
            ns.wimax.ServiceFlow.SF_TYPE_RTPS, clrec2)
        )
        ns3dev2.AddServiceFlow(self.wimax.CreateServiceFlow(
            ns.wimax.ServiceFlow.SF_DIRECTION_DOWN,
            ns.wimax.ServiceFlow.SF_TYPE_RTPS, clrec2)
        )
        ns3dev2.AddServiceFlow(self.wimax.CreateServiceFlow(
            ns.wimax.ServiceFlow.SF_DIRECTION_UP,
            ns.wimax.ServiceFlow.SF_TYPE_RTPS, clrec1)
        )


class Ns3Session(Session):
    """
    A Session that starts an ns-3 simulation thread.
    """

    def __init__(self, session_id, persistent=False, duration=600):
        self.duration = duration
        self.nodes = ns.network.NodeContainer()
        self.mobhelper = ns.mobility.MobilityHelper()
        Session.__init__(self, session_id, persistent=persistent)

    def run(self, vis=False):
        """
        Run the ns-3 simulation and return the simulator thread.
        """

        def runthread():
            ns.core.Simulator.Stop(ns.core.Seconds(self.duration))
            logger.info("running ns-3 simulation for %d seconds", self.duration)
            if vis:
                try:
                    import visualizer
                except ImportError:
                    logger.exception("visualizer is not available")
                    ns.core.Simulator.Run()
                else:
                    visualizer.start()
            else:
                ns.core.Simulator.Run()

        # self.evq.run() # event queue may have WayPointMobility events
        self.set_state(EventTypes.RUNTIME_STATE, send_event=True)
        t = threading.Thread(target=runthread)
        t.daemon = True
        t.start()
        return t

    def shutdown(self):
        # TODO: the following line tends to segfault ns-3 (and therefore core-daemon)
        ns.core.Simulator.Destroy()
        Session.shutdown(self)

    def addnode(self, name):
        """
        A convenience helper for Session.addobj(), for adding CoreNs3Nodes
        to this session. Keeps a NodeContainer for later use.
        """
        n = self.add_object(cls=CoreNs3Node, name=name)
        self.nodes.Add(n)
        return n

    def setupconstantmobility(self):
        """
        Install a ConstantPositionMobilityModel.
        """
        palloc = ns.mobility.ListPositionAllocator()
        for i in xrange(self.nodes.GetN()):
            (x, y, z) = ((100.0 * i) + 50, 200.0, 0.0)
            palloc.Add(ns.core.Vector(x, y, z))
            node = self.nodes.Get(i)
            node.position.set(x, y, z)
        self.mobhelper.SetPositionAllocator(palloc)
        self.mobhelper.SetMobilityModel("ns3::ConstantPositionMobilityModel")
        self.mobhelper.Install(self.nodes)

    def setuprandomwalkmobility(self, bounds, time=10, speed=25.0):
        """
        Set up the random walk mobility model within a bounding box.
           - bounds is the max (x, y, z) boundary
           - time is the number of seconds to maintain the current speed
             and direction
           - speed is the maximum speed, with node speed randomly chosen
             from [0, speed]
        """
        x, y, z = map(float, bounds)
        self.mobhelper.SetPositionAllocator(
            "ns3::RandomBoxPositionAllocator",
            "X",
            ns.core.StringValue("ns3::UniformRandomVariable[Min=0|Max=%s]" % x),
            "Y",
            ns.core.StringValue("ns3::UniformRandomVariable[Min=0|Max=%s]" % y),
            "Z",
            ns.core.StringValue("ns3::UniformRandomVariable[Min=0|Max=%s]" % z)
        )
        self.mobhelper.SetMobilityModel(
            "ns3::RandomWalk2dMobilityModel",
            "Mode", ns.core.StringValue("Time"),
            "Time", ns.core.StringValue("%ss" % time),
            "Speed",
            ns.core.StringValue("ns3::UniformRandomVariable[Min=0|Max=%s]" % speed),
            "Bounds", ns.core.StringValue("0|%s|0|%s" % (x, y))
        )
        self.mobhelper.Install(self.nodes)

    def startns3mobility(self, refresh_ms=300):
        """
        Start a thread that updates CORE nodes based on their ns-3
        positions.
        """
        self.set_state(EventTypes.INSTANTIATION_STATE)
        self.mobilitythread = threading.Thread(
            target=self.ns3mobilitythread,
            args=(refresh_ms,))
        self.mobilitythread.daemon = True
        self.mobilitythread.start()

    def ns3mobilitythread(self, refresh_ms):
        """
        Thread target that updates CORE nodes every refresh_ms based on
        their ns-3 positions.
        """
        valid_states = (
            EventTypes.RUNTIME_STATE.value,
            EventTypes.INSTANTIATION_STATE.value
        )
        while self.state in valid_states:
            for i in xrange(self.nodes.GetN()):
                node = self.nodes.Get(i)
                x, y, z = node.getns3position()
                if (x, y, z) == node.position.get():
                    continue
                # from WayPointMobility.setnodeposition(node, x, y, z)
                node.position.set(x, y, z)
                node_data = node.data(0)
                self.broadcast_node(node_data)
                self.sdt.updatenode(node.objid, flags=0, x=x, y=y, z=z)
            time.sleep(0.001 * refresh_ms)

    def setupmobilitytracing(self, net, filename, nodes):
        """
        Start a tracing thread using the ASCII output from the ns3
        mobility helper.
        """
        net.mobility = WayPointMobility(session=self, object_id=net.objid, config=None)
        net.mobility.setendtime()
        net.mobility.refresh_ms = 300
        net.mobility.empty_queue_stop = False
        of = ns.network.OutputStreamWrapper(filename, filemode=777)
        self.mobhelper.EnableAsciiAll(of)
        self.mobilitytracethread = threading.Thread(
            target=self.mobilitytrace,
            args=(net, filename, nodes)
        )
        self.mobilitytracethread.daemon = True
        self.mobilitytracethread.start()

    def mobilitytrace(self, net, filename, nodes, verbose):
        nodemap = {}
        # move nodes to initial positions
        for node in nodes:
            x, y, z = node.getns3position()
            net.mobility.setnodeposition(node, x, y, z)
            nodemap[node.GetId()] = node

        logger.info("mobilitytrace opening '%s'", filename)

        f = None
        try:
            f = open(filename)
            f.seek(0, 2)

            sleep = 0.001
            kickstart = True
            while True:
                if self.state != EventTypes.RUNTIME_STATE.value:
                    break
                line = f.readline()
                if not line:
                    time.sleep(sleep)
                    if sleep < 1.0:
                        sleep += 0.001
                    continue
                sleep = 0.001
                items = dict(map(lambda x: x.split('='), line.split()))
                logger.info("trace: %s %s %s", items['node'], items['pos'], items['vel'])
                x, y, z = map(float, items['pos'].split(':'))
                vel = map(float, items['vel'].split(':'))
                node = nodemap[int(items['node'])]
                net.mobility.addwaypoint(time=0, nodenum=node.objid,
                                         x=x, y=y, z=z, speed=vel)
                if kickstart:
                    kickstart = False
                    self.event_loop.add_event(0, net.mobility.start)
                    self.event_loop.run()
                else:
                    if net.mobility.state != net.mobility.STATE_RUNNING:
                        net.mobility.state = net.mobility.STATE_RUNNING
                        self.event_loop.add_event(0, net.mobility.runround)
        except IOError:
            logger.exception("mobilitytrace error opening '%s': %s", filename)
        finally:
            if f:
                f.close()
