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

import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.xml.namespace.QName;
import javax.xml.stream.Location;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import javax.xml.transform.stream.StreamSource;
import java.io.StringReader;
import java.util.*;

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

/**
 * RspecCompare
 * <p>
 * This class can compare 2 rspecs, and determine if they are equal.
 * <p>
 * Restrictions:
 * - The nodes and links MUST be in the same order.   (this assumption is needed to make this fast enough)
 * - no support for text mixed with elements
 * <p>
 * Things that are ignored when comparing:
 * - Where namespaces are declared, and which namespaces are declared.
 * - Order of elements within nodes and links
 * - Order of non node and link elements within <rspec>
 * - comments
 * - generated and generated_by attribute in <rspec>
 * - http://jfed.iminds.be/rspec/ext/jfed/1 namespace
 * <p>
 * Things that are certainly cause of inequality:
 * - extra or missing attributes
 * - extra or missing elements
 */
public class RspecCompare {
    private final static Logger LOG = LoggerFactory.getLogger(RspecCompare.class);

    private boolean equal = false;
    private final List<Difference> differences;

    private final String rspecNameA;
    private final String rspecNameB;

    public RspecCompare(@Nonnull String rspecA, @Nonnull String rspecB) throws XMLStreamException, RspecParseException {
        this(rspecA, rspecB, "RSpec A", "RSpec B");
    }

    public RspecCompare(@Nonnull String rspecA, @Nonnull String rspecB,
                        @Nonnull String rspecNameA, @Nonnull String rspecNameB) throws XMLStreamException, RspecParseException {
        this.differences = new ArrayList<>();
        this.rspecNameA = rspecNameA;
        this.rspecNameB = rspecNameB;
        this.equal = equalRspec(rspecA, rspecB);
    }

    public boolean isEqual() {
        return equal;
    }

    public List<Difference> getDifferences() {
        return differences;
    }


    private final static QName FIXHACK_BAD = Q_JFEDCOMMAND_EXP_COMMAND_OLDERROR;
    private final static QName FIXHACK_GOOD = Q_JFEDCOMMAND_EXP_COMMAND;

    private static QName fixNameHack(QName name) {
        if (Objects.equals(name, FIXHACK_BAD))
            return FIXHACK_GOOD;
        return name;
    }

