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

import be.iminds.ilabt.jfed.experimenter_gui.editor.properties.map.ImageMap;
import be.iminds.ilabt.jfed.experimenter_gui.util.TestbedNodesMapFetcher;
import be.iminds.ilabt.jfed.fedmon.webapi.client.FedmonWebApiClient;
import be.iminds.ilabt.jfed.fedmon.webapi.service.json.*;
import be.iminds.ilabt.jfed.lowlevel.TestbedInfoSource;
import be.iminds.ilabt.jfed.lowlevel.authority.AuthorityFinder;
import be.iminds.ilabt.jfed.lowlevel.fedmon.TestResult;
import be.iminds.ilabt.jfed.lowlevel.resourceid.ResourceUrn;
import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.rspec.rspec_source.AdvertisementRspecSource;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.TextUtil;
import com.google.common.collect.ImmutableSet;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.scene.paint.Color;
import javafx.util.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * JFedGuiConfigImpl
 */
@Singleton
public class JFedGuiConfigImpl implements JFedGuiConfig {
    private static final Logger LOG = LoggerFactory.getLogger(JFedGuiConfigImpl.class);

    private final TestbedInfoSource testbedInfoSource;
    private final AuthorityFinder authorityFinder;
    private final JFedExperimenterGuiConfig jFedExperimenterGuiConfig;

    private final Map<String, ResourceClass> resourceClassById;
    private final Map<ResourceType, List<ResourceClass>> resourceClassesByType;

    private final Map<String, Integer> randomByResourceClassId;

    private final List<Server> bindableServers;
    private final List<Server> serversInVisibleResources;
    private final List<Server> hiddenServers;
    private final List<TriggeredMessage> triggeredMessages;

    /**
     * aggregated advertisement info from multiple sources, with ListProperty for javaFX usage.
     */
    private static class AdvertisementProperties {
        private final int serverId;

        private final
        @Nonnull
        ListProperty<DiskImage> diskImages;
        private final
        @Nonnull
        Map<String, ListProperty<DiskImage>> diskImagesBySliverType;
        private final
        @Nonnull
        ListProperty<String> sliverTypeNames;
        private final
        @Nonnull
        ListProperty<HardwareTypeInfo> allHardwareTypes;
        private final
        @Nonnull
        ListProperty<HardwareTypeInfo> availableHardwareTypes;
        private final
        @Nonnull
        ListProperty<GeniUrn> componentManagerIds;
        private final
        @Nonnull
        ListProperty<ComponentInfo> allComponentInfos;
        private final
        @Nonnull
        ListProperty<ComponentInfo> availableComponentInfos;

        public AdvertisementProperties(int serverId) {
            this.serverId = serverId;

            this.diskImages = new SimpleListProperty<>(FXCollections.observableArrayList());
            this.diskImagesBySliverType = new HashMap<>();
            this.sliverTypeNames = new SimpleListProperty<>(FXCollections.observableArrayList());
            this.allHardwareTypes = new SimpleListProperty<>(FXCollections.observableArrayList());
            this.availableHardwareTypes = new SimpleListProperty<>(FXCollections.observableArrayList());
            this.componentManagerIds = new SimpleListProperty<>(FXCollections.observableArrayList());
            this.allComponentInfos = new SimpleListProperty<>(FXCollections.observableArrayList());
            this.availableComponentInfos = new SimpleListProperty<>(FXCollections.observableArrayList());
        }

        public void set(AdvertisementInfo infoAll, AdvertisementInfo infoAvail) {
            this.allComponentInfos.setAll(infoAll.componentInfos);
            this.availableComponentInfos.setAll(infoAvail.componentInfos.stream().filter(ComponentInfo::isAvailable).collect(Collectors.toList()));
            this.componentManagerIds.setAll(infoAvail.componentManagerIds);
            this.diskImages.setAll(infoAvail.diskImages);
            this.allHardwareTypes.setAll(infoAll.hardwareTypes);
            this.availableHardwareTypes.setAll(infoAvail.hardwareTypes.stream().filter(hti -> hti.isAvailable() != Boolean.FALSE).collect(Collectors.toList()));
            this.sliverTypeNames.setAll(infoAvail.sliverTypeNames);

            Set<String> removedKeys = new HashSet<>(this.diskImagesBySliverType.keySet());
            removedKeys.removeAll(infoAvail.diskImagesBySliverType.keySet());
            for (String key : removedKeys) {
                this.diskImagesBySliverType.remove(key);
            }
            for (Map.Entry<String, SortedSet<DiskImage>> e : infoAvail.diskImagesBySliverType.entrySet()) {
                String sliverType = e.getKey();
                SortedSet<DiskImage> target = e.getValue();
                ListProperty<DiskImage> orig =
                        this.diskImagesBySliverType.computeIfAbsent(sliverType, k -> new SimpleListProperty<>(FXCollections.observableArrayList()));
                orig.setAll(target);
            }
        }
    }


    private enum AdvertisementInfoSourceType {
        ServerGlimpse, AvailableAdvertisementRspec, AllAdvertisementRspec, FedmonNodeStatus, ResourceClass, Best, User
    }

    private static class AdvertisementInfoSourceId {
        public final Integer serverId;
        public final AdvertisementInfoSourceType type;

        public AdvertisementInfoSourceId(Integer serverId, AdvertisementInfoSourceType type) {
            assert serverId != null;
            assert type != null;
            this.serverId = serverId;
            this.type = type;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;

            AdvertisementInfoSourceId that = (AdvertisementInfoSourceId) o;

            if (!Objects.equals(serverId, that.serverId)) return false;
            if (type != that.type) return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = serverId.hashCode();
            result = 31 * result + type.hashCode();
            return result;
        }
    }

    /**
     * advertisement info from 1 specific source, with SortedSets for easy adding and sorting.
     */
    private static class AdvertisementInfo {
        private final
        @Nonnull
        AdvertisementInfoSourceId sourceId;

        private
        @Nullable
        Date date;

        private final
        @Nonnull
        SortedSet<DiskImage> diskImages;
        private final
        @Nonnull
        Map<String, SortedSet<DiskImage>> diskImagesBySliverType;
        private final
        @Nonnull
        SortedSet<String> sliverTypeNames;
        private final
        @Nonnull
        SortedSet<HardwareTypeInfo> hardwareTypes;
        private final
        @Nonnull
        SortedSet<GeniUrn> componentManagerIds;
        private final
        @Nonnull
        SortedSet<ComponentInfo> componentInfos;

        private final static Comparator<GeniUrn> geniUrnComparator = Comparator.comparing(ResourceUrn::getValue);
        private final static Comparator<DiskImage> diskImageComparator = (o1, o2) -> {
            //if name or url are the same, they are always the same!
            if (o1.getName() != null && o2.getName() != null && Objects.equals(o1.getName(), o2.getName()))
                return 0;
            if (o1.getUrl() != null && o2.getUrl() != null && Objects.equals(o1.getUrl(), o2.getUrl()))
                return 0;

            //compare name if available, URL if name is not available
            String s1 = o1.getName() == null ? o1.getUrl() : o1.getName();
            String s2 = o2.getName() == null ? o2.getUrl() : o2.getName();
            assert s1 != null;
            assert s2 != null;
            return s1.compareTo(s2);
        };

        public AdvertisementInfo(Integer serverId, AdvertisementInfoSourceType type) {
            this(new AdvertisementInfoSourceId(serverId, type));
        }

        public AdvertisementInfo(@Nonnull AdvertisementInfoSourceId sourceId) {
            this.sourceId = sourceId;


            this.diskImages = new TreeSet<>(diskImageComparator);
            this.diskImagesBySliverType = new HashMap<>();
            this.sliverTypeNames = new TreeSet<>();
            this.hardwareTypes = new TreeSet<>();
            this.componentManagerIds = new TreeSet<>(geniUrnComparator);
            this.componentInfos = new TreeSet<>();
        }

        public SortedSet<DiskImage> getDiskImages(String sliverType) {
            return diskImagesBySliverType.computeIfAbsent(sliverType,
                    key -> new TreeSet<>(diskImageComparator)
            );
        }

        public boolean hasInfo() {
            if (!diskImages.isEmpty()) return true;
            if (!sliverTypeNames.isEmpty()) return true;
            if (!hardwareTypes.isEmpty()) return true;
            if (!componentManagerIds.isEmpty()) return true;
            if (!componentInfos.isEmpty()) return true;
            return false;
        }

        public boolean olderThan(Duration duration) {
            if (date == null)
                return false;

            Date now = new Date();
            Date comp = new Date(now.getTime() - duration.toMillis());
            return date.before(comp);
        }

