package de.renew.shadowcompiler;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.apache.log4j.Logger;

import de.renew.net.Net;
import de.renew.net.NetLookup;
import de.renew.simulatorontology.serialisation.SerialisationListener;
import de.renew.simulatorontology.serialisation.SerialisationListenerRegistry;
import de.renew.simulatorontology.shadow.ShadowNet;
import de.renew.simulatorontology.shadow.ShadowNetSystem;
import de.renew.simulatorontology.shadow.SyntaxException;

/**
 * The {@code ShadowNetSystemCompiler} is used to compile a {@link ShadowNetSystem}.
 */
public class ShadowNetSystemCompiler implements IShadowNetSystemCompiler {

    private static final Logger LOGGER = Logger.getLogger(ShadowNetSystemCompiler.class);
    private static final Map<ShadowNetSystem, ShadowCompilerFactory> SHADOW_NET_SYSTEM_COMPILER_FACTORIES =
        new HashMap<>();
    private static final Map<ShadowNet, ShadowCompilerFactory> SHADOW_NET_COMPILER_FACTORIES =
        new HashMap<>();

    /**
     * Realises a Bill Pugh Singleton so that access is lazy and synchronised.
     */
    private static final class InstanceHolder {
        private static final IShadowNetSystemCompiler INSTANCE = new ShadowNetSystemCompiler();
    }

    /**
     * Returns the singleton instance. Access is synchronized to ensure that only one thread can create the compiler at once.
     * @return the singleton instance.
     */
    public static IShadowNetSystemCompiler getInstance() {
        return InstanceHolder.INSTANCE;
    }

    /**
     * Private, as only one instance of this class is allowed. We need to ensure that a listener for
     * serialisation is set so that we can save the compiler alongside the net (system).
     */
    private ShadowNetSystemCompiler() {
        SerialisationListenerRegistry
            .addListener(ShadowNetSystem.class, new ShadowNetSystemSerialisationListener());
        SerialisationListenerRegistry
            .addListener(ShadowNet.class, new ShadowNetSerialisationListener());

    }

    @Override
    public void setDefaultCompilerFactory(
        ShadowCompilerFactory compilerFactory, ShadowNetSystem netSystem)
    {
        SHADOW_NET_SYSTEM_COMPILER_FACTORIES.put(netSystem, compilerFactory);
        netSystem.recompile();
    }

    @Override
    public void setCompilerFactory(ShadowCompilerFactory compilerFactory, ShadowNet shadowNet) {
        if (compilerFactory == null) {
            SHADOW_NET_COMPILER_FACTORIES.remove(shadowNet);
            return;
        }
        SHADOW_NET_COMPILER_FACTORIES.put(shadowNet, compilerFactory);
    }

    @Override
    public InscriptionValidator createInscriptionValidator(ShadowNet net) {
        return new DefaultInscriptionValidator(net);
    }

    @Override
    public void switchNetSystem(ShadowNet shadowNet, ShadowNetSystem shadowNetSystem) {
        // Add net-specific compiler information if the net has none, and
        // the new net system uses a different compiler than the old one.
        ShadowCompilerFactory defaultCompilerFactory =
            SHADOW_NET_SYSTEM_COMPILER_FACTORIES.get(shadowNet.getShadowNetSystem());
        ShadowCompilerFactory currentFactory = SHADOW_NET_COMPILER_FACTORIES.get(shadowNet);
        ShadowCompilerFactory newFactory =
            SHADOW_NET_SYSTEM_COMPILER_FACTORIES.get(shadowNetSystem);
        if (currentFactory == null) {
            currentFactory = defaultCompilerFactory;
            if ((currentFactory != null) && !currentFactory.equals(newFactory)) {
                SHADOW_NET_COMPILER_FACTORIES.put(shadowNet, currentFactory);
            }
        }
        shadowNet.switchNetSystem(shadowNetSystem);

        // Remove specific compiler information if the new net system has
        // the same setting as the net.
        if (currentFactory != null && currentFactory.equals(newFactory)) {
            SHADOW_NET_COMPILER_FACTORIES.remove(shadowNet);
        }
    }

    @Override
    public ShadowCompiler createShadowNetCompiler(ShadowLookup lookup, ShadowNet net)
        throws SyntaxException
    {
        ShadowCompiler compiler = createShadowNetCompiler(net);
        compiler.setLoopbackNetLoader(new LoopbackNetLoader(net.getShadowNetSystem(), lookup));
        compiler.setShadowLookup(lookup);
        return compiler;
    }

