package apoc.export.graphml;

import apoc.export.util.ExportConfig;
import apoc.export.util.FormatUtils;
import apoc.export.util.MetaInformation;
import apoc.export.util.Reporter;
import apoc.util.Util;
import org.neo4j.cypher.export.SubGraph;
import org.neo4j.graphdb.Node;
import org.neo4j.graphdb.PropertyContainer;
import org.neo4j.graphdb.Relationship;

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;

import static apoc.export.util.MetaInformation.getLabelsString;
import static apoc.export.util.MetaInformation.updateKeyTypes;

/**
 * @author mh
 * @since 21.01.14
 */
public class XmlGraphMLWriter {

    public void write(SubGraph graph, Writer writer, Reporter reporter, ExportConfig config) throws Exception {
        XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance();
        XMLStreamWriter xmlWriter = xmlOutputFactory.createXMLStreamWriter(writer);
        writeHeader(xmlWriter);
        if (config.useTypes()) writeKeyTypes(xmlWriter, graph);
        for (Node node : graph.getNodes()) {
            int props = writeNode(xmlWriter, node);
            reporter.update(1, 0, props);
        }
        for (Relationship rel : graph.getRelationships()) {
            int props = writeRelationship(xmlWriter, rel);
            reporter.update(0, 1, props);
        }
        writeFooter(xmlWriter);
    }

    private void writeKeyTypes(XMLStreamWriter writer, SubGraph ops) throws Exception {
        Map<String, Class> keyTypes = new HashMap<>();
        for (Node node : ops.getNodes()) {
            updateKeyTypes(keyTypes, node);
        }
        writeKeyTypes(writer, keyTypes, "node");
        keyTypes.clear();
        for (Relationship rel : ops.getRelationships()) {
            updateKeyTypes(keyTypes, rel);
        }
        writeKeyTypes(writer, keyTypes, "edge");
    }

    private void writeKeyTypes(XMLStreamWriter writer, Map<String, Class> keyTypes, String forType) throws IOException, XMLStreamException {
        for (Map.Entry<String, Class> entry : keyTypes.entrySet()) {
            Class typeClass = entry.getValue();
            String type = MetaInformation.typeFor(typeClass, MetaInformation.GRAPHML_ALLOWED);
            if (type == null) continue;
            writer.writeEmptyElement("key");
            writer.writeAttribute("id", entry.getKey());
            writer.writeAttribute("for", forType);
            writer.writeAttribute("for", forType);
            writer.writeAttribute("attr.name", entry.getKey());
            if (typeClass.isArray()) {
                writer.writeAttribute("attr.type", "string");
                writer.writeAttribute("attr.list",type);
            } else {
                writer.writeAttribute("attr.type", type);
            }
            newLine(writer);
        }
    }

    private int writeNode(XMLStreamWriter writer, Node node) throws IOException, XMLStreamException {
        writer.writeStartElement("node");
        writer.writeAttribute("id", id(node));
        writeLabels(writer, node);
        writeLabelsAsData(writer, node);
        int props = writeProps(writer, node);
        endElement(writer);
        return props;
    }

    private String id(Node node) {
        return "n" + node.getId();
    }

    private void writeLabels(XMLStreamWriter writer, Node node) throws IOException, XMLStreamException {
        String labelsString = getLabelsString(node);
        if (!labelsString.isEmpty()) writer.writeAttribute("labels", labelsString);
    }

    private void writeLabelsAsData(XMLStreamWriter writer, Node node) throws IOException, XMLStreamException {
        String labelsString = getLabelsString(node);
        if (labelsString.isEmpty()) return;
        writeData(writer, "labels", labelsString);
    }

    private int writeRelationship(XMLStreamWriter writer, Relationship rel) throws IOException, XMLStreamException {
        writer.writeStartElement("edge");
        writer.writeAttribute("id", id(rel));
        writer.writeAttribute("source", id(rel.getStartNode()));
        writer.writeAttribute("target", id(rel.getEndNode()));
        writer.writeAttribute("label", rel.getType().name());
        writeData(writer, "label", rel.getType().name());
        int props = writeProps(writer, rel);
        endElement(writer);
        return props;
    }

    private String id(Relationship rel) {
        return "e" + rel.getId();
    }

    private void endElement(XMLStreamWriter writer) throws XMLStreamException {
        writer.writeEndElement();
        newLine(writer);
    }

    private int writeProps(XMLStreamWriter writer, PropertyContainer node) throws IOException, XMLStreamException {
        int count = 0;
        for (String prop : node.getPropertyKeys()) {
            Object value = node.getProperty(prop);
            writeData(writer, prop, value);
            count++;
        }
        return count;
    }

    private void writeData(XMLStreamWriter writer, String prop, Object value) throws IOException, XMLStreamException {
        writer.writeStartElement("data");
        writer.writeAttribute("key", prop);
        if (value != null) {
            writer.writeCharacters(toString(value));
        }
        writer.writeEndElement();
    }

    private String toString(Object value) {
        if (value.getClass().isArray()) {
            return arrayToString(value);
        }
        if (value instanceof Number) {
            return FormatUtils.formatNumber((Number)value);
        }
        return value.toString();
    }

    private String arrayToString(Object value) {
        return Util.toJson(value);
    }

    private void writeFooter(XMLStreamWriter writer) throws IOException, XMLStreamException {
        endElement(writer);
        endElement(writer);
        writer.writeEndDocument();
    }

    private void writeHeader(XMLStreamWriter writer) throws IOException, XMLStreamException {
        writer.writeStartDocument("UTF-8", "1.0");
        newLine(writer);
        writer.writeStartElement("graphml"); // todo properties
        writer.writeNamespace("xmlns", "http://graphml.graphdrawing.org/xmlns");
        writer.writeAttribute("xmlns", "http://graphml.graphdrawing.org/xmlns", "xsi", "http://www.w3.org/2001/XMLSchema-instance");
        writer.writeAttribute("xsi", "", "schemaLocation", "http://graphml.graphdrawing.org/xmlns http://graphml.graphdrawing.org/xmlns/1.0/graphml.xsd");
        newLine(writer);
        writer.writeStartElement("graph");
        writer.writeAttribute("id", "G");
        writer.writeAttribute("edgedefault", "directed");
        newLine(writer);
    }

    private void newLine(XMLStreamWriter writer) throws XMLStreamException {
        writer.writeCharacters("\n");
    }
}

@@ -6863,12 +6863,44 @@
 ers(
-%22%5Cn%22
+System.getProperty(%22line.separator%22)
 );%0A 

make line endings for XmlWriter System agnostic ISSUE #171