        /**
         * Fill all sets with copies of the values in the given AdvertisementInfo.
         * Any existing data not in the new AdvertisementInfo is discarded.
         */
        public void set(AdvertisementInfo other) {
            this.componentInfos.clear();
            this.componentManagerIds.clear();
            this.diskImages.clear();
            this.hardwareTypes.clear();
            this.sliverTypeNames.clear();

            this.componentInfos.addAll(other.componentInfos);
            this.componentManagerIds.addAll(other.componentManagerIds);
            this.diskImages.addAll(other.diskImages);
            this.hardwareTypes.addAll(other.hardwareTypes);
            this.sliverTypeNames.addAll(other.sliverTypeNames);

            Set<String> removedKeys = new HashSet<>(this.diskImagesBySliverType.keySet());
            removedKeys.removeAll(other.diskImagesBySliverType.keySet());
            for (String key : removedKeys) {
                this.diskImagesBySliverType.remove(key);
            }
            for (Map.Entry<String, SortedSet<DiskImage>> e : other.diskImagesBySliverType.entrySet()) {
                String sliverType = e.getKey();
                SortedSet<DiskImage> target = e.getValue();
                SortedSet<DiskImage> orig =
                        this.diskImagesBySliverType.computeIfAbsent(sliverType, k -> new TreeSet<>(diskImageComparator));
                orig.clear();
                orig.addAll(target);
            }
        }

        public void addSliverTypeNames(Collection<String> sliverTypeNames) {
            this.sliverTypeNames.addAll(sliverTypeNames);
        }

        public void addSliverTypeName(String sliverTypeName) {
            this.sliverTypeNames.add(sliverTypeName);
        }

        public void addHardwareTypeNames(Collection<String> hardwareTypeNames, Boolean available) {
            for (String hardwareTypeName : hardwareTypeNames)
                addHardwareTypeName(hardwareTypeName, available);
        }

        public void addHardwareTypes(Collection<HardwareTypeInfo> hardwareTypes) {
            this.hardwareTypes.addAll(hardwareTypes);
        }

        public void addHardwareTypeName(String hardwareTypeName, Boolean available) {
            this.hardwareTypes.add(new HardwareTypeInfo(hardwareTypeName, available));
        }

        public void addHardwareType(HardwareTypeInfo hardwareType) {
            this.hardwareTypes.add(hardwareType);
        }

        public void addDiskImages(Collection<DiskImage> diskImages) {
            this.diskImages.addAll(diskImages);
        }

        public void addDiskImage(DiskImage diskImage) {
            this.diskImages.add(diskImage);
        }

        public void addDiskImage(String sliverType, DiskImage diskImage) {
            this.getDiskImages(sliverType).add(diskImage);
        }

        public void addComponentInfos(Collection<ComponentInfo> componentInfos) {
            this.componentInfos.addAll(componentInfos);
        }

        public void addComponentInfo(ComponentInfo componentInfo) {
            this.componentInfos.add(componentInfo);
        }

        public void addDiskImage(String diskImageId) {
            assert diskImageId != null;
            DiskImage diskImage =
                    diskImageId.startsWith("http") ?
                            new DiskImage(null, diskImageId, null, null, null, null) :
                            new DiskImage(diskImageId);

            addDiskImage(diskImage);
        }

        public void addComponentInfo(GeniUrn componentUrn, boolean available) {
            ComponentInfo componentInfo =
                    new ComponentInfo(componentUrn.getResourceName(), componentUrn, available, null, null, null);
            addComponentInfo(componentInfo);
        }

        public void clear() {
            this.componentInfos.clear();
            this.componentManagerIds.clear();
            this.diskImages.clear();
            this.hardwareTypes.clear();
            this.sliverTypeNames.clear();
            this.diskImagesBySliverType.clear();
            date = null;
        }
    }

    private final Map<AdvertisementInfoSourceId, AdvertisementInfo> advertisementInfoById = new HashMap<>();

    private final Map<Integer, AdvertisementProperties> advertisementPropertiesByServerId = new HashMap<>();


    Map<Integer, ObjectProperty<AdvertisementRspecSource>> availableAdvertisementByServerId;
    Map<Integer, ObjectProperty<AdvertisementRspecSource>> allAdvertisementByServerId;
    Map<Integer, ObjectProperty<AdvertisementRspecSource>> anyAdvertisementByServerId;

    private final HashMap<Integer, ImageMap<GeniUrn>> imageMapByServerId;

    private final HashMap<Integer, ServerGlimpse> serverGlimpseByServerId;

    private final List<Resource> allResources;
    private final Map<Integer, List<Resource>> resourcesByServerId;
    private final Map<Pair<Integer, String>, List<Resource>> resourcesByServerIdAndSliverName;

    private final ResourceClass otherServerResourceClass;

    private final static Random rand = new Random();

    @Inject
    public JFedGuiConfigImpl(
            TestbedInfoSource testbedInfoSource,
            AuthorityFinder authorityFinder,
            JFedExperimenterGuiConfigProvider jFedExperimenterGuiConfigProvider,
            TestbedNodesMapFetcher testbedNodesMapFetcher,
            FedmonWebApiClient fedmonWebApiClient) {
        LOG.debug("Initialising JFedGuiConfigImpl");

        this.testbedInfoSource = testbedInfoSource;
        this.authorityFinder = authorityFinder;
        this.jFedExperimenterGuiConfig = jFedExperimenterGuiConfigProvider.get();

        assert testbedInfoSource != null;
        assert jFedExperimenterGuiConfig != null;

        resourceClassById = link(testbedInfoSource, jFedExperimenterGuiConfig.getResourceClasses());

        //add automatically created resourceClass with Resources for all servers not yet used in a Resource
        otherServerResourceClass = generateOtherServerResourceClass(testbedInfoSource, resourceClassById.values());
        resourceClassById.put(otherServerResourceClass.getId(), otherServerResourceClass);

        resourceClassesByType = new EnumMap<>(ResourceType.class);
        allResources = new ArrayList<>();
        resourcesByServerId = new HashMap<>();
        resourcesByServerIdAndSliverName = new HashMap<>();
        for (ResourceClass resourceClass : resourceClassById.values()) {
            ResourceType resourceType = getResourceType(resourceClass);
            assert resourceType != null;
            if (resourceType != null) {
                List<ResourceClass> resourceClasses =
                        resourceClassesByType.computeIfAbsent(resourceType, key -> new ArrayList<>());
                resourceClasses.add(resourceClass);
            } else {
                LOG.error("getResourceType(" + resourceClass.getId() + ") returns null");
            }

            for (Resource resource : resourceClass.getResources()) {
                Server server = resource.getServer();
                String sliverType = resource.getSliverType();

//                assert server != null;
//                assert server.getId() != null;

                if (server != null && server.getId() != null) {
                    allResources.add(resource);

                    resourcesByServerId.
                            computeIfAbsent(server.getId(), key -> new ArrayList<>()).
                            add(resource);

                    if (sliverType != null) {
                        resourcesByServerIdAndSliverName.
                                computeIfAbsent(new Pair<>(server.getId(), sliverType), key -> new ArrayList<>()).
                                add(resource);
                    }
                }
            }
        }

        this.bindableServers = getFullServers(jFedExperimenterGuiConfig.getBindableServers());
        this.hiddenServers = getFullServers(jFedExperimenterGuiConfig.getHideServers());

        triggeredMessages =
                jFedExperimenterGuiConfig.getTriggeredMessages() == null ?
                        Collections.emptyList() :
                        Collections.unmodifiableList(new ArrayList<>(jFedExperimenterGuiConfig.getTriggeredMessages()));

        Set<Server> serversInVisibleResourcesTmp = new TreeSet<>(
                Comparator.comparing(Server::getId, Comparator.nullsLast(Comparator.naturalOrder()))
        );

        //fill advertisement RSpec caches with info from Resources
        for (ResourceClass resourceClass : resourceClassById.values()) {
            assert resourceClass.getResources() != null;
            if (resourceClass.getResources() == null)
                continue;
            for (Resource resource : resourceClass.getResources()) {
                Server server = resource.getServer();
//                assert server != null;
                if (server == null)
                    continue;

                if (resourceClass.getVisibleInToolbox()) {
                    serversInVisibleResourcesTmp.add(server);
                }

                if (resource.getSliverType() != null) {
                    getAdvertisementInfo(server, AdvertisementInfoSourceType.ResourceClass)
                            .addSliverTypeName(resource.getSliverType());
                }
                if (resource.getHardwareType() != null) {
                    getAdvertisementInfo(server, AdvertisementInfoSourceType.ResourceClass)
                            .addHardwareTypeName(resource.getHardwareType(), null);
                }
                if (resource.getDiskImage() != null) {
                    DiskImage thisDiskImage;
                    if (resource.getDiskImage().startsWith("http"))
                        thisDiskImage = new DiskImage(null, resource.getDiskImage(), null, null, null, null);
                    else
                        thisDiskImage = new DiskImage(resource.getDiskImage());

                    getAdvertisementInfo(server, AdvertisementInfoSourceType.ResourceClass)
                            .addDiskImage(thisDiskImage);

                    if (resource.getSliverType() != null) {
                        getAdvertisementInfo(server, AdvertisementInfoSourceType.ResourceClass)
                                .addDiskImage(resource.getSliverType(), thisDiskImage);
                    }

                }
            }
        }

        this.serversInVisibleResources = Collections.unmodifiableList(new ArrayList<>(serversInVisibleResourcesTmp));

        //these will be filled in on the fly
        availableAdvertisementByServerId = new HashMap<>();
        allAdvertisementByServerId = new HashMap<>();
        anyAdvertisementByServerId = new HashMap<>();

        imageMapByServerId = new HashMap<>();
        if (testbedNodesMapFetcher != null) {
            CompletableFuture.supplyAsync(testbedNodesMapFetcher::call)
                    .thenAccept(map -> {
                        for (Map.Entry<GeniUrn, ImageMap<GeniUrn>> nodesMapEntry : map.entrySet()) {
                            GeniUrn urn = nodesMapEntry.getKey();
                            Server server = authorityFinder.findByAnyUrn(urn, AuthorityFinder.Purpose.REQUEST_RSPEC);

                            if (server != null) {
                                imageMapByServerId.put(server.getId(), nodesMapEntry.getValue());
                            } else {
                                LOG.info("Received nodes map of unknown testbed '{}'", nodesMapEntry.getKey());
                            }
                        }
                    });
        }

        serverGlimpseByServerId = new HashMap<>();
        if (fedmonWebApiClient != null) {
            CompletableFuture.supplyAsync(() -> {
                try {
                    return fedmonWebApiClient.getAll(ServerGlimpse.class);
                } catch (FedmonWebApiClient.FedmonWebApiClientException e) {
                    LOG.error("Error fetching all ServerGlimpse", e);
                    return Collections.<ServerGlimpse>emptyList();
                }
            }).thenAccept(serverGlimpseList -> {
                LOG.info("Received ServerGlimpse list with {} ServerGlimpses", serverGlimpseList.size());
                int nonNullCount = 0;
                int nullCount = 0;
                int failCount = 0;
                for (ServerGlimpse serverGlimpse : serverGlimpseList) {
                    if (serverGlimpse.getServerId() != null) {
                        try {
                            serverGlimpseByServerId.put(serverGlimpse.getServerId(), serverGlimpse);
                            setServerGlimpse(serverGlimpse.getServerId(), serverGlimpse);
                            nonNullCount++;
                        } catch (Throwable e) {
                            LOG.error("Exception while processing received ServerGlimpse", e);
                            failCount++;
                        }
                    } else {
                        LOG.info("Received ServerGlimpse without server: {} ", serverGlimpse);
                        nullCount++;
                    }
                }
                LOG.info("Processed ServerGlimpse list with {} nonNull ServerGlimpses, {} exceptions and {} nulls",
                        nonNullCount, failCount, nullCount);
            });
        }


        randomByResourceClassId = new HashMap<>();

        initResourceClassAdvertisementInfo();
    }