    /**
     * Checks if 2 lists of element events are the same.
     * <p>
     * Assumptions:
     * - first event is startElement
     * - last event is matching endElement
     */
    private boolean compareElement(List<XMLEvent> elementEventsA, List<XMLEvent> elementEventsB, boolean logDiffs) throws XMLStreamException, RspecParseException {
        if (elementEventsA.isEmpty()) {
            if (!elementEventsB.isEmpty() && logDiffs)
                differences.add(new Difference(rspecNameA + " has empty element, where " + rspecNameB +
                        " has non empty element (" + elementEventsB.get(0).asStartElement().getName() + ")",
                        null, elementEventsB.get(0).asStartElement().getLocation(), rspecNameA, rspecNameB));
            return elementEventsB.isEmpty();
        }
        if (elementEventsB.isEmpty()) {
            if (logDiffs) //elementsEventsA is always non-empty if we are here
                differences.add(new Difference(rspecNameB + " has empty element, where " + rspecNameA +
                        " has non empty element (" + elementEventsA.get(0).asStartElement().getName() + ")",
                        null, elementEventsA.get(0).asStartElement().getLocation(), rspecNameA, rspecNameB));
            return false;
        }

        assert elementEventsA.size() >= 2 : "sizeA=" + elementEventsA.size() + " sizeB=" + elementEventsB.size();
        assert elementEventsB.size() >= 2 : "sizeA=" + elementEventsA.size() + " sizeB=" + elementEventsB.size();

        XMLEvent firstEventA = elementEventsA.get(0);
        XMLEvent firstEventB = elementEventsB.get(0);
        XMLEvent lastEventA = elementEventsA.get(elementEventsA.size() - 1);
        XMLEvent lastEventB = elementEventsB.get(elementEventsB.size() - 1);

        assert firstEventA.isStartElement();
        assert firstEventB.isStartElement();
        assert lastEventA.isEndElement();
        assert lastEventB.isEndElement();

        StartElement startA = firstEventA.asStartElement();
        StartElement startB = firstEventB.asStartElement();
        EndElement endA = lastEventA.asEndElement();
        EndElement endB = lastEventB.asEndElement();

        assert Objects.equals(fixNameHack(startA.getName()), fixNameHack(endA.getName()));
        assert Objects.equals(fixNameHack(startB.getName()), fixNameHack(endB.getName()));

        if (!Objects.equals(fixNameHack(startA.getName()), fixNameHack(startB.getName()))) {
            if (logDiffs)
                differences.add(new Difference("" + rspecNameA + " has different name than " + rspecNameB + "." +
                        " " + rspecNameA + " name=" + startA.getName() +
                        " " + rspecNameB + " name=" + startB.getName(),
                        startA.getLocation(), startB.getLocation(), rspecNameA, rspecNameB));
            return false;
        }

        assert Objects.equals(fixNameHack(endA.getName()), fixNameHack(endB.getName()));

        if (!sameAttributes(startA, startB, logDiffs)) {
            if (logDiffs) differences.add(
                    new Difference("" + rspecNameA + " has different attributes than " + rspecNameB + " for element \"" + startA.getName() + "\"." +
                            " " + rspecNameA + " attributes: " + attributesToString(startA) +
                            " " + rspecNameB + " attributes: " + attributesToString(startB)
                            , startA.getLocation(), startB.getLocation(), rspecNameA, rspecNameB));
            return false;
        }

        List<List<XMLEvent>> subElementsA = new LinkedList<>();
        List<String> charsA = new LinkedList<>();

        for (Iterator<XMLEvent> it = elementEventsA.iterator(); it.hasNext(); ) {
            XMLEvent e = it.next();

            if (e == firstEventA || e == lastEventA)
                continue;

            if (e.isStartElement()) {
                if (isIgnoredElement(e.asStartElement().getName()))
                    getAllElementEventsFromIt(e.asStartElement(), it);
                else
                    subElementsA.add(getAllElementEventsFromIt(e.asStartElement(), it));
            }
            if (e.isCharacters() && !e.asCharacters().isWhiteSpace())
                charsA.add(e.asCharacters().getData());
        }
        List<List<XMLEvent>> subElementsB = new LinkedList<>();
        List<String> charsB = new LinkedList<>();
        for (Iterator<XMLEvent> it = elementEventsB.iterator(); it.hasNext(); ) {
            XMLEvent e = it.next();

            if (e == firstEventB || e == lastEventB)
                continue;

            if (e.isStartElement()) {
                if (isIgnoredElement(e.asStartElement().getName()))
                    getAllElementEventsFromIt(e.asStartElement(), it);
                else
                    subElementsB.add(getAllElementEventsFromIt(e.asStartElement(), it));
            }
            if (e.isCharacters() && !e.asCharacters().isWhiteSpace())
                charsB.add(e.asCharacters().getData());
        }

        if (charsA.size() != charsB.size()) {
            if (logDiffs)
                differences.add(new Difference("" + rspecNameA + " has " + charsA.size() + " CharacterElements, while " + rspecNameB + " has " + charsB.size()
                        + " " + rspecNameA + "text=" + (charsA.size() == 1 ? "\"" + charsA.get(0) + "\""
                        : (charsA.isEmpty() ? "" : charsA))
                        + " " + rspecNameB + "text=" + (charsB.size() == 1 ? "\"" + charsB.get(0) + "\""
                        : (charsB.isEmpty() ? "" : charsB)),
                        startA.getLocation(), startB.getLocation(), rspecNameA, rspecNameB));
            return false;
        }
        if (!charsA.isEmpty()) {
            if (!Objects.equals(charsA, charsB)) {
                if (logDiffs)
                    differences.add(new Difference("Text in " + rspecNameA + " and " + rspecNameB + " differs:" +
                            " " + rspecNameA + "text=" + (charsA.size() == 1 ? "\"" + charsA.get(0) + "\"" : charsA) +
                            " " + rspecNameB + "text=" + (charsB.size() == 1 ? "\"" + charsB.get(0) + "\"" : charsB),
                            startA.getLocation(), startB.getLocation(), rspecNameA, rspecNameB));
                return false;
            }
        }

        for (List<XMLEvent> subElA : subElementsA) {
            assert !subElA.isEmpty();
            StartElement startElementA = subElA.get(0).asStartElement();
            assert subElA.size() >= 2 : "sizeA=" + subElA.size();

            boolean found = false;
            for (Iterator<List<XMLEvent>> itB = subElementsB.iterator(); itB.hasNext(); ) {
                List<XMLEvent> subElB = itB.next();
                assert subElA.size() >= 2 : "sizeA=" + subElA.size() + " sizeB=" + subElB.size();
                assert subElB.size() >= 2 : "sizeA=" + subElA.size() + " sizeB=" + subElB.size();
                if (compareElement(subElA, subElB, false)) { //todo: solve non recursively
                    itB.remove();
                    found = true;
                    break;
                }
            }
            if (!found) {
                if (logDiffs) differences.add(new Difference(
                        "" + rspecNameA + " has element \"" + startElementA.getName() + "\". " +
                                "" + rspecNameB + " does not have a matching element",
                        startElementA.getLocation(), null,
                        rspecNameA, rspecNameB));
                return false;
            }
        }
        if (!subElementsB.isEmpty()) {
            if (logDiffs) {
                for (List<XMLEvent> e : subElementsB) {
                    assert !e.isEmpty();
                    assert e.get(0).isStartElement();
                    StartElement bStart = e.get(0).asStartElement();
                    differences.add(new Difference(
                            "" + rspecNameB + " has element \"" + bStart.getName() + "\"" +
                                    " which " + rspecNameA + " does not have",
                            null, bStart.getLocation(),
                            rspecNameA, rspecNameB));
                }
            }
            return false;
        }

        return true;
    }

