using System;
using System.Diagnostics;
using System.IO.Abstractions;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Quadarax.Foundation.Core.Json
{
    public class Binder
    {
        private IFileSystem _fileSystem;

        public JsonSerializerOptions SerializerOptions {get;private set;}

        public Binder(JsonSerializerOptions options): this(new FileSystem(), options)
        {
        }
        public Binder(IFileSystem fileSystemAbstraction, JsonSerializerOptions options)
        {
            _fileSystem = fileSystemAbstraction ?? throw new ArgumentNullException(nameof(fileSystemAbstraction));
            SerializerOptions = options;
        }

        public Binder(IFileSystem fileSystemAbstraction, bool isPrettyPrint = true, bool isIgnoringNullValues = true, int maxDepth = 100)
        {
            _fileSystem = fileSystemAbstraction ?? throw new ArgumentNullException(nameof(fileSystemAbstraction));
            var jsonOpt = new JsonSerializerOptions
            {
                IgnoreNullValues = isIgnoringNullValues,
                WriteIndented = isPrettyPrint,
                PropertyNameCaseInsensitive = true,
                PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                MaxDepth = maxDepth
            };
            jsonOpt.Converters.Add(new JsonStringEnumConverter());
            SerializerOptions = jsonOpt;
        }

        public TObject? Load<TObject>(string fileName)
        {
            return (TObject?) Load(fileName, typeof(TObject));
        }
        public object? Load(string fileName, Type targetType)
        {
            if (targetType == null)
                throw new ArgumentNullException(nameof(targetType));

            if (string.IsNullOrEmpty(fileName))
                throw new ArgumentNullException(nameof(fileName));

            using (var reader = _fileSystem.File.OpenText(fileName))
            {
                var result = JsonSerializer.Deserialize(reader.ReadToEnd(),targetType, SerializerOptions);
                return result;
            }
        }

        public object? LoadFromString(string jsonString, Type targetType)
        {
            if (targetType == null)
                throw new ArgumentNullException(nameof(targetType));

            if (string.IsNullOrEmpty(jsonString))
                throw new ArgumentNullException(nameof(jsonString));

            
            var result = JsonSerializer.Deserialize(jsonString,targetType, SerializerOptions);
            return result;
        }

        public void LoadTo(string fileName, object target, Type targetType)
        {
            if (targetType == null)
                throw new ArgumentNullException(nameof(targetType));
     
            if (target == null)
                throw new ArgumentNullException(nameof(target));
            var shadow = Load(fileName, targetType);
            if (shadow == null) throw new InvalidOperationException($"Cannot load file '{fileName}' to '{targetType.FullName}'");
            
            foreach (var property in shadow.GetType().GetProperties())
            {
                if (target.GetType().GetProperties().Any(x => x.Name == property.Name && property.GetValue(shadow) != null))
                {
                    if (property.GetType().IsClass && property.PropertyType.Assembly.FullName == targetType.Assembly.FullName)
                    {
                        var mapMethod = typeof(Binder).GetMethod("LoadTo");
                        var genericMethod = mapMethod?.MakeGenericMethod(property.GetValue(shadow).GetType());
                        var obj2 = genericMethod.Invoke(null, new object[] { property.GetValue(shadow), JsonSerializer.Serialize(property.GetValue(shadow)) });

                        foreach (var property2 in obj2.GetType().GetProperties())
                        {
                            if (property2.GetValue(obj2) != null)
                            {
                                property.GetValue(target).GetType().GetProperty(property2.Name).SetValue(property.GetValue(target), property2.GetValue(obj2));
                            }
                        }
                    }
                    else if (property.CanWrite)
                    {
                        property.SetValue(target, property.GetValue(shadow));
                    }
                }
            }

        }

        public void LoadFromStringTo(string jsonString, object target, Type targetType)
        {
            if (targetType == null)
                throw new ArgumentNullException(nameof(targetType));
     
            if (target == null)
                throw new ArgumentNullException(nameof(target));
            var shadow = LoadFromString(jsonString, targetType);
            
            foreach (var property in shadow.GetType().GetProperties())
            {
                if (target.GetType().GetProperties().Any(x => x.Name == property.Name && property.GetValue(shadow) != null))
                {
                    if (property.GetType().IsClass && property.PropertyType.Assembly.FullName == targetType.Assembly.FullName)
                    {
                        var mapMethod = typeof(Binder).GetMethod("LoadFromStringTo");
                        var genericMethod = mapMethod.MakeGenericMethod(property.GetValue(shadow).GetType());
                        var obj2 = genericMethod.Invoke(null, new object[] { property.GetValue(shadow), JsonSerializer.Serialize(property.GetValue(shadow)) });

                        foreach (var property2 in obj2.GetType().GetProperties())
                        {
                            if (property2.GetValue(obj2) != null)
                            {
                                property.GetValue(target).GetType().GetProperty(property2.Name).SetValue(property.GetValue(target), property2.GetValue(obj2));
                            }
                        }
                    }
                    else if (property.CanWrite)
                    {
                        property.SetValue(target, property.GetValue(shadow));
                    }
                }
            }

        }

        public void LoadTo<TObject>(string fileName, TObject target)
        {
            LoadTo(fileName, target, typeof(TObject));
        }

        public void Save<TObject>(string fileName, TObject bindingObject)
        {
            if (string.IsNullOrEmpty(fileName))
                throw new ArgumentNullException(nameof(fileName));
            if (bindingObject==null)
                throw new ArgumentNullException(nameof(bindingObject));

            var configuration = JsonSerializer.SerializeToUtf8Bytes(bindingObject,SerializerOptions);
            using (var writer = _fileSystem.File.Create(fileName))
            {
                writer.Write(configuration);
                writer.Flush();
            }
        }

        public string SaveToString<TObject>(TObject bindingObject)
        {
            if (bindingObject==null)
                throw new ArgumentNullException(nameof(bindingObject));

            return JsonSerializer.Serialize(bindingObject,bindingObject.GetType());
        }
    }
}