    @Nonnull
    private static ResourceClass generateOtherServerResourceClass(TestbedInfoSource testbedInfoSource, Collection<ResourceClass> resourceClasses) {
        ResourceClassBuilder res = new ResourceClassBuilder();

        Set<Integer> serverIds = new HashSet<>();
        for (ResourceClass rc : resourceClasses) {
            for (Resource r: rc.getResources()) {
                if (r.getServerId() != null) {
                    serverIds.add(r.getServerId());
                }
            }
        }

        AtomicInteger fakeId = new AtomicInteger(10000);
        List<ResourceBuilder> otherServersResources =
                testbedInfoSource.getServers().stream()
                        .filter(s -> s.getId() != null)
                        .filter(s -> !serverIds.contains(s.getId()))
                        .map(s -> {
                            ResourceBuilder newResource = new ResourceBuilder();

                            newResource.setId(fakeId.getAndIncrement());
                            newResource.setServer(s);

                            newResource.setAllowDiskImage(true);
                            newResource.setAllowFixedNodeAssignment(true);
                            newResource.setAllowHardwareType(true);
                            newResource.setAllowModifyExclusive(true);

                            newResource.setRequireDiskImage(false);
                            newResource.setRequireFixedNodeAssignment(false);
                            newResource.setRequireHardwareType(false);

                            newResource.setComponentUrn(null);
                            newResource.setDiskImage(null);
                            newResource.setHardwareType(null);
                            newResource.setResourceClassBuilder(res);
                            newResource.setRspecElementName("node");

                            newResource.setSliverType("unknown"); //TODO sliver_type for Server without Resource  (get from ServerGlimpse?)
                            newResource.setDefaultExclusive(true);
                            newResource.setLayer3(true);

                            return newResource;
                        })
                        .collect(Collectors.toList());

        res.setId("other-servers");
        res.setDraggableFromToolbox(true);
        res.setVisibleInToolbox(false); //todo make this configurable
        res.setWeight(-10); //if shown, shown at the bottom
        res.setHrn("Other Servers");
        res.setImageId("other-servers");
        res.setResourceBuilders(otherServersResources);

        LOG.debug("Created other-servers ResourceClass with {} Resources", otherServersResources.size());

        return res.create();
    }

    @Nonnull
    private List<Server> getFullServers(@Nullable List<Server> in) {
        if (in != null) {
            List<Server> res = new ArrayList<>();
            for (Server minimizedServer : in) {
                assert minimizedServer != null;
                assert minimizedServer.getId() != null;
                Server fullServer = testbedInfoSource.getServerById(minimizedServer.getId());
                //this does a lot of asserts about the internal linking of the testbedInfoSource result
                assert fullServer != null :
                        "Did not find server " + minimizedServer.getId() + " in testbedInfoSource";
                if (fullServer == null) {
                    LOG.error("Server " + minimizedServer.getId() + " that was in a jFedExperimenterGuiConfig list, was not found in testbedInfoSource");
                    continue;
                }
                assert fullServer.getName() != null;
                assert Objects.equals(fullServer.getId(), minimizedServer.getId());
                assert fullServer.getTestbed() != null;
                assert fullServer.getTestbed().getId() != null;
                assert fullServer.getTestbed().getServers() != null;
                assert fullServer.getTestbed().getServers().contains(fullServer);
                assert fullServer.getServices() != null;
                assert fullServer.getServices().isEmpty() || fullServer.getServices().get(0).getServer() == fullServer;
                res.add(fullServer);
            }
            return Collections.unmodifiableList(res);
        } else {
            return Collections.emptyList();
        }
    }



    @Nonnull
    public ResourceClass getOtherServersResourceClass() {
        return otherServerResourceClass;
    }

    public boolean isOtherServersResourceClass(@Nonnull ResourceClass resourceClass) {
        return Objects.equals(resourceClass.getId(), getOtherServersResourceClass().getId());
    }