    @SuppressWarnings("unchecked")
    public static String attributesToString(StartElement startElement) {
        String res = "";
        for (Iterator<Attribute> it = startElement.getAttributes(); it.hasNext(); ) {
            Attribute att = it.next();
            if (!res.isEmpty())
                res += " ";
            res += "" + att.getName() + "=" + att.getValue();
        }
        return res;
    }

    private static boolean isIgnoredRspecAttribute(QName name) {
        if (Objects.equals(name, Q_ATT_RSPEC_GEN_BY)) return true;
        if (Objects.equals(name, Q_ATT_RSPEC_GEN_DATE)) return true;
        if (Objects.equals(name, Q_XSI_SCHEMALOCATION)) return true;
        return false;
    }

    private static boolean isIgnoredAttribute(QName name) {
//        if (name.getNamespaceURI().equals(NAMESPACE_JFED)) return true;
        return false;
    }

    private static boolean isIgnoredElement(QName name) {
        return Objects.equals(name.getNamespaceURI(), NAMESPACE_JFED) && !Objects.equals(name, FIXHACK_BAD);
    }

    @SuppressWarnings("unchecked")
    private boolean sameAttributes(StartElement startA, StartElement startB, boolean logDiffs) {
        boolean isRspec = Objects.equals(startA.getName(), Q_RSPEC);
        Map<QName, String> attributesB = new HashMap<>();
        Map<QName, String> attributesA = new HashMap<>();

        for (Iterator<Attribute> itB = startB.getAttributes(); itB.hasNext(); ) {
            Attribute attB = itB.next();
            if (!isIgnoredAttribute(attB.getName()) && (!isRspec || !isIgnoredRspecAttribute(attB.getName())))
                attributesB.put(attB.getName(), attB.getValue());
        }
        for (Iterator<Attribute> itA = startA.getAttributes(); itA.hasNext(); ) {
            Attribute attA = itA.next();
            if (!isIgnoredAttribute(attA.getName()) && (!isRspec || !isIgnoredRspecAttribute(attA.getName())))
                attributesA.put(attA.getName(), attA.getValue());
        }
        //rely on HashMap equals. It might be the most efficient method here.
        return Objects.equals(attributesA, attributesB);
    }

