using InnovEnergy.Lib.Protocols.Modbus.Clients;
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames;
using InnovEnergy.Lib.Protocols.Modbus.Protocol.Frames.Accessors;

namespace InnovEnergy.Lib.Protocols.Modbus.Reflection;

#pragma warning disable CS8509
#pragma warning disable CS8524

internal record Batch<R>(Action<R> Read, Action<R> Write, String DebugString)
{
    public override String ToString() => DebugString;
}

public static class Batches
{
    internal static IReadOnlyList<Batch<R>> MakeBatchesFor<R>(this ModbusClient modbusClient, Int32 addressOffset)
    {
        var members = ModbusMembers
                     .From<R>(addressOffset)
                     .OrderBy(m => m.Kind)
                     .ThenBy(m => m.StartAddress)
                     .ThenBy(m => m.EndAddress);

        return MakeBatches<R>(modbusClient, members).ToList();
    }

    private static IEnumerable<Batch<R>> MakeBatches<R>(ModbusClient mb, IEnumerable<ModbusMember> modbusMembers)
    {
        var batchMembers = new List<ModbusMember>();

        foreach (var member in modbusMembers)
        {
            if (CloseBatch(member))
            {
                yield return MakeBatch<R>(mb, batchMembers);
                batchMembers = new List<ModbusMember>();
            }

            batchMembers.Add(member);
        }

        if (batchMembers.Count > 0)
            yield return MakeBatch<R>(mb, batchMembers);

        Boolean CloseBatch(ModbusMember m)
        {
            if (batchMembers.Count == 0)
                return false;

            return m.StartAddress > batchMembers[^1].EndAddress                       // gap between registers 
                || m.EndAddress   > batchMembers[0].StartAddress + Constants.MaxRegs  // max batch size reached
                || m.Kind        != batchMembers[0].Kind;                             // different Kind 
        }
    }
    

    private static Batch<R> MakeBatch<R>(ModbusClient modbusClient, IReadOnlyList<ModbusMember> members)
    {
        var startAddress = members[0].StartAddress;
        var endAddress   = members[^1].EndAddress;
        var count        = (UInt16)(endAddress - startAddress);
        var kind         = members[0].Kind;
        var isWritable   = kind is ModbusKind.HoldingRegister or ModbusKind.Coil;
        var debugString  = $"{kind}: {startAddress}" + (endAddress - 1 == startAddress ? "" : $"-{endAddress - 1}");
        
        
        // var modPoll      = $"{kind}: {startAddress}-{endAddress}"; // TODO
        var read         = MakeRead();
        var write        = MakeWrite();
        
        return new Batch<R>(read, write, debugString);
        
        Action<R> MakeRead()
        {
            Func<MbData> readModbus = kind switch
            {
                ModbusKind.InputRegister   => () => modbusClient.ReadInputRegisters  (startAddress, count),
                ModbusKind.HoldingRegister => () => modbusClient.ReadHoldingRegisters(startAddress, count),
                ModbusKind.DiscreteInput   => () => modbusClient.ReadDiscreteInputs  (startAddress, count),
                ModbusKind.Coil            => () => modbusClient.ReadCoils           (startAddress, count),
            };

            //Console.WriteLine("start: " + startAddress + " count: " + count);
            
            return record =>
            {
                var mbData = readModbus();
                foreach (var member in members)
                {
                    member.ModbusToRecord(mbData, record!);
                }
            };
        }
        
        Action<R> MakeWrite()
        {
            if (!isWritable)
                return _ => { }; // nop

            Func<MbData> createMbData = kind switch
            {
                ModbusKind.HoldingRegister => () => MbData.Registers(startAddress, count),
                ModbusKind.Coil            => () => MbData.Coils    (startAddress, count),
            };


            Action<MbData> writeModbus = kind switch
            {
                ModbusKind.HoldingRegister => d => modbusClient.WriteRegisters(startAddress, d.GetRegisters()),
                ModbusKind.Coil            => d => modbusClient.WriteCoils    (startAddress, d.GetCoils().Take(count).ToList()), 
                // ^^  TODO: Coils.count is broken, fix when refactoring to use direct binary codec
            };


            return rec =>
            {
                var mbData = createMbData();

                foreach (var member in members)
                    member.RecordToModbus(rec!, mbData);
                
                writeModbus(mbData);
            };
        }
    }
}