    /* fill in resourceClassById using jFedExperimenterGuiConfig.getResourceClasses() as base and make sure this includes:
     *  - actual objects for getResources()
     *  - actual objects for getDefaultResources()
     *  - Resources that point back to actual ResourceClass
     *  - actual server objects for all Resource.getServer()   (and all references in Server, TestbedInfoSource should guarantee these)
     *  - replace any null in resource.getAllow... or getRequired  by the default value
     *
     *  What can never be guaranteed:
     *    - ResourceClass.getParent and ResourceClass.getIsntantiateAs point to actual fully filled in ResourceClass
     */
    private static Map<String, ResourceClass> link(TestbedInfoSource testbedInfoSource, @Nullable List<ResourceClass> resourceClasses) {
        Map<Integer, ResourceBuilder> rBuilders = new HashMap<>();

        Map<String, ResourceClass> res = new HashMap<>();

        if (resourceClasses != null) {
            for (ResourceClass sourceResourceClass : resourceClasses) {
                assert sourceResourceClass.getId() != null;

                ResourceClassBuilder rcBuilder = new ResourceClassBuilder(sourceResourceClass);

                rcBuilder.setDefaultResource(null);
                rcBuilder.setResources(null);
                rcBuilder.setDefaultResourceBuilders(null);
                rcBuilder.setResourceBuilders(null);

                //leave these. they cannot be full references ever anyway!
//            rcBuilder.setInstantiateAs(null);
//            rcBuilder.setParent(null);
//            rcBuilder.setInstantiateAsBuilder(null);
//            rcBuilder.setParentBuilder(null);

                Set<Integer> seenServerIds = new HashSet<>();

                List<ResourceBuilder> resourceBuilders = new ArrayList<>(sourceResourceClass.getResources().size());
                for (Resource sourceResource : sourceResourceClass.getResources()) {

                    //make sure we don't have duplicate servers in a resource-class
                    assert sourceResource.getServerId() != null;
                    if(seenServerIds.contains(sourceResource.getServerId())){
                        LOG.warn("Found duplicate server {} in resouceClass {}. Ignoring Resource {}",
                                sourceResource.getServerId(),
                                sourceResourceClass.getId(),
                                sourceResource.getId());
                        continue;
                    }
                    seenServerIds.add(sourceResource.getServerId());

                    ResourceBuilder resourceBuilder = rBuilders.computeIfAbsent(sourceResource.getId(), key -> {
                        ResourceBuilder rb = new ResourceBuilder(sourceResource);

                        rb.setServer(null);
                        rb.setServerBuilder(null);

                        //set default for getAllow and getRequire and others that can be null
                        if (rb.getAllowDiskImage() == null) {
                            LOG.warn("null ad bool in resource " + rb.getId());
                            rb.setAllowDiskImage(true);
                        }
                        if (rb.getAllowFixedNodeAssignment() == null) {
                            LOG.warn("null af bool in resource " + rb.getId());
                            rb.setAllowFixedNodeAssignment(true);
                        }
                        if (rb.getLayer3() == null) {
                            LOG.warn("null l  bool in resource " + rb.getId());
                            rb.setLayer3(true);
                        }
                        if (rb.getDefaultExclusive() == null) {
                            LOG.warn("null ae bool in resource " + rb.getId());
                            rb.setDefaultExclusive(true);
                        }
                        if (rb.getAllowHardwareType() == null) {
                            LOG.warn("null ah bool in resource " + rb.getId());
                            rb.setAllowHardwareType(false);
                        }
                        if (rb.getAllowModifyExclusive() == null) {
                            LOG.warn("null am bool in resource " + rb.getId());
                            rb.setAllowModifyExclusive(false);
                        }
                        if (rb.getRequireFixedNodeAssignment() == null) {
                            LOG.warn("null rf bool in resource " + rb.getId());
                            rb.setRequireFixedNodeAssignment(false);
                        }
                        if (rb.getRequireDiskImage() == null) {
                            LOG.warn("null rd bool in resource " + rb.getId());
                            rb.setRequireDiskImage(false);
                        }
                        if (rb.getRequireHardwareType() == null) {
                            LOG.warn("null rh bool in resource " + rb.getId());
                            rb.setRequireHardwareType(false);
                        }

                        assert sourceResource.getServerId() != null;
                        if (sourceResource.getServerId() != null) {
                            Server actualServer = testbedInfoSource.getServerById(sourceResource.getServerId());
                            //assert actualServer != null;
                            if (actualServer != null) {
                                rb.setServer(actualServer);
                            } else {
                                LOG.error("Server " + sourceResource.getServerId() + " in Resource " + sourceResource.getId() + " not found in TestbedInfoSource");
                            }
                        }

                        //will be done automatically by ResourceClassBuilder. We set it to null to be sure nothing else can happen.
                        rb.setResourceClass(null);
                        rb.setResourceClassBuilder(null);

                        return rb;
                    });
                    resourceBuilders.add(resourceBuilder);
                }
                rcBuilder.setResourceBuilders(resourceBuilders);

                if (sourceResourceClass.getDefaultResource() != null && !sourceResourceClass.getDefaultResource().isEmpty()) {
                    List<ResourceBuilder> defaultResource =
                            new ArrayList<>(sourceResourceClass.getDefaultResource().size());
                    for (Resource sourceDefaultResource : sourceResourceClass.getDefaultResource()) {
                        ResourceBuilder defaultResourceBuilder = rBuilders.get(sourceDefaultResource.getId());
//                        assert defaultResourceBuilder != null;
                        if (defaultResourceBuilder != null)
                            defaultResource.add(defaultResourceBuilder);
                        else
                            LOG.warn("Default resource is NOT in getResourceBuilders(). Ignoring.");
                    }
                    //Note: whenever the same ResourceBuilder is used for setResources end setDefaultResources,
                    //the ResourceClassBuilder garuantees they will point to the same Resource object in the end
                    rcBuilder.setDefaultResourceBuilders(defaultResource);
                } else {
                    rcBuilder.setDefaultResourceBuilders(Collections.emptyList());
                }

                try {
                    ResourceClass rc = rcBuilder.create();
                    res.put(rc.getId(), rc);
                }catch(IllegalStateException ex){
                    LOG.error("Error while creating resourceClass {}. It will be IGNORED!", sourceResourceClass.getId(), ex);
                }
            }
        }

        return res;
    }

    @Override
    public GtsConfig getGtsConfig() {
        return jFedExperimenterGuiConfig.getGtsConfig();
    }

    @Override
    public boolean hasFlag(String flagName) {
        if (jFedExperimenterGuiConfig.getFlags() == null) return false;
        return jFedExperimenterGuiConfig.getFlags().containsKey(flagName);
    }

    @Override
    public Boolean getBooleanFlag(String flagName) {
        if (jFedExperimenterGuiConfig.getFlags() == null) return null;
        Object flagVal = jFedExperimenterGuiConfig.getFlags().get(flagName);
        if (flagVal == null)
            return null;
        return TextUtil.objectToBoolean(flagVal);
    }

    @Override
    public String getStringFlag(String flagName) {
        if (jFedExperimenterGuiConfig.getFlags() == null) return null;
        Object flagVal = jFedExperimenterGuiConfig.getFlags().get(flagName);
        if (flagVal == null)
            return null;
        return flagVal + "";
    }

    @Override
    public Integer getIntegerFlag(String flagName) {
        if (jFedExperimenterGuiConfig.getFlags() == null) return null;
        Object flagVal = jFedExperimenterGuiConfig.getFlags().get(flagName);
        if (flagVal == null)
            return null;
        try {
            return Integer.parseInt(flagVal + "");
        } catch (NumberFormatException e) {
            LOG.error("Flag \"" + flagName + "\" has non int value \"" + flagVal + "\" (falling back to null)", e);
            return null;
        }
    }

    @Override
    public Long getLongFlag(String flagName) {
        if (jFedExperimenterGuiConfig.getFlags() == null) return null;
        Object flagVal = jFedExperimenterGuiConfig.getFlags().get(flagName);
        if (flagVal == null)
            return null;
        try {
            return Long.parseLong(flagVal + "");
        } catch (NumberFormatException e) {
            LOG.error("Flag \"" + flagName + "\" has non long value \"" + flagVal + "\" (falling back to null)", e);
            return null;
        }
    }

    @Override
    @Nullable
    public ResourceClass getResourceClass(String id) {
        return resourceClassById.get(id);
    }

    @Override
    public Collection<ResourceClass> getResourceClasses() {
        return resourceClassById.values();
    }

    @Nonnull
    @Override
    public List<TriggeredMessage> getTriggeredMessages() {
        return triggeredMessages;
    }

    /*
     * detection of specific resource types contained in a resourceClass
     * if the resourceClass is not a "leaf", this is the type of the default.
     *
     * (don't figure this out manually using the ResourceClass etc, let this class determine it.)
     * */
    @Override
    @Nullable
    public ResourceType getResourceType(ResourceClass resourceClass) {
        if (resourceClass.getInstantiateAsId() != null) {
            resourceClass = resourceClassById.get(resourceClass.getInstantiateAsId());
            assert resourceClass != null;
            if (resourceClass == null)
                return null;
        }

        if (resourceClass.getDefaultResource() == null)
            return ResourceType.OTHER;
        if (resourceClass.getDefaultResource().isEmpty())
            return ResourceType.OTHER;
        Resource firstDefaultResource = resourceClass.getDefaultResource().get(0);
        if (firstDefaultResource == null)
            return ResourceType.OTHER;
        if (firstDefaultResource.getSliverType() == null)
            return ResourceType.OTHER;

        if (firstDefaultResource.getSliverType().equalsIgnoreCase("raw-pc"))
            return ResourceType.RAW_PC;

        if (firstDefaultResource.getSliverType().equalsIgnoreCase("default-vm"))
            return ResourceType.VM;
        if (firstDefaultResource.getSliverType().equalsIgnoreCase("emulab-openvz"))
            return ResourceType.VM;
        if (firstDefaultResource.getSliverType().equalsIgnoreCase("emulab-xen"))
            return ResourceType.VM;

        return ResourceType.OTHER;
    }

    @Override
    public Collection<ResourceClass> getResourceClassesByType(ResourceType resourceType) {
        Collection<ResourceClass> res = resourceClassesByType.get(resourceType);
        if (res == null) return Collections.emptyList();
        return res;
    }

    @Override
    public ResourceClass getDefaultResourceClass() {
        assert jFedExperimenterGuiConfig != null;
//        assert jFedExperimenterGuiConfig.getResourceClasses() != null;
        if (jFedExperimenterGuiConfig.getResourceClasses() == null || jFedExperimenterGuiConfig.getResourceClasses().isEmpty())
            return null;
        String id = jFedExperimenterGuiConfig.getResourceClasses().get(0).getId();
        assert id != null;
        return resourceClassById.get(id);
    }

    @Override
    public List<Server> getBindableServers() {
        return bindableServers;
    }

    @Override
    public List<Server> getAllServersInVisibleResources() {
        return serversInVisibleResources;
    }

    @Override
    public List<Server> getHiddenServers() {
        return hiddenServers;
    }

