package be.iminds.ilabt.jfed.rspec.rspec_source;

import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.rspec.parser.RspecCompare;
import be.iminds.ilabt.jfed.rspec.parser.RspecParseException;
import be.iminds.ilabt.jfed.rspec.parser.StaxRspecParser;
import be.iminds.ilabt.jfed.util.GeniUrn;
import be.iminds.ilabt.jfed.util.ProgressHandler;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.xml.stream.XMLStreamException;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/**
 * RspecSource
 * <p>
 * Note: this is a somewhat immutable class
 * <p>
 * RspecSource takes care of converting and caching different StringRspecs and ModelRspecs.
 * You can create one by providing either a StringRspec (assumed ModelRspec.RequestRspecSpecialCases.NONE) or a ModelRspec.
 * Then you can request any type of special case StringRspec, or a ModelRspec for any type of factory.
 * The RspecSource will convert what it has to create the requested rspec if needed, and cache the result so these conversions need to be done as little as possible.
 * <p>
 * This class tries to minimize blocking on RSpec-parsing by performing this in a separate thread.
 * If parsing hasn't completed yet, and a ModelRspec is requested, the call will block on this request until the parsing has completed.
 */
public abstract class RspecSource {
    private static final Logger LOG = LoggerFactory.getLogger(RspecSource.class);

    private static final ExecutorService executorService = Executors.newCachedThreadPool(
            new ThreadFactoryBuilder().setNameFormat("RspecSource-%d").build());

    protected final Map<ModelRspec.RequestRspecSpecialCases, StringRspec> stringRspecBySpecialCase =
            new EnumMap<>(ModelRspec.RequestRspecSpecialCases.class);
    protected final Map<ModelRspecType, StaxRspecParser> modelRspecParserMap =
            new EnumMap<>(ModelRspecType.class);
    protected final Map<ModelRspecType, Future<ModelRspec>> modelRspecFutureMap =
            new EnumMap<>(ModelRspecType.class);
    protected final Map<ModelRspecType, ModelRspec> modelRspecMap =
            new EnumMap<>(ModelRspecType.class);

    private boolean initialized = false;
    protected boolean isXmlBased = false;

    private ModelRspecType previousFactoryType; //may never be null!

    protected RspecSource(StringRspec stringRspec) {
        this(stringRspec, null);
        assert stringRspec != null;
        this.stringRspecBySpecialCase.put(ModelRspec.RequestRspecSpecialCases.NONE, stringRspec);
        isXmlBased = true;
        previousFactoryType = ModelRspecType.BASIC;
    }

    /**
     * @param defaultModelRspecType default ModelRspecType to use. may be null. (= BASIC in that case)
     */
    protected RspecSource(StringRspec stringRspec, ModelRspecType defaultModelRspecType) {
        assert stringRspec != null;
        this.stringRspecBySpecialCase.put(ModelRspec.RequestRspecSpecialCases.NONE, stringRspec);
        isXmlBased = true;
        previousFactoryType = defaultModelRspecType == null ? ModelRspecType.BASIC : defaultModelRspecType;
    }

    protected RspecSource(ModelRspec modelRspec) {
        assert modelRspec != null;
        modelRspecMap.put(modelRspec.getModelRspecType(), modelRspec); //TODO make a copy of modelRspec
        previousFactoryType = modelRspec.getModelRspecType();
        isXmlBased = false;
        initialized = true; // no lazy initialization needed anymore
    }

    /**
     * @param defaultModelRspecType default ModelRspecType to use. may be null. (= BASIC in that case)
     */
    public RspecSource(String xmlRspecString, ModelRspecType defaultModelRspecType) {
        StringRspec stringRspec = new StringRspec(xmlRspecString);
        this.stringRspecBySpecialCase.put(ModelRspec.RequestRspecSpecialCases.NONE, stringRspec);
        isXmlBased = true;
        previousFactoryType = defaultModelRspecType != null ? defaultModelRspecType : ModelRspecType.BASIC;
    }

    /**
     * This method is used for lazy initialization of the first ModelRspec
     */
    private void initialize() {
        getModelRspec(); //the RspecSource will be fully initialized after the first ModelRspec has been created
    }

    public boolean isXmlBased() {
        if (!initialized)
            initialize();

        return isXmlBased;
    }

    @Nullable
    public String getRspecXmlString() {
        return getRspecXmlString(ModelRspec.RequestRspecSpecialCases.NONE);
    }

