using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors;
using InnovEnergy.Lib.Protocols.Modbus.Reflection.Attributes;
using InnovEnergy.Lib.Utils;
using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes;
using static System.Reflection.BindingFlags;

namespace InnovEnergy.Lib.Protocols.Modbus.Reflection;

#pragma warning disable CS8509

internal record ModbusMember
(
    UInt16                 StartAddress,
    UInt16                 EndAddress,
    ModbusKind             Kind,
    Action<MbData, Object> ModbusToRecord,
    Action<Object, MbData> RecordToModbus
);

internal static class ModbusMembers
{
    private static readonly (Double scale, Double offset) NoTransform = (scale:1, offset:0);

    internal static IEnumerable<ModbusMember> From<R>(Int32 globalAddressOffset, Endian globalEndian = Endian.Undefined)
    {
        var recordType = typeof(R);

        // "=======================================================================".WriteLine();
        // recordType.Name.WriteLine();

        var offset = recordType.GetRecordOffset(globalAddressOffset);
        var endian = recordType.GetEndian(globalEndian);
        
        return recordType
              .GetDataMembers()
              .Where(HasAttribute<ModbusAttribute>)
              .Select(m => m.CreateModbusMember(offset, endian));
    }

    private static Int32 GetRecordOffset([DynamicallyAccessedMembers(All)] this Type recordType, Int32 globalAddressOffset)
    {
        return recordType
              .GetCustomAttributes<AddressOffset>()
              .Aggregate(globalAddressOffset, (a, b) => a + b.Offset);
    }

    private static Endian GetEndian([DynamicallyAccessedMembers(All)] this Type recordType, Endian endian)
    {
        return recordType
              .GetCustomAttributes<EndianAttribute>()
              .Aggregate(endian, (a, b) => b.Endian.InheritFrom(a));
    }

