using System.Diagnostics;
using ConeCalorimeter.Models;

namespace ConeCalorimeter.Services;

public sealed class ModbusRealtimeDataService : IRealtimeDataService
{
    private const ushort OxygenRegister = 10;
    private const ushort OrificeFlowRegister = 14;
    private const ushort OrificePressureRegister = 16;
    private const ushort CarbonMonoxideRegister = 18;
    private const ushort CarbonDioxideRegister = 20;
    private const ushort ConeTemperatureRegister = 26;
    private const ushort OrificeTemperatureRegister = 30;
    private const ushort SampleTemperatureRegister = 36;
    private const ushort CFactorRegister = 312;
    private const ushort HeatReleaseRateRegister = 354;
    private const ushort PeakHeatReleaseRateRegister = 376;
    private const ushort Qa180Register = 366;
    private const ushort Qa300Register = 370;
    private const ushort SmokeProductionRegister = 392;
    private const ushort IrradianceRegister = 410;
    private const ushort IgnitionSecondsRegister = 1014;
    private const ushort TestSecondsRegister = 1015;
    private const ushort M3FlameMonitorBit = 3;
    private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
    private readonly IScaleService _scaleService;
    private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
    private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];

    public ModbusRealtimeDataService(
        ITcpDeviceConnectionService tcpDeviceConnectionService,
        IScaleService scaleService)
    {
        _tcpDeviceConnectionService = tcpDeviceConnectionService;
        _scaleService = scaleService;
    }

    public RealtimeSnapshot GetCurrentSnapshot(TimeSpan elapsed)
    {
        return new RealtimeSnapshot(
            OrificeFlow: ReadRangedFloatOrEmpty("OrificeFlow", OrificeFlowRegister, 0, 10),
            OrificePressure: ReadRangedFloatOrEmpty("OrificePressure", OrificePressureRegister, -5000, 5000),
            OrificeTemperature: ReadRangedFloatOrEmpty("OrificeTemperature", OrificeTemperatureRegister, 200, 700),
            ConeTemperature: ReadRangedFloatOrEmpty("ConeTemperature", ConeTemperatureRegister, 0, 1200),
            SampleTemperature: ReadRangedFloatOrEmpty("SampleTemperature", SampleTemperatureRegister, -50, 1200),
            Irradiance: ReadRangedFloatOrEmpty("Irradiance", IrradianceRegister, 0, 100),
            FlameDetected: ReadCoilOrFalse(M3FlameMonitorBit),
            Oxygen: ReadRangedFloatOrEmpty("O2", OxygenRegister, 0, 30),
            CarbonDioxide: ReadRangedFloatOrEmpty("CO2", CarbonDioxideRegister, 0, 20),
            CarbonMonoxide: ReadRangedFloatOrEmpty("CO", CarbonMonoxideRegister, 0, 10),
            HeatReleaseRate: ReadRangedFloatOrEmpty("HeatReleaseRate", HeatReleaseRateRegister, 0, 5000),
            PeakHeatReleaseRate: ReadRangedFloatOrEmpty("PeakHeatReleaseRate", PeakHeatReleaseRateRegister, 0, 5000),
            CFactor: ReadRangedFloatOrEmpty("CFactor", CFactorRegister, 0, 1000),
            Qa180: ReadRangedFloatOrEmpty("Qa180", Qa180Register, 0, 1000),
            Qa300: ReadRangedFloatOrEmpty("Qa300", Qa300Register, 0, 1000),
            TotalHeatRelease: double.NaN,
            SmokeProduction: ReadRangedFloatOrEmpty("SmokeProduction", SmokeProductionRegister, 0, 100),
            CurrentMass: ReadScaleCurrentMassOrEmpty(),
            InitialMass: double.NaN,
            MassLoss: double.NaN,
            MassLossRate: double.NaN,
            IgnitionSeconds: ReadInt16OrEmpty(IgnitionSecondsRegister),
            TestSeconds: ReadInt16OrEmpty(TestSecondsRegister),
            TotalSmoke: double.NaN);
    }

    private double ReadRangedFloatOrEmpty(string label, ushort registerAddress, double minimum, double maximum)
    {
        if (!_tcpDeviceConnectionService.TryReadFloatValues(registerAddress, out var result))
        {
            return double.NaN;
        }

        if (!ModbusFloatSelector.TrySelectRangedFloat(result, minimum, maximum, out var byteOrder, out var value, out var matchCount))
        {
            LogFloatDiagnostic(label, registerAddress, result, null, matchCount);
            return double.NaN;
        }

        LogFloatDiagnostic(label, registerAddress, result, byteOrder, matchCount);
        return NormalizeRealtimeValue(value);
    }

    private void LogFloatDiagnostic(
        string label,
        ushort registerAddress,
        ModbusFloatReadResult result,
        ModbusFloatByteOrder? selectedByteOrder,
        int matchCount)
    {
        if (selectedByteOrder is null)
        {
            if (!_loggedInvalidFloatDiagnostics.Add(registerAddress))
            {
                return;
            }
        }
        else if (!_loggedFloatDiagnostics.Add(registerAddress))
        {
            return;
        }

        var selectedText = selectedByteOrder?.ToString() ?? "none";

        Debug.WriteLine(
            $"Realtime float {label} register {registerAddress} raw [{result.RawHex}], "
            + $"ABCD={result.Abcd:G9}, CDAB={result.Cdab:G9}, "
            + $"BADC={result.Badc:G9}, DCBA={result.Dcba:G9}, "
            + $"selected={selectedText}, matches={matchCount}.");
    }

    private static double NormalizeRealtimeValue(double value)
    {
        return Math.Abs(value) < 0.005 ? 0 : value;
    }

    private int ReadInt16OrEmpty(ushort registerAddress)
    {
        return _tcpDeviceConnectionService.TryReadInt16(registerAddress, out var value)
            ? value
            : -1;
    }

    private double ReadScaleCurrentMassOrEmpty()
    {
        return _scaleService.TryReadCurrentMass(out var value)
            ? NormalizeRealtimeValue(value)
            : double.NaN;
    }

    private bool ReadCoilOrFalse(ushort coilAddress)
    {
        return _tcpDeviceConnectionService.TryReadCoil(coilAddress, out var value) && value;
    }
}
