using InnovEnergy.Lib.Protocols.Modbus.Channels;
using InnovEnergy.Lib.Protocols.Modbus.Clients;
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
using InnovEnergy.Lib.Units.Composite;
using InnovEnergy.Lib.Utils;

namespace InnovEnergy.Lib.Devices.AMPT;

public class AmptDevices
{
    private readonly ModbusDevice<CommunicationUnitRegisters>            _CommunicationUnit;
    private readonly IEnumerable<ModbusDevice<StringOptimizerRegisters>> _StringOptimizers;

    public AmptDevices(String hostname, UInt16 port = 502) : this(new TcpChannel(hostname, port))
    {
    }

    public AmptDevices(Channel transport) : this(new ModbusTcpClient(transport, 2))
    {
    }
    
    public AmptDevices(ModbusClient modbusClient)
    {
        _CommunicationUnit = new ModbusDevice<CommunicationUnitRegisters>(modbusClient);
        _StringOptimizers  = StringOptimizers(modbusClient);
    }

    public AmptStatus? Read()
    {
        try
        {
            return TryRead();
        }
        catch ( Exception e)
        {
            if( e is not NullChannelException)
                Console.WriteLine("Failed to read Ampt data \n" + e.Message);

            return null;
        }
    }
    
    public AmptStatus TryRead()
    {
        var cuStatus = _CommunicationUnit.Read();
        
        // CommunicationUnit knows how many StringOptimizers are connected
        var nStringOptimizers = cuStatus.NumberOfStringOptimizers;
        
        //Factor
        var nEnergyFactor  = cuStatus.EnergyScaleFactor;
        var nCurrentFactor = cuStatus.CurrentScaleFactor;
        var nVoltageFactor = cuStatus.VoltageScaleFactor;
        
        // hardcoded: every SO has 2 strings (produced like this by AMPT)
        var nStrings = nStringOptimizers * 2; 
        
        // read stati from optimizers
        var soStati = _StringOptimizers
                      .Take(nStringOptimizers)
                      .Select(so => so.Read())
                      .ToArray(nStringOptimizers);
        
        // every SO has 2 strings but ONE Dc Link Connection 
        // they are connected to a shared Dc Link, so Voltage seen by them should be approx the same.
        // voltages are averaged, currents added
        
        // TODO: alarm when we see substantially different voltages
        
        var busVoltage        = nStringOptimizers == 0 ? 0 : soStati.Average(r => r.Voltage);
        var busCurrent        = nStringOptimizers == 0 ? 0 : soStati.Sum    (r => r.Current);
        var dailyOutputEnergy = nStringOptimizers == 0 ? 0 : soStati.Sum    (r => r.ProductionToday);
                
        var dc         = new DcBus
        {
            Voltage = busVoltage,
            Current = busCurrent
        };
        
        // flatten the output strings of each SO into one array 
        var strings = soStati.SelectMany(GetDc).ToArray(nStrings);

        return new AmptStatus
        {
            Dc = dc,
            NbrOfStrings = nStringOptimizers,
            Strings = strings,
            DcWh = dailyOutputEnergy
        };
    }

    
    
    private static IEnumerable<DcBus> GetStrings(StringOptimizerRegisters r)
    {
        // hardcoded: every SO has 2 strings (produced like this by AMPT)
        
        yield return new()
        {
            Voltage = r.String1Voltage,
            Current = r.String1Current,
        };
        yield return new()
        {
            Voltage = r.String2Voltage,
            Current = r.String2Current,
        };
    }

    private static IEnumerable<DcBus> GetDc(StringOptimizerRegisters r)
    {
        // hardcoded: every SO has 2 strings (produced like this by AMPT)
        
        yield return new()
        {
            Voltage = r.Voltage,
            Current = r.Current,
        };
    }

    private static IEnumerable<ModbusDevice<StringOptimizerRegisters>> StringOptimizers(ModbusClient modbusClient)
    {
        var cache = new List<ModbusDevice<StringOptimizerRegisters>>();

        ModbusDevice<StringOptimizerRegisters> GetOptimizer(Int32 i)
        {
            if (i < cache.Count)
                return cache[i];

            var modbusDevice = new ModbusDevice<StringOptimizerRegisters>(modbusClient, i * 16);
            cache.Add(modbusDevice);
            return modbusDevice;
        }

        return Enumerable
              .Range(0, Byte.MaxValue)
              .Select(GetOptimizer);
    }
}