    /**
     * Note: the calculation punishes conflicts harder than matches are rewarded.
     *
     * @param tested
     * @param node
     * @return a score of how much the rspecNode matches the resource
     */
    private int matchScore(@Nonnull Resource tested, @Nonnull RspecNode node) {
        int score = 0;

        Server server = tested.getServer();
        assert server != null;
        assert server.getId() != null;

        assert server.getDefaultComponentManagerAsGeniUrn() != null;

        if (node.getSliverTypeName() != null) {
            if (tested.getSliverType() == null)
                score += -15;
            else {
                if (!Objects.equals(tested.getSliverType(), node.getSliverTypeName())) {
                    if (!Objects.equals(tested.getSliverType(), "default-vm"))
                        score += -15;
                    //else  "default-vm" typically gets changes by testbed
                }
            }
        } else {
            if (tested.getSliverType() != null)
                score += -15;
        }

        if (node.getComponentManagerId() != null) {
            if (server.getDefaultComponentManagerAsGeniUrn() == null)
                score += -20;
            else {
                if (!Objects.equals(node.getComponentManagerId(), tested.getServer().getDefaultComponentManagerAsGeniUrn()))
                    score += -20;
            }
        }

        if (node.getHardwareTypes() != null && !node.getHardwareTypes().isEmpty() && node.getHardwareTypes().get(0).getName() != null) {
            //ignore others if more than 1 (because more than one should never occur in request or manifest, only in advertisement)
            String hwTypeName = node.getHardwareTypes().get(0).getName();

            if (!tested.getAllowHardwareType())
                score -= 10;
            else {
                if (tested.getHardwareType() == null) {
                    //no penalty or bonus
                } else {
                    if (Objects.equals(tested.getHardwareType(), hwTypeName))
                        score += 4;
                    else
                        score -= 10;
                }
            }
        } else {
            if (tested.getRequireHardwareType())
                score -= 10;
            else {
                if (tested.getAllowHardwareType() && tested.getHardwareType() != null)
                    score -= 5;
            }
        }


        if (node.getComponentManagerId() != null) {
            if (!tested.getAllowFixedNodeAssignment())
                score -= 10;
            else {
                if (tested.getComponentUrn() != null) {
                    if (Objects.equals(tested.getComponentUrn(), node.getComponentManagerId().toString()))
                        score += 4;
                    else
                        score -= 10;
                } else {
                    //no penalty or bonus
                }
            }
        } else {
            if (tested.getRequireFixedNodeAssignment())
                score -= 10;
            else {
                if (tested.getAllowFixedNodeAssignment() && tested.getComponentUrn() != null)
                    score -= 5;
            }
        }

        if (node.getSliverTypes() != null && !node.getSliverTypes().isEmpty() &&
                node.getSliverTypes().get(0).getDiskImages() != null && !node.getSliverTypes().get(0).getDiskImages().isEmpty()) {
            //ignore others if more than 1 slivertype AND/OR diskimage (because more than one should never occur in request or manifest, only in advertisement)

            DiskImage diskImage = node.getSliverTypes().get(0).getDiskImages().get(0);

            if (!tested.getAllowDiskImage())
                score -= 10;
            else {
                if (tested.getDiskImage() == null) {
                    //no penalty or bonus
                } else {
                    boolean matchName =
                            diskImage.getName() != null && Objects.equals(diskImage.getName(), tested.getDiskImage());
                    boolean matchUrl = diskImage.getUrl() != null && Objects.equals(diskImage.getUrl(), tested.getDiskImage());
                    if (matchName || matchUrl)
                        score += 4;
                    else
                        score -= 10;
                }
            }
        } else {
            if (tested.getRequireDiskImage())
                score -= 10;
            else {
                if (tested.getAllowDiskImage() && tested.getDiskImage() != null)
                    score -= 2; //minor penalty for this
            }
        }

        if (node.getExclusive() != null && tested.getDefaultExclusive() != null) {
            if (tested.getDefaultExclusive() != node.getExclusive()) {
                if (tested.getAllowModifyExclusive())
                    score -= 0; //no penalty
                else
                    score -= 10; //major penalty
            } else {
                score += 1; //minor bonus if matching
            }
        } else {
            if (tested.getDefaultExclusive() != null)
                score -= 1; //small penalty  (which only can make a difference in hackish edge cases,
            //                as tested.getDefaultExclusive() is assumed to never be null)
        }

        return score;
    }

    @Override
    @Nullable
    public ResourceClass findBestResourceClass(RspecNode node) {
        Resource bestResource = findBestResource(null, node, null);
        return bestResource != null ? bestResource.getResourceClass() : null;
    }

    @Override
    public List<ResourceClass> findAllPossibleResourceClasses(RspecNode node) {
        List<Resource> resources = findAllPossibleResources(null, node, null);
        Set<ResourceClass> res = new HashSet<>();
        for (Resource resource : resources) {
            res.add(resource.getResourceClass());
        }
        return new ArrayList<>(res);
    }

    @Override
    @Nullable
    public Resource findBestResource(@Nullable ResourceClass resourceClass, RspecNode node, @Nullable Server overrideComponentManager) {
        List<Resource> all = findAllPossibleResources(resourceClass, node, overrideComponentManager);
        if (all.isEmpty())
            return null;
        Resource best = all.get(0);
        if (all.size() == 1)
            return best;
        int bestScore = matchScore(best, node);
        LOG.debug("score for resource " + best.getId() + " of " + best.getResourceClassId() + " is " + bestScore);
        for (int i = 1; i < all.size(); i++) {
            int curScore = matchScore(all.get(i), node);
            LOG.debug("score for resource " + all.get(i).getId() + " of " + all.get(i).getResourceClassId() + " is " + curScore);
            if (curScore > bestScore
                    || (curScore == bestScore && best.getResourceClassId() != null && best.getResourceClassId().contains("generic")) //TODO remove this quick hack
                    ) {
                bestScore = curScore;
                best = all.get(i);
            }
        }
        return best;
    }

    @Override
    public List<Resource> findAllPossibleResources(@Nullable ResourceClass resourceClass, RspecNode node, @Nullable Server overrideComponentManager) {
        assert overrideComponentManager == null || overrideComponentManager.getDefaultComponentManagerAsGeniUrn() != null;

        GeniUrn componentManagerUrn =
                overrideComponentManager != null ?
                        overrideComponentManager.getDefaultComponentManagerAsGeniUrn() :
                        node.getComponentManagerId();
        Server server =
                overrideComponentManager != null ?
                        overrideComponentManager : (
                        componentManagerUrn == null ?
                                null :
                                authorityFinder.findByUrn(componentManagerUrn, AuthorityFinder.Purpose.REQUEST_RSPEC)
                );

        String sliverTypeName = node.getSliverTypeName();

        if (server != null) {
            assert server.getId() != null;

            List<Resource> resources;
            if (sliverTypeName != null) {
                resources = resourcesByServerIdAndSliverName.get(new Pair<>(server.getId(), sliverTypeName));

                if (resources == null) {
                    //perhaps server changed sliver_type. this happens with "default-vm". So try that first.
                    resources = resourcesByServerIdAndSliverName.get(new Pair<>(server.getId(), "default-vm"));

                    if (resources == null) {
                        //fall back to ignoring sliver_type.
                        resources = resourcesByServerId.get(server.getId());
                    }
                }
            } else {
                resources = resourcesByServerId.get(server.getId());
            }
            if (resources == null)
                return Collections.emptyList();
            List<Resource> res = new ArrayList<>(resources.size());
            for (Resource candidate : resources) {
                if (resourceClass == null || candidate.getResourceClass().getId().equals(resourceClass.getId()))
                    res.add(candidate);
            }
            return res;
        } else {
            if (componentManagerUrn == null) {
                assert node.getComponentManagerId() == null;
                //this is an unbound node

                List<Resource> res = new ArrayList<>(allResources.size());
                for (Resource candidate : allResources) {
                    if (resourceClass == null || candidate.getResourceClass().getId().equals(resourceClass.getId())) {
                        if (sliverTypeName == null || candidate.getSliverType() == null || Objects.equals(sliverTypeName, candidate.getSliverType())) {
                            res.add(candidate);
                        }
                    }
                }
                return res;

            } else {
                LOG.warn("No server found for node with component_manager_id=" + componentManagerUrn);
                return null;
            }
        }
    }

    public boolean isLeaf(ResourceClass resourceClass) {
        if (resourceClass.getInstantiateAsId() == null)
            return true;
        if (Objects.equals(resourceClass.getInstantiateAsId(), resourceClass.getId())) {
            LOG.warn("error in resourceClass InstantiateAs: " + resourceClass.getId() + " InstantiateAs itself");
            return true;
        }
        return false;
    }

    public ResourceClass getLeaf(ResourceClass resourceClass) {
        if (isLeaf(resourceClass)) {
            LOG.warn("Called getLeaf on a leaf " + resourceClass.getId());
            return resourceClass;
        }

        ResourceClass res = resourceClass;
        while (!isLeaf(res)) {
            assert res.getInstantiateAsId() != null;
            res = getResourceClass(res.getInstantiateAsId());
            if (res == null) {
                LOG.error("Error in resourceClass InstantiateAs. res = null");
                return null;
            }
        }
        assert !isLeaf(res);
        return res;
    }