    @Nullable
    public String getRspecXmlString(ModelRspec.RequestRspecSpecialCases rspecSpecialCase) {
        final StringRspec stringRspec = getStringRspec(rspecSpecialCase);
        if (stringRspec != null)
            return stringRspec.getXmlString();
        else return null;
    }

    protected boolean parseFailed = false;

    protected String modelToString(ModelRspec modelRspec, ModelRspec.RequestRspecSpecialCases rspecSpecialCase) {
        assert (modelRspec != null);
        return modelRspec.toGeni3Rspec(rspecSpecialCase);
    }


    /**
     * @return a modelRspec, created by any RspecFactory
     */
    @Nullable
    public ModelRspec getModelRspec() {
        ModelRspec modelRspec = getModelRspec(previousFactoryType);
        if (modelRspec == null && !modelRspecMap.isEmpty())
            modelRspec = modelRspecMap.entrySet().iterator().next().getValue();
        return modelRspec;
    }

    /**
     * @return a modelRspec, created by any RspecFactory
     */
    @Nullable
    public ModelRspec getModelRspec(ProgressHandler... progressHandlers) {
        ModelRspec modelRspec = getModelRspec(previousFactoryType, progressHandlers);
        if (modelRspec == null && !modelRspecMap.isEmpty())
            modelRspec = modelRspecMap.entrySet().iterator().next().getValue();
        return modelRspec;
    }


//    public ModelRspec getModelRspec(ModelRspecType modelRspecType) {
//        if (modelRspecMap.containsKey(modelRspecType)) return modelRspecMap.get(modelRspecType);
//        if (parseFailed)
//            return null;  //this will be true if the parsing of the StringRspec already failed on a previous call to this method.
//        assert !stringRspecBySpecialCase.isEmpty();
//        try {
//            StringRspec stringRspec;
//            if (stringRspecBySpecialCase.containsKey(FXModelRspec.RequestRspecSpecialCases.NONE))
//                stringRspec = stringRspecBySpecialCase.get(FXModelRspec.RequestRspecSpecialCases.NONE);
//            else
//                stringRspec = stringRspecBySpecialCase.get(stringRspecBySpecialCase.keySet().iterator().next());
//            return storeModelRspec(stringToModel(modelRspecType, stringRspec));
//        } catch (Exception | AssertionError t) {
//            if (t instanceof RspecParseException) {
//                lastInvalidRspecException = (RspecParseException) t;
//            }
//
//            LOG.warn("RSpec String parse failed. Will not return ModelRspec", t);
//            parseFailed = true;
//            return null;
//        }
//    }

    public boolean isModelRspecAvailable(ModelRspecType modelRspecType) {
        return modelRspecMap.containsKey(modelRspecType);
    }


    @SuppressWarnings("unchecked")
    @Nullable
    public <M extends ModelRspec> M getModelRspec(ModelRspecType modelRspecType, ProgressHandler... progressHandlers) {
        if (modelRspecMap.containsKey(modelRspecType))
            return (M)modelRspecMap.get(modelRspecType);
        if (parseFailed)
            return null;

        Future<ModelRspec> future = getModelRspecAsync(modelRspecType, progressHandlers);

        try {
            if (future == null) {
                throw new ExecutionException(
                        new RspecParseException(null, "Cannot create a modelRspec from an empty Rspec"));
            }

            return (M)future.get();
        } catch (ExecutionException e) {
            if (e.getCause() instanceof RspecParseException) {
                lastInvalidRspecException = (RspecParseException) e.getCause();
            }

            LOG.warn("RSpec String parse failed. Will not return ModelRspec", e);
            parseFailed = true;
            isXmlBased = true;
            return null;
        } catch (InterruptedException e) {
            LOG.error("Rspec parsing failed unexpectedly!", e);
            parseFailed = true;
            return null;
        } finally {
            initialized = true;
            if (future != null && future.isDone()) {
                try {
                    modelRspecMap.put(modelRspecType, future.get());
                } catch (InterruptedException | ExecutionException ignored) {
                }
            }
        }
    }

