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

import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.XMLEvent;
import java.util.*;

/**
 * StaxHelper
 */
public class StaxHelper {
    public static XMLEventReader makeXmlEventReader(final List<XMLEvent> inputEvents) {
        return new XMLEventReader() {
            List<XMLEvent> events = new ArrayList<>(inputEvents);
            int i = -1;
            int lastremoved = -1;

            @Override
            public XMLEvent nextEvent() throws XMLStreamException {
                return next();
            }

            @Override
            public boolean hasNext() {
                return events != null && i < events.size() - 1;
            }

            @Override
            public XMLEvent next() {
                i++;
                if (i >= events.size()) throw new NoSuchElementException();
                return events.get(i);
            }

            @Override
            public void remove() {
                if (i == lastremoved) throw new IllegalStateException("next() already called for this position");
                if (i == 0) throw new IllegalStateException("next() not yet called");
                events.remove(i);
                lastremoved = i - 1;
                i--;
            }

            @Override
            public XMLEvent peek() throws XMLStreamException {
                if (!hasNext()) return null;
                return events.get(i+1);
            }

            @Override
            public String getElementText() throws XMLStreamException {
                XMLEvent startE = events.get(i);
                if (!startE.isStartElement()) throw new XMLStreamException("not a start element");
                String res = "";
                while (hasNext()) {
                    XMLEvent e = next();
                    if (e.isCharacters()) {
                        res += e.asCharacters().getData();
                    } else {
                        if (e.isEndElement() && Objects.equals(e.asEndElement().getName(), startE.asStartElement().getName())) {
                            return res;
                        } else {
                            if (e.isEndElement())
                                throw new XMLStreamException("encountered end event that is not corresponding end event: end="+e.asEndElement().getName()+"  start="+startE.asStartElement().getName());
                            else {
                                if (e.isStartElement())
                                    throw new XMLStreamException("encountered start event that is not corresponding end event: "+e.asStartElement().getName()+"  start="+startE.asStartElement().getName());
                                else
                                    throw new XMLStreamException("encountered non text event that is not corresponding end event: "+e);
                            }
                        }
                    }
                }
                throw new XMLStreamException("encountered end of stream before end element");
            }

            @Override
            public XMLEvent nextTag() throws XMLStreamException {
                while (hasNext()) {
                    XMLEvent e = next();
                    if (e.isCharacters()) {
                        if (!e.asCharacters().isIgnorableWhiteSpace())
                            throw new XMLStreamException("encountered characters are not ignorable white space: "+e);
                        else
                            continue;
                    } else {
                        if (e.isEndElement() || e.isStartElement())
                            return e;
                        else
                            throw new XMLStreamException("unexpected element: "+e);
                    }
                }
                throw new XMLStreamException("encountered end of stream before start or end element");
            }

            @Override
            public Object getProperty(String name) throws IllegalArgumentException {
                throw new RuntimeException("unsupported. this is a fake XMLEventReader");
            }

            @Override
            public void close() throws XMLStreamException {
                events.clear();
                events = null;
            }
        };
    }

    /**
     * read events up until the matching end element.
     * @param startNodeEvent the startElement Event
     * @return a list of the events, including start and end element event.
     * */
    public static XMLEventReader getEventsUntilEndTag(XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException {
        List<XMLEvent> res = new ArrayList<>();
        res.add(startNodeEvent);

        String name = startNodeEvent.asStartElement().getName().getLocalPart();
        int depth = 0;
        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.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)
                        break;
                    depth--;
                }
            }
        }

        return makeXmlEventReader(res);
    }

    /**
     * read and discard up until the matching end element
     *
     * @return true if data loss has been detected (now or if detectedLosingData is true), false if otherwise.
     * */
    public static boolean readAllExpectingNothingMore(XMLEvent startNodeEvent, XMLEventReader eventReader) throws XMLStreamException {
        boolean detectedLosingData = false;
        String name = startNodeEvent.asStartElement().getName().getLocalPart();
        int depth = 0;
        while (eventReader.hasNext()) {
            XMLEvent e = eventReader.nextEvent();
            if (e.isStartElement()) {
                if (Objects.equals(e.asStartElement().getName().getLocalPart(), name)) {
                    depth++;
                } else {
                    detectedLosingData = true;
                }
            }
            if (e.isEndElement()) {
                if (Objects.equals(e.asEndElement().getName().getLocalPart(), name)) {
                    if (depth == 0)
                        return detectedLosingData;
                    else
                        detectedLosingData = true;
                    depth--;
                } else
                    detectedLosingData = true;
            }
        }
        return detectedLosingData;
    }

    public static String xmlEventToString(XMLEvent e) {
        if (e.isStartDocument())
            return "StartDocument";
        if (e.isEndDocument())
            return "EndDocument";
        if (e.isStartElement())
            return "StartElement(name="+e.asStartElement().getName()+")";
        if (e.isEndElement())
            return "EndElement(name="+e.asEndElement().getName()+")";
        if (e.isCharacters())
            return "Charachters(data="+e.asCharacters().getData()+")";
        if (e.isAttribute())
            return "Attribute";

        return "unknown";
    }
    public static String xmlEventToStringWithAttributes(XMLEvent e) {
        if (e.isStartElement()) {
            StringBuilder sb = new StringBuilder();
            @SuppressWarnings("unchecked")
            Iterator<Attribute> it = e.asStartElement().getAttributes();
            while(it.hasNext()) {
                Attribute att = it.next();
                sb.append(att.getName()).append("=\"").append(att.getValue()).append("\"");
                if (it.hasNext())
                    sb.append(", ");
            }
            return "StartElement("+e.asStartElement().getName()+" "+sb.toString()+")";
        }

        return xmlEventToString(e);
    }
}
