/*
 * 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.gryo;

import org.apache.tinkerpop.gremlin.structure.Graph;
import org.apache.tinkerpop.gremlin.structure.io.IoRegistry;
import org.apache.tinkerpop.gremlin.structure.io.Mapper;
import org.apache.tinkerpop.gremlin.structure.io.gryo.kryoshim.SerializerShim;
import org.apache.tinkerpop.shaded.kryo.ClassResolver;
import org.apache.tinkerpop.shaded.kryo.Kryo;
import org.apache.tinkerpop.shaded.kryo.Serializer;
import org.apache.tinkerpop.shaded.kryo.util.DefaultStreamFactory;
import org.apache.tinkerpop.shaded.kryo.util.MapReferenceResolver;
import org.javatuples.Pair;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;

/**
 * A {@link Mapper} implementation for Kryo. This implementation requires that all classes to be serialized by
 * Kryo are registered to it.
 * <p/>
 * {@link Graph} implementations providing an {@link IoRegistry} should register their custom classes and/or
 * serializers in one of three ways:
 * <p/>
 * <ol>
 * <li>Register just the custom class with a {@code null} {@link Serializer} implementation</li>
 * <li>Register the custom class with a {@link Serializer} implementation</li>
 * <li>
 * Register the custom class with a {@code Function<Kryo, Serializer>} for those cases where the
 * {@link Serializer} requires the {@link Kryo} instance to get constructed.
 * </li>
 * </ol>
 * <p/>
 * For example:
 * <pre>
 * {@code
 * public class MyGraphIoRegistry extends AbstractIoRegistry {
 *   public MyGraphIoRegistry() {
 *     register(GryoIo.class, MyGraphIdClass.class, new MyGraphIdSerializer());
 *   }
 * }
 * }
 * </pre>
 *
 * @author Stephen Mallette (http://stephen.genoprime.com)
 */
public final class GryoMapper implements Mapper<Kryo> {
    public static final byte[] GIO = "gio".getBytes();
    public static final byte[] HEADER = Arrays.copyOf(GIO, 16);
    private final List<TypeRegistration<?>> typeRegistrations;
    private final boolean registrationRequired;
    private final boolean referenceTracking;
    private final Supplier<ClassResolver> classResolver;
    private final GryoVersion version;

    private GryoMapper(final Builder builder) {
        this.typeRegistrations = builder.typeRegistrations;
        this.version = builder.version;
        validate();

        this.registrationRequired = builder.registrationRequired;
        this.referenceTracking = builder.referenceTracking;
        this.classResolver = builder.classResolver;
    }

    @Override
    public Kryo createMapper() {
        final Kryo kryo = new Kryo(classResolver.get(), new MapReferenceResolver(), new DefaultStreamFactory());
        kryo.addDefaultSerializer(Map.Entry.class, new UtilSerializers.EntrySerializer());
        kryo.setRegistrationRequired(registrationRequired);
        kryo.setReferences(referenceTracking);
        for (TypeRegistration tr : typeRegistrations)
            tr.registerWith(kryo);
        return kryo;
    }

    public GryoVersion getVersion() {
        return version;
    }

    public List<Class> getRegisteredClasses() {
        return this.typeRegistrations.stream().map(TypeRegistration::getTargetClass).collect(Collectors.toList());
    }

    public List<TypeRegistration<?>> getTypeRegistrations() {
        return typeRegistrations;
    }

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

    private void validate() {
        final Set<Integer> duplicates = new HashSet<>();

        final Set<Integer> ids = new HashSet<>();
        typeRegistrations.forEach(t -> {
            if (!ids.contains(t.getId()))
                ids.add(t.getId());
            else
                duplicates.add(t.getId());
        });

        if (duplicates.size() > 0)
            throw new IllegalStateException("There are duplicate kryo identifiers in use: " + duplicates);
    }

    /**
     * A builder to construct a {@link GryoMapper} instance.
     */
    public static class Builder implements Mapper.Builder<Builder> {

        private GryoVersion version = GryoVersion.V1_0;

        /**
         * Note that the following are pre-registered boolean, Boolean, byte, Byte, char, Character, double, Double,
         * int, Integer, float, Float, long, Long, short, Short, String, void.
         */
        private List<TypeRegistration<?>> typeRegistrations = version.cloneRegistrations();

        private final List<IoRegistry> registries = new ArrayList<>();

        /**
         * Starts numbering classes for Gryo serialization at 65536 to leave room for future usage by TinkerPop.
         */
        private final AtomicInteger currentSerializationId = new AtomicInteger(65536);

        private boolean registrationRequired = true;
        private boolean referenceTracking = true;
        private Supplier<ClassResolver> classResolver = GryoClassResolver::new;

        private Builder() { }

        /**
         * {@inheritDoc}
         */
        @Override
        public Builder addRegistry(final IoRegistry registry) {
            if (null == registry) throw new IllegalArgumentException("The registry cannot be null");
            this.registries.add(registry);
            return this;
        }

        /**
         * The version of Gryo to use in the mapper. Defaults to 1.0. Calls to this method will reset values specified
         * to {@link #addCustom(Class, Function)} and related overloads.
         */
        public Builder version(final GryoVersion version) {
            this.version = version;
            this.typeRegistrations = version.cloneRegistrations();
            return this;
        }

