/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.tinkerpop.gremlin.structure.io.graphson;

import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Edge;
import org.apache.tinkerpop.gremlin.structure.Element;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.Property;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.io.GraphWriter;
import org.apache.tinkerpop.gremlin.structure.io.Mapper;
import org.apache.tinkerpop.gremlin.structure.util.star.DirectionalStarGraph;
import org.apache.tinkerpop.gremlin.structure.util.star.StarGraph;
import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;

import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.util.Iterator;
import java.util.function.Function;

/**
 * A @{link GraphWriter} that writes a graph and its elements to a JSON-based representation. This implementation
 * only supports JSON data types and is therefore lossy with respect to data types (e.g. a float will become a double).
 * Further note that serialized {@code Map} objects do not support complex types for keys.  {@link Edge} and
 * {@link Vertex} objects are serialized to {@code Map} instances. If an
 * {@link Element} is used as a key, it is coerced to its identifier.  Other complex
 * objects are converted via {@link Object#toString()} unless a mapper serializer is supplied.
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public final class GraphSONWriter implements GraphWriter {
    private final ObjectMapper mapper;
    private final boolean wrapAdjacencyList;

    private GraphSONWriter(final Builder builder) {
        mapper = builder.mapper.createMapper();
        wrapAdjacencyList = builder.wrapAdjacencyList;
    }

    /**
     * Writes a {@link Graph} to stream in an adjacency list format where vertices are written with edges from both
     * directions.  Under this serialization model, edges are grouped by label.
     *
     * @param outputStream the stream to write to.
     * @param g the graph to write to stream.
     */
    @Override
    public void writeGraph(final OutputStream outputStream, final Graph g) throws IOException {
        writeVertices(outputStream, g.vertices(), Direction.BOTH);
    }

    /**
     * Writes a single {@link Vertex} to stream where edges only from the specified direction are written.
     * Under this serialization model, edges are grouped by label.
     *
     * @param direction the direction of edges to write or null if no edges are to be written.
     */
    @Override
    public void writeVertex(final OutputStream outputStream, final Vertex v, final Direction direction) throws IOException {
        mapper.writeValue(outputStream, new DirectionalStarGraph(StarGraph.of(v), direction));
    }

    /**
     * Writes a single {@link Vertex} with no edges serialized.
     *
     * @param outputStream the stream to write to.
     * @param v            the vertex to write.
     */
    @Override
    public void writeVertex(final OutputStream outputStream, final Vertex v) throws IOException {
        writeVertex(outputStream, v, null);
    }

    /**
     * Writes a list of vertices in adjacency list format where vertices are written with edges from both
     * directions.  Under this serialization model, edges are grouped by label.
     *
     * @param outputStream the stream to write to.
     * @param vertexIterator    a traversal that returns a list of vertices.
     * @param direction    if direction is null then no edges are written.
     */
    @Override
    public void writeVertices(final OutputStream outputStream, final Iterator<Vertex> vertexIterator, final Direction direction) throws IOException {
        final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(outputStream));
        try (final ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
            if (wrapAdjacencyList) writer.write("{\"" + GraphSONTokens.VERTICES + "\":[");
            while (vertexIterator.hasNext()) {
                writeVertex(baos, vertexIterator.next(), direction);
                writer.write(new String(baos.toByteArray()));
                if (wrapAdjacencyList) {
                    if (vertexIterator.hasNext())
                        writer.write(",");
                } else {
                    writer.newLine();
                }
                baos.reset();
            }
            if (wrapAdjacencyList) writer.write("]}");
        }

        writer.flush();
    }

    /**
     * Writes a list of vertices without edges.
     *
     * @param outputStream the stream to write to.
     * @param vertexIterator    a iterator that returns a list of vertices.
     */
    @Override
    public void writeVertices(final OutputStream outputStream, final Iterator<Vertex> vertexIterator) throws IOException {
        writeVertices(outputStream, vertexIterator, null);
    }

    /**
     * Writes an {@link Edge} object to the stream.  Note that this format is different from the format of an
     * {@link Edge} when serialized with a {@link Vertex} as done with
     * {@link #writeVertex(OutputStream, Vertex, Direction)} or
     * {@link #writeVertices(OutputStream, Iterator, Direction)} in that the edge label is part of the object and
     * vertex labels are included with their identifiers.
     *
     * @param outputStream the stream to write to.
     * @param e the edge to write.
     */
    @Override
    public void writeEdge(final OutputStream outputStream, final Edge e) throws IOException {
        mapper.writeValue(outputStream, e);
    }

    /**
     * Write a {@link VertexProperty} object to the stream.
     *
     * @param outputStream the stream to write to.
     * @param vp the vertex property to write.
     */
    @Override
    public void writeVertexProperty(final OutputStream outputStream, final VertexProperty vp) throws IOException {
        mapper.writeValue(outputStream, vp);
    }

    /**
     * Write a {@link Property} object to the stream.
     *
     * @param outputStream the stream to write to.
     * @param p the property to write.
     */
    @Override
    public void writeProperty(final OutputStream outputStream, final Property p) throws IOException {
        mapper.writeValue(outputStream, p);
    }

    /**
     * Writes an arbitrary object to the stream.  Note that Gremlin Server uses this method when serializing output,
     * thus the format of the GraphSON for a {@link Vertex} will be somewhat different from the format supplied
     * when using {@link #writeVertex(OutputStream, Vertex, Direction)}. For example, edges will never be included.
     *
     * @param outputStream the stream to write to
     * @param object the object to write which will use the standard serializer set
     */
    @Override
    public void writeObject(final OutputStream outputStream, final Object object) throws IOException {
        this.mapper.writeValue(outputStream, object);
    }

    public static Builder build() {
        return new Builder();
    }

    public static class Builder implements WriterBuilder<GraphSONWriter> {

        private Mapper<ObjectMapper> mapper = GraphSONMapper.build().create();
        private boolean wrapAdjacencyList = false;

        private Builder() { }

        /**
         * Override all of the builder options with this mapper.  If this value is set to something other than
         * null then that value will be used to construct the writer.
         */
        public Builder mapper(final Mapper<ObjectMapper> mapper) {
            this.mapper = mapper;
            return this;
        }

        /**
         * Wraps the output of {@link #writeGraph(OutputStream, Graph)}, {@link #writeVertices(OutputStream, Iterator)}
         * and {@link #writeVertices(OutputStream, Iterator, Direction)} in a JSON object.  By default, this value
         * is {@code false} which means that the output is such that there is one JSON object (vertex) per line.
         * When {@code true} the line breaks are not written and instead a valid JSON object is formed where the
         * vertices are part of a JSON array in a key called "vertices".
         * <p/>
         * By setting this value to {@code true}, the generated JSON is no longer "splittable" by line and thus not
         * suitable for OLAP processing.  Furthermore, reading this format of the JSON with
         * {@link GraphSONReader#readGraph(InputStream, Graph)} or
         * {@link GraphSONReader#readVertices(InputStream, Function, Function, Direction)} requires that the
         * entire JSON object be read into memory, so it is best saved for "small" graphs.
         */
        public Builder wrapAdjacencyList(final boolean wrapAdjacencyListInObject) {
            this.wrapAdjacencyList = wrapAdjacencyListInObject;
            return this;
        }

        public GraphSONWriter create() {
            return new GraphSONWriter(this);
        }
    }
}
