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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.namespace.NamespaceContext;
import javax.xml.namespace.QName;
import javax.xml.stream.*;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.XMLEvent;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

/**
 * ExtraXml
 */
public class EventListExtraXml implements ExtraXml {
    private final static Logger LOG = LoggerFactory.getLogger(EventListExtraXml.class);

    final List<XMLEvent> events = new ArrayList<>();
    private QName startElementName;
    private StartElement startElement;

    public EventListExtraXml(StartElement extraXmlElementStartEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        this.startElement = extraXmlElementStartEvent;
        startElementName = extraXmlElementStartEvent.getName();
        String name = startElementName.getLocalPart();
        events.add(extraXmlElementStartEvent);
        int depth = 0; //depth is used for support for recursive extra xml  (ex:  <a><a> </a></a>  )
        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();
            events.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)
                        return;
                    depth--;
                }
            }
        }
        throw new RspecParseException(extraXmlElementStartEvent.getLocation(), "<"+startElementName+"> tag was never closed");
    }

    public EventListExtraXml(List<XMLEvent> allEvents) throws XMLStreamException, RspecParseException {
        assert allEvents != null;
        assert !allEvents.isEmpty();
        assert allEvents.get(0).isStartElement();

        this.startElement = allEvents.get(0).asStartElement();
        startElementName = startElement.getName();
        events.addAll(allEvents);
    }

    public EventListExtraXml(ExtraXml orig) {
        this.startElementName = orig.getStartElementName();

        XMLEventWriter w = new XMLEventWriter() {
            @Override public void flush() throws XMLStreamException { }
            @Override public void close() throws XMLStreamException { }
            @Override public String getPrefix(String uri) throws XMLStreamException { return null; }
            @Override public void setPrefix(String prefix, String uri) throws XMLStreamException { }
            @Override public void setDefaultNamespace(String uri) throws XMLStreamException { }
            @Override public void setNamespaceContext(NamespaceContext context) throws XMLStreamException { }
            @Override public NamespaceContext getNamespaceContext() { return null; }
            @Override public void add(XMLEventReader reader) throws XMLStreamException { }

            @Override
            public void add(XMLEvent event) {
                if (!event.isStartElement()) {
                    LOG.warn("Ignoring element: " + event.getEventType());
                    return;
                }
                if (events.isEmpty())
                    startElement = event.asStartElement();
                events.add(event);
            }
        };

        try {
            orig.writeEvents(w);
        } catch (XMLStreamException e) {
            //should never occur
            LOG.error("Unhandled XMLStreamException: constructed EventListExtraXml may not be a full copy", e);
        }
    }

    @Override
    public void writeEvents(XMLEventWriter w) throws XMLStreamException {
        for (XMLEvent e : events)
            w.add(e);
    }

    public static List<ExtraXml> readExtraElements(XMLEvent enclosingStartEvent, XMLEventReader eventReader) throws XMLStreamException, RspecParseException {
        ArrayList<ExtraXml> res = new ArrayList<>();

        String name = enclosingStartEvent.asStartElement().getName().getLocalPart();
        //TODO: add support for nested enclosingStartEvent elements!

        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();
            if (e.isStartElement()) {
                res.add(new EventListExtraXml(e.asStartElement(), eventReader));
            }
            if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName().getLocalPart(), name)) {
                    return res;
                }
            }
        }

        throw new RspecParseException(enclosingStartEvent.getLocation(), "The \""+name+"\" element is never closed");
    }

    @Override
    public QName getStartElementName() {
        return startElementName;
    }

    public StartElement getStartElement() {
        return startElement;
    }

    @Override
    public Location getLocation() {
        if (events != null && !events.isEmpty())
            return events.get(0).getLocation();
        return null;
    }

    public XMLEventReader getAsEventReader() {
        return StaxHelper.makeXmlEventReader(events);
    }

    /** convert to string to help compare*/
    private String cachedComparisonHelper;
    private String getComparisonHelper() {
        if (cachedComparisonHelper != null) return cachedComparisonHelper;

        try {
            XMLOutputFactory outputFactory = XMLOutputFactory.newInstance();
            outputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
            StringWriter sw = new StringWriter();
            XMLEventWriter eventWriter = outputFactory.createXMLEventWriter(sw);
            XMLEventFactory eventFactory = XMLEventFactory.newInstance();
            eventWriter.add(eventFactory.createStartDocument());
            for (XMLEvent event : events)
                eventWriter.add(event);
            eventWriter.add(eventFactory.createEndDocument());
            eventWriter.close();
            cachedComparisonHelper = sw.getBuffer().toString();
        } catch (XMLStreamException e) {
            LOG.error("Error creating EventListExtraXml comparisonHelper. Because of this, EventListExtraXml equals and hashCode won't work correctly.");
            //fall back to hopefully unique string
            cachedComparisonHelper = this+"";
        }

        return cachedComparisonHelper;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof EventListExtraXml)) return false;

        EventListExtraXml that = (EventListExtraXml) o;

        if (!Objects.equals(getComparisonHelper(), that.getComparisonHelper())) return false;

        return true;
    }

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