package be.iminds.ilabt.jfed.experimenter_gui.canvas.rspec;

import be.iminds.ilabt.jfed.experimenter_gui.config.JFedGuiConfig;
import be.iminds.ilabt.jfed.experimenter_gui.canvas.ExperimentCanvas;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.ResourceClass;
import be.iminds.ilabt.jfed.gts.GtsDslReader;
import be.iminds.ilabt.jfed.gts.model.GtsPort;
import be.iminds.ilabt.jfed.gts.model.impl.FXGtsExternalNetwork;
import be.iminds.ilabt.jfed.gts.model.impl.FXGtsNode;
import be.iminds.ilabt.jfed.gts.model.impl.FXGtsType;
import be.iminds.ilabt.jfed.rspec.model.GeantTestbedType;
import be.iminds.ilabt.jfed.rspec.model.javafx_impl.*;
import be.iminds.ilabt.jfed.util.GeniUrn;
import javafx.beans.InvalidationListener;
import javafx.collections.ListChangeListener;
import javafx.collections.WeakListChangeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.StringReader;
import java.util.*;

/**
 * User: twalcari
 * Date: 2/9/2016
 * Time: 15:26
 */
public class ModelRspecToCanvasBinding {
    private static final Logger LOG = LoggerFactory.getLogger(ModelRspecToCanvasBinding.class);

    protected final FXModelRspec model;
    private final ExperimentCanvas canvas;
    private final JFedGuiConfig config;
    private final CanvasNodeFactory canvasNodeFactory;


    private final Map<FXRspecNode, RspecCanvasNode> nodes = new HashMap<>();
    private final Map<FXRspecLink, RspecCanvasLink> links = new HashMap<>();
    private final Map<FXRspecChannel, RspecChannelCanvasNode> channels = new HashMap<>();
    private final Map<FXGeantTestbedType, RspecGTSNode> geantTestbedTypes = new HashMap<>();
    private final Map<FXAddressPool, AddressPoolCanvasNode> addressPools = new HashMap<>();
    private final Map<FXGeantTestbedType.ActiveExternalDomain, GtsVlanLink> gtsVlanLinksByActiveExternalDomain =
            new HashMap<>();
    private final Map<FXRspecNode, GtsVlanLink> gtsVlanLinksByNode = new HashMap<>();

    private final ListChangeListener<FXRspecNode> rspecNodeListChangeListener = change -> {
        while (change.next()) {
            if (change.wasAdded()) {
                change.getAddedSubList().forEach(this::addNodeToCanvas);
            }

            if (change.wasRemoved()) {
                change.getRemoved().forEach(this::removeNodeFromCanvas);
            }
        }
    };
    private final ListChangeListener<FXRspecLink> rspecLinkListChangeListener = change -> {
        while (change.next()) {
            if (change.wasAdded()) {
                change.getAddedSubList().forEach(this::addLinkToCanvas);
            }

            if (change.wasRemoved()) {
                change.getRemoved().forEach(this::removeLinkFromCanvas);
            }
        }
    };
    private final ListChangeListener<FXRspecChannel> rspecChannelListChangeListener = change -> {
        while (change.next()) {
            if (change.wasAdded()) {
                change.getAddedSubList().forEach(this::addChannelToCanvas);
            }

            if (change.wasRemoved()) {
                change.getRemoved().forEach(this::removeChannelFromCanvas);
            }
        }
    };
    private final ListChangeListener<FXAddressPool> addressPoolListChangeListener = change -> {
        while (change.next()) {
            if (change.wasAdded()) {
                change.getAddedSubList().forEach(this::addAddressPoolToCanvas);
            }

            if (change.wasRemoved()) {
                change.getRemoved().forEach(this::removeAddressPoolFromCanvas);
            }
        }
    };
    private final ListChangeListener<FXGeantTestbedType> geantTestbedTypeListChangeListener = change -> {
        while (change.next()) {
            if (change.wasAdded()) {
                change.getAddedSubList().forEach(this::addGeantTestbedTypeToCanvas);
            }

            if (change.wasRemoved()) {
                change.getRemoved().forEach(this::removeGeantTestbedTypeFromCanvas);
            }
        }
    };

    private final InvalidationListener gtsVlanListener = observable -> checkGtsVlans();
    private final ListChangeListener<FXGeantTestbedType.ActiveExternalDomain> checkRemovedDomainsListener =
            c -> {
                while (c.next()) {
                    c.getRemoved().forEach(this::removeGtsVlanLink);
                }
            };