    @Override
    public Resource getDefaultResource(ResourceClass resourceClass) {
        if (resourceClass.getDefaultResource() == null || resourceClass.getDefaultResource().isEmpty()) {
            if (!isLeaf(resourceClass)) {
                ResourceClass parent = resourceClass;
                resourceClass = getLeaf(resourceClass);
                if (resourceClass == null) {
                    LOG.error("resourceClass {} has no leaf!", parent.getId());
                    return null;
                }
            }
        }

        if (resourceClass.getDefaultResource() == null || resourceClass.getDefaultResource().isEmpty()) {
            if (resourceClass.getResources() == null || resourceClass.getResources().isEmpty())
                return null;
            //when no default, take first. (no random!)
            return resourceClass.getResources().get(0);
        }

        Integer randomIndex = randomByResourceClassId.get(resourceClass.getId());
        if (randomIndex != null) {
            assert randomIndex >= 0;
            assert randomIndex < resourceClass.getDefaultResource().size();
            return resourceClass.getDefaultResource().get(randomIndex);
        }

        randomIndex = rand.nextInt(resourceClass.getDefaultResource().size());
        randomByResourceClassId.put(resourceClass.getId(), randomIndex);
        return resourceClass.getDefaultResource().get(randomIndex);
    }

    @Override
    public Boolean getNodeExclusivity(GeniUrn componentManagerUrn, String sliverTypeName) {
        Server server = authorityFinder.findByUrn(componentManagerUrn, AuthorityFinder.Purpose.REQUEST_RSPEC);
        if (server == null)
            return null;

        List<Resource> resources = resourcesByServerIdAndSliverName.get(new Pair<>(server.getId(), sliverTypeName));
        if (resources == null || resources.isEmpty())
            return null;

        Boolean res = null;
        for (Resource resource : resources) {
            if (resource.getDefaultExclusive() != null) {
                if (res == null)
                    res = resource.getDefaultExclusive();
                else {
                    if (res != resource.getDefaultExclusive()) {
                        LOG.warn("Conflicting node exclusivity for " +
                                "server=" + server.getId() + " " +
                                "sliverType=" + sliverTypeName + " " +
                                "all resource IDs: " + resources.stream().map(Resource::getId).collect(Collectors.toList()));
                        //use exclusive=false as default
                        return false;
                    } //else: continue;
                }
            }
        }
        return res;
    }

    @Override
    public boolean hasUpToDateComponentInfo(Server server, Scope scope) {
        int serverId = server.getId();

        AdvertisementInfo allRspec =
                getAdvertisementInfo(serverId, AdvertisementInfoSourceType.AllAdvertisementRspec);
        AdvertisementInfo availableRspec =
                getAdvertisementInfo(serverId, AdvertisementInfoSourceType.FedmonNodeStatus);
        AdvertisementInfo serverGlimpse =
                getAdvertisementInfo(serverId, AdvertisementInfoSourceType.ServerGlimpse);

        //use best from allRspec, availableRspec and serverGlimpse
        boolean allRspecIsCandidate =
                allRspec.hasInfo() && !allRspec.olderThan(Duration.ofHours(1));
        boolean availableRspecIsCandidate =
                availableRspec.hasInfo() && !availableRspec.olderThan(Duration.ofHours(1));
        boolean serverGlimpseIsCandidate =
                serverGlimpse.hasInfo() && !serverGlimpse.olderThan(Duration.ofHours(1));

        switch (scope) {
            case ALL: return allRspecIsCandidate || serverGlimpseIsCandidate;
            case AVAILABLE_ONLY: return allRspecIsCandidate || serverGlimpseIsCandidate || availableRspecIsCandidate;
            default: throw new IllegalArgumentException("Invalid scope: "+scope);
        }
    }

    @Override
    public ReadOnlyListProperty<ComponentInfo> getComponentInfo(Server server, Scope scope) {
        switch (scope) {
            case ALL:
                return getBestAdvertisementProperties(server.getId()).allComponentInfos;
            case AVAILABLE_ONLY:
                return getBestAdvertisementProperties(server.getId()).availableComponentInfos;
            default:
                throw new IllegalArgumentException("Invalid scope: " + scope);
        }
    }

    @Override
    public ReadOnlyListProperty<String> getSliverTypeNames(Server server) {
        return getBestAdvertisementProperties(server.getId()).sliverTypeNames;
    }

    @Override
    public ReadOnlyListProperty<HardwareTypeInfo> getHardwareTypes(Server server, Scope scope) {
        switch (scope) {
            case ALL:
                return getBestAdvertisementProperties(server.getId()).allHardwareTypes;
            case AVAILABLE_ONLY:
                return getBestAdvertisementProperties(server.getId()).availableHardwareTypes;
            default:
                throw new IllegalArgumentException("Invalid scope: " + scope);
        }
    }

    @Override
    public ReadOnlyListProperty<DiskImage> getDiskImages(Server server, @Nullable String sliverTypeName) {
        if (sliverTypeName == null) {
            return getBestAdvertisementProperties(server.getId()).diskImages;
        } else {
            return getBestAdvertisementProperties(server.getId()).diskImagesBySliverType.computeIfAbsent(sliverTypeName, key -> new SimpleListProperty<>(FXCollections.observableArrayList()));
        }
    }

    @Override
    public ReadOnlyObjectProperty<AdvertisementRspecSource> getAdvertisement(Server server, @Nullable Boolean availableOnly) {
        if (availableOnly != null) {
            if (availableOnly)
                return availableAdvertisementByServerId.computeIfAbsent(server.getId(), key -> new SimpleObjectProperty<>());
            else
                return allAdvertisementByServerId.computeIfAbsent(server.getId(), key -> new SimpleObjectProperty<>());
        } else {
            return anyAdvertisementByServerId.computeIfAbsent(server.getId(), key -> new SimpleObjectProperty<>());
        }
    }

    @Override
    public NodesVisualization getNodesVisualization(Server server) {
        return imageMapByServerId.containsKey(server.getId()) ? NodesVisualization.IMAGE : NodesVisualization.MAP;
    }

    @Override
    public ImageMap<GeniUrn> getImageMap(Server server) {
        return imageMapByServerId.get(server.getId());
    }

    @Override
    public ServerGlimpse getServerGlimpse(Server server) {
        return serverGlimpseByServerId.get(server.getId());
    }

    @Override
    public Color getColor(Server server) {
        return server == null || server.getTestbed() == null
                ? null
                : getColor(server.getTestbed());
    }

    @Override
    public Color getColor(Testbed testbed) {
        return testbed == null || testbed.getColor() == null || testbed.getColor().trim().isEmpty()
                ? null
                : getColor(testbed.getColor());
    }

    @Override
    public Color getColor(String colorName) {
        if (colorName == null)
            return Color.TRANSPARENT;
        try {
            return Color.valueOf(colorName.toUpperCase());
        } catch (IllegalArgumentException ex) {
            LOG.warn("Cannot convert color \"{}\" to javafx.scene.paint.Color", colorName, ex);
            return Color.TRANSPARENT;
        }
    }


    private AdvertisementInfo getAdvertisementInfo(@Nonnull Server server, @Nonnull AdvertisementInfoSourceType type) {
        return getAdvertisementInfo(server.getId(), type);
    }

    private AdvertisementInfo getAdvertisementInfo(@Nonnull Integer serverId, @Nonnull AdvertisementInfoSourceType type) {
        return advertisementInfoById.computeIfAbsent(
                new AdvertisementInfoSourceId(serverId, type),
                AdvertisementInfo::new);
    }

    private AdvertisementProperties getBestAdvertisementProperties(@Nonnull Integer serverId) {
        //updates should make sure updateBestAdvertisementInfo is called, so that this is always up to date
        AdvertisementProperties res = advertisementPropertiesByServerId.get(serverId);
        if (res == null) {
            return updateBestAdvertisementInfo(serverId);
        } else {
            return res;
        }
    }

