package be.iminds.ilabt.jfed.rspec.model.javafx_impl;

import be.iminds.ilabt.jfed.lowlevel.api.user_spec.UserSpec;
import be.iminds.ilabt.jfed.rspec.model.*;
import be.iminds.ilabt.jfed.rspec.model.controller.ExperimentBarrierSegment;
import be.iminds.ilabt.jfed.rspec.model.controller.ExperimentCommand;
import be.iminds.ilabt.jfed.rspec.model.impl.*;
import be.iminds.ilabt.jfed.rspec.parser.*;
import be.iminds.ilabt.jfed.rspec.request.ext.delay.SliverTypeShaping;
import be.iminds.ilabt.jfed.util.GeniUrn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLStreamException;
import java.util.*;

import static be.iminds.ilabt.jfed.rspec.parser.RspecXmlConstants.*;

public class FXRspecFactory implements RspecFactory {
    public static final QName Q_SLIVER_TYPE_SHAPING =
            new QName("http://www.protogeni.net/resources/rspec/ext/delay/1", "sliver_type_shaping");
    private final static Logger LOG = LoggerFactory.getLogger(FXRspecFactory.class);
    private static final FXRspecFactory INSTANCE = new FXRspecFactory();

    private FXRspecFactory() {
    }

    public static FXRspecFactory getInstance() {
        return INSTANCE;
    }

    @Override
    public ModelRspecType getModelRspecType() {
        return ModelRspecType.FX;
    }

    @Override
    public FXRspecNode createNode(ModelRspec modelRspec) {
        assert modelRspec == null || modelRspec instanceof FXModelRspec;
        return new FXRspecNode((FXModelRspec) modelRspec, null);
    }

    @Override
    public FXRspecNode createNodeWithClientId(ModelRspec modelRspec, String id) {
        assert modelRspec == null || modelRspec instanceof FXModelRspec;
        return new FXRspecNode((FXModelRspec) modelRspec, id);
    }

    @Override
    public FXRspecNode copyNode(ModelRspec newModelRspec, RspecNode rspecNode, RspecNode.InterfaceCopyMethod interfaceCopyMethod) {
        assert rspecNode instanceof FXRspecNode;
        assert newModelRspec instanceof FXModelRspec;
        return new FXRspecNode((FXModelRspec) newModelRspec, rspecNode, interfaceCopyMethod);
    }

    @Override
    public FXRspecNode copyNodeIncludingInterfaces(ModelRspec newModelRspec, RspecNode rspecNode) {
        assert rspecNode instanceof FXRspecNode;
        assert newModelRspec instanceof FXModelRspec;
        return new FXRspecNode((FXModelRspec) newModelRspec, rspecNode, RspecNode.InterfaceCopyMethod.CREATE_NEW_INTERFACE);
    }

    @Override
    public RspecNode convertToSpecialNode(RspecNode origNode) throws RspecParseException {
        assert origNode != null;
        assert origNode instanceof FXRspecNode;

        EventListExtraXml delayShapingXml = null;
        if (origNode.getSliverTypes().size() == 1)
            for (ExtraXml extraXml : origNode.getSliverTypes().get(0).getExtraXml()) {
                if (Objects.equals(Q_SLIVER_TYPE_SHAPING, extraXml.getStartElementName())) {
                    assert delayShapingXml == null;
                    assert extraXml instanceof EventListExtraXml;
                    delayShapingXml = (EventListExtraXml) extraXml;
                }
            }

        boolean isDelayBridge = origNode.getSliverTypes().size() == 1 &&
                Objects.equals(origNode.getSliverTypes().get(0).getName(), "delay");

        if (isDelayBridge) {
            //remove ExtraXml
            SliverType origSliverType = origNode.getSliverTypes().get(0);
            List<ExtraXml> sliverTypeExtraXml = new ArrayList<>(origSliverType.getExtraXml());
            sliverTypeExtraXml.remove(delayShapingXml);
            SliverType newSliverType =
                    new SliverTypeBuilder(origSliverType.getName()).setDiskImages(origSliverType.getDiskImages()).setExtraXml(sliverTypeExtraXml).build();
            origNode.getSliverTypes().set(0, newSliverType);

            DelayRspecNode res =
                    new DelayRspecNode((FXModelRspec) origNode.getRspec(), (FXRspecNode) origNode, RspecNode.InterfaceCopyMethod.STEAL_INTERFACE);
            if (delayShapingXml != null) {
                try {
                    JAXBContext jc = JAXBContext.newInstance(SliverTypeShaping.class);
                    Unmarshaller unmarshaller = jc.createUnmarshaller();
                    XMLEventReader eventReader = delayShapingXml.getAsEventReader();
                    JAXBElement<SliverTypeShaping> jb = unmarshaller.unmarshal(eventReader, SliverTypeShaping.class);
                    eventReader.close();

                    SliverTypeShaping sliverTypeShaping = jb.getValue();
                    res.setPropertiesFromSliverTypeShaping(sliverTypeShaping);
                } catch (XMLStreamException | JAXBException e) {
                    throw new RspecParseException(delayShapingXml.getLocation(), "Error parsing SliverTypeShaping: " + e.getMessage(), e);
                }
            }
            return res;
        }

        return origNode;
    }

