using System.Diagnostics;
using ConeCalorimeter.Models;

namespace ConeCalorimeter.Services;

public sealed class ModbusRealtimeDataService : IRealtimeDataService
{
    private const int ScaleCurrentMassStabilityKey = -1;
    private const double RealtimeZeroTolerance = 0.005;
    private const int ZeroValueStableReadCount = 2;
    private const double HeatReleaseRateDisplayScale = 1000;
    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 AbsorbanceRegister = 120;
    private const ushort CFactorRegister = 308;
    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 DeviceTestSecondsRegister = 1015;
    private const ushort M3FlameMonitorBit = 3;
    private static readonly ModbusFloatByteOrder[] RealtimeFloatByteOrders =
    [
        ModbusFloatByteOrder.Abcd,
        ModbusFloatByteOrder.Cdab,
        ModbusFloatByteOrder.Badc,
        ModbusFloatByteOrder.Dcba
    ];

    private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
    private readonly IScaleService _scaleService;
    private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
    private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
    private readonly Dictionary<int, double> _stableRealtimeValues = [];
    private readonly Dictionary<int, int> _pendingZeroReadCounts = [];
    private double? _lastAbsorbanceValue;
    private ModbusFloatByteOrder? _preferredFloatByteOrder;

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

    public RealtimeSnapshot GetCurrentSnapshot(TimeSpan elapsed)
    {
        // Read a non-zero temperature early so the shared realtime path can lock the PLC float word order
        // before zero-capable fields such as gas concentrations and flow rates are evaluated.
        var orificeTemperature = ReadRangedFloatOrEmpty("OrificeTemperature", OrificeTemperatureRegister, 200, 700);
        var coneTemperature = ReadRangedFloatOrEmpty("ConeTemperature", ConeTemperatureRegister, 0, 1200);
        var sampleTemperature = ReadRangedFloatOrEmpty("SampleTemperature", SampleTemperatureRegister, -50, 1200);

        return new RealtimeSnapshot(
            OrificeFlow: ReadRangedFloatOrEmpty("OrificeFlow", OrificeFlowRegister, 0, 10),
            OrificePressure: ReadRangedFloatOrEmpty("OrificePressure", OrificePressureRegister, -5000, 5000),
            OrificeTemperature: orificeTemperature,
            ConeTemperature: coneTemperature,
            SampleTemperature: sampleTemperature,
            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),
            Absorbance: ReadAbsorbanceOrEmpty(),
            HeatReleaseRate: ReadHeatReleaseRateOrEmpty(),
            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),
            DeviceTestSeconds: ReadInt16OrEmpty(DeviceTestSecondsRegister),
            TestSeconds: ToElapsedSeconds(elapsed),
            TotalSmoke: double.NaN);
    }

    private static int ToElapsedSeconds(TimeSpan elapsed)
    {
        return elapsed >= TimeSpan.Zero
            ? (int)Math.Floor(elapsed.TotalSeconds)
            : -1;
    }

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

        if (!TrySelectRealtimeFloat(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 StabilizeRealtimeValue(registerAddress, value);
    }

    private double ReadAbsorbanceOrEmpty()
    {
        var value = ReadRangedFloatOrEmpty("Absorbance", AbsorbanceRegister, 0, 100);
        if (double.IsFinite(value))
        {
            _lastAbsorbanceValue = value;
            return value;
        }

        return _lastAbsorbanceValue.HasValue
            ? _lastAbsorbanceValue.Value
            : double.NaN;
    }

    private double ReadHeatReleaseRateOrEmpty()
    {
        var value = ReadRangedFloatOrEmpty("HeatReleaseRate", HeatReleaseRateRegister, 0, 5000);
        return double.IsFinite(value)
            ? value * HeatReleaseRateDisplayScale
            : double.NaN;
    }

    private bool TrySelectRealtimeFloat(
        ModbusFloatReadResult result,
        double minimum,
        double maximum,
        out ModbusFloatByteOrder byteOrder,
        out double value,
        out int matchCount)
    {
        var preferredByteOrder = _preferredFloatByteOrder;
        if (!ModbusFloatSelector.TrySelectRangedFloat(
                result,
                minimum,
                maximum,
                out byteOrder,
                out value,
                out matchCount,
                preferredByteOrder))
        {
            return false;
        }

        if (!preferredByteOrder.HasValue
            && IsZeroLike(value)
            && TrySelectSingleNonZeroCandidate(result, minimum, maximum, out var nonZeroByteOrder, out var nonZeroValue))
        {
            byteOrder = nonZeroByteOrder;
            value = nonZeroValue;
        }

        RememberPreferredFloatByteOrder(byteOrder, value);
        return true;
    }

    private static bool TrySelectSingleNonZeroCandidate(
        ModbusFloatReadResult result,
        double minimum,
        double maximum,
        out ModbusFloatByteOrder byteOrder,
        out double value)
    {
        var matches = RealtimeFloatByteOrders
            .Select(candidate => new
            {
                ByteOrder = candidate,
                Value = result.GetValue(candidate)
            })
            .Where(candidate => ModbusFloatSelector.IsInRange(candidate.Value, minimum, maximum)
                && !IsZeroLike(candidate.Value))
            .ToArray();

        if (matches.Length != 1)
        {
            byteOrder = default;
            value = double.NaN;
            return false;
        }

        byteOrder = matches[0].ByteOrder;
        value = matches[0].Value;
        return true;
    }

    private void RememberPreferredFloatByteOrder(ModbusFloatByteOrder byteOrder, double value)
    {
        if (!IsZeroLike(value))
        {
            _preferredFloatByteOrder = byteOrder;
        }
    }

    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 IsZeroLike(value) ? 0 : value;
    }

    private static bool IsZeroLike(double value)
    {
        return double.IsFinite(value) && Math.Abs(value) < RealtimeZeroTolerance;
    }

    private double StabilizeRealtimeValue(int key, double value)
    {
        var normalizedValue = NormalizeRealtimeValue(value);

        if (!_stableRealtimeValues.TryGetValue(key, out var stableValue))
        {
            AcceptStableRealtimeValue(key, normalizedValue);
            return normalizedValue;
        }

        if (IsZeroLike(normalizedValue) && !IsZeroLike(stableValue))
        {
            var pendingCount = _pendingZeroReadCounts.GetValueOrDefault(key) + 1;
            _pendingZeroReadCounts[key] = pendingCount;

            if (pendingCount < ZeroValueStableReadCount)
            {
                return stableValue;
            }
        }

        AcceptStableRealtimeValue(key, normalizedValue);
        return normalizedValue;
    }

    private void AcceptStableRealtimeValue(int key, double value)
    {
        _stableRealtimeValues[key] = value;
        _pendingZeroReadCounts.Remove(key);
    }

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

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

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