using Amazon.Lambda.Serialization.SystemTextJson.Converters;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Amazon.Lambda.Serialization.SystemTextJson
{
    /// <summary>
    /// Base class of serializers using System.Text.Json
    /// </summary>
    public abstract class AbstractLambdaJsonSerializer
    {
        private const string DEBUG_ENVIRONMENT_VARIABLE_NAME = "LAMBDA_NET_SERIALIZER_DEBUG";

        private readonly bool _debug;

        /// <summary>
        /// Options settings used for the JSON writer
        /// </summary>
        protected JsonWriterOptions WriterOptions { get; }

        /// <summary>
        /// Create instance
        /// </summary>
        /// <param name="jsonWriterCustomizer"></param>
        protected AbstractLambdaJsonSerializer(Action<JsonWriterOptions> jsonWriterCustomizer)
        {
            WriterOptions = new JsonWriterOptions()
            {
                Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
            };
            jsonWriterCustomizer?.Invoke(this.WriterOptions);

            this._debug = string.Equals(Environment.GetEnvironmentVariable(DEBUG_ENVIRONMENT_VARIABLE_NAME), "true",
                StringComparison.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Serializes a particular object to a stream.
        /// </summary>
        /// <typeparam name="T">Type of object to serialize.</typeparam>
        /// <param name="response">Object to serialize.</param>
        /// <param name="responseStream">Output stream.</param>        
        public void Serialize<T>(T response, Stream responseStream)
        {
            try
            {
                if (_debug)
                {
                    using (var debugStream = new MemoryStream())
                    using (var utf8Writer = new Utf8JsonWriter(debugStream, WriterOptions))
                    {
                        InternalSerialize(utf8Writer, response);

                        debugStream.Position = 0;
                        using var debugReader = new StreamReader(debugStream);
                        var jsonDocument = debugReader.ReadToEnd();
                        Console.WriteLine($"Lambda Serialize {response.GetType().FullName}: {jsonDocument}");

                        var writer = new StreamWriter(responseStream);
                        writer.Write(jsonDocument);
                        writer.Flush();
                    }
                }
                else
                {
                    using (var writer = new Utf8JsonWriter(responseStream, WriterOptions))
                    {
                        InternalSerialize(writer, response);
                    }
                }
            }
            catch (Exception e)
            {
                throw new JsonSerializerException($"Error converting the response object of type {typeof(T).FullName} from the Lambda function to JSON: {e.Message}", e);
            }
        }


        /// <summary>
        /// Deserializes a stream to a particular type.
        /// </summary>
        /// <typeparam name="T">Type of object to deserialize to.</typeparam>
        /// <param name="requestStream">Stream to serialize.</param>
        /// <returns>Deserialized object from stream.</returns>
        public T Deserialize<T>(Stream requestStream)
        {
            try
            {
                byte[] utf8Json = null;
                if (_debug)
                {
                    var json = new StreamReader(requestStream).ReadToEnd();
                    Console.WriteLine($"Lambda Deserialize {typeof(T).FullName}: {json}");
                    utf8Json = UTF8Encoding.UTF8.GetBytes(json);
                }

                if (utf8Json == null)
                {
                    if (requestStream is MemoryStream ms)
                    {
                        utf8Json = ms.ToArray();
                    }
                    else
                    {
                        using (var copy = new MemoryStream())
                        {
                            requestStream.CopyTo(copy);
                            utf8Json = copy.ToArray();
                        }
                    }
                }

                return InternalDeserialize<T>(utf8Json);
            }
            catch (Exception e)
            {
                throw new JsonSerializerException($"Error converting the Lambda event JSON payload to type {typeof(T).FullName}: {e.Message}", e);
            }
        }

        /// <summary>
        /// Create the default instance of JsonSerializerOptions used in serializer
        /// </summary>
        /// <returns></returns>
        protected virtual JsonSerializerOptions CreateDefaultJsonSerializationOptions()
        {
            var serializer = new JsonSerializerOptions()
            {
#if NET6_0_OR_GREATER
                DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
#else
                IgnoreNullValues = true,
#endif
                PropertyNameCaseInsensitive = true,
                PropertyNamingPolicy = new AwsNamingPolicy(),
                Converters =
                {
                    new DateTimeConverter(),
                    new MemoryStreamConverter(),
                    new ConstantClassConverter(),
                    new ByteArrayConverter()
                }
            };

            return serializer;
        }

        /// <summary>
        /// Perform the actual serialization after the public method had done the safety checks.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="writer"></param>
        /// <param name="response"></param>
        protected abstract void InternalSerialize<T>(Utf8JsonWriter writer, T response);

        /// <summary>
        /// Perform the actual deserialization after the public method had done the safety checks.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="utf8Json"></param>
        /// <returns></returns>
        protected abstract T InternalDeserialize<T>(byte[] utf8Json);
    }
}