        /**
         * Provides a custom Kryo {@code ClassResolver} to be supplied to a {@code Kryo} instance.  If this value is
         * not supplied then it will default to the {@link GryoClassResolver}. To ensure compatibility with Gryo it
         * is highly recommended that objects passed to this method extend that class.
         * <p/>
         * If the {@code ClassResolver} implementation share state, then the {@link Supplier} should typically create
         * new instances when requested, as the {@link Supplier} will be called for each {@link Kryo} instance created.
         */
        public Builder classResolver(final Supplier<ClassResolver> classResolverSupplier) {
            if (null == classResolverSupplier)
                throw new IllegalArgumentException("The classResolverSupplier cannot be null");
            this.classResolver = classResolverSupplier;
            return this;
        }

        /**
         * Register custom classes to serializes with gryo using default serialization. Note that calling this method
         * for a class that is already registered will override that registration.
         */
        public Builder addCustom(final Class... custom) {
            if (custom != null && custom.length > 0) {
                for (Class c : custom) {
                    addOrOverrideRegistration(c, id -> GryoTypeReg.of(c, id));
                }
            }
            return this;
        }

        /**
         * Register custom class to serialize with a custom serialization class. Note that calling this method for
         * a class that is already registered will override that registration.
         */
        public Builder addCustom(final Class clazz, final Serializer serializer) {
            addOrOverrideRegistration(clazz, id -> GryoTypeReg.of(clazz, id, serializer));
            return this;
        }

        /**
         * Register custom class to serialize with a custom serialization shim.
         */
        public Builder addCustom(final Class clazz, final SerializerShim serializer) {
            addOrOverrideRegistration(clazz, id -> GryoTypeReg.of(clazz, id, serializer));
            return this;
        }

        /**
         * Register a custom class to serialize with a custom serializer as returned from a {@link Function}. Note
         * that calling this method for a class that is already registered will override that registration.
         */
        public Builder addCustom(final Class clazz, final Function<Kryo, Serializer> functionOfKryo) {
            addOrOverrideRegistration(clazz, id -> GryoTypeReg.of(clazz, id, functionOfKryo));
            return this;
        }

        /**
         * When set to {@code true}, all classes serialized by the {@code Kryo} instances created from this
         * {@link GryoMapper} must have their classes known up front and registered appropriately through this
         * builder.  By default this value is {@code true}.  This approach is more efficient than setting the
         * value to {@code false}.
         *
         * @param registrationRequired set to {@code true} if the classes should be registered up front or
         *                             {@code false} otherwise
         */
        public Builder registrationRequired(final boolean registrationRequired) {
            this.registrationRequired = registrationRequired;
            return this;
        }

        /**
         * By default, each appearance of an object in the graph after the first is stored as an integer ordinal.
         * This allows multiple references to the same object and cyclic graphs to be serialized. This has a small
         * amount of overhead and can be disabled to save space if it is not needed.
         *
         * @param referenceTracking set to {@code true} to enable and {@code false} otherwise
         */
        public Builder referenceTracking(final boolean referenceTracking) {
            this.referenceTracking = referenceTracking;
            return this;
        }

        /**
         * Creates a {@code GryoMapper}.
         */
        public GryoMapper create() {
            // consult the registry if provided and inject registry entries as custom classes.
            registries.forEach(registry -> {
                final List<Pair<Class, Object>> serializers = registry.find(GryoIo.class);
                serializers.forEach(p -> {
                    if (null == p.getValue1())
                        addCustom(p.getValue0());
                    else if (p.getValue1() instanceof Serializer)
                        addCustom(p.getValue0(), (Serializer) p.getValue1());
                    else if (p.getValue1() instanceof Function)
                        addCustom(p.getValue0(), (Function<Kryo, Serializer>) p.getValue1());
                    else
                        throw new IllegalStateException(String.format(
                                "Unexpected value provided by %s for serializable class %s - expected a parameter in [null, %s implementation or Function<%s, %s>], but received %s",
                                registry.getClass().getSimpleName(), p.getValue0().getClass().getCanonicalName(),
                                Serializer.class.getName(), Kryo.class.getSimpleName(),
                                Serializer.class.getSimpleName(), p.getValue1()));
                });
            });

            return new GryoMapper(this);
        }

        private <T> void addOrOverrideRegistration(final Class<?> clazz,
                                                   final Function<Integer, TypeRegistration<T>> newRegistrationBuilder) {
            final Iterator<TypeRegistration<?>> iter = typeRegistrations.iterator();
            Integer registrationId = null;
            while (iter.hasNext()) {
                final TypeRegistration<?> existingRegistration = iter.next();
                if (existingRegistration.getTargetClass().equals(clazz)) {
                    // when overridding a registration, use the old id
                    registrationId = existingRegistration.getId();
                    // remove the old registration (we install its override below)
                    iter.remove();
                    break;
                }
            }
            if (null == registrationId) {
                // when not overridding a registration, get an id from the counter
                registrationId = currentSerializationId.getAndIncrement();
            }
            typeRegistrations.add(newRegistrationBuilder.apply(registrationId));
        }
    }
}