    private void checkGtsVlans() {
        for (FXGeantTestbedType geantTestbedType : geantTestbedTypes.keySet()) {
            for (FXGeantTestbedType.ActiveExternalDomain activeExternalDomain : geantTestbedType.getActiveExternalDomains()) {

                if (gtsVlanLinksByActiveExternalDomain.containsKey(activeExternalDomain))
                    continue;

                FXGeantTestbedType.GtsVlan vlan = FXGeantTestbedType.findGtsVlan(activeExternalDomain);

                if (vlan == null)
                    continue;

                final FXRspecNode dedicatedConnectionNode = findDedicatedConnectionNode(vlan.getVlanUrn());
                if (dedicatedConnectionNode == null)
                    continue;

                GtsVlanLink link =
                        new GtsVlanLink(geantTestbedTypes.get(geantTestbedType), nodes.get(dedicatedConnectionNode), activeExternalDomain);
                gtsVlanLinksByActiveExternalDomain.put(activeExternalDomain, link);
                gtsVlanLinksByNode.put(dedicatedConnectionNode, link);
                canvas.addLink(link);

            }
        }
    }

    private void removeGtsVlanLink(FXGeantTestbedType.ActiveExternalDomain activeExternalDomain) {
        GtsVlanLink link = gtsVlanLinksByActiveExternalDomain.remove(activeExternalDomain);
        if (link != null) {
            gtsVlanLinksByNode.remove(link.getNodeB().getRspecNode());
            canvas.removeLink(link);
        }
    }


    private FXRspecNode findDedicatedConnectionNode(GeniUrn urn) {
        for (FXRspecNode node : model.getNodes()) {
            if (Objects.equals(urn, node.getComponentManagerId()))
                return node;
        }
        return null;
    }


    public ModelRspecToCanvasBinding(FXModelRspec model, ExperimentCanvas canvas, JFedGuiConfig config, CanvasNodeFactory canvasNodeFactory) {
        this.model = model;
        this.canvas = canvas;
        this.config = config;
        this.canvasNodeFactory = canvasNodeFactory;

        for (FXRspecNode node : model.getNodes()) {
            if (node.getResourceClassId() == null) {
                ResourceClass resourceClass = this.config.findBestResourceClass(node);
                if (resourceClass != null) {
                    node.setResourceClassId(resourceClass.getId());
                }
            }

            addNodeToCanvas(node);
        }

        model.getLinks().forEach(this::addLinkToCanvas);
        model.getChannels().forEach(this::addChannelToCanvas);
        model.getGeantTestbedTypes().forEach(this::addGeantTestbedTypeToCanvas);
        model.getAddressPools().forEach(this::addAddressPoolToCanvas);

        //bind model and GUI
        model.getNodes().addListener(new WeakListChangeListener<>(rspecNodeListChangeListener));
        model.getLinks().addListener(new WeakListChangeListener<>(rspecLinkListChangeListener));
        model.getChannels().addListener(new WeakListChangeListener<>(rspecChannelListChangeListener));
        model.getGeantTestbedTypes().addListener(new WeakListChangeListener<>(geantTestbedTypeListChangeListener));
        model.getAddressPools().addListener(new WeakListChangeListener<>(addressPoolListChangeListener));

        //check if we have to draw links between rspec-vlan-nodes and geant-testbed-nodes
        computeGtsActiveDomains();
        checkGtsVlans();
    }

    private void computeGtsActiveDomains(){
        for (FXGeantTestbedType gtt : geantTestbedTypes.keySet()) {
            if(gtt.getDsl() == null)
                continue;

            try {
                GtsDslReader reader = new GtsDslReader(new StringReader(gtt.getDsl()));
                FXGtsType type = (FXGtsType) reader.call();

                List<FXGeantTestbedType.ActiveExternalDomain> currentDomains = new ArrayList<>();

                for (FXGtsNode node : type.getNodes()) {
                    if (node instanceof FXGtsExternalNetwork) {
                        FXGtsExternalNetwork externalNetwork = (FXGtsExternalNetwork) node;
                        if (externalNetwork.getPorts().isEmpty())
                            continue;
                        GtsPort port = externalNetwork.getPorts().values().iterator().next();

                        currentDomains.add(new FXGeantTestbedType.ActiveExternalDomain(node.getLocation(), port.getId()));
                    }
                }
                gtt.getActiveExternalDomains().setAll(currentDomains);
            }catch(GtsDslReader.GtsDslParseException ex){
                LOG.warn("Could not parse dsl of {}. Ignoring", gtt.getName());
            }
        }
    }

