package de.renew.net;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.log4j.Logger;

import de.renew.engine.thread.SimulationLockExecutorProvider;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.loading.NetLoader;
import de.renew.simulator.api.ISimulationLockExecutor;
import de.renew.simulatorontology.loading.NetNotFoundException;

/**
 * A {@code NetLookup} is used to make {@link Net} instances known to the compiler.
 */
public class NetLookup implements INetLookup {
    private static final Logger LOGGER = Logger.getLogger(NetLookup.class);
    /**
     * A global map from names to {@code Net} instances.  Synchronize on the facade
     * of the given {@link #_simulationLockExecutor}
     * to get a consistent view on the set of registered nets.
     */
    private static final Map<String, Net> NETS = new HashMap<>();
    private static NetLoader _netLoader;
    private final ISimulationLockExecutor _simulationLockExecutor;

    /**
     * Creates a new {@code NetLookup}. The default {@link ISimulationLockExecutor} is used as a lock
     * to ensure that all actions are performed mutually exclusive.
     */
    public NetLookup() {
        this(SimulationLockExecutorProvider.provider());
    }

    /**
     * Creates a new {@code NetLookup} that will use the given {@code ISimulationLockExecutor}
     * to ensure that all actions are performed mutually exclusive.
     *
     * @param lockExecutor will be used to ensure that all actions are performed mutually exclusive
     */
    public NetLookup(ISimulationLockExecutor lockExecutor) {
        _simulationLockExecutor = lockExecutor;
    }

    @Override
    public Net findForName(String name) throws NetNotFoundException {
        _simulationLockExecutor.lock();
        Future<Net> future = SimulationThreadPool.getCurrent().submitAndWait(() -> {
            Net net = NETS.get(name);
            if (net != null) {
                return net;
            }
            // no net was found, maybe we can load it
            if (_netLoader == null) {
                throw new NetNotFoundException(name);
            }
            // Check again if the net is still unknown.
            // Things may have changed during the synchronization sleep...
            net = NETS.get(name);
            if (net == null) {
                return _netLoader.loadNet(name);
            }
            return net;
        });
        try {
            return future.get();
        } catch (InterruptedException e) {
            LOGGER.error("Timeout while waiting for simulation thread to finish", e);
        } catch (ExecutionException e) {
            if (e.getCause() instanceof NetNotFoundException netNotFoundException) {
                throw netNotFoundException;
            }
            LOGGER.error("Simulation thread threw an exception", e);
        } finally {
            _simulationLockExecutor.unlock();
        }
        // should never get here
        return null;
    }

    @Override
    public void forgetAllNets() {
        _simulationLockExecutor.runWithLock(NETS::clear);
    }

    @Override
    public Collection<Net> getAllKnownNets() {
        return new HashSet<>(NETS.values());
    }

    @Override
    public boolean isKnownNet(String name) {
        return NETS.containsKey(name);
    }

    @Override
    public void makeNetKnown(Net net) {
        Objects.requireNonNull(net);
        _simulationLockExecutor.runWithLock(() -> {
            NETS.put(net.getName(), net);
        });
    }

    @Override
    public void makeNetsKnown(Collection<Net> nets) {
        _simulationLockExecutor.runWithLock(() -> {
            NETS.putAll(nets.stream().collect(Collectors.toMap(Net::getName, Function.identity())));
        });
    }

    @Override
    public void setNetLoader(NetLoader netLoader) {
        _simulationLockExecutor.runWithLock(() -> {
            if (NETS.isEmpty()) {
                _netLoader = netLoader;
            } else {
                throw new IllegalStateException("Cannot change net loader while nets are known.");
            }
        });
    }

    @Override
    public NetLoader getNetLoader() {
        return _netLoader;
    }
}
