"""Adapter for ns-2.

This module contains the code for converting an FNSS topology object into a Tcl
script to deploy such topology into ns-2.
"""
from warnings import warn

from fnss.units import time_units, capacity_units
from fnss.netconfig.nodeconfig import get_stack, \
                                      get_application_names, \
                                      get_application_properties


__all__ = [
    'to_ns2',
    'validate_ns2_stacks'
          ]


# Template text rendered by the template engine
__TEMPLATE = r"""# Code generated by Fast Network Simulator Setup (FNSS)
<%
from fnss.units import time_units, capacity_units
from fnss.netconfig.nodeconfig import get_stack, \
                                      get_application_names, \
                                      get_application_properties

# Convert capacity in Mb
capacity_norm = capacity_units[topology.graph['capacity_unit']] / 1000000.0
# Convert delay it in ms
if set_delays: delay_norm = time_units[topology.graph['delay_unit']]
%>
# Create a simulator object
set ns [new Simulator]

# Create nodes
% for node in topology.nodes():
set n${str(node)} [$ns node]
% endfor

# Create all links
set qtype DropTail
## if topology is undirected, create duplex links, otherwise simplex links
% if topology.is_directed():
    % for u, v in topology.edges():
<% delay = "0" if not set_delays else str(topology.adj[u][v]['delay'] * delay_norm) %>\
${"$ns simplex-link $n%s $n%s %sMb %sms $qtype" % (str(u), str(v), str(topology.adj[u][v]['capacity'] * capacity_norm), delay)}
    % endfor
% else:
    % for u, v in topology.edges():
<% delay = "0" if not set_delays else str(topology.adj[u][v]['delay'] * delay_norm) %>\
${"$ns duplex-link $n%s $n%s %sMb %sms $qtype" % (str(u), str(v), str(topology.adj[u][v]['capacity'] * capacity_norm), delay)}
    % endfor
%endif

% if set_weights:
# Set link weights
    % for u, v in topology.edges():
${"$ns cost $n%s $n%s %s" % (str(u), str(v), str(topology.adj[u][v]['weight']))}
        % if not topology.is_directed():
${"$ns cost $n%s $n%s %s" % (str(v), str(u), str(topology.adj[v][u]['weight']))}
        % endif
    % endfor
% endif

% if set_buffers:
# Set queue sizes
    % for u, v in topology.edges():
${"$ns queue-limit $n%s $n%s %s" % (str(u), str(v), str(topology.adj[u][v]['buffer']))}
        % if not topology.is_directed():
${"$ns queue-limit $n%s $n%s %s" % (str(v), str(u), str(topology.adj[v][u]['buffer']))}
        % endif
    % endfor
% endif

% if deploy_stacks:
# Deploy applications and agents
    % for node in topology.nodes():
<%
stack = get_stack(topology, node)
if stack is None:
    continue
stack_name, stack_props = stack
stack_class = stack_props['class']
%>\
${"set %s [new %s]" % (str(stack_name), str(stack_class))}
        % for prop_name, prop_val in stack_props.items():
<% if prop_name == 'class': continue %>\
${"$%s set %s %s" % (str(stack_name), str(prop_name), str(prop_val))}
        % endfor
${"$ns attach-agent $n%s $%s" % (str(node), str(stack_name))}
        % for app_name in get_application_names(topology, node):
<%
app_properties = get_application_properties(topology, node, app_name)
app_class = app_properties['class']
%>\
${"set %s [new %s]" % (str(app_name), str(app_class))}
            % for prop_name, prop_val in app_properties.items():
<% if prop_name == 'class': continue %>\
${"$%s set %s %s" % (str(app_name), str(prop_name), str(prop_val))}
            % endfor
${"$%s attach-agent $%s" % (str(app_name), str(stack_name))}
        % endfor
    % endfor
% endif
"""


def validate_ns2_stacks(topology):
    """
    Validate whether the stacks and applications of a topology are valid for
    ns-2 deployment

    Parameters
    ----------
    topology : Topology
        The topology object to validate

    Returns
    -------
    valid : bool
        *True* if stacks are valid ns-2 stacks, *False* otherwise
    """
    for node in topology.nodes():
        applications = get_application_names(topology, node)
        for name in applications:
            if not 'class' in get_application_properties(topology, node, name):
                # Each application must have a class attribute to work
                return False
        stack = get_stack(topology, node)
        if stack is None:
            # Each node must have a stack if it has an application
            if len(applications) > 0: return False
        else:
            # If there is a stack, it must have a class attribute
            if 'class' not in stack[1]: return False
    return True


def to_ns2(topology, path, stacks=True):
    """Convert topology object into an ns-2 Tcl script that deploys that
    topology into ns-2.

    Parameters
    ----------
    topology : Topology
        The topology object to convert
    path : str
        The path to the output Tcl file
    stacks : bool, optional
        If True, read the stacks on nodes and write them into the output file.
        For this to work, stacks must be formatted in a way understandable by
        ns-2. For example, stack and applications must have a 'class' attribute
        whose value is the name of the ns-2 class implementing it.

    Notes
    -----
    In order for the function to parse stacks correctly, the input topology
    must satisfy the following requirements:
     * each stack and each application must have a `class` attribute whose
       value is the ns-2 class implementing such stack or application, such as
       `Agent/TCP` or `Application/FTP`.
     * All names and values of stack and application properties must be valid
       properties recognized by the ns-2 application or protocol stack.
    """
    try:
        from mako.template import Template
    except ImportError:
        raise ImportError('Cannot import mako.template module. '
                          'Make sure mako is installed on this machine.')
    set_buffers = True
    set_delays = True
    # if all links are annotated with weights, then set weights
    set_weights = all('weight' in topology.adj[u][v]
                      for u, v in topology.edges())

    if not 'capacity_unit' in topology.graph:
        raise ValueError('The given topology does not have capacity data.')
    if not topology.graph['capacity_unit'] in capacity_units:
        raise ValueError('The given topology does not have a valid capacity unit')
    if not 'buffer_unit' in topology.graph:
        warn('Missing buffer size unit attribute in the topology. '\
             'Output file will be generated without buffer assignments.')
        set_buffers = False
    elif not topology.graph['buffer_unit'] == 'packets':
        warn('The buffer size unit of the topology is %s. The only supported '
             'unit is packets. Output file will be generated without buffer '
             'assignments' % topology.graph['buffer_unit'])
        set_buffers = False
    if not 'delay_unit' in topology.graph or not topology.graph['delay_unit'] in time_units:
        warn('Missing or invalid delay unit attribute in the topology. The '
             'output file will be generated with all link delays set to 0.')
        set_delays = False
    if stacks:
        if not validate_ns2_stacks(topology):
            warn('Some application stacks cannot be parsed correctly. The '
                 'output file will be generated without stack assignments.')
            stacks = False
        if not any('stack' in topology.node[v] for v in topology.nodes()):
            stacks = False
    template = Template(__TEMPLATE)
    variables = {
        'topology':        topology,
        'deploy_stacks':   stacks,
        'set_delays':      set_delays,
        'set_buffers':     set_buffers,
        'set_weights':     set_weights
                }
    with open(path, "w") as out:
        out.write(template.render(**variables))