    private AdvertisementProperties updateBestAdvertisementInfo(@Nonnull Integer serverId) {
        AdvertisementInfo tmpAll = new AdvertisementInfo(serverId, AdvertisementInfoSourceType.Best);
        AdvertisementInfo tmpAvail = new AdvertisementInfo(serverId, AdvertisementInfoSourceType.Best);

        AdvertisementInfo allRspec = getAdvertisementInfo(serverId, AdvertisementInfoSourceType.AllAdvertisementRspec);
        AdvertisementInfo availableRspec =
                getAdvertisementInfo(serverId, AdvertisementInfoSourceType.AvailableAdvertisementRspec);
        AdvertisementInfo fedmonNodeStatus =
                getAdvertisementInfo(serverId, AdvertisementInfoSourceType.FedmonNodeStatus);
        AdvertisementInfo serverGlimpse = getAdvertisementInfo(serverId, AdvertisementInfoSourceType.ServerGlimpse);
        AdvertisementInfo resourceClass = getAdvertisementInfo(serverId, AdvertisementInfoSourceType.ResourceClass);
        AdvertisementInfo user = getAdvertisementInfo(serverId, AdvertisementInfoSourceType.User);

        //use best from allRspec, availableRspec and serverGlimpse
        boolean allRspecIsCandidate = allRspec.hasInfo() && !allRspec.olderThan(Duration.ofHours(1));
        boolean availableRspecIsCandidate = availableRspec.hasInfo() && !availableRspec.olderThan(Duration.ofHours(1));
        boolean serverGlimpseIsCandidate =
                serverGlimpse.hasInfo() && !serverGlimpse.olderThan(Duration.ofHours(1));



        AdvertisementInfoSourceType preferenceAll = null;
        AdvertisementInfoSourceType preferenceAvail = null;

        if (!allRspecIsCandidate && !serverGlimpseIsCandidate && availableRspecIsCandidate) {
            preferenceAvail = AdvertisementInfoSourceType.AvailableAdvertisementRspec;
            preferenceAll = AdvertisementInfoSourceType.AvailableAdvertisementRspec;
        } else {
            //find preferenceAll
            if (allRspecIsCandidate && serverGlimpseIsCandidate) {
                //select most recent of glimpse or all for ALL
                if (serverGlimpse.date != null && allRspec.date != null && allRspec.date.before(serverGlimpse.date)) {
                    preferenceAll = AdvertisementInfoSourceType.ServerGlimpse;
                } else {
                    preferenceAll = AdvertisementInfoSourceType.AllAdvertisementRspec;
                }
            } else {
                if (serverGlimpseIsCandidate)
                    preferenceAll = AdvertisementInfoSourceType.ServerGlimpse;
                if (allRspecIsCandidate)
                    preferenceAll = AdvertisementInfoSourceType.AllAdvertisementRspec;
            }

            //find preferenceAvail
            if (allRspecIsCandidate && serverGlimpseIsCandidate && availableRspecIsCandidate) {
                //select most recent of all 3 for AVAIL
                if (serverGlimpse.date != null &&
                        allRspec.date != null &&
                        availableRspec.date != null) {
                    if (allRspec.date.before(serverGlimpse.date) && availableRspec.date.before(serverGlimpse.date)) {
                        preferenceAvail = AdvertisementInfoSourceType.ServerGlimpse;
                    } else if (allRspec.date.after(serverGlimpse.date) && allRspec.date.after(availableRspec.date)) {
                        preferenceAvail = AdvertisementInfoSourceType.AvailableAdvertisementRspec;
                    } else {
                        preferenceAvail = AdvertisementInfoSourceType.AllAdvertisementRspec;
                    }
                } else {
                    preferenceAvail = AdvertisementInfoSourceType.AllAdvertisementRspec;
                }
            } else {
                //TODO: could select most recent if multiple available here
                if (serverGlimpseIsCandidate)
                    preferenceAvail = AdvertisementInfoSourceType.ServerGlimpse;
                if (allRspecIsCandidate)
                    preferenceAvail = AdvertisementInfoSourceType.AllAdvertisementRspec;
                if (availableRspecIsCandidate)
                    preferenceAvail = AdvertisementInfoSourceType.AvailableAdvertisementRspec;
            }
        }

        if (preferenceAll != null) {
            switch (preferenceAll) {
                case AllAdvertisementRspec: {
                    tmpAll.set(allRspec);
                    break;
                }
                case AvailableAdvertisementRspec: {
                    tmpAll.set(availableRspec);
                    break;
                }
                case ServerGlimpse: {
                    tmpAll.set(serverGlimpse);
                    break;
                }
                default:
                    throw new RuntimeException("Bad preferenceAll: bug in code. " + preferenceAll);
            }
        }
        if (preferenceAvail != null) {
            switch (preferenceAvail) {
                case AllAdvertisementRspec: {
                    tmpAvail.set(allRspec);
                    break;
                }
                case AvailableAdvertisementRspec: {
                    tmpAvail.set(availableRspec);
                    break;
                }
                case ServerGlimpse: {
                    tmpAvail.set(serverGlimpse);
                    break;
                }
                default:
                    throw new RuntimeException("Bad preferenceAvail: bug in code. " + preferenceAvail);
            }
        }

        //integrate info from resourceClass
        if (resourceClass.hasInfo()) {
            tmpAll.addSliverTypeNames(resourceClass.sliverTypeNames);
            tmpAll.addHardwareTypes(resourceClass.hardwareTypes);
            tmpAll.addDiskImages(resourceClass.diskImages);
            tmpAll.addComponentInfos(resourceClass.componentInfos);

            tmpAvail.addSliverTypeNames(resourceClass.sliverTypeNames);
            tmpAvail.addHardwareTypes(resourceClass.hardwareTypes);
            tmpAvail.addDiskImages(resourceClass.diskImages);
            tmpAvail.addComponentInfos(resourceClass.componentInfos);
        }

        //integrate fedmonNodeStatus
        if (fedmonNodeStatus.hasInfo()) {
            Map<GeniUrn, ComponentInfo> componentInfoByUrn = new HashMap<>();
            for (ComponentInfo ci : tmpAll.componentInfos) {
                if (ci.getUrn() != null) {
                    componentInfoByUrn.put(ci.getUrn(), ci);
                }
            }
            for (ComponentInfo fnsCi : fedmonNodeStatus.componentInfos) {
                if (fnsCi.getUrn() != null) {
                    ComponentInfo tmpCi = componentInfoByUrn.get(fnsCi.getUrn());
                    if (tmpCi != null) {
                        ComponentInfo newCi = new ComponentInfo(
                                tmpCi.getName(),
                                tmpCi.getUrn(),
                                tmpCi.isAvailable(),
                                fnsCi.isNodeLoginTestSuccess(),
                                tmpCi.getLocation(),
                                tmpCi.getHardwareTypeNames());
                        componentInfoByUrn.put(fnsCi.getUrn(), newCi);
                    }
                }
            }
            tmpAll.componentInfos.clear();
            tmpAll.componentInfos.addAll(componentInfoByUrn.values());
        }

        //integrate implicit info from Server
        Server server = testbedInfoSource.getServerById(serverId);
        if (server != null) {
            GeniUrn defaultComponentManagerId = server.getDefaultComponentManagerAsGeniUrn();
            if (defaultComponentManagerId != null && !tmpAll.componentManagerIds.contains(defaultComponentManagerId)) {
                tmpAll.componentManagerIds.add(defaultComponentManagerId);
            }
            if (defaultComponentManagerId != null && !tmpAvail.componentManagerIds.contains(defaultComponentManagerId)) {
                tmpAvail.componentManagerIds.add(defaultComponentManagerId);
            }
        }

        //if the user has used any nodes explicitly, and we don't know about them, add them
        tmpAll.componentInfos.addAll(user.componentInfos);
        tmpAvail.componentInfos.addAll(user.componentInfos);

        //copy values to result
        AdvertisementProperties res = advertisementPropertiesByServerId.computeIfAbsent(
                serverId,
                si -> new AdvertisementProperties(serverId)
        );
        res.set(tmpAll, tmpAvail);

        return res;
    }

    /**
     * Blacklists as mentioned in FEDIBBTDEV-2100
     */
    private static final ImmutableSet<String> HARDWARE_TYPE_NAMES_SUBSTRING_BLACKLIST =
            ImmutableSet.of("80211", "flex900", "xcvr2450", "delay", "interconnect", "ipv4", "lan", "vm", "switch");
    private static final ImmutableSet<String> HARDWARE_TYPE_NAMES_BLACKLIST =
            ImmutableSet.of("pc");

    public static boolean isHardwareTypeToIgnore(HardwareType hardwareType) {
        String name = hardwareType.getName();

        if (name == null)
            return true; //invalid hardware_type if name is missing

        final String lowerCaseName = name.toLowerCase();

        if (HARDWARE_TYPE_NAMES_BLACKLIST.contains(lowerCaseName))
            return true;
        if (HARDWARE_TYPE_NAMES_SUBSTRING_BLACKLIST.stream().
                anyMatch(lowerCaseName::contains))
            return true;

        return false;
    }

