using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Windows.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ConeCalorimeter.Services;
using Serilog;

namespace ConeCalorimeter.ViewModels;

public sealed class SmokeDensitySettingsViewModel : PageViewModel
{
    private const ushort AbsorbanceRegister = 120;
    private const double AbsorbanceMinimum = 0;
    private const double AbsorbanceMaximum = 100;
    private const double AbsorbanceStableTolerance = 0.25;
    private const int AbsorbanceStableReadCount = 2;
    private const double AbsorbanceZeroTolerance = 0.000005;
    private const string AbsorbanceFormat = "0.00000";
    private static readonly ModbusFloatByteOrder[] AbsorbanceByteOrders =
    [
        ModbusFloatByteOrder.Abcd,
        ModbusFloatByteOrder.Cdab,
        ModbusFloatByteOrder.Badc,
        ModbusFloatByteOrder.Dcba
    ];

    private readonly Action _closeAction;
    private readonly ITcpDeviceConnectionService _tcpDeviceConnectionService;
    private readonly DispatcherTimer _refreshTimer;
    private readonly HashSet<ushort> _loggedFloatDiagnostics = [];
    private readonly HashSet<ushort> _loggedInvalidFloatDiagnostics = [];
    private readonly HashSet<string> _pressedCalibrationLabels = [];
    private double? _stableAbsorbance;
    private double? _pendingAbsorbance;
    private int _pendingAbsorbanceReadCount;
    private ModbusFloatByteOrder? _absorbanceByteOrder;
    private string _absorbanceText = "";
    private string _lastCalibration = "待校准";

    public SmokeDensitySettingsViewModel(
        Action closeAction,
        ITcpDeviceConnectionService tcpDeviceConnectionService) : base("烟密度设置")
    {
        _closeAction = closeAction;
        _tcpDeviceConnectionService = tcpDeviceConnectionService;
        CloseCommand = new RelayCommand(_closeAction);

        CalibrationActions =
        [
            new SmokeDensityCalibrationViewModel("0%校准"),
            new SmokeDensityCalibrationViewModel("25%校准"),
            new SmokeDensityCalibrationViewModel("50%校准"),
            new SmokeDensityCalibrationViewModel("75%校准"),
            new SmokeDensityCalibrationViewModel("100%校准")
        ];

        RefreshDeviceValues();
        _refreshTimer = new DispatcherTimer
        {
            Interval = TimeSpan.FromSeconds(1)
        };
        _refreshTimer.Tick += (_, _) => RefreshDeviceValues();
        _refreshTimer.Start();
    }

    public ObservableCollection<SmokeDensityCalibrationViewModel> CalibrationActions { get; }

    public IRelayCommand CloseCommand { get; }

    public string AbsorbanceText
    {
        get => _absorbanceText;
        private set => SetProperty(ref _absorbanceText, value);
    }

    public string LastCalibration
    {
        get => _lastCalibration;
        private set => SetProperty(ref _lastCalibration, value);
    }

