package be.iminds.ilabt.jfed.experimenter_gui.gts_editor;

import be.iminds.ilabt.jfed.gts.model.*;
import be.iminds.ilabt.jfed.gts.model.impl.*;
import javafx.geometry.Point2D;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;

import static be.iminds.ilabt.jfed.gts.GtsDslConstants.GTS_CONTROL_ROLE;
import static be.iminds.ilabt.jfed.gts.GtsDslConstants.GTS_TUNNEL_ID;

/**
 * User: twalcari
 * Date: 5/21/2015
 * Time: 15:36
 */
public class GTSModelEditor {

    public static final String DEFAULT_IPV4_NETMASK = "255.255.255.0";

    private static final Logger LOG = LoggerFactory.getLogger(GTSModelEditor.class);
    private static final String PORT_PREFIX = "p";
    private final FXGtsType model;

    private final FXGtsFactory gtsFactory = new FXGtsFactory();

    public GTSModelEditor(FXGtsType model) {
        this.model = model;
    }

    public FXGtsType getModel() {
        return model;
    }

    public FXGtsHost addHost(Point2D location) {
        FXGtsHost host = gtsFactory.createGtsHost();
        host.setId(nextNodeName("host"));
        host.setEditorX(location.getX());
        host.setEditorY(location.getY());

        model.addNode(host);
        return host;
    }


    public FXGtsPort addPort(Point2D location) {
        FXGtsPort port = gtsFactory.createGtsPort();
        port.setId(nextPortName(model));
        port.setEditorX(location.getX());
        port.setEditorY(location.getY());

        model.addPort(port);

        return port;
    }


    public FXGtsOfx addOFX(Point2D location) {
        FXGtsOfx ofx = gtsFactory.createGtsOfx();
        ofx.setId(nextNodeName("ofx"));
        ofx.setEditorX(location.getX());
        ofx.setEditorY(location.getY());


        ofx.setControllerIPv4("10.10.100.100");
        ofx.setControllerPort(9966);

        ofx.setFabricIPv4("10.10.100.1");
        ofx.setFabricSubnetmaskv4(DEFAULT_IPV4_NETMASK);

        FXGtsPort controlPort = gtsFactory.createGtsPort();
        controlPort.setId("pControl");
        controlPort.setMode(GTS_CONTROL_ROLE);

        ofx.addPort(controlPort);

        model.addNode(ofx);
        return ofx;
    }

    public FXGtsExternalNetwork addExternalInterface(Point2D location) {
        FXGtsExternalNetwork lab = gtsFactory.createGtsExternalNetwork();
        lab.setId(nextNodeName("lab"));
        lab.setEditorX(location.getX());
        lab.setEditorY(location.getY());

        FXGtsPort tunnelPort = gtsFactory.createGtsPort();
        tunnelPort.setId(GTS_TUNNEL_ID);

        lab.addPort(tunnelPort);

        model.addNode(lab);

        return lab;
    }


    private String nextNodeName(String prefix) {
        int c = 0;
        String res = prefix + c;
        while (model.getNodeById(res) != null)
            res = prefix + (++c);

        return res;
    }

    private String nextLinkName(String prefix) {
        int c = 0;
        String res = prefix + c;
        while (model.getLinkById(res) != null)
            res = prefix + (++c);

        return res;
    }

    private static String nextPortName(GtsNode node) {
        int c = 1;
        String res = PORT_PREFIX + c;
        while (node.getPorts().get(res) != null)
            res = PORT_PREFIX + (++c);

        return res;
    }

    private static String nextPortName(GtsType type) {
        int c = 1;
        String res = PORT_PREFIX + c;
        while (type.getPorts().get(res) != null)
            res = PORT_PREFIX + (++c);

        return res;
    }