    @Override
    public FXRspecLink createLink(ModelRspec rspec, String addDefaultLinkType) {
        assert rspec instanceof FXModelRspec;
        return new FXRspecLink((FXModelRspec) rspec, null, addDefaultLinkType);
    }

    @Override
    public FXRspecLink createLinkWithClientId(ModelRspec rspec, String id, String addDefaultLinkType) {
        assert rspec instanceof FXModelRspec;
        return new FXRspecLink((FXModelRspec) rspec, id, addDefaultLinkType);
    }

    @Override
    public FXRspecLink copyLink(ModelRspec newModelRspec, RspecLink rspecLink, RspecNode.InterfaceCopyMethod interfaceCopyMethod) {
        assert newModelRspec instanceof FXModelRspec;
        return new FXRspecLink((FXModelRspec) newModelRspec, rspecLink, interfaceCopyMethod);
    }

    @Override
    public FXRspecInterface createInterface(RspecNode node, String id) {
        assert node instanceof FXRspecNode;
        return new FXRspecInterface((FXRspecNode) node, id);
    }

    @Override
    public FXRspecInterface createInterface(RspecLink link, String id) {
        assert link instanceof FXRspecLink;
        return new FXRspecInterface((FXRspecLink) link, id);
    }

    @Override
    public FXRspecInterface createInterface(RspecNode node, RspecLink link, String id) {
        assert node instanceof FXRspecNode;
        assert link instanceof FXRspecLink;
        return new FXRspecInterface((FXRspecNode) node, (FXRspecLink) link, id);
    }

    @Override
    public FXRspecInterface createInterface(RspecNode node, RspecLink link) {
        assert node instanceof FXRspecNode;
        assert link instanceof FXRspecLink;
        assert node.getRspec() == link.getRspec();
        assert node.getRspec() instanceof FXModelRspec;
        String newClientId = node.getRspec().nextIfaceName(node);
        return new FXRspecInterface((FXRspecNode) node, (FXRspecLink) link, newClientId);
    }

    @Override
    public FXLinkSetting createLinkSetting(RspecInterface fromIface, RspecInterface toIface) {
        assert fromIface instanceof FXRspecInterface;
        assert toIface instanceof FXRspecInterface;
        return new FXLinkSetting((FXRspecInterface) fromIface, (FXRspecInterface) toIface);
    }

    @Override
    public ExecuteService createExecuteService(String shell, String command) {
        return new FXExecuteService(shell, command);
    }

    @Override
    public InstallService createInstallService(String installPath, String url) {
        return new FXInstallService(installPath, url);
    }

    @Override
    public AnsibleService createAnsibleService() {
        return new BasicAnsibleService();
    }

    @Override
    public FXModelRspec createModelRspec(String rspecType) {
        return new FXModelRspec(rspecType);
    }

