package be.iminds.ilabt.jfed.gts.model.impl;

import be.iminds.ilabt.jfed.gts.model.*;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * User: twalcari
 * Date: 6/5/2015
 * Time: 15:56
 */
public class FXGtsType implements GtsType {
    private static final Logger LOG = LoggerFactory.getLogger(FXGtsType.class);

    private final StringProperty name = new SimpleStringProperty();
    private final StringProperty description = new SimpleStringProperty();
    private final StringProperty providerId = new SimpleStringProperty();
    private final BooleanProperty definition = new SimpleBooleanProperty();
    private final ObjectProperty<GtsStatus> status = new SimpleObjectProperty<>();
    private final ObservableMap<String, FXGtsPort> ports = FXCollections.observableHashMap();

    private final ObservableList<FXGtsNode> nodes = FXCollections.observableArrayList();
    private final ObservableList<FXGtsLink> links = FXCollections.observableArrayList();
    private final ObservableList<Adjacency> adjacencies = FXCollections.observableArrayList();

    private final ObservableMap<String, String> extraProperties = FXCollections.observableHashMap();

    {
        adjacencies.addListener((ListChangeListener<? super Adjacency>) change -> {
            while (change.next()) {
                if (change.wasAdded()) {
                    change.getAddedSubList().forEach(this::registerAdjacency);
                }

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

    public FXGtsType() {
    }

    public FXGtsType(String name) {
        this.name.set(name);
    }

    @Override
    public String getName() {
        return name.get();
    }

    public StringProperty nameProperty() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name.set(name);
    }

    @Override
    public String getDescription() {
        return description.get();
    }

    public StringProperty descriptionProperty() {
        return description;
    }

    public void setDescription(String description) {
        this.description.set(description);
    }

    @Override
    public ObservableMap<String, FXGtsPort> getPorts() {
        return ports;
    }

    @Override
    public void setPorts(Map<String, ? extends GtsPort> ports) {
        ports.forEach((id, port) -> this.ports.put(id, new FXGtsPort(port)));
        this.ports.keySet().retainAll(ports.keySet());
    }

    @Override
    public String getProviderId() {
        return providerId.get();
    }

    public StringProperty providerIdProperty() {
        return providerId;
    }

    public void setProviderId(String providerId) {
        this.providerId.set(providerId);
    }

    @Override
    public GtsStatus getStatus() {
        return status.get();
    }

    public ObjectProperty<GtsStatus> statusProperty() {
        return status;
    }

    public void setStatus(GtsStatus status) {
        this.status.set(status);
    }

    @Override
    public ObservableList<Adjacency> getAdjacencies() {
        return adjacencies;
    }

    @Override
    public void setAdjacencies(List<Adjacency> adjacencies) {
        this.adjacencies.setAll(adjacencies);
    }

    @Override
    public void addPort(GtsPort port) {
        assert port instanceof FXGtsPort;
        ports.put(port.getId(), (FXGtsPort) port);
    }

    @Override
    public ObservableList<FXGtsNode> getNodes() {
        return nodes;
    }

    @Override
    public void addNode(GtsNode node) {
        assert node instanceof FXGtsNode;
        nodes.add((FXGtsNode) node);
    }

    @Override
    public ObservableList<FXGtsLink> getLinks() {
        return links;
    }

    @Override
    public void addLink(GtsLink link) {
        assert link instanceof FXGtsLink;
        links.add((FXGtsLink) link);
    }

    @Override
    public ObservableMap<String, String> getExtraProperties() {
        return extraProperties;
    }

    public FXGtsNode getNodeById(String id) {
        return nodes.stream().filter(node -> Objects.equals(id, node.getId())).findFirst().orElse(null);
    }

    public FXGtsLink getLinkById(String id) {
        return links.stream().filter(link -> Objects.equals(id, link.getId())).findFirst().orElse(null);
    }

    public FXGtsPort getPortById(String id) {
        return ports.get(id);
    }

    public boolean isDefinition() {
        return definition.get();
    }

    public BooleanProperty definitionProperty() {
        return definition;
    }

    public void setDefinition(boolean definition) {
        this.definition.set(definition);
    }

    public GtsType.Adjacency findAdjacency(String parentName, String portName) {
        GtsAdjacency.PortId portId = new GtsAdjacency.PortId(parentName, portName);

        return adjacencies.stream()
                .filter(adj -> adj.getPortId1().equals(portId) || adj.getPortId2().equals(portId))
                .findFirst()
                .orElse(null);
    }

    public AdjacencyInfo getAdjacencyInfo(GtsType.Adjacency adjacency) {
        return adjacencyInfos.get(adjacency);
    }

    public AdjacencyInfo getAdjacencyInfo(FXGtsPort port) {
        return portToAdjacencyInfo.get(port);
    }

    public ObservableMap<Adjacency, AdjacencyInfo> getAdjacencyInfos() {
        return adjacencyInfos;
    }


    public class AdjacencyInfo {
        private final Adjacency adjacency;

        protected AdjacencyInfo(GtsType.Adjacency adjacency) {
            this.adjacency = adjacency;
        }

        public void register() {
            adjacencyInfos.put(adjacency, this);
        }

        public void unregister() {
            adjacencyInfos.remove(adjacency);
        }

        public Adjacency getAdjacency() {
            return adjacency;
        }
    }

    public class NodeToLinkAdjacencyInfo extends AdjacencyInfo {
        private final FXGtsNode node;
        private final FXGtsPort nodePort;

        private final FXGtsLink link;
        private final FXGtsPort linkPort;

        private final boolean nodeIsPortId1;

        private NodeToLinkAdjacencyInfo(GtsType.Adjacency adjacency, FXGtsNode node, FXGtsPort nodePort, FXGtsLink link, FXGtsPort linkPort, boolean nodeIsPortId1) {
            super(adjacency);
            this.node = node;
            this.nodePort = nodePort;
            this.link = link;
            this.linkPort = linkPort;
            this.nodeIsPortId1 = nodeIsPortId1;

            node.idProperty().addListener(observable -> {
                final GtsAdjacency.PortId newPortId = new GtsAdjacency.PortId(node.getId(), nodePort.getId());
                if (nodeIsPortId1) {
                    adjacency.setPortId1(newPortId);
                } else {
                    adjacency.setPortId2(newPortId);
                }
            });

            link.idProperty().addListener(observable -> {
                final GtsAdjacency.PortId newPortId = new GtsAdjacency.PortId(link.getId(), linkPort.getId());
                if (nodeIsPortId1) {
                    adjacency.setPortId2(newPortId);
                } else {
                    adjacency.setPortId1(newPortId);
                }
            });
        }

        public FXGtsNode getNode() {
            return node;
        }

        public FXGtsPort getNodePort() {
            return nodePort;
        }

        public FXGtsLink getLink() {
            return link;
        }

        public FXGtsPort getLinkPort() {
            return linkPort;
        }

        @Override
        public void register() {
            super.register();
            portToAdjacencyInfo.put(nodePort, this);
            portToAdjacencyInfo.put(linkPort, this);
        }

        @Override
        public void unregister() {
            super.unregister();
            portToAdjacencyInfo.remove(nodePort, this);
            portToAdjacencyInfo.remove(linkPort, this);

        }
    }

    public class PortsAdjacencyInfo extends AdjacencyInfo {
        private final FXGtsNode node;
        private final FXGtsPort nodePort;

        private final FXGtsPort standalonePort;

        private final boolean nodeIsPortId1;

        private PortsAdjacencyInfo(GtsType.Adjacency adjacency, FXGtsNode node, FXGtsPort nodePort, FXGtsPort standalonePort, boolean nodeIsPortId1) {
            super(adjacency);
            this.node = node;
            this.nodePort = nodePort;
            this.standalonePort = standalonePort;
            this.nodeIsPortId1 = nodeIsPortId1;

            node.idProperty().addListener(observable -> {
                final GtsAdjacency.PortId newPortId = new GtsAdjacency.PortId(node.getId(), nodePort.getId());
                if (nodeIsPortId1) {
                    adjacency.setPortId1(newPortId);
                } else {
                    adjacency.setPortId2(newPortId);
                }
            });

            standalonePort.idProperty().addListener(observable -> {
                final GtsAdjacency.PortId newPortId = new GtsAdjacency.PortId(null, standalonePort.getId());
                if (nodeIsPortId1) {
                    adjacency.setPortId2(newPortId);
                } else {
                    adjacency.setPortId1(newPortId);
                }
            });
        }

        public FXGtsNode getNode() {
            return node;
        }

        public FXGtsPort getNodePort() {
            return nodePort;
        }

        public FXGtsPort getStandalonePort() {
            return standalonePort;
        }

        @Override
        public void register() {
            super.register();
            portToAdjacencyInfo.put(nodePort, this);
            portToAdjacencyInfo.put(standalonePort, this);
        }

        @Override
        public void unregister() {
            super.unregister();
            portToAdjacencyInfo.remove(nodePort);
            portToAdjacencyInfo.remove(standalonePort);
        }
    }

    private final ObservableMap<GtsType.Adjacency, AdjacencyInfo> adjacencyInfos = FXCollections.observableHashMap();
    private final Map<FXGtsPort, AdjacencyInfo> portToAdjacencyInfo = new HashMap<>();

    private void registerAdjacency(final GtsType.Adjacency adjacency) {
        Object port1Parent = getPortParent(adjacency.getPortId1());
        Object port2Parent = getPortParent(adjacency.getPortId2());

        AdjacencyInfo adjacencyInfo = null;
        if (port1Parent instanceof FXGtsNode) {
            FXGtsNode node1 = (FXGtsNode) port1Parent;
            FXGtsPort node1Port = node1.getPorts().get(adjacency.getPortId1().getPortId());

            if (port2Parent instanceof FXGtsPort) {
                adjacencyInfo = new PortsAdjacencyInfo(adjacency, node1, node1Port, (FXGtsPort) port2Parent, true);
            } else if (port2Parent instanceof FXGtsLink) {
                FXGtsLink link2 = (FXGtsLink) port2Parent;
                FXGtsPort link2Port = link2.getPorts().get(adjacency.getPortId2().getPortId());
                adjacencyInfo = new NodeToLinkAdjacencyInfo(adjacency, node1, node1Port, link2, link2Port, true);
            }
        } else if (port1Parent instanceof FXGtsPort) {
            if (port2Parent instanceof FXGtsNode) {
                FXGtsNode node2 = (FXGtsNode) port2Parent;
                FXGtsPort node2Port = node2.getPorts().get(adjacency.getPortId2().getPortId());

                adjacencyInfo = new PortsAdjacencyInfo(adjacency, node2, node2Port, (FXGtsPort) port1Parent, false);
            }
        } else if (port1Parent instanceof FXGtsLink) {
            FXGtsLink link1 = (FXGtsLink) port1Parent;
            FXGtsPort link1Port = link1.getPorts().get(adjacency.getPortId1().getPortId());

            if (port2Parent instanceof FXGtsNode) {
                FXGtsNode node2 = (FXGtsNode) port2Parent;
                FXGtsPort node2Port = node2.getPorts().get(adjacency.getPortId2().getPortId());

                adjacencyInfo = new NodeToLinkAdjacencyInfo(adjacency, node2, node2Port, link1, link1Port, false);
            }
        }

        if (adjacencyInfo == null) {
            LOG.error("Adjacency {} is not valid: could not create a AdjacencyInfo for it", adjacency);
            return;
        }

        adjacencyInfo.register();
    }

    private void unregisterAdjacency(final GtsType.Adjacency adjacency) {
        adjacencyInfos.get(adjacency).unregister();
    }


    private Object getPortParent(GtsAdjacency.PortId portId) {
        if (portId.getParentId() == null)
            return getPorts().get(portId.getPortId());

        FXGtsNode node = getNodeById(portId.getParentId());
        if (node != null) {
            if (!node.getPorts().containsKey(portId.getPortId())) {
                LOG.error("Found node with id {}, but it does not have a port called {}", portId.getParentId(), portId.getPortId());
                return null;
            } else
                return node;
        }

        FXGtsLink link = getLinkById(portId.getParentId());
        if (link != null) {
            if (!link.getPorts().containsKey(portId.getPortId())) {
                LOG.error("Found link with id {}, but it does not have a port called {}", portId.getParentId(), portId.getPortId());
                return null;
            } else
                return link;
        }

        LOG.error("Could not find a node or link with id {}", portId.getParentId());
        return null;
    }
}
