#nullable enable
using Barotrauma.Extensions;
using Microsoft.Xna.Framework;
using System;
using System.Collections.Immutable;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using System.Xml.Schema;

namespace Barotrauma
{
    public static class StructSerialization
    {
        private static readonly ImmutableDictionary<Type, MethodInfo> deserializeMethods;
        private static readonly ImmutableDictionary<Type, MethodInfo> serializeMethods;
        
        public class SkipAttribute : Attribute { }

        private static bool ShouldSkip(this FieldInfo field)
            => field.GetCustomAttribute<SkipAttribute>() != null;

        private static HandlerAttribute? ExtractHandler(this FieldInfo field)
            => field.GetCustomAttribute<HandlerAttribute>();
        
        public class HandlerAttribute : Attribute
        {
            public readonly Func<string?, object?> Read;
            public readonly Func<object?, string?> Write;
            
            public HandlerAttribute(Type handlerType)
            {
                var readAction =
                    handlerType.GetMethod(nameof(Read), BindingFlags.Public | BindingFlags.Static)
                    ?? throw new Exception($"Type {handlerType.Name} does not have a static {nameof(Read)} method");
                var writeAction = 
                    handlerType.GetMethod(nameof(Write), BindingFlags.Public | BindingFlags.Static)
                    ?? throw new Exception($"Type {handlerType.Name} does not have a static {nameof(Write)} method");
                var paramArray = new object?[1];
                Read = (s) =>
                {
                    paramArray[0] = s;
                    return readAction.Invoke(null, paramArray);
                };
                Write = (o) =>
                {
                    paramArray[0] = o;
                    return writeAction.Invoke(null, paramArray)?.ToString();
                };
            }
        }
        
        static StructSerialization()
        {
            deserializeMethods =
                typeof(StructSerialization)
                    .GetMethods(BindingFlags.Static | BindingFlags.Public)
                .Where(m =>
                {
                    if (!m.Name.StartsWith("Deserialize")) { return false; }
                    var parameters = m.GetParameters();
                    if (parameters.Length < 1 || parameters.Length > 2 ||
                        parameters[0].ParameterType != typeof(string))
                    {
                        return false;
                    }
                    return true;
                })
                .Select(m => (m.ReturnType, m))
                .ToImmutableDictionary();
            
            serializeMethods =
                typeof(StructSerialization)
                    .GetMethods(BindingFlags.Static | BindingFlags.Public)
                    .Where(m =>
                    {
                        if (!m.Name.StartsWith("Serialize")) { return false; }
                        var parameters = m.GetParameters();
                        if (parameters.Length != 1 ||
                            m.ReturnType != typeof(string))
                        {
                            return false;
                        }
                        return true;
                    })
                    .Select(m => (m.GetParameters()[0].ParameterType, m))
                    .ToImmutableDictionary();
        }

        public static void CopyPropertiesFrom<T>(this ref T self, in T other) where T : struct
        {
            var fields = self.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance).Where(f => !f.IsInitOnly).ToArray();
            foreach (var field in fields)
            {
                if (field.ShouldSkip()) { continue; }
                field.SetValue(self, field.GetValue(other));
            }
        }

        public static void DeserializeElement<T>(this ref T self, XElement element) where T : struct
        {
            var fields = self.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance).ToArray();

            //box the struct here so we don't end up
            //making a copy every time we feed this to reflection
            object boxedSelf = self;
            foreach (var field in fields)
            {
                if (field.ShouldSkip()) { continue; }
                boxedSelf.TryDeserialize(field, element);
            }
            //copy the boxed struct into the original
            self = (T)boxedSelf;
        }

        private static void TryDeserialize(this object boxedSelf, FieldInfo field, XElement element)
        {
            string fieldName = field.Name.ToLowerInvariant();
            string valueStr = element.GetAttributeString(fieldName, field.GetValue(boxedSelf)?.ToString() ?? "");

            var handler = field.ExtractHandler();

            if (handler != null)
            {
                field.SetValue(boxedSelf, handler.Read(valueStr));
            }
            else if (deserializeMethods.TryGetValue(field.FieldType, out MethodInfo? deserializeMethod))
            {
                object?[] parameters = { valueStr };
                if (deserializeMethod.GetParameters().Length > 1)
                {
                    Array.Resize(ref parameters, 2);
                    parameters[1] = field.GetValue(boxedSelf);
                }
                field.SetValue(boxedSelf, deserializeMethod.Invoke(boxedSelf, parameters));
            }
            else if (field.FieldType.IsEnum)
            {
                field.SetValue(boxedSelf, DeserializeEnum(field.FieldType, valueStr, (Enum)field.GetValue(boxedSelf)!));
            }
        }

        public static string DeserializeString(string str)
        {
            return str;
        }

        public static bool DeserializeBool(string str, bool defaultValue)
        {
            if (bool.TryParse(str, out bool result)) { return result; }
            return defaultValue;
        }

        public static float DeserializeFloat(string str, float defaultValue)
        {
            if (float.TryParse(str, NumberStyles.Float, CultureInfo.InvariantCulture, out float result)) { return result; }
            return defaultValue;
        }

        public static Int32 DeserializeInt32(string str, Int32 defaultValue)
        {
            if (Int32.TryParse(str, NumberStyles.Any, CultureInfo.InvariantCulture, out Int32 result)) { return result; }
            return defaultValue;
        }

        public static Identifier DeserializeIdentifier(string str)
        {
            return str.ToIdentifier();
        }

        public static LanguageIdentifier DeserializeLanguageIdentifier(string str)
        {
            return str.ToLanguageIdentifier();
        }

        public static Color DeserializeColor(string str)
        {
            return XMLExtensions.ParseColor(str);
        }

        public static Enum DeserializeEnum(Type enumType, string str, Enum defaultValue)
        {
            if (Enum.TryParse(enumType, str, out object? result)) { return (Enum)result!; }
            return defaultValue;
        }
        
        public static void SerializeElement<T>(this ref T self, XElement element) where T : struct
        {
            var fields = self.GetType().GetFields(BindingFlags.Public | BindingFlags.Instance).ToArray();

            foreach (var field in fields)
            {
                if (field.ShouldSkip()) { continue; }
                self.TrySerialize(field, element);
            }
        }

        public static void TrySerialize<T>(this T self, FieldInfo field, XElement element) where T : struct
        {
            string fieldName = field.Name.ToLowerInvariant();
            object? fieldValue = field.GetValue(self);

            string valueStr = fieldValue?.ToString() ?? "";

            var handler = field.ExtractHandler();

            if (handler != null)
            {
                valueStr = handler.Write(valueStr) ?? "";
            }
            else if (serializeMethods.TryGetValue(field.FieldType, out MethodInfo? method))
            {
                object?[] parameters = { fieldValue };
                valueStr = (string)method.Invoke(self, parameters)!;
            }
            
            element.SetAttributeValue(fieldName, valueStr);
        }

        public static string SerializeBool(bool val)
            => val ? "true" : "false";

        public static string SerializeInt32(Int32 val)
            => val.ToString(CultureInfo.InvariantCulture);
        
        public static string SerializeFloat(float val)
            => val.ToString(CultureInfo.InvariantCulture);

        public static string SerializeColor(Color val)
            => val.ToStringHex();
    }
}