    @Nullable
    private Future<ModelRspec> getModelRspecAsync(ModelRspecType modelRspecType, ProgressHandler... progressHandlers) {
        Future<ModelRspec> future;
        synchronized (this) {
            StaxRspecParser parser;
            if (!modelRspecParserMap.containsKey(modelRspecType)) {
                //get the correct StringRspec
                StringRspec stringRspec;
                if (stringRspecBySpecialCase.containsKey(ModelRspec.RequestRspecSpecialCases.NONE))
                    stringRspec = stringRspecBySpecialCase.get(ModelRspec.RequestRspecSpecialCases.NONE);
                else {
                    if (!stringRspecBySpecialCase.isEmpty())
                        stringRspec = stringRspecBySpecialCase.get(stringRspecBySpecialCase.keySet().iterator().next());
                    else {
                        //this onlt occurs when we do not have a stingrspec, but we have a modelrspec of the wrong type
                        stringRspec =
                                getStringRspec(); //no stringRspec stored. getStringRspec() will generate one from the modelRspec
                        //this can be done more efficiently, as ModelRspec can be copied. But this is probably the safest method
                    }
                }

                //patch the xml if needed
                String xmlString = stringRspec.getXmlString();

                //handle empty String
                if (xmlString == null)
                    return null;

                //quick HACK to allow the planetlab Europe rspec manifest
                if (xmlString.contains("<RSpec")) {
                    LOG.debug("HACK: rewriting received planetlab manifest XML to make it parsable xml");
                    xmlString =
                            xmlString.replace("<RSpec type=\"SFA\"", "<rspec xmlns=\"http://www.geni.net/resources/rspec/3\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" type=\"manifest\"");
                    xmlString =
                            xmlString.replace("<RSpec ", "<rspec xmlns=\"http://www.geni.net/resources/rspec/3\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" type=\"manifest\"");
                    xmlString = xmlString.replace("</RSpec>", "</rspec>");
                    xmlString = xmlString.replace("<network name=\"ple\">", "");
                    xmlString = xmlString.replace("</network>", "");
                    LOG.debug("HACK: rewritten xml: " + xmlString);
                }

                parser = new StaxRspecParser(xmlString, RspecFactoryFactory.getRspecFactoryInstance(modelRspecType));
                future = executorService.submit(parser);


            } else {
                parser = modelRspecParserMap.get(modelRspecType);
                future = modelRspecFutureMap.get(modelRspecType);
            }

            //add progress-handlers
            for (ProgressHandler handler : progressHandlers)
                parser.addProgressHandler(handler);
        }
        return future;
    }


    private RspecParseException lastInvalidRspecException = null;

    @Nullable
    public RspecParseException getLastInvalidRspecException() {
        return lastInvalidRspecException;
    }

    @Nullable
    public StringRspec getStringRspec() {
        return getStringRspec(ModelRspec.RequestRspecSpecialCases.NONE);
    }

    @Nullable
    public StringRspec getStringRspec(ModelRspec.RequestRspecSpecialCases rspecSpecialCase) {
        if (stringRspecBySpecialCase.containsKey(rspecSpecialCase))
            return stringRspecBySpecialCase.get(rspecSpecialCase);
        ModelRspec modelRspec = getModelRspec();
        if (modelRspec == null) {
            assert !stringRspecBySpecialCase.isEmpty() :
                    "both modelRspec == null and stringRspecBySpecialCase.isEmpty()";

            try {
                modelRspec = getModelRspec(previousFactoryType);
            } catch (AssertionError | Exception t) {
                LOG.warn("RSpec String parse failed. Cannot change type of rspec to " + rspecSpecialCase, t);
                parseFailed = true;
                return null;
            }
        }
        assert modelRspec != null;

        String rspecXml = modelToString(modelRspec, rspecSpecialCase);
        StringRspec stringRspec = new StringRspec(rspecXml);
        stringRspecBySpecialCase.put(rspecSpecialCase, stringRspec);
        return stringRspec;
    }


    public List<GeniUrn> getAllComponentManagerUrns() {
        ModelRspec model = getModelRspec();
        if (model != null) {
            List<GeniUrn> res = model.getAllComponentManagerUrns();

            assert getStringRspec().isWellFormed();

            //parse generated rspec for openflow component URNs.
            //   Note: This could be done more efficiently, by parsing "others", but it's not worth the trouble

            for (String urnString : StringRspec.parseOpenflowComponentManagers(getStringRspec().getXmlString())) {
                GeniUrn geniUrn = GeniUrn.parse(urnString);

                if (geniUrn != null)
                    res.add(geniUrn);
                else {
                    LOG.warn("Could not parse openflowComponentManager-urn {}", urnString);
                }
            }

            return res;
        } else {
            if (getStringRspec().isWellFormed()) {
                List<GeniUrn> res = new ArrayList<>();
                for (String urnString : getStringRspec().getAllComponentManagerUrns()) {
                    GeniUrn geniUrn = GeniUrn.parse(urnString);

                    if (geniUrn != null)
                        res.add(geniUrn);
                    else {
                        LOG.warn("Could not parse openflowComponentManager-urn {}", urnString);
                    }
                }

                return res;
            } else
                return null;
        }
    }