    /**
     * assumed that iterator won't return first element
     */
    public List<XMLEvent> getAllElementEventsFromIt(StartElement startEvent, Iterator<XMLEvent> it) throws XMLStreamException, RspecParseException {
        QName startElementName = startEvent.getName();
        String name = startElementName.getLocalPart();
        List<XMLEvent> res = new LinkedList<>();
        res.add(startEvent);
        try {
            int depth = 0; //depth is used for support for recursive extra xml  (ex:  <a><a> </a></a>  )
            while (it.hasNext()) {
                XMLEvent e = it.next();

                res.add(e);
                if (e.isStartElement()) {
                    if (Objects.equals(e.asStartElement().getName().getLocalPart(), name)) {
                        depth++;
                    }
                }
                if (e.isEndElement()) {
                    if (Objects.equals(e.asEndElement().getName().getLocalPart(), name)) {
                        if (depth == 0) {
                            assert res.size() >= 2 :
                                    "not enough elements (" + res.size() + ") for startElementName=" + startElementName + " @ " + RspecParseException.locationToString(startEvent.getLocation());
                            return res;
                        }
                        depth--;
                    }
                }
            }
            throw new RspecParseException(startEvent.getLocation(), "<" + startElementName + "> tag was never closed");
        } catch (RspecParseException e) {
            String curList = "";
            for (XMLEvent event : res)
                curList += "      " + StaxHelper.xmlEventToStringWithAttributes(event) + "\n";
            LOG.error("Parse error in getAllElementEvents. \n      startEvent=" + StaxHelper.xmlEventToString(startEvent) + "\n    Current list:\n" + curList, e);
            throw e;
        } catch (Exception e) {
            String curList = "";
            for (XMLEvent event : res)
                curList += "      " + StaxHelper.xmlEventToStringWithAttributes(event) + "\n";
            LOG.error("RspecCompare error in getAllElementEvents. \n      startEvent=" + StaxHelper.xmlEventToString(startEvent) + "\n    Current list:\n" + curList, e);
            throw new RuntimeException("RspecCompare error in getAllElementEvents. \n      startEvent=" + StaxHelper.xmlEventToString(startEvent) + "\n    Current list:\n" + curList, e);
        }
    }


    private static class RspecParts {
        private final Map<String, List<XMLEvent>> nodeEventsMap = new HashMap<>();
        private final Map<String, List<XMLEvent>> linkEventsMap = new HashMap<>();
        private final List<XMLEvent> restRspec = new LinkedList<>();