    @Override
    public FXModelRspec copyModelRspec(ModelRspec orig, @Nullable String newType) {
        //////////////////////////////////////////////////////////////////
        //WARNING: This is cut-and-paste reuse from BasicRspecFactory!
        //////////////////////////////////////////////////////////////////
        FXModelRspec res = createModelRspec(newType == null ? orig.getType() : newType);

        res.setParseData(orig.getSchemaLocation(), orig.getOrigDefaultNamespace(), orig.getNameSpaceInfo());

        for (RspecNode n : orig.getNodes()) {
            res.addNode(new FXRspecNode(res, n, RspecNode.InterfaceCopyMethod.NO_COPY));
        }

        //copy unbound interfaces
        for (RspecNode n : orig.getNodes()) {
            FXRspecNode copiedNode = res.getNodeByUniqueId(n.getUniqueId());
            assert copiedNode != null;
            for (RspecInterface iface : n.getInterfaces())
                if (!iface.isLinkBound()) {
                    //clone interface
                    FXRspecInterface ifaceCopy =
                            new FXRspecInterface(copiedNode, iface.getClientId()); //since we copy to new model, just copy interface name
                    ifaceCopy.overwritePropertiesWith(iface, true);
                }
        }

        for (RspecLink l : orig.getLinks()) {
            FXRspecLink newLink = new FXRspecLink(res, l, RspecNode.InterfaceCopyMethod.CREATE_NEW_INTERFACE);
            res.addLink(newLink);
            for (RspecInterface i : l.getInterfaces()) {
                FXRspecInterface newIface = newLink.getInterfaceByClientId(i.getClientId());
                FXRspecNode newNode = i.getNode() == null ? null : res.getNodeByUniqueId(i.getNode().getUniqueId());
                newIface.bindNode(newNode);
                newIface.overwritePropertiesWith(i, true);
            }
        }

        for (OpenflowDataPath odp : orig.getOpenflowDataPaths()) {
            OpenflowDataPath newOdp =
                    new BasicOpenflowDataPath(odp.getAsExtraXml(), odp.getComponentManagerId(), odp.getComponentId(), odp.getDpid());
            res.addOpenflowDataPath(newOdp);
        }

        for (Lease l : orig.getLeases()) {
            res.addLease(new BasicLease(l));
        }

        for (Channel channel : orig.getChannels()) {
            res.addChannel(new FXRspecChannel(channel));
        }

        for (GeantTestbedType gtt : orig.getGeantTestbedTypes()) {
            res.addGeantTestbedType(new FXGeantTestbedType(gtt));
        }

        for (AddressPool addressPool : orig.getAddressPools()) {
            res.addAddressPool(new FXAddressPool(addressPool));
        }


        if (orig instanceof FXModelRspec) {
            for (ExperimentBarrierSegment bar : ((FXModelRspec) orig).getBarrierSegments()) {
                ExperimentBarrierSegment copyBar = new ExperimentBarrierSegment(bar.getTag(), bar.getOrderNumber());
                //copy commands is done by finishParsing
                res.addBarrierSegment(copyBar);
            }
        }

        List<ExtraXml> extraXmlCopy = new ArrayList<>();
        for (ExtraXml extraXml : orig.getExtraXml()) {
            boolean noStoreNeeded = false;
            try {
                //if this is a FXModelRspec, no extraXml will be processed here, because we will have copied it above already
                noStoreNeeded = this.processExtraRspecXml(res, new EventListExtraXml(extraXml));
            } catch (RspecParseException e) {
                LOG.error("RspecParseException while processing extra node XML: Extra node XML will be ignored", e);
                noStoreNeeded = false;
            }
            if (!noStoreNeeded)
                extraXmlCopy.add(extraXml); //no copy. That should be ok, as only parser will normally change this and nothing after that.
        }
        res.setExtraXml(extraXmlCopy);

        List<UserSpec> userSpecCopy = new ArrayList<>();
        for (UserSpec u : orig.getSshKeys()) {
            userSpecCopy.add(new UserSpec(u));
        }
        res.setSshKeys(userSpecCopy);

        res.setExpires(orig.getExpiresString());

        res.setRoutableAddressesAvailable(orig.getRoutableAddressesAvailable());
        res.setRoutableAddressesConfigured(orig.getRoutableAddressesConfigured());

        //copy command to experiment barrier
        finishParsing(res);

        return res;
        //////////////////////////////////////////////////////////////////
        //WARNING: This is cut-and-paste reuse from BasicRspecFactory!
        //////////////////////////////////////////////////////////////////
    }