    private static ModbusMember CreateModbusMember(this MemberInfo info, Int32 addressOffset, Endian globalEndian) 
    {
        var attribute = info.GetCustomAttributes<ModbusAttribute>().Single();
        var endian    = info.GetCustomAttributes<EndianAttribute>()
                            .Select(a => a.Endian)
                            .Append(Endian.Undefined)
                            .First()
                            .InheritFrom(globalEndian);  
        
        var address    = (UInt16)(attribute.Address + addressOffset);
        var endAddress = (UInt16)(address + attribute.Size);
        var modbusType = attribute.ModbusType;
        var transform  = attribute is ModbusRegister mra
                       ? (mra.Scale, mra.Offset)
                       : NoTransform;

        //Console.WriteLine(info.Name +" " + address + " " + modbusType);
        
        return new ModbusMember
        (
            address,
            endAddress,
            attribute.Kind, 
            ModbusToRecord(), 
            RecordToModbus()
        );

        Action<MbData, Object> ModbusToRecord()
        {
            var decode = ConvertModbusToRecord(transform);

            Func<MbData, IConvertible> readFromMbData =
                modbusType == typeof(Boolean) ? d => d.GetInput(address) :
                modbusType == typeof(UInt16)  ? d => d.GetUInt16(address) :
                modbusType == typeof(Int16)   ? d => d.GetInt16(address) :
                modbusType == typeof(UInt32)  ? d => d.GetUInt32(address) :
                modbusType == typeof(Int32)   ? d => d.GetInt32(address) :
                modbusType == typeof(Single)  ? d => d.GetFloat32(address) :
                modbusType == typeof(UInt64)  ? d => d.GetUInt64(address) :
                modbusType == typeof(Int64)   ? d => d.GetInt64(address) :
                modbusType == typeof(Double)  ? d => d.GetFloat64(address) :
                throw new ArgumentException(nameof(modbusType));


            var memberType = info switch
            {
                FieldInfo    fi => fi.FieldType,
                PropertyInfo pi => pi.PropertyType,
            };

            var ctr = memberType.GetConstructor(new[] { typeof(Double) }); // TODO: hardcoded double constructor for Units 

            Func<IConvertible, Object> convert = ctr is null
                                               ? value => value.ConvertTo(memberType)
                                               : value => ctr.Invoke(new Object[] { value.ConvertTo<Double>() });
            
            Action<Object, IConvertible> set = info switch
            {
                FieldInfo    fi => (rec, value) => fi.SetValue(rec, convert(value)),
                PropertyInfo pi => (rec, value) => pi.SetValue(rec, convert(value)),
            };

            return (mbData, rec) =>
            {
                var rawModbusValue = readFromMbData(mbData.WithEndian(endian));
                var decoded = decode(rawModbusValue);

                set(rec, decoded);
            };
        }

        Action<Object, MbData> RecordToModbus()
        {
            var encode = ConvertRecordToModbus(transform);

            Func<Object, IConvertible> get = info switch
            {
                FieldInfo    fi => rec => (IConvertible)fi.GetValue(rec)!,
                PropertyInfo pi => rec => (IConvertible)pi.GetValue(rec)!,
            };

            Action<IConvertible, MbData> writeToMbData =
                modbusType == typeof(Boolean)? (value, mbData) => mbData.SetCoil   (address, value.ConvertTo<Boolean>()) :
                modbusType == typeof(UInt16) ? (value, mbData) => mbData.SetUInt16 (address, value.ConvertTo<UInt16>()) :
                modbusType == typeof(Int16)  ? (value, mbData) => mbData.SetInt16  (address, value.ConvertTo<Int16>()) :
                modbusType == typeof(UInt32) ? (value, mbData) => mbData.SetUInt32 (address, value.ConvertTo<UInt32>()) :
                modbusType == typeof(Int32)  ? (value, mbData) => mbData.SetInt32  (address, value.ConvertTo<Int32>()) :
                modbusType == typeof(Single) ? (value, mbData) => mbData.SetFloat32(address, value.ConvertTo<Single>()) :
                modbusType == typeof(UInt64) ? (value, mbData) => mbData.SetUInt64 (address, value.ConvertTo<UInt64>()) :
                modbusType == typeof(Int64)  ? (value, mbData) => mbData.SetInt64  (address, value.ConvertTo<Int64>()) :
                modbusType == typeof(Double) ? (value, mbData) => mbData.SetFloat64(address, value.ConvertTo<Double>()) :
                throw new ArgumentException(nameof(modbusType));

            return (rec, mbData) =>
            {
                var memberValue = get(rec);
                var encoded     = encode(memberValue);

                writeToMbData(encoded, mbData.WithEndian(endian));
            };
        }
    }


    private static Func<IConvertible, IConvertible> ConvertModbusToRecord((Double scale, Double offset) transform)
    {
        if (transform == NoTransform)
            return Nop;
        
        var scale  = transform.scale.ConvertTo<Decimal>();
        var offset = transform.offset.ConvertTo<Decimal>();
        
        return c =>
        {
            var value = c.ConvertTo<Decimal>();
            
            return
            /**********************************/
            /**/  (value + offset) * scale; /**/ 
            /**********************************/
        };
    }

    private static Func<IConvertible, IConvertible> ConvertRecordToModbus((Double scale, Double offset) transform)
    {
        if (transform == NoTransform)
            return Nop;

        var scale  = transform.scale.ConvertTo<Decimal>();
        var offset = transform.offset.ConvertTo<Decimal>();
        
        return c =>
        {
            var value = c.ConvertTo<Decimal>();
            
            return
            /*******************************/
            /**/ value / scale - offset; /**/
            /*******************************/
        };
    }


    private static T Nop<T>(T c) => c;


    private static IEnumerable<MemberInfo> GetDataMembers([DynamicallyAccessedMembers(All)] this Type recordType)
    {
        const BindingFlags bindingFlags = Instance
                                        | Public
                                        | NonPublic
                                        | FlattenHierarchy;

        var fields = recordType.GetFields(bindingFlags);
        var props  = recordType.GetProperties(bindingFlags);

        return fields.Concat<MemberInfo>(props);
    }

    private static Boolean HasAttribute<T>(MemberInfo i) where T : Attribute
    {
        return i.GetCustomAttributes<T>().Any();
    }
}