    @Override
    public synchronized ShadowLookup compile(ShadowNetSystem shadowNetSystem)
        throws SyntaxException
    {
        return compileInternal(shadowNetSystem, new ShadowLookup(), true);
    }

    @Override
    public synchronized ShadowLookup compileMore(ShadowNetSystem shadowNetSystem)
        throws SyntaxException
    {
        ShadowLookup shadowLookup = new ShadowLookup();
        Collection<Net> allKnownNets = new NetLookup().getAllKnownNets();
        allKnownNets.forEach(net -> shadowLookup.setNet(net.getName(), net));
        compileInternal(shadowNetSystem, shadowLookup, false);
        return shadowLookup;
    }

    /**
     * Performs the actual compilation. If {@code mayRunPreprocessors} is set, the nets will be preprocessed first.
     */
    private ShadowLookup compileInternal(
        ShadowNetSystem shadowNetSystem, ShadowLookup lookup, boolean mayRunPreprocessors)
        throws SyntaxException
    {
        createNets(lookup, shadowNetSystem);
        preprocessNets(lookup, shadowNetSystem, mayRunPreprocessors);

        boolean preprocessorsHaveRun = mayRunPreprocessors;
        while (shadowNetSystem.hasUncompiledNets()) {
            Set<ShadowNet> shadowNets = shadowNetSystem.getUncompiledNets();
            for (ShadowNet shadowNet : shadowNets) {
                ShadowCompiler compiler = createShadowNetCompiler(lookup, shadowNet);
                ShadowPreprocessor[] preprocessors = compiler.getRequiredPreprocessors();
                if (preprocessors != null && preprocessors.length > 0 && !preprocessorsHaveRun) {
                    // A net that was loaded after the start of compilation
                    // requires a preprocessing phase. Too bad, we do not
                    // support this.
                    throw new SyntaxException(
                        "Net " + shadowNet.getName()
                            + " was dynamically loaded and requires a preprocessing phase. that is not supported",
                        new String[0]);
                }
                LOGGER.debug("compiling " + shadowNet.getName() + " using " + compiler);
                compiler.compile(shadowNet);
                // Mark the net as compiled.
                shadowNetSystem.markAsCompiled(shadowNet);
            }
            preprocessorsHaveRun = false;
            // Repeat, if the compiler net loader has appended nets
            // to the net system.
        }
        return lookup;
    }

    /**
     * Creates a {@link Net} for each {@link ShadowNet} contained in the given {@code ShadowNetSystem}. The created {@code Net}
     * is then registered in the given {@code ShadowLookup}.
     */
    private void createNets(ShadowLookup lookup, ShadowNetSystem shadowNetSystem)
        throws SyntaxException
    {
        Set<ShadowNet> uncompiledElements = shadowNetSystem.getUncompiledNets();
        for (ShadowNet shadowNet : uncompiledElements) {
            String netName = shadowNet.getName();
            if (new NetLookup().isKnownNet(netName) || (lookup.getNet(netName) != null)) {
                throw new SyntaxException(
                    "Detected two nets with the same name: " + shadowNet.getName() + ".");
            }
            Net net = createShadowNetCompiler(lookup, shadowNet).createNet(shadowNet.getName());
            lookup.setNet(netName, net);
        }

    }

    private void preprocessNets(
        ShadowLookup lookup, ShadowNetSystem shadowNetSystem, boolean mayRunPreprocessors)
        throws SyntaxException
    {
        // Collect all preprocessors.
        Set<ShadowPreprocessor> preprocessors = new HashSet<>();
        Set<String> preprocessorNets = new HashSet<>();
        Set<ShadowNet> iterator = shadowNetSystem.getUncompiledNets();
        for (ShadowNet shadowNet : iterator) {
            ShadowCompiler compiler = createShadowNetCompiler(lookup, shadowNet);
            ShadowPreprocessor[] requiredPreprocessors = compiler.getRequiredPreprocessors();
            if (requiredPreprocessors != null && requiredPreprocessors.length > 0) { //NOTICEredundant da war vorher ein &
                preprocessors.addAll(Arrays.asList(requiredPreprocessors));
                preprocessorNets.add(shadowNet.getName());
            }
        }

        // Check whether preprocessors are permitted.
        if (preprocessors.isEmpty()) {
            return;
        }
        if (!mayRunPreprocessors) {
            StringBuilder builder = new StringBuilder();
            if (preprocessorNets.size() == 1) {
                builder.append("The dynamically loaded net ");
                builder.append(preprocessorNets.iterator().next());
                builder.append(" requires");
            } else {
                builder.append("Some dynamically loaded nets (");
                builder.append(String.join(", ", preprocessorNets));
                builder.append(") require");
            }
            builder.append(" a preprocessing phase. This is not allowed.");
            throw new SyntaxException(builder.toString(), new String[0]);
        }


        // Run all preprocessors.
        for (ShadowPreprocessor preprocessor : preprocessors) {
            preprocessor.setShadowLookup(lookup);
            preprocessor.preprocess(shadowNetSystem);
        }
    }

