/*
 * 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.driver.ser.binary;

import io.netty.buffer.ByteBufAllocator;
import org.apache.tinkerpop.gremlin.driver.ser.NettyBufferFactory;
import org.apache.tinkerpop.gremlin.process.computer.Computer;
import org.apache.tinkerpop.gremlin.process.computer.traversal.strategy.decoration.VertexProgramStrategy;
import org.apache.tinkerpop.gremlin.process.remote.traversal.DefaultRemoteTraverser;
import org.apache.tinkerpop.gremlin.process.traversal.Bytecode;
import org.apache.tinkerpop.gremlin.process.traversal.Operator;
import org.apache.tinkerpop.gremlin.process.traversal.Order;
import org.apache.tinkerpop.gremlin.process.traversal.P;
import org.apache.tinkerpop.gremlin.process.traversal.Pop;
import org.apache.tinkerpop.gremlin.process.traversal.SackFunctions;
import org.apache.tinkerpop.gremlin.process.traversal.Scope;
import org.apache.tinkerpop.gremlin.process.traversal.TextP;
import org.apache.tinkerpop.gremlin.process.traversal.TraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalOptionParent;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.BulkSet;
import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.TraversalStrategyProxy;
import org.apache.tinkerpop.gremlin.process.traversal.strategy.decoration.SubgraphStrategy;
import org.apache.tinkerpop.gremlin.process.traversal.util.DefaultTraversalMetrics;
import org.apache.tinkerpop.gremlin.process.traversal.util.Metrics;
import org.apache.tinkerpop.gremlin.process.traversal.util.MutableMetrics;
import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalMetrics;
import org.apache.tinkerpop.gremlin.structure.Column;
import org.apache.tinkerpop.gremlin.structure.Direction;
import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.T;
import org.apache.tinkerpop.gremlin.structure.Vertex;
import org.apache.tinkerpop.gremlin.structure.VertexProperty;
import org.apache.tinkerpop.gremlin.structure.io.Buffer;
import org.apache.tinkerpop.gremlin.structure.io.IoTest;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryReader;
import org.apache.tinkerpop.gremlin.structure.io.binary.GraphBinaryWriter;
import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceEdge;
import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceProperty;
import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertex;
import org.apache.tinkerpop.gremlin.structure.util.reference.ReferenceVertexProperty;
import org.apache.tinkerpop.gremlin.tinkergraph.structure.TinkerFactory;
import org.apache.tinkerpop.gremlin.util.function.Lambda;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.internal.matchers.apachecommons.ReflectionEquals;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.MonthDay;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.Period;
import java.time.Year;
import java.time.YearMonth;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

import static org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__.hasLabel;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;

@RunWith(Parameterized.class)
public class GraphBinaryReaderWriterRoundTripTest {
    private final GraphBinaryWriter writer = new GraphBinaryWriter();
    private final GraphBinaryReader reader = new GraphBinaryReader();
    private final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT;
    private static NettyBufferFactory bufferFactory = new NettyBufferFactory();

    private static final GraphTraversalSource g = TinkerFactory.createModern().traversal();

    @Parameterized.Parameters(name = "Type{0}")
    public static Collection input() throws Exception {
        final Bytecode bytecode = new Bytecode();
        bytecode.addStep("V");
        bytecode.addStep("tail", 3);
        bytecode.addSource(TraversalSource.Symbols.withComputer, "myComputer");

        final Map<String, Integer> map = new HashMap<>();
        map.put("one", 1);
        map.put("two", 2);

        final Map<String, Map<String, Integer>> nestedMap = new HashMap<>();
        nestedMap.put("first", map);

        final List<Object> list = new ArrayList<>();
        list.add("string 1");
        list.add("string 1");
        list.add(200);
        list.add("string 2");

        final List<List<Object>> nestedList = new ArrayList<>();
        nestedList.add(list);

        final Set<Object> set = new HashSet<>();
        set.add("one");
        set.add(2);

        final Set<Set<Object>> nestedSet = new HashSet<>();
        nestedSet.add(set);

        final BulkSet<String> bulkSet = new BulkSet<>();
        bulkSet.add("marko", 1);
        bulkSet.add("josh", 3);

        final Tree<Vertex> tree = new Tree<>();
        final Tree<Vertex> subTree = new Tree<>();
        final Tree<Vertex> subSubTree = new Tree<>();
        subSubTree.put(new ReferenceVertex(1, "animal"), new Tree<>());
        subSubTree.put(new ReferenceVertex(2, "animal"), new Tree<>());
        subTree.put(new ReferenceVertex(100, "animal"), subSubTree);
        tree.put(new ReferenceVertex(1000, "animal"), subTree);

        final MutableMetrics metrics = new MutableMetrics("id1", "name1");
        metrics.setDuration(123, TimeUnit.MICROSECONDS);
        metrics.setCount("c1", 20);
        metrics.setAnnotation("a", "b");
        metrics.addNested(new MutableMetrics("idNested", "nameNested"));

        // can't use the existing 'metrics' because traversal metrics modifies its nested metrics
        final MutableMetrics metrics1 = metrics.clone();

        final MutableMetrics metrics2 = new MutableMetrics("id2", "name2");
        metrics2.setDuration(456, TimeUnit.MICROSECONDS);
        metrics2.setCount("c2", 40);
        metrics2.setAnnotation("c", "d");
        metrics2.addNested(new MutableMetrics("idNested2", "nameNested2"));

        List<MutableMetrics> nestedMetrics = Arrays.asList(metrics1, metrics2);
        final DefaultTraversalMetrics traversalMetrics = new DefaultTraversalMetrics(666, nestedMetrics);
        final DefaultTraversalMetrics emptyTraversalMetrics = new DefaultTraversalMetrics(444, Collections.emptyList());

        return Arrays.asList(
                new Object[] {"String", "ABC", null},
                new Object[] {"Char", '£', null},

                // numerics
                new Object[] {"Byte", 1, null},
                new Object[] {"Integer", 1, null},
                new Object[] {"Float", 2f, null},
                new Object[] {"Double", 3.1d, null},
                new Object[] {"Double", Double.NaN, null},
                new Object[] {"Double", Double.POSITIVE_INFINITY, null},
                new Object[] {"Double", Double.NEGATIVE_INFINITY, null},
                new Object[] {"Long", 10122L, null},
                new Object[] {"IntegerZero", 0, null},
                new Object[] {"FloatZero", 0f, null},
                new Object[] {"IntegerMin", Integer.MIN_VALUE, null},
                new Object[] {"IntegerMax", Integer.MAX_VALUE, null},
                new Object[] {"LongMax", Long.MAX_VALUE, null},
                new Object[] {"BigIntegerPos", new BigInteger("1234567890987654321"), null},
                new Object[] {"BigIntegerNeg", new BigInteger("-1234567890987654321"), null},
                new Object[] {"BigDecimalPos", new BigDecimal("1234567890987654321.1232132"), null},
                new Object[] {"BigDecimalNeg", new BigDecimal("-1234567890987654321.1232132"), null},

                // date+time
                new Object[] {"Date", DateFormat.getDateInstance(DateFormat.MEDIUM, Locale.US).parse("Jan 12, 1952"), null},
                new Object[] {"Timestamp", Timestamp.valueOf("2016-01-15 12:01:02"), null},
                new Object[] {"Duration", Duration.ofSeconds(213123213, 400), null},
                new Object[] {"Instant", Instant.ofEpochSecond(213123213, 400), null},
                new Object[] {"LocalDate", LocalDate.of(2016, 10, 21), null},
                new Object[] {"LocalTime", LocalTime.of(12, 20, 30, 300), null},
                new Object[] {"LocalDateTime", LocalDateTime.of(2016, 10, 21, 12, 20, 30, 300), null},
                new Object[] {"MonthDay", MonthDay.of(12, 28), null},
                new Object[] {"OffsetDateTime", OffsetDateTime.of(2017, 11, 15, 12, 30, 45, 300, ZoneOffset.ofTotalSeconds(400)), null},
                new Object[] {"OffsetTime", OffsetTime.of(12, 30, 45, 300, ZoneOffset.ofTotalSeconds(400)), null},
                new Object[] {"Period", Period.of(1, 6, 15), null},
                new Object[] {"Year", Year.of(1996), null},
                new Object[] {"YearMonth", YearMonth.of(2016, 11), null},
                new Object[] {"ZonedDateTime", ZonedDateTime.of(2016, 11, 15, 12, 30, 45, 300, ZoneOffset.ofTotalSeconds(200)), null},
                new Object[] {"ZoneOffset", ZoneOffset.ofTotalSeconds(100), null},

                new Object[] {"UUID", UUID.randomUUID(), null},
                new Object[] {"Bytecode", bytecode, null},
                new Object[] {"Binding", new Bytecode.Binding<>("x", 123), null},
                new Object[] {"Traverser", new DefaultRemoteTraverser<>("marko", 100), null},
                new Object[] {"Class", Bytecode.class, null},
                new Object[] {"ByteBuffer", ByteBuffer.wrap(new byte[]{ 1, 2, 3 }), null},
                new Object[] {"InetAddressV4", InetAddress.getByName("localhost"), null},
                new Object[] {"InetAddressV6", InetAddress.getByName("::1"), null},
                new Object[] {"Lambda0", Lambda.supplier("return 1"), null},
                new Object[] {"Lambda1", Lambda.consumer("it"), null},
                new Object[] {"Lambda2", Lambda.biFunction("x,y -> x + y"), null},
                new Object[] {"LambdaN", new Lambda.UnknownArgLambda("x,y,z -> x + y + z", "gremlin-groovy", 3), null},

                // enums
                new Object[] {"Barrier", SackFunctions.Barrier.normSack, null},
                new Object[] {"Cardinality", VertexProperty.Cardinality.list, null},
                new Object[] {"Columns", Column.values, null},
                new Object[] {"Direction", Direction.BOTH, null},
                new Object[] {"Operator", Operator.sum, null},
                new Object[] {"Operator", Operator.div, null},
                new Object[] {"Order", Order.desc, null},
                new Object[] {"Pick", TraversalOptionParent.Pick.any, null},
                new Object[] {"Pop", Pop.mixed, null},
                new Object[] {"Scope", Scope.global, null},
                new Object[] {"T", T.label, null},
                new Object[] {"Pgt", P.gt(0), null},
                new Object[] {"Pgte", P.gte(0), null},
                new Object[] {"Pbetween", P.between(0,1), null},
                new Object[] {"Pand", P.gt(1).and(P.lt(2)), null},
                new Object[] {"Por", P.gt(1).or(P.lt(2)), null},
                new Object[] {"Pnot", P.not(P.lte(1)), null},
                new Object[] {"Pwithout", P.without(1,2,3,4), null},
                new Object[] {"Pinside", P.inside(0.0d, 0.6d), null},
                new Object[] {"TextP", TextP.startingWith("mark"), null},

                // graph
                new Object[] {"ReferenceEdge", new ReferenceEdge(123, "person", new ReferenceVertex(123, "person"), new ReferenceVertex(321, "person")), null},
                new Object[] {"TinkerEdge", g.E().hasLabel("knows").next(), null},
                new Object[] {"ReferenceProperty", new ReferenceProperty<>("name", "john"), (Consumer<ReferenceProperty>) referenceProperty -> {
                    assertEquals("name", referenceProperty.key());
                    assertEquals("john", referenceProperty.value());
                }},
                new Object[] {"ReferenceVertex", new ReferenceVertex(123, "person"), null},
                new Object[] {"TinkerVertex", g.V().hasLabel("person").next(), null},
                new Object[] {"ReferenceVertexProperty", new ReferenceVertexProperty<>(123, "name", "john"), (Consumer<ReferenceVertexProperty>) referenceProperty -> {
                    assertEquals("name", referenceProperty.key());
                    assertEquals("john", referenceProperty.value());
                    assertEquals(123, referenceProperty.id());
                }},
                new Object[] {"PathLabelled", g.V().as("a", "b").out().as("c").path().next(), null},
                new Object[] {"PathNotLabelled", g.V().out().inE().values().path().next(), null},
                new Object[] {"Graph", g.E().subgraph("k").cap("k").next(), (Consumer<Graph>) graph -> {
                    IoTest.assertModernGraph(graph, true, false);
                }},
                new Object[] {"TraversalStrategyVertexProgram", new VertexProgramStrategy(Computer.compute()), (Consumer<TraversalStrategyProxy>) strategy -> {
                    assertEquals(VertexProgramStrategy.class, strategy.getStrategyClass());
                    assertEquals("org.apache.tinkerpop.gremlin.process.computer.GraphComputer", strategy.getConfiguration().getProperty(VertexProgramStrategy.GRAPH_COMPUTER));
                }},
                new Object[] {"TraversalStrategySubgraph", SubgraphStrategy.build().vertices(hasLabel("person")).create(), (Consumer<TraversalStrategyProxy>) strategy -> {
                    assertEquals(SubgraphStrategy.class, strategy.getStrategyClass());
                    assertEquals(hasLabel("person").asAdmin().getBytecode(), strategy.getConfiguration().getProperty(SubgraphStrategy.VERTICES));
                }},
                new Object[] {"BulkSet", bulkSet, null},
                new Object[] {"Tree", tree, null},
                new Object[] {"EmptyMetrics", new MutableMetrics("idEmpty", "nameEmpty"), (Consumer<Metrics>) m -> {
                    assertThat(m, new ReflectionEquals(new MutableMetrics("idEmpty", "nameEmpty")));
                }},
                new Object[] {"Metrics", metrics, (Consumer<Metrics>) m -> {
                    assertThat(m, new ReflectionEquals(metrics, "nested", "counts"));
                    assertEquals(new ArrayList(metrics.getCounts().values()), new ArrayList(m.getCounts().values()));
                    assertThat(m.getNested(), new ReflectionEquals(metrics.getNested()));
                }},
                new Object[] {"EmptyTraversalMetrics", emptyTraversalMetrics, (Consumer<TraversalMetrics>) m -> {
                    assertThat(m, new ReflectionEquals(emptyTraversalMetrics));
                }},
                new Object[] {"TraversalMetrics", traversalMetrics, (Consumer<TraversalMetrics>) m -> {
                    assertEquals(m.toString(), traversalMetrics.toString());
                    assertThat(m, new ReflectionEquals(traversalMetrics, "stepIndexedMetrics", "positionIndexedMetrics"));
                }},

                // collections
                new Object[] {"ListSingle", list, null},
                new Object[] {"ListNested", nestedList, null},
                new Object[] {"Map", map, null},
                new Object[] {"Map", nestedMap, null},
                new Object[] {"Set", set, null},
                new Object[] {"SetNested", nestedSet, null});
    }

    @Parameterized.Parameter(value = 0)
    public String name;

    @Parameterized.Parameter(value = 1)
    public Object value;

    @Parameterized.Parameter(value = 2)
    public Consumer<?> assertion;

    @Test
    public void shouldWriteAndRead() throws Exception {
        // Test it multiple times as the type registry might change its internal state
        for (int i = 0; i < 5; i++) {
            final Buffer buffer = bufferFactory.create(allocator.buffer());
            writer.write(value, buffer);
            buffer.readerIndex(0);
            final Object result = reader.read(buffer);

            Optional.ofNullable(assertion).orElse((Consumer) r -> assertEquals(value, r)).accept(result);
        }
    }
}
