"""
Created on Oct 1, 2014

@author: fabiomignini
@author: stefanopetrangeli
"""
import random
import logging
from collections import OrderedDict
from nffg_library.nffg import NF_FG
from orchestrator_core.nffg_manager import NFFG_Manager
from orchestrator_core.exception import GraphError, UnsupportedLabelingMethod, IncoherentDomainInformation
from orchestrator_core.sql.domain import Domain
from orchestrator_core.sql.domains_info import DomainInformation


class Splitter(object):

    def split(self, nffg, split_flows):
        """
        Given an nffg with NF and end-points labeled with a domain,
        returns a sub-graph for each domain.
        Also characterize auto-generated endpoints with inter-domain traffic steering information.
        :param nffg:
        :param split_flows: info about virtual channels to use for flows split across domains
        :type nffg: NF_FG
        :type split_flows: dict
        :return:
        """
        domain_list = []
        nffg_list = [] 
        # Mapping between domain_names and elements belonging to each domain
        domains_dict = self._check_elements_annotations(nffg)
        domain_names = list(domains_dict.keys())
        if len(domains_dict) == 1:
            # All elements are tagged with the same domain.
            domain = Domain().getDomainFromName(domain_names[0])
            domain_list.append(domain)
            nffg_list.append(nffg)
            return domain_list, nffg_list
        else:
            domains_info = DomainInformation().get_domains_info()
            if not domains_info:
                raise GraphError("Domains information are not present, so it's not possible to split and deploy the " +
                                 "graph according to the domain tags you provided")
            for domain_name in domain_names:
                if Domain().getDomainFromName(domain_name).id not in domains_info:
                    raise GraphError("Domain " + domain_name + " that you specified in the NF-FG has not sent " +
                                     "domains information, so it's not possible to split and deploy the graph")
            # Dict that maps each domain to the related sub-graph
            domain_to_nffg = OrderedDict()
            for domain_name, domain_elements in domains_dict.items():
                other_elements = list(domains_dict.values())
                other_elements_list = []
                for element in other_elements:
                    if element != domain_elements:
                        other_elements_list = other_elements_list + element
                domain_to_nffg[domain_name] = nffg.split(domain_elements, other_elements_list)

            # process sub-flows generated by graph split
            for split_flow in split_flows:
                original_flow_id = split_flow.split('_', 1)[0]
                flow_elements = split_flow.split('_', 1)[1]
                reverse_flow_elements = flow_elements.split('/')[1] + '/' + flow_elements.split('/')[0]
                reverse_flow_id = original_flow_id + "_" + reverse_flow_elements

                # characterize endpoints and generate additional sub-graphs if needed
                for i, (domain_connection, vc) in enumerate(split_flows[split_flow].items()):
                    domain1 = domain_connection.split('/')[0]
                    domain2 = domain_connection.split('/')[1]

                    # generate additional sub-graphs if needed
                    if len(split_flows[split_flow]) != 1 and i != len(split_flows[split_flow]) - 1:
                        # the second domain of this connection is an intermediate domain
                        middle_nffg = domain_to_nffg.get(domain2)
                        if middle_nffg is None:
                            middle_nffg = NF_FG()
                            middle_nffg.id = nffg.id
                            middle_nffg.name = nffg.name + "_pass_through_" + str(i-1)
                            middle_nffg.domain = domain2
                            domain_to_nffg[domain2] = middle_nffg

                        NFFG_Manager(middle_nffg).addEndpointsCoupleAndFlowrules(
                            split_flow, "auto-generated-split_"+reverse_flow_elements,
                            "auto-generated-split_"+flow_elements)

                    # characterize end-points
                    domain1_nffg = domain_to_nffg[domain1]
                    domain2_nffg = domain_to_nffg[domain2]
                    endpoint1 = domain1_nffg.getEndPoint("auto-generated-split_"+flow_elements)
                    endpoint2 = domain2_nffg.getEndPoint("auto-generated-split_"+reverse_flow_elements)
                    if self._is_endpoint_characterized(endpoint1) and self._is_endpoint_characterized(endpoint2):
                        continue
                    self._characterize_endpoint_couple(domain1, domain2, endpoint1, endpoint2, vc)

            for domain, nffg in domain_to_nffg.items():
                domain_list.append(Domain().getDomainFromName(domain))
                nffg_list.append(nffg)
            logging.debug("Graph can be split!")
            return domain_list, nffg_list

    @classmethod
    def _characterize_endpoint_couple(cls, domain1, domain2, endpoint1, endpoint2, vc):
        interface1 = DomainInformation().get_domain_info(domain1).hardware_info.get_interface(vc["interface_a"])
        interface2 = DomainInformation().get_domain_info(domain2).hardware_info.get_interface(vc["interface_b"])
        if vc["labeling-method"] == "gre":
            # TODO should we instead read OpenConfig IP address from domain information?
            if interface1.node is not None:
                ip1 = interface1.node
            else:
                raise IncoherentDomainInformation("Ip address not specified in Gre interface for domain '" + domain1 +
                                                  "', interface '" + interface1.name + "'")
            if interface2.node is not None:
                ip2 = interface2.node
            else:
                raise IncoherentDomainInformation("Ip address not specified in Gre interface for domain '" + domain2 +
                                                  "', interface '" + interface2.name + "'")
            gre_key = vc["label"]
            cls._characterize_endpoint(endpoint1, Gre(ip1, ip2, gre_key))
            cls._characterize_endpoint(endpoint2, Gre(ip2, ip1, gre_key))
        elif vc["labeling-method"] == "vlan":
            vlan_id = vc["label"]
            logging.debug("vlan match found")
            cls._characterize_endpoint(endpoint1, Vlan(interface1.node, interface1.name, interface2.node,
                                                       interface2.name, vlan_id))
            cls._characterize_endpoint(endpoint2, Vlan(interface2.node, interface2.name, interface1.node,
                                                       interface1.name, vlan_id))
        else:
            logging.error("Labeling method '" + vc["labeling-method"] + "' not supported.")
            raise UnsupportedLabelingMethod("Graph cannot be split because scheduled domains '" + domain1 +
                                            "' and '" + domain2 + "' have a virtual channel through the " +
                                            "unsupported labeling method '" + vc["labeling-method"])

    @staticmethod
    def _characterize_endpoint(endpoint, characterization):
        """

        :param endpoint:
        :param characterization:
        :type endpoint: nffg_library.nffg.EndPoint
        :return:
        """
        if type(characterization) is Vlan:
            endpoint.type = "vlan"
            endpoint.node_id = characterization.node_1
            endpoint.interface = characterization.port_1
            endpoint.vlan_id = str(characterization.vlan)
        elif type(characterization) is Gre:
            ip_1 = characterization.local_ip
            ip_2 = characterization.remote_ip
            endpoint.type = "gre-tunnel"
            endpoint.local_ip = ip_1
            endpoint.remote_ip = ip_2
            # endpoint.interface = characterization.port_1
            endpoint.gre_key = characterization.gre_key

    @staticmethod
    def _is_endpoint_characterized(endpoint):
        """

        :param endpoint:
        :type endpoint: nffg_library.nffg.EndPoint
        :return:
        :rtype: bool
        """
        return endpoint.type is not None

    @staticmethod
    def _check_elements_annotations(nffg):
        domains_dict = OrderedDict()
        endp_and_vnf = nffg.end_points + nffg.vnfs
        for element in endp_and_vnf:
            if element.domain is None:
                if nffg.domain is not None:
                    element.domain = nffg.domain
                else:
                    raise GraphError("Unable to deploy the graph: neither graph nor its elements are associated to a " +
                                     "domain and DEFAULT_DOMAIN is not specified in the configuration file")
            if element.domain not in domains_dict:
                domains_dict[element.domain] = []
            domains_dict[element.domain].append(element)
        return domains_dict


class Vlan(object):
    def __init__(self, node_1=None, port_1=None, node_2=None, port_2=None, vlan=None, partial=0):
        """
        Partial can be 0 if the characterization involves directly domain_1 and domain_2, 1 if it involves the domain_1
        and a domain in the middle or 2 if it involves a domain in the middle and the domain_2
        """
        self.node_1 = node_1
        self.port_1 = port_1
        self.node_2 = node_2
        self.port_2 = port_2 
        self.vlan = vlan
        self.partial = partial


class Gre(object):
    def __init__(self, local_ip=None, remote_ip=None, gre_key=None):
        self.local_ip = local_ip
        self.remote_ip = remote_ip
        if gre_key is not None:
            if type(gre_key) is int:
                self.gre_key = '0x%08x' % gre_key
            else:
                self.gre_key = gre_key
        else:
            self.gre_key = '0x%08x' % random.getrandbits(32)