    @Override
    public boolean processExtraRspecXml(ModelRspec model, EventListExtraXml extraXml) throws RspecParseException {
        if (Objects.equals(Q_JFEDCOMMAND_EXP_BARRIER_SEG, extraXml.getStartElementName())) {
            ExperimentBarrierSegment barrierSegment =
                    (ExperimentBarrierSegment) StaxRspecParser.parseWithJaxb(extraXml.getStartElement().getLocation(), extraXml.getAsEventReader(), ExperimentBarrierSegment.class);
            ((FXModelRspec) model).getBarrierSegments().add(barrierSegment);
            return true;
        }
        return false;
    }

    @Override
    public boolean processExtraNodeXml(RspecNode node, EventListExtraXml extraXml) throws RspecParseException {
        if (Q_JFEDCOMMAND_EXP_COMMAND.equals(extraXml.getStartElementName())
                || Q_JFEDCOMMAND_EXP_COMMAND_OLDERROR.equals(extraXml.getStartElementName())) {
            ExperimentCommand c = null;
            try {
                c =
                        (ExperimentCommand) StaxRspecParser.parseWithJaxb(extraXml.getStartElement().getLocation(), extraXml.getAsEventReader(), ExperimentCommand.class);
                c.setNodeUniqueID(node.getUniqueId());
                assert c.getNodeUniqueID() != null;
                assert !c.getNodeUniqueID().isEmpty();
                assert c.getTag() != null;
                assert !c.getTag().isEmpty();
                assert c.getStartingTime() != null;
                assert c.getDuration() != null;
            } catch (Exception | AssertionError t) {
                LOG.error("Something went wrong while using JAXB to parse ExperimentCommand: Will ignore ExperimentCommand (and treat it as unknown extra XML).", t);

                //say we don't know this, so it is still stored in ModelRspec
                return false;
            }
            assert c != null;
            assert node instanceof FXRspecNode;
            ((FXRspecNode) node).getCommands().add(c);
            assert node.getUniqueId().endsWith(c.getNodeUniqueID());

            return true;
        }
        return false;
    }

    @Override
    public boolean processExtraLinkXml(RspecLink link, EventListExtraXml extraXml) throws RspecParseException {
        return false;
    }

    @Override
    public void writeExtraRspecXml(ModelRspec aModelRspec, XMLEventWriter eventWriter) {
        FXModelRspec modelRspec = (FXModelRspec) aModelRspec;
        List<ExperimentBarrierSegment> barrierSegments = modelRspec.getBarrierSegments();
        if (barrierSegments != null)
            for (ExperimentBarrierSegment barrierSegment : barrierSegments) {
                StaxRspecWriter.writeWithJaxb(eventWriter, Q_JFEDCOMMAND_EXP_BARRIER_SEG, barrierSegment, ExperimentBarrierSegment.class);
            }
    }

    @Override
    public void writeExtraNodeXml(RspecNode node, XMLEventWriter eventWriter) {
        FXRspecNode fxRspecNode = (FXRspecNode) node;
        for (ExperimentCommand c : fxRspecNode.getCommands()) {
            StaxRspecWriter.writeWithJaxb(eventWriter, Q_JFEDCOMMAND_EXP_COMMAND, c, ExperimentCommand.class);
        }
    }

    @Override
    public void writeExtraLinkXml(RspecLink link, XMLEventWriter eventWriter) {

    }