        public RspecParts(String rspecA) throws RspecParseException {
            try {
                XMLInputFactory xif = XMLInputFactory.newFactory();
                StreamSource xml = new StreamSource(new StringReader(rspecA));
                XMLEventReader eventReader = xif.createXMLEventReader(xml);
                XMLEvent event = eventReader.nextEvent();
                assert event.isStartDocument();


                while (eventReader.hasNext()) {
                    event = eventReader.nextEvent();

                    if (event.isStartElement()) {
                        //special case for root element: we don't want to read all it's children!
                        if (Objects.equals(event.asStartElement().getName(), Q_RSPEC)) {
                            restRspec.add(event);
                            continue;
                        } else if (Objects.equals(event.asStartElement().getName(), Q_NODE)) {

                            //generate unique id
                            String nodeId = generateUniqueNodeId(event);

                            List<XMLEvent> nodeEvents =
                                    getAllElementEventsFromReader(event.asStartElement(), eventReader);

                            while (this.nodeEventsMap.containsKey(nodeId)) {
                                LOG.warn("Found a duplicate node with id '{}'. We cannot guarantee correct comparisons anymore!", nodeId);
                                nodeId += "!DUP!";
                            }
                            this.nodeEventsMap.put(nodeId, nodeEvents);
                        } else if (Objects.equals(event.asStartElement().getName(), Q_LINK)) {
                            String linkId = generateUniqueLinkId(event);
                            List<XMLEvent> linkEvents =
                                    getAllElementEventsFromReader(event.asStartElement(), eventReader);

                            while (this.linkEventsMap.containsKey(linkId)) {
                                LOG.warn("Found a duplicate link with id '{}'. We cannot guarantee correct comparisons anymore!", linkId);
                                linkId += "!DUP!";
                            }
                            this.linkEventsMap.put(linkId, linkEvents);
                        } else {
                            //read the element and all it's children

                            if (Objects.equals(event.asStartElement().getName(), Q_NITOS_E_NODE_B)) {
                                //HACK: FEDIBBTDEV-2578 ignore flex:e_node_b for now
                                getAllElementEventsFromReader(event.asStartElement(), eventReader);
                            } else {
                                restRspec.addAll(getAllElementEventsFromReader(event.asStartElement(), eventReader));
                            }
                        }
                    }
                    if (event.isEndElement()) {
                        if (Objects.equals(event.asEndElement().getName(), Q_RSPEC)) {
                            restRspec.add(event);
                        } else {
                            LOG.warn("Unexpected end element: {}", event);
                        }
                    }
                }
            } catch (XMLStreamException ex) {
                throw new RspecParseException(null, "Problem while parsing Rspec", ex);
            }
        }

        public Map<String, List<XMLEvent>> getNodeEventsMap() {
            return nodeEventsMap;
        }

        public Map<String, List<XMLEvent>> getLinkEventsMap() {
            return linkEventsMap;
        }

        public List<XMLEvent> getRestRspec() {
            return restRspec;
        }

        /**
         * assumed that iterator won't return first element
         */
        private static List<XMLEvent> getAllElementEventsFromReader(StartElement startEvent, XMLEventReader it)
                throws XMLStreamException, RspecParseException {
            QName startElementName = startEvent.getName();
            String name = startElementName.getLocalPart();
            List<XMLEvent> res = new LinkedList<>();
            res.add(startEvent);
            try {
                int depth = 0; //depth is used for support for recursive extra xml  (ex:  <a><a> </a></a>  )
                while (it.hasNext()) {
                    XMLEvent e = it.nextEvent();

                    res.add(e);
                    if (e.isStartElement()) {
                        if (Objects.equals(e.asStartElement().getName().getLocalPart(), name)) {
                            depth++;
                        }
                    }
                    if (e.isEndElement()) {
                        if (Objects.equals(e.asEndElement().getName().getLocalPart(), name)) {
                            if (depth == 0) {
                                assert res.size() >= 2 :
                                        "not enough elements (" + res.size() + ") for startElementName=" + startElementName + " @ " + RspecParseException.locationToString(startEvent.getLocation());
                                return res;
                            }
                            depth--;
                        }
                    }
                }
                throw new RspecParseException(startEvent.getLocation(), "<" + startElementName + "> tag was never closed");
            } catch (RspecParseException e) {
                String curList = "";
                for (XMLEvent event : res)
                    curList += "      " + StaxHelper.xmlEventToStringWithAttributes(event) + "\n";
                LOG.error("Parse error in getAllElementEvents. \n      startEvent=" + StaxHelper.xmlEventToString(startEvent) + "\n    Current list:\n" + curList, e);
                throw e;
            } catch (Exception e) {
                String curList = "";
                for (XMLEvent event : res)
                    curList += "      " + StaxHelper.xmlEventToStringWithAttributes(event) + "\n";
                LOG.error("Parse error in getAllElementEvents. \n      startEvent=" + StaxHelper.xmlEventToString(startEvent) + "\n    Current list:\n" + curList, e);
                throw new RuntimeException("Parse error in getAllElementEvents. \n      startEvent=" + StaxHelper.xmlEventToString(startEvent) + "\n    Current list:\n" + curList, e);
            }
        }