    public FXGtsLink addLink(GtsNode node1, GtsNode node2) {
        FXGtsLink link = gtsFactory.createGtsLink();
        link.setId(nextLinkName("link"));
        model.getLinks().add(link);

        //connect node1 to src-port of link
        GtsPort srcPort = gtsFactory.createGtsPort();
        srcPort.setId("src");
        link.addPort(srcPort);

        FXGtsPort port1 = addNextPortToNode(node1);
        connectPorts(node1, port1, link, srcPort);

        //connect node2 to dst-port of link
        GtsPort dstPort = gtsFactory.createGtsPort();
        dstPort.setId("dst");
        link.addPort(dstPort);

        FXGtsPort port2 = addNextPortToNode(node2);
        connectPorts(node2, port2, link, dstPort);


        return link;
    }

    public GtsType.Adjacency addAdjacency(GtsNode node, GtsPort port) {
        FXGtsPort newNodePort = addNextPortToNode(node);
        return connectPorts(node, newNodePort, null, port);
    }


    private FXGtsPort addNextPortToNode(GtsNode node) {
        String nextPortName;

        if (node instanceof FXGtsExternalNetwork) {
            if (!node.getPorts().isEmpty()) {
                return (FXGtsPort) node.getPorts().values().iterator().next();
            } else {
                nextPortName = ((FXGtsExternalNetwork) node).getRequiredPortName();
            }
        } else {
            nextPortName = nextPortName(node);
        }

        FXGtsPort port = gtsFactory.createGtsPort();
        port.setId(nextPortName);
        node.addPort(port);

        return port;
    }

    private GtsType.Adjacency connectPorts(@Nullable GtsNode node, @Nonnull GtsPort nodePort,
                                           @Nullable GtsLink link, @Nonnull GtsPort linkPort) {
        final GtsType.Adjacency adjacency =
                new GtsType.Adjacency(new GtsAdjacency.PortId(node != null ? node.getId() : null, nodePort.getId()),
                        new GtsAdjacency.PortId(link != null ? link.getId() : null, linkPort.getId()));
        model.getAdjacencies().add(adjacency);

        return adjacency;
    }

    public void removeNode(FXGtsNode node) {
        assert model.getNodes().contains(node);

        //remove all adjacency's to this node

        //save the links to remove in a list first, to prevent ConcurrentModificationExceptions
        List<FXGtsLink> linksToRemove = new ArrayList<>();
        for (FXGtsPort port : node.getPorts().values()) {
            FXGtsType.AdjacencyInfo adjacencyInfo = model.getAdjacencyInfo(port);

            if (adjacencyInfo instanceof FXGtsType.NodeToLinkAdjacencyInfo) {
                linksToRemove.add(((FXGtsType.NodeToLinkAdjacencyInfo) adjacencyInfo).getLink());
            } else if (adjacencyInfo instanceof FXGtsType.PortsAdjacencyInfo) {
                FXGtsType.PortsAdjacencyInfo pAdjInfo = (FXGtsType.PortsAdjacencyInfo) adjacencyInfo;
                model.getAdjacencies().remove(pAdjInfo.getAdjacency());
            }
        }
        linksToRemove.forEach(this::removeLink);

        assert node.getPorts().isEmpty() || node instanceof GtsExternalNetwork;

        model.getNodes().remove(node);
    }

    public void removeLink(FXGtsLink link) {
        assert model.getLinks().contains(link);

        for (FXGtsPort port : link.getPorts().values()) {
            FXGtsType.NodeToLinkAdjacencyInfo ntlAdjInfo =
                    (FXGtsType.NodeToLinkAdjacencyInfo) model.getAdjacencyInfo(port);

            model.getAdjacencies().remove(ntlAdjInfo.getAdjacency());

            if (!(ntlAdjInfo.getNode() instanceof GtsExternalNetwork))
                ntlAdjInfo.getNode().getPorts().remove(ntlAdjInfo.getNodePort().getId());
            //linkport will be removed when deleting the link
        }
        model.getLinks().remove(link);
    }
}