    public void PressCalibration(string? label)
    {
        if (string.IsNullOrWhiteSpace(label))
        {
            return;
        }

        if (_pressedCalibrationLabels.Contains(label))
        {
            return;
        }

        LastCalibration = label;

        if (!TryGetCalibrationCoil(label, out var coilAddress))
        {
            return;
        }

        if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, true))
        {
            LastCalibration = $"{label}失败";
            Debug.WriteLine($"Smoke density calibration '{label}' press write failed.");
            return;
        }

        _pressedCalibrationLabels.Add(label);
    }

    public void ReleaseCalibration(string? label)
    {
        if (string.IsNullOrWhiteSpace(label))
        {
            return;
        }

        if (!_pressedCalibrationLabels.Remove(label))
        {
            return;
        }

        if (!TryGetCalibrationCoil(label, out var coilAddress))
        {
            return;
        }

        if (!_tcpDeviceConnectionService.TryWriteCoil(coilAddress, false))
        {
            LastCalibration = $"{label}复位失败";
            Debug.WriteLine($"Smoke density calibration '{label}' release write failed.");
        }
    }

    private void RefreshDeviceValues()
    {
        if (!TryReadAbsorbanceValue(out var value))
        {
            if (!_stableAbsorbance.HasValue)
            {
                AbsorbanceText = string.Empty;
            }

            return;
        }

        if (TryUpdateStableAbsorbance(value, out var stableValue))
        {
            AbsorbanceText = FormatAbsorbance(stableValue);
        }
        else if (_stableAbsorbance.HasValue)
        {
            AbsorbanceText = FormatAbsorbance(_stableAbsorbance.Value);
        }
    }

    private bool TryReadAbsorbanceValue(out double value)
    {
        value = double.NaN;

        if (!_tcpDeviceConnectionService.TryReadFloatValues(AbsorbanceRegister, out var result))
        {
            return false;
        }

        var preferredByteOrder = _absorbanceByteOrder;
        if (!ModbusFloatSelector.TrySelectRangedFloat(
                result,
                AbsorbanceMinimum,
                AbsorbanceMaximum,
                out var byteOrder,
                out var selectedValue,
                out var matchCount,
                preferredByteOrder))
        {
            if (!TrySelectSingleNonZeroAbsorbanceCandidate(result, out byteOrder, out selectedValue))
            {
                LogFloatDiagnostic(result, null, matchCount, preferredByteOrder);
                return false;
            }
        }

        if (!preferredByteOrder.HasValue
            && IsZeroLikeAbsorbance(selectedValue)
            && TrySelectSingleNonZeroAbsorbanceCandidate(result, out var nonZeroByteOrder, out var nonZeroValue))
        {
            byteOrder = nonZeroByteOrder;
            selectedValue = nonZeroValue;
        }

        LogFloatDiagnostic(result, byteOrder, matchCount, preferredByteOrder);
        RememberAbsorbanceByteOrder(byteOrder, selectedValue);
        value = NormalizeDisplayValue(selectedValue);
        return true;
    }

    private bool TryUpdateStableAbsorbance(double value, out double stableValue)
    {
        value = NormalizeDisplayValue(value);

        if (!_stableAbsorbance.HasValue)
        {
            AcceptStableAbsorbance(value);
            stableValue = value;
            return true;
        }

        stableValue = _stableAbsorbance.Value;

        if (Math.Abs(value - _stableAbsorbance.Value) <= AbsorbanceStableTolerance)
        {
            AcceptStableAbsorbance(value);
            stableValue = value;
            return true;
        }

        if (!_pendingAbsorbance.HasValue
            || Math.Abs(value - _pendingAbsorbance.Value) > AbsorbanceStableTolerance)
        {
            _pendingAbsorbance = value;
            _pendingAbsorbanceReadCount = 1;
            return false;
        }

        _pendingAbsorbanceReadCount++;

        if (_pendingAbsorbanceReadCount < AbsorbanceStableReadCount)
        {
            return false;
        }

        AcceptStableAbsorbance(value);
        stableValue = value;
        return true;
    }

    private void AcceptStableAbsorbance(double value)
    {
        _stableAbsorbance = value;
        _pendingAbsorbance = null;
        _pendingAbsorbanceReadCount = 0;
    }

    private static bool TrySelectSingleNonZeroAbsorbanceCandidate(
        ModbusFloatReadResult result,
        out ModbusFloatByteOrder byteOrder,
        out double value)
    {
        var matches = AbsorbanceByteOrders
            .Select(candidate => new
            {
                ByteOrder = candidate,
                Value = result.GetValue(candidate)
            })
            .Where(candidate => ModbusFloatSelector.IsInRange(
                    candidate.Value,
                    AbsorbanceMinimum,
                    AbsorbanceMaximum)
                && !IsZeroLikeAbsorbance(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 RememberAbsorbanceByteOrder(ModbusFloatByteOrder byteOrder, double value)
    {
        if (!IsZeroLikeAbsorbance(value))
        {
            _absorbanceByteOrder = byteOrder;
        }
    }

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

        var selectedText = selectedByteOrder?.ToString() ?? "none";
        var preferredText = preferredByteOrder?.ToString() ?? "none";
        var message =
            $"Smoke density absorbance D{AbsorbanceRegister} raw [{result.RawHex}], "
            + $"ABCD={result.Abcd:G9}, CDAB={result.Cdab:G9}, "
            + $"BADC={result.Badc:G9}, DCBA={result.Dcba:G9}, "
            + $"selected={selectedText}, preferred={preferredText}, matches={matchCount}.";

        Debug.WriteLine(message);
        Log.Debug(message);
    }

    private static double NormalizeDisplayValue(double value)
    {
        return IsZeroLikeAbsorbance(value) ? 0 : value;
    }

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

    private static string FormatAbsorbance(double value)
    {
        return value.ToString(AbsorbanceFormat, CultureInfo.InvariantCulture);
    }

    private static bool TryGetCalibrationCoil(string label, out ushort coilAddress)
    {
        switch (label)
        {
            case "0%校准":
                coilAddress = 28;
                return true;
            case "25%校准":
                coilAddress = 26;
                return true;
            case "50%校准":
                coilAddress = 24;
                return true;
            case "75%校准":
                coilAddress = 22;
                return true;
            case "100%校准":
                coilAddress = 20;
                return true;
            default:
                coilAddress = 0;
                return false;
        }
    }
}