        private static String generateUniqueNodeId(XMLEvent nodeEvent) {
            assert nodeEvent.isStartElement();
            assert Objects.equals(nodeEvent.asStartElement().getName(), Q_NODE);
            StringBuilder result = new StringBuilder();

            Attribute idAtt = nodeEvent.asStartElement().getAttributeByName(Q_ATT_CLIENT_ID);
            if (idAtt != null)
                result.append(idAtt.getValue());
            result.append('#');
            Attribute comIdAtt = nodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_ID);
            if (comIdAtt != null)
                result.append(comIdAtt.getValue());
            result.append('#');
            Attribute comManIdAtt = nodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_MAN_ID);
            if (comManIdAtt != null)
                result.append(comManIdAtt.getValue());
            result.append('#');
            Attribute sliverIdAtt = nodeEvent.asStartElement().getAttributeByName(Q_ATT_SLIVER_ID);
            if (sliverIdAtt != null)
                result.append(sliverIdAtt.getValue());
            result.append('#');
            Attribute conNameAtt = nodeEvent.asStartElement().getAttributeByName(Q_ATT_COM_NAME);
            if (conNameAtt != null)
                result.append(conNameAtt.getValue());
            return result.toString();
        }

        private static String generateUniqueLinkId(XMLEvent linkEvent) {
            assert linkEvent.isStartElement();
            assert Objects.equals(linkEvent.asStartElement().getName(), Q_LINK);
            String result = "";

            Attribute idAtt = linkEvent.asStartElement().getAttributeByName(Q_ATT_CLIENT_ID);
            if (idAtt != null)
                result += idAtt.getValue();
            result += "#";

            Attribute sliverIdAtt = linkEvent.asStartElement().getAttributeByName(Q_ATT_SLIVER_ID);
            if (sliverIdAtt != null)
                result += sliverIdAtt.getValue();
            return result;
        }

    }


    private boolean equalRspec(String rspecA, String rspecB) throws XMLStreamException, RspecParseException {
        RspecParts partsA = new RspecParts(rspecA);
        RspecParts partsB = new RspecParts(rspecB);

        //check if nodes match up
        final Sets.SetView<String> differenceA =
                Sets.difference(partsA.getNodeEventsMap().keySet(), partsB.getNodeEventsMap().keySet());
        if (!differenceA.isEmpty()) {
            for (String uniqueNodeId : differenceA) {
                List<XMLEvent> nodeEventsA = partsA.getNodeEventsMap().get(uniqueNodeId);
                differences.add(new Difference(rspecNameB + " does not contain a node from " + rspecNameA,
                        nodeEventsA.get(0).getLocation(), null, rspecNameA, rspecNameB));
            }
            return false;
        }
        final Sets.SetView<String> differenceB =
                Sets.difference(partsB.getNodeEventsMap().keySet(), partsA.getNodeEventsMap().keySet());
        if (!differenceB.isEmpty()) {
            for (String uniqueNodeId : differenceB) {
                List<XMLEvent> nodeEventsB = partsB.getNodeEventsMap().get(uniqueNodeId);
                differences.add(new Difference(rspecNameA + " does not contain a node from " + rspecNameB,
                        null, nodeEventsB.get(0).getLocation(), rspecNameA, rspecNameB));
            }
            return false;
        }

        for (String uniqueNodeId : partsA.getNodeEventsMap().keySet()) {
            List<XMLEvent> nodeEventsA = partsA.getNodeEventsMap().get(uniqueNodeId);
            List<XMLEvent> nodeEventsB = partsB.getNodeEventsMap().get(uniqueNodeId);

            if (!compareElement(nodeEventsA, nodeEventsB, true)) {
                differences.add(new Difference(rspecNameA + " and " + rspecNameB + " have a different node",
                        nodeEventsA.get(0).getLocation(), nodeEventsB.get(0).getLocation(), rspecNameA, rspecNameB));
                return false;
            }
        }
        //check if links match up

        final Sets.SetView<String> differenceLinksA =
                Sets.difference(partsA.getLinkEventsMap().keySet(), partsB.getLinkEventsMap().keySet());
        if (!differenceLinksA.isEmpty()) {
            for (String uniqueLinkId : differenceLinksA) {
                List<XMLEvent> linkEventsA = partsA.getLinkEventsMap().get(uniqueLinkId);
                differences.add(new Difference(rspecNameB + " does not contain a link from " + rspecNameA,
                        linkEventsA.get(0).getLocation(), null, rspecNameA, rspecNameB));
            }
            return false;
        }
        final Sets.SetView<String> differenceLinksB =
                Sets.difference(partsB.getLinkEventsMap().keySet(), partsA.getLinkEventsMap().keySet());
        if (!differenceLinksB.isEmpty()) {
            for (String uniqueLinkId : differenceLinksB) {
                List<XMLEvent> linkEventsB = partsB.getLinkEventsMap().get(uniqueLinkId);
                differences.add(new Difference(rspecNameA + " does not contain a link from " + rspecNameB,
                        null, linkEventsB.get(0).getLocation(), rspecNameA, rspecNameB));
            }
            return false;
        }

        for (String uniqueLinkId : partsA.getLinkEventsMap().keySet()) {
            List<XMLEvent> linkEventsA = partsA.getLinkEventsMap().get(uniqueLinkId);
            List<XMLEvent> linkEventsB = partsB.getLinkEventsMap().get(uniqueLinkId);

            if (!compareElement(linkEventsA, linkEventsB, true)) {
                differences.add(new Difference(rspecNameA + " and " + rspecNameB + " have a different link",
                        linkEventsA.get(0).getLocation(), linkEventsB.get(0).getLocation(), rspecNameA, rspecNameB));
                return false;
            }
        }


        //check if the rest matches up
        if (!compareElement(partsA.getRestRspec(), partsB.getRestRspec(), true)) {
            differences.add(new Difference(rspecNameA + " and " + rspecNameB + " have the same node and link elements. But the rest of the RSpec differs", null, null, rspecNameA, rspecNameB));
            return false;
        }

        return true;
    }


    public static class Difference {
        private final String reason;
        private final Location locationA;
        private final Location locationB;
        private final String rspecNameA;
        private final String rspecNameB;


        private Difference(String reason, Location locationA, Location locationB, String rspecNameA, String rspecNameB) {
            this.reason = reason;
            this.locationA = locationA;
            this.locationB = locationB;
            this.rspecNameA = rspecNameA;
            this.rspecNameB = rspecNameB;
        }

        public String getReason() {
            return reason;
        }

        public Location getLocationA() {
            return locationA;
        }

        public Location getLocationB() {
            return locationB;
        }

        public String getRspecNameB() {
            return rspecNameB;
        }

        public String getRspecNameA() {
            return rspecNameA;
        }

        @Override
        public String toString() {
            return reason
                    + (locationA != null ? " in " + rspecNameA + " @ " + RspecParseException.locationToString(locationA)
                    : "")
                    + (locationB != null ? " in " + rspecNameB + " @ " + RspecParseException.locationToString(locationB)
                    : "");
        }
    }

    public static String differencesListToString(List<Difference> diffs) {
        StringBuilder sb = new StringBuilder();
        for (Difference diff : diffs) {
            sb.append(diff.getReason()).append("\n");
            if (diff.getLocationA() != null)
                sb.append("   " + " in ").append(diff.getRspecNameA()).append(" @ ").append(RspecParseException.locationToString(diff.getLocationA())).append("\n");
            if (diff.getLocationB() != null)
                sb.append("   " + " in ").append(diff.getRspecNameB()).append(" @ ").append(RspecParseException.locationToString(diff.getLocationB())).append("\n");
            sb.append("\n");
        }
        return sb.toString();
    }
}