    public List<BasicStringRspec.BasicNodeInfo> getBasicNodeInfo() {
        ModelRspec model = getModelRspec();
        if (model != null) {
            List<BasicStringRspec.BasicNodeInfo> res = new ArrayList<>();
            List<? extends RspecNode> modelNodes = model.getNodes();

            assert getStringRspec().isWellFormed();

            for (RspecNode node : modelNodes) {
                List<String> sliverTypeNames = new ArrayList<>();
                for (SliverType sliverType : node.getSliverTypes())
                    sliverTypeNames.add(sliverType.getName());
                BasicStringRspec.BasicNodeInfo ni = new BasicStringRspec.BasicNodeInfo(
                        node.getClientId(),
                        node.getComponentId() == null ? null : node.getComponentId().toString(),
                        node.getComponentManagerId() == null ? null : node.getComponentManagerId().toString(),
                        node.getAvailable(),
                        node.getExclusive(),
                        sliverTypeNames);
                res.add(ni);
            }

            return res;
        } else {
            LOG.debug("The RSpec could not be parsed using ModelRspec, will try StringRspec method.");
            if (getStringRspec().isWellFormed()) {
                return getStringRspec().getBasicNodeInfo();
            } else {
                LOG.warn("The RSpec could not be parsed using any method. Will return null.");
                return null;
            }
        }
    }

    private final Map<ModelRspecType, LosingDataInfo> losingDataInfoMap = new HashMap<>();

    private LosingDataInfo getLosingDataInfo(ModelRspecType modelRspecType) {
        if (!isXmlBased)
            return null; //cannot lose data when not xml-based!

        if (!losingDataInfoMap.containsKey(modelRspecType)) {


            ModelRspec modelRspec = getModelRspec(modelRspecType);

            LosingDataInfo losingDataInfo;
            //set isLosingData, but handle any failure.
            try {
                RspecCompare compareWithOrig =
                        new RspecCompare(modelRspec.toGeni3Rspec(), getRspecXmlString(), "the converted RSpec", "the original RSpec");
                losingDataInfo =
                        new LosingDataInfo(!compareWithOrig.isEqual(), compareWithOrig.getDifferences()); //TODO: only calculate and set losingData this when it is actually needed
            } catch (XMLStreamException ex) {
                LOG.error("Exception while using RspecCompare at end of parse. location=" + RspecParseException.locationToString(ex.getLocation()), ex);
                losingDataInfo = new LosingDataInfo(true, /*"Exception while comparing: "+ex.getMessage()*/null);
            } catch (AssertionError | Exception ex) {
                LOG.error("Exception while using RspecCompare at end of parse. location=unknown", ex);
                losingDataInfo = new LosingDataInfo(true, /*"Exception while comparing: "+ex.getMessage()*/null);
            }

            losingDataInfoMap.put(modelRspecType, losingDataInfo);
        }

        return losingDataInfoMap.get(modelRspecType);
    }

    public boolean isLosingData(ModelRspecType modelRspecType) {
        LosingDataInfo losingDataInfo = getLosingDataInfo(modelRspecType);
        return losingDataInfo != null && losingDataInfo.isLosingData();
    }

    public List<RspecCompare.Difference> getLosingDataDifferences(ModelRspecType modelRspecType) {
        LosingDataInfo losingDataInfo = getLosingDataInfo(modelRspecType);
        if (losingDataInfo != null)
            return losingDataInfo.getLosingDataDifferences();
        else
            return null;
    }

    public static class LosingDataInfo {
        private final boolean losingData;
        private final List<RspecCompare.Difference> losingDataDifferences;


        public LosingDataInfo(boolean losingData, List<RspecCompare.Difference> losingDataDifferences) {
            this.losingData = losingData;
            this.losingDataDifferences = losingDataDifferences;
        }

        public boolean isLosingData() {
            return losingData;
        }

        public List<RspecCompare.Difference> getLosingDataDifferences() {
            return losingDataDifferences;
        }
    }

}
