#region License

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

#endregion

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using System.Text.Encodings.Web;
using System.Text.Json;
using Gremlin.Net.Driver.Messages;
using Gremlin.Net.Process.Traversal;
using Gremlin.Net.Process.Traversal.Strategy;

namespace Gremlin.Net.Structure.IO.GraphSON
{
    /// <summary>
    ///     Allows to serialize objects to GraphSON.
    /// </summary>
    public abstract class GraphSONWriter
    {
        private static readonly JsonSerializerOptions JsonOptions = new()
            { Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

        /// <summary>
        /// Contains the information of serializers by type.
        /// </summary>
        protected readonly Dictionary<Type, IGraphSONSerializer> Serializers = new()
        {
            { typeof(ITraversal), new TraversalSerializer() },
            { typeof(Bytecode), new BytecodeSerializer() },
            { typeof(Binding), new BindingSerializer() },
            { typeof(RequestMessage), new RequestMessageSerializer() },
            { typeof(int), new Int32Converter() },
            { typeof(long), new Int64Converter() },
            { typeof(float), new FloatConverter() },
            { typeof(double), new DoubleConverter() },
            { typeof(Guid), new UuidSerializer() },
            { typeof(Type), new ClassSerializer() },
            { typeof(EnumWrapper), new EnumSerializer() },
            { typeof(P), new PSerializer() },
            { typeof(TextP), new TextPSerializer() },
            { typeof(Vertex), new VertexSerializer() },
            { typeof(Edge), new EdgeSerializer() },
            { typeof(Property), new PropertySerializer() },
            { typeof(VertexProperty), new VertexPropertySerializer() },
            { typeof(AbstractTraversalStrategy), new TraversalStrategySerializer() },
            { typeof(ILambda), new LambdaSerializer() },

            //Extended
            { typeof(DateTimeOffset), new OffsetDateTimeSerializer() }, // DateTimeOffset will be serialized as OffsetDateTimeSerializer by default
            { typeof(decimal), new DecimalConverter() },
            { typeof(TimeSpan), new DurationSerializer() },
            { typeof(BigInteger), new BigIntegerSerializer() },
            { typeof(sbyte), new SByteConverter() },
            { typeof(byte[]), new ByteBufferSerializer() },
            { typeof(char), new CharConverter() },
            { typeof(short), new Int16Converter() }
        };

        /// <summary>
        ///     Initializes a new instance of the <see cref="GraphSONWriter" /> class.
        /// </summary>
        protected GraphSONWriter()
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="GraphSONWriter" /> class.
        /// </summary>
        /// <param name="customSerializerByType">
        ///     <see cref="IGraphSONSerializer" /> serializers identified by their
        ///     <see cref="Type" />.
        /// </param>
        protected GraphSONWriter(IReadOnlyDictionary<Type, IGraphSONSerializer> customSerializerByType)
        {
            foreach (var serializerAndType in customSerializerByType)
                Serializers[serializerAndType.Key] = serializerAndType.Value;
        }

        /// <summary>
        ///     Serializes an object to GraphSON.
        /// </summary>
        /// <param name="objectData">The object to serialize.</param>
        /// <returns>The serialized GraphSON.</returns>
        public virtual string WriteObject(dynamic objectData)
        {
            return JsonSerializer.Serialize(ToDict(objectData), JsonOptions);
        }

        /// <summary>
        ///     Transforms an object into its GraphSON representation including type information.
        /// </summary>
        /// <param name="objectData">The object to transform.</param>
        /// <returns>A GraphSON representation of the object ready to be serialized.</returns>
        [return: NotNullIfNotNull("objectData")]
        public dynamic? ToDict(dynamic? objectData)
        {
            if (objectData != null)
            {
                var type = objectData.GetType();

                IGraphSONSerializer serializer = TryGetSerializerFor(type);

                if (serializer != null)
                    return serializer.Dictify(objectData, this);
                if (IsDictionaryType(type))
                    return DictToGraphSONDict(objectData);
                if (IsCollectionType(type))
                    return CollectionToGraphSONCollection(objectData);
            }
            return objectData;         
        }

        private IGraphSONSerializer? TryGetSerializerFor(Type type)
        {
            if (Serializers.TryGetValue(type, out var serializer))
            {
                return serializer;
            }
            foreach (var supportedType in new List<Type>(Serializers.Keys))
                if (supportedType.IsAssignableFrom(type))
                {
                    return Serializers[supportedType];
                }
            return null;
        }

        private bool IsDictionaryType(Type type)
        {
            return type
                .GetInterfaces()
                .Any(interfaceType => interfaceType.IsConstructedGenericType
                    && interfaceType.GetGenericTypeDefinition() == typeof(IEnumerable<>) 
                    && interfaceType.GenericTypeArguments[0] is { IsConstructedGenericType: true } genericArgument 
                    && genericArgument.GetGenericTypeDefinition() == typeof(KeyValuePair<,>));
        }

        private Dictionary<string, dynamic> DictToGraphSONDict(dynamic dict)
        {
            var graphSONDict = new Dictionary<string, dynamic>();
            foreach (var keyValue in dict)
                graphSONDict.Add(ToDict(keyValue.Key), ToDict(keyValue.Value));
            return graphSONDict;
        }

        private bool IsCollectionType(Type type)
        {
            return type.GetInterfaces().Contains(typeof(ICollection));
        }

        private IEnumerable<dynamic> CollectionToGraphSONCollection(dynamic collection)
        {
            foreach (var e in collection)
                yield return ToDict(e);
        }
    }
}