    /**
     * Should only be called by the addNodeToCanvasAndModel or registerModel-method
     */
    private void addNodeToCanvas(FXRspecNode rspecNode) {
        RspecCanvasNode newNode = canvasNodeFactory.createRspecCanvasNode(rspecNode);
        nodes.put(rspecNode, newNode);
        canvas.addNode(newNode);
    }

    private void addLinkToCanvas(FXRspecLink rspecLink) {
        RspecCanvasLink newLink = new RspecCanvasLink(rspecLink, this);
        links.put(rspecLink, newLink);

    }

    private void removeLinkFromCanvas(FXRspecLink rspecLink) {
        RspecCanvasLink rspecCanvasLink = links.get(rspecLink);
        rspecCanvasLink.remove();
        links.remove(rspecLink);
    }

    private void addChannelToCanvas(FXRspecChannel rspecChannel) {
        RspecChannelCanvasNode newChannel = canvasNodeFactory.createRspecChannelCanvasNode(rspecChannel);
        channels.put(rspecChannel, newChannel);
        canvas.addNode(newChannel);
    }

    private void removeChannelFromCanvas(FXRspecChannel rspecChannel) {
        RspecChannelCanvasNode rspecChannelCanvasNode = channels.remove(rspecChannel);
        canvas.removeNode(rspecChannelCanvasNode);
    }

    public RspecGTSNode getRspecGTSNode(GeantTestbedType gtt) {
        //noinspection SuspiciousMethodCalls
        return geantTestbedTypes.get(gtt);
    }

    public AddressPoolCanvasNode getAddressPoolCanvasNode(FXAddressPool addressPool) {
        return addressPools.get(addressPool);
    }

    private void addGeantTestbedTypeToCanvas(final FXGeantTestbedType gtt) {
        RspecGTSNode newGTSNode = canvasNodeFactory.createRspecGTSNode(gtt);
        geantTestbedTypes.put(gtt, newGTSNode);
        canvas.addNode(newGTSNode);

        gtt.getActiveExternalDomains().addListener(gtsVlanListener);
        gtt.getActiveExternalDomains().addListener(checkRemovedDomainsListener);
    }

    private void removeGeantTestbedTypeFromCanvas(FXGeantTestbedType gtt) {
        RspecGTSNode gtsNode = geantTestbedTypes.remove(gtt);
        canvas.removeNode(gtsNode);
    }

    private void addAddressPoolToCanvas(FXAddressPool addressPool) {
        AddressPoolCanvasNode newAddressPoolCanvasNode =
                canvasNodeFactory.createAddressPoolCanvasNode(addressPool);
        addressPools.put(addressPool, newAddressPoolCanvasNode);
        canvas.addNode(newAddressPoolCanvasNode);
    }

    private void removeAddressPoolFromCanvas(FXAddressPool addressPool) {
        AddressPoolCanvasNode addressPoolCanvasNode = addressPools.remove(addressPool);
        canvas.removeNode(addressPoolCanvasNode);
    }

    private void removeNodeFromCanvas(FXRspecNode rspecNode) {
        //remove from canvas
        RspecCanvasNode rspecCanvasNode = nodes.remove(rspecNode);
        canvas.removeNode(rspecCanvasNode);

        GtsVlanLink gtsVlanLink = gtsVlanLinksByNode.remove(rspecCanvasNode.getRspecNode());
        if (gtsVlanLink != null) {
            gtsVlanLinksByActiveExternalDomain.remove(gtsVlanLink.getActiveExternalDomain());
            canvas.removeLink(gtsVlanLink);
        }
    }


    public RspecCanvasNode getRspecCanvasNode(FXRspecNode node) {
        return nodes.get(node);
    }

    public RspecCanvasLink getRspecCanvasLink(FXRspecLink link) {
        return links.get(link);
    }

    public FXModelRspec getModelRspec() {
        return model;
    }

    public ExperimentCanvas getExperimentCanvas() {
        return canvas;
    }
}