    private ShadowCompiler createShadowNetCompiler(ShadowNet net) throws SyntaxException {
        if (SHADOW_NET_COMPILER_FACTORIES.containsKey(net)) {
            return SHADOW_NET_COMPILER_FACTORIES.get(net).createCompiler();
        }
        ShadowNetSystem shadowNetSystem = net.getShadowNetSystem();
        if (SHADOW_NET_SYSTEM_COMPILER_FACTORIES.containsKey(shadowNetSystem)) {
            return SHADOW_NET_SYSTEM_COMPILER_FACTORIES.get(shadowNetSystem).createCompiler();
        }
        throw new SyntaxException(
            "No compiler or default compiler set for net " + net.getName(), new String[0]);
    }

    private final class DefaultInscriptionValidator implements InscriptionValidator {
        private final ShadowNet _net;

        /**
         * Creates a new validator that is used to validate the given net.
         */
        DefaultInscriptionValidator(ShadowNet net) {
            _net = net;
        }

        @Override
        public String checkArcInscription(String inscription, boolean special)
            throws SyntaxException
        {
            return createShadowNetCompiler(_net).checkArcInscription(inscription, special, _net);
        }

        @Override
        public String checkDeclarationNode(String inscription, boolean special)
            throws SyntaxException
        {
            return createShadowNetCompiler(_net).checkDeclarationNode(inscription, special, _net);
        }

        @Override
        public String checkPlaceInscription(String inscription, boolean special)
            throws SyntaxException
        {
            return createShadowNetCompiler(_net).checkPlaceInscription(inscription, special, _net);
        }

        @Override
        public String checkTransitionInscription(String inscription, boolean special)
            throws SyntaxException
        {
            return createShadowNetCompiler(_net)
                .checkTransitionInscription(inscription, special, _net);
        }
    }

    private static final class ShadowNetSystemSerialisationListener
        implements SerialisationListener<ShadowNetSystem>
    {

        @Override
        public void onWrite(ShadowNetSystem netSystem, ObjectOutputStream outputStream)
            throws IOException
        {
            Optional<ShadowCompilerFactory> factory =
                Optional.ofNullable(SHADOW_NET_SYSTEM_COMPILER_FACTORIES.get(netSystem));
            if (factory.isPresent()) {
                outputStream.writeObject(factory.get());
            }
        }

        @Override
        public void onRead(ShadowNetSystem netSystem, ObjectInputStream inputStream)
            throws IOException, ClassNotFoundException
        {
            ShadowCompilerFactory factory = (ShadowCompilerFactory) inputStream.readObject();
            SHADOW_NET_SYSTEM_COMPILER_FACTORIES.put(netSystem, factory);
        }
    }

    private static final class ShadowNetSerialisationListener
        implements SerialisationListener<ShadowNet>
    {

        @Override
        public void onWrite(ShadowNet net, ObjectOutputStream outputStream) throws IOException {
            Optional<ShadowCompilerFactory> factory =
                Optional.ofNullable(SHADOW_NET_COMPILER_FACTORIES.get(net));
            if (factory.isPresent()) {
                outputStream.writeObject(factory.get());
            }
        }

        @Override
        public void onRead(ShadowNet net, ObjectInputStream inputStream)
            throws IOException, ClassNotFoundException
        {
            try {
                ShadowCompilerFactory factory = (ShadowCompilerFactory) inputStream.readObject();
                SHADOW_NET_COMPILER_FACTORIES.put(net, factory);
            } catch (OptionalDataException e) {
                LOGGER.debug("No compiler was read for net " + net.getName(), e);
            }
        }
    }
}