    @Override
    public void setAdvertisementRspec(Integer serverId, boolean availableAdvertisementRspec, List<? extends RspecNode> nodes, Date fetchDate) {
        AdvertisementInfo advInfo = availableAdvertisementRspec ?
                getAdvertisementInfo(serverId, AdvertisementInfoSourceType.AvailableAdvertisementRspec) :
                getAdvertisementInfo(serverId, AdvertisementInfoSourceType.AllAdvertisementRspec);
        advInfo.clear();

        advInfo.date = fetchDate;

        for (RspecNode node : nodes) {
            if (node.getHardwareTypes() != null) {
                for (HardwareType hardwareType : node.getHardwareTypes()) {
                    if (hardwareType.getName() == null) continue;
                    if (isHardwareTypeToIgnore(hardwareType)) continue;

                    advInfo.addHardwareTypeName(hardwareType.getName(),
                            node.getAvailable() != null ? node.getAvailable()
                                    : (availableAdvertisementRspec ? true : null));
                }
            }
            if (node.getComponentManagerId() != null) {
                advInfo.componentManagerIds.add(node.getComponentManagerId());
            }
            if (node.getComponentId() != null) {
                boolean available = node.getAvailable() != null ? node.getAvailable() : availableAdvertisementRspec;
                List<String> hardwareTypeNames =  node.getHardwareTypes() == null ?
                        null :
                        node.getHardwareTypes().stream().filter(Objects::nonNull).map(HardwareType::getName).filter(Objects::nonNull).collect(Collectors.toList());
                ComponentInfo componentInfo =
                        new ComponentInfo(node.getComponentName(), node.getComponentId(), available, null, node.getLocation(),
                                hardwareTypeNames);
                advInfo.addComponentInfo(componentInfo);
            }
            if (node.getSliverTypes() != null) {
                for (SliverType sliverType : node.getSliverTypes()) {
                    String sliverTypeName = sliverType.getName();

                    if (sliverTypeName != null) {
                        advInfo.addSliverTypeName(sliverTypeName);
                    } else {
                        LOG.warn("sliver_type without name attribute in advertisement RSpec. That should never occur.");
                    }

                    if (sliverType.getDiskImages() == null)
                        continue;
                    for (DiskImage diskImage : sliverType.getDiskImages()) {
                        assert diskImage != null;
                        advInfo.addDiskImage(diskImage);
                        if (sliverTypeName != null) {
                            advInfo.addDiskImage(sliverTypeName, diskImage);
                        }
                    }
                }
            }
        }

        updateBestAdvertisementInfo(serverId);
    }

    @Override
    public void setServerGlimpse(@Nonnull Integer serverId, @Nonnull ServerGlimpse serverGlimpse) {
        LOG.debug("Setting ServerGlimpse for server " + serverId);
        assert serverGlimpse != null;
        AdvertisementInfo advInfo = getAdvertisementInfo(serverId, AdvertisementInfoSourceType.ServerGlimpse);
        assert advInfo != null;

        ServerGlimpse.AdvertisementInfo sai = serverGlimpse.getAdvertisementInfo();
        if (sai != null) {
            advInfo.clear();

            advInfo.date = sai.getTimestamp();
            if (sai.getComponentManagerIds() != null) {
                assert advInfo.componentManagerIds != null;
                advInfo.componentManagerIds.addAll(
                        sai.getComponentManagerIds().stream()
                                .map(GeniUrn::parse)
                                .filter(Objects::nonNull)
                                .collect(Collectors.toList())
                );
            }

            if (sai.getDiskImages() != null) {
                assert advInfo.diskImages != null;
                for (ServerGlimpse.AdvertisementInfo.DiskImage di : sai.getDiskImages()) {
                    if (di != null && (di.getName() != null || di.getUrl() != null)) {
                        DiskImage diskImage =
                                new DiskImage(di.getName(), di.getUrl(), di.getOs(), di.getVersion(), di.getDescription(), false);
                        advInfo.diskImages.add(diskImage);
                        for (String sliverTypeName : di.getSliverTypes()) {
                            advInfo.addDiskImage(sliverTypeName, diskImage);
                        }
                    }
                }
            }

            if (sai.getSliverTypeInfo() != null)
                for (ServerGlimpse.AdvertisementInfo.SliverTypeInfo st : sai.getSliverTypeInfo()) {
                    if (st != null && st.getSliverType() != null) {
                        advInfo.sliverTypeNames.add(st.getSliverType());

                        for (String hwt : st.getHardwareTypes()) {
                            if (hwt != null) {
                                advInfo.hardwareTypes.add(new HardwareTypeInfo(hwt));
                            }
                        }
                    }
                }

            if (sai.getComponentDetails() != null) {
                for (ServerGlimpse.AdvertisementInfo.ComponentDetails comDet : sai.getComponentDetails()) {
                    GeniUrn componentUrn = GeniUrn.parse(comDet.getId());
                    if (componentUrn != null) {
                        NodeLocation nodeLocation = comDet.getLocation() == null ? null :
                                new NodeLocation(comDet.getLocation().getCountry(),
                                        comDet.getLocation().getLatitude() == null ? null : comDet.getLocation().getLatitude().toString(),
                                        comDet.getLocation().getLongitude() == null ? null : comDet.getLocation().getLongitude().toString());
                        advInfo.addComponentInfo(
                                new ComponentInfo(
                                        componentUrn.getResourceName(),
                                        componentUrn,
                                        !Objects.equals(comDet.getAvailable(), Boolean.FALSE),
                                        null,
                                        nodeLocation,
                                        comDet.getHardwareTypes())
                        );
                    }
                }
            }

//        if (sai.getAvailableComponentIds() != null)
//            for (String compUrn : sai.getAvailableComponentIds()) {
//                try {
//                    advInfo.addComponentInfo(new GeniUrn(compUrn), true);
//                } catch (GeniUrn.GeniUrnParseException e) {
//                    LOG.error("Error parsing component urn in ServerGlimpse: \"" + compUrn + "\"");
//                }
//            }
//        if (sai.getUnavailableComponentIds() != null)
//            for (String compUrn : sai.getUnavailableComponentIds()) {
//                try {
//                    advInfo.addComponentInfo(new GeniUrn(compUrn), false);
//                } catch (GeniUrn.GeniUrnParseException e) {
//                    LOG.error("Error parsing component urn in ServerGlimpse: \"" + compUrn + "\"");
//                }
//            }
        }

        updateBestAdvertisementInfo(serverId);
    }

    @Override
    public void setFedmonNodeTestInfo(Integer serverId, Map<GeniUrn, TestResult> fedmonResults) {
        assert serverId != null;
        assert fedmonResults != null;
        AdvertisementInfo advInfo = getAdvertisementInfo(serverId, AdvertisementInfoSourceType.FedmonNodeStatus);
        advInfo.clear();

        for (Map.Entry<GeniUrn, TestResult> entry : fedmonResults.entrySet()) {
            GeniUrn compId = entry.getKey();
            TestResult testResult = entry.getValue();

            ComponentInfo componentInfo =
                    new ComponentInfo(null, compId, testResult.isSuccessSummary(), testResult.isSuccessSummary(), null, null);
            advInfo.addComponentInfo(componentInfo);
        }

        updateBestAdvertisementInfo(serverId);
    }

    @Override
    public ComponentInfo addUserComponentInfo(Integer serverId, GeniUrn componentId) {
        AdvertisementInfo advInfo = getAdvertisementInfo(serverId, AdvertisementInfoSourceType.User);

        ComponentInfo componentInfo = new ComponentInfo(componentId.getResourceName(), componentId, true, null, null, null);
        advInfo.addComponentInfo(componentInfo);
        updateBestAdvertisementInfo(serverId);
        return componentInfo;
    }

    @Override
    public HardwareTypeInfo addUserHardwareTypeInfo(Integer serverId, String hardwareTypeName, Boolean available) {
        AdvertisementInfo advInfo = getAdvertisementInfo(serverId, AdvertisementInfoSourceType.User);

        HardwareTypeInfo hardwareTypeInfo = new HardwareTypeInfo(hardwareTypeName, available);
        advInfo.addHardwareType(hardwareTypeInfo);
        updateBestAdvertisementInfo(serverId);
        return hardwareTypeInfo;
    }

    @Override
    public void clearUserInfo(Integer serverId) {
        AdvertisementInfo advInfo = getAdvertisementInfo(serverId, AdvertisementInfoSourceType.User);
        advInfo.clear();

        updateBestAdvertisementInfo(serverId);
    }

    @Override
    public void initResourceClassAdvertisementInfo() {
        for (Map.Entry<Integer, List<Resource>> e : resourcesByServerId.entrySet()) {
            Integer serverId = e.getKey();
            List<Resource> resources = e.getValue();
            AdvertisementInfo resourceClassAdvertisementInfo =
                    getAdvertisementInfo(serverId, AdvertisementInfoSourceType.ResourceClass);
            for (Resource resource : resources) {
                if (resource.getSliverType() != null) {
                    resourceClassAdvertisementInfo.addSliverTypeName(resource.getSliverType());
                }
                if (resource.getHardwareType() != null) {
                    resourceClassAdvertisementInfo.addHardwareTypeName(resource.getHardwareType(), null);
                }
                if (resource.getDiskImage() != null) {
                    resourceClassAdvertisementInfo.addDiskImage(resource.getDiskImage());
                }
                if (resource.getComponentUrn() != null) {
                    try {
                        resourceClassAdvertisementInfo.addComponentInfo(new GeniUrn(resource.getComponentUrn()), true/*available  (actually, unknown...)*/);
                    } catch (GeniUrn.GeniUrnParseException e1) {
                        LOG.error("Resource " + resource.getId() + " contained invalid component_urn", e);
                    }
                }
            }
        }
        //no update needed here: this part of init
        //when anyone needs info on a server, that request will call update when not yet done
    }
}