    @Override
    public boolean equals(Object o) {
        if (!(o instanceof FXRspecFactory)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        return FXRspecFactory.class.hashCode();
    }

    @Override
    public void mergeExtra(ModelRspec aMergedModel, ModelRspec aPartialRspec) {
        FXModelRspec mergedModel = (FXModelRspec) aMergedModel;
        FXModelRspec partialRspec = (FXModelRspec) aPartialRspec;

        partialRspec.getChannels().forEach(mergedModel::addChannel);
        partialRspec.getLeases().forEach(mergedModel::addLease);
        partialRspec.getGeantTestbedTypes().forEach(mergedModel::addGeantTestbedType);


        Set<Integer> mergedOrderNumbers = new HashSet<>();
        for (ExperimentBarrierSegment bs : mergedModel.getBarrierSegments())
            mergedOrderNumbers.add(bs.getOrderNumber());
        for (ExperimentBarrierSegment bs : partialRspec.getBarrierSegments())
            if (!mergedOrderNumbers.contains(bs.getOrderNumber())) {
                mergedModel.addBarrierSegment(new ExperimentBarrierSegment(bs.getTag(), bs.getOrderNumber()));
                mergedOrderNumbers.add(bs.getOrderNumber());
            }

        //make sure the commands are added to the barrier
        finishParsing(aMergedModel);
    }

    @Override
    public void finishParsing(ModelRspec modelRspec) {
        FXModelRspec fxModelRspec = (FXModelRspec) modelRspec;
        for (ExperimentBarrierSegment barrierSegment : fxModelRspec.getBarrierSegments())
            for (FXRspecNode n : fxModelRspec.getNodes())
                for (ExperimentCommand command : n.getCommands())
                    if (barrierSegment.getOrderNumber() == command.getBarrierSegmentOrderNumber())
                        barrierSegment.addCommand(command);
    }

    @Override
    public OpenflowDataPath createOpenflowDataPath(ModelRspec modelRspec, EventListExtraXml allXml, String componentManagerId, String componentId, String dpidAttValue) {
        //use BasicOpenflowDataPath, as we do nothing special with it for the moment
        assert modelRspec instanceof FXModelRspec;
        return new BasicOpenflowDataPath(allXml, componentManagerId, componentId, dpidAttValue);
    }

    @Override
    public Lease createLease(ModelRspec rspec, String leaseId, String clientId, Date validFromDate, Date validUntilDate, String validFromString, String validUntilString) {
        //use BasicLease, as we do nothing special with it for the moment
        assert (rspec instanceof FXModelRspec);
        return new BasicLease(leaseId, clientId, validFromDate, validUntilDate, validFromString, validUntilString);
    }

    @Override
    public Lease createLease(ModelRspec rspec, String leaseId, String clientId, Date validFromDate, Date validUntilDate) {
        //use BasicLease, as we do nothing special with it for the moment
        assert (rspec instanceof FXModelRspec);
        return new BasicLease(leaseId, clientId, validFromDate, validUntilDate);
    }

    @Override
    public Channel createChannel(ModelRspec modelRspec) {
        return new FXRspecChannel();
    }

    @Override
    public GeantTestbedType createGeantTestbedType(String name) {
        return new FXGeantTestbedType(name);
    }

    @Override
    public AddressPool createAddressPool(ModelRspec modelRspec) {
        assert modelRspec instanceof FXModelRspec;
        return new FXAddressPool();
    }

    @Override
    public AddressPool copyAddressPool(ModelRspec modelRspec, AddressPool addressPool) {
        assert modelRspec instanceof FXModelRspec;
        return new FXAddressPool(addressPool);
    }

    @Override
    public IPv4 createIPv4(ModelRspec modelRspec) {
        assert modelRspec instanceof FXModelRspec;
        return new BasicIPv4();
    }

    @Override
    public IPv4 createIPv4(ModelRspec modelRspec, String address, String netmask) {
        assert modelRspec instanceof FXModelRspec;
        return new BasicIPv4(address, netmask);
    }

    @Override
    public ExecuteAnsiblePlaybook createExecuteAnsibleCookbook(ModelRspec rspec, String source, String outputFilename) {
        return new BasicExecuteAnsiblePlaybook(source, outputFilename);
    }

    @Override
    public DistributeSshKeypair createDistributeSshKeypair(ModelRspec rspec, String location, GeniUrn userUrn) {
        return new BasicDistributeSshKeypair(location, userUrn);
    }
}
