using TwinCAT;
using TwinCAT.Ads;
using TwinCAT.Ads.TypeSystem;

namespace PLCIOPointTool.Services;

public class AdsClientService : IClientService
{
    private readonly ILogService _logService;

    private readonly AdsClient _adsClient;
    private readonly Timer _timer;
    private bool _previousCheckState = false;

    private bool disposedValue;

    public AdsClientService(ILogService logService)
    {
        _logService = logService;

        _adsClient = new AdsClient();
        _adsClient.ConnectionStateChanged += OnAdsClientConnectionStateChanged;
        _timer = new Timer(OnHealthCheck, null, Timeout.Infinite, Timeout.Infinite);
    }

    public event EventHandler<ConnectionState>? ConnectionStateChanged;

    public event EventHandler<bool>? HealthCheckingChanged;

    public async Task ConnectAsync(string netId, int port)
    {
        if (string.IsNullOrWhiteSpace(netId) || port < 0)
        {
            _logService.Warning("Invalid connection parameters");
            return;
        }

        try
        {
            await _adsClient.ConnectAsync(new AmsAddress(netId, port), new CancellationToken());
            if (_adsClient.TryReadState(out _) != AdsErrorCode.NoError)
            {
                OnAdsClientConnectionStateChanged(
                    null,
                    new ConnectionStateChangedEventArgs(
                        ConnectionStateChangedReason.Established,
                        ConnectionState.Disconnected,
                        ConnectionState.Disconnected));
                _logService.Warning("Failed to connect ADS server, please try later");
                return;
            }

            _logService.Info("Succeeded to connect ADS server");
            _timer.Change(3000, 3000);
        }
        catch (Exception ex)
        {
            _logService.Error($"Got an exception while connecting; {ex}");
        }
    }

    public async Task DisconnectAsync()
    {
        try
        {
            await _adsClient.DisconnectAsync(new CancellationToken());
            _logService.Info("Succeeded to disconnect ADS server");
        }
        catch (Exception ex)
        {
            _logService.Error($"Got an exception while disconnecting; {ex}");
        }
        finally
        {
            _timer.Change(Timeout.Infinite, Timeout.Infinite);
        }
    }

    public async Task<object?> ReadValue(string symbolName)
    {
        if (string.IsNullOrWhiteSpace(symbolName))
        {
            _logService.Warning("Invalid variable path");
            return null;
        }

        try
        {
            var symbol = await _adsClient.ReadSymbolAsync(symbolName, new CancellationToken());
            if (symbol is null || symbol.Value is null || symbol.Succeeded == false)
            {
                _logService.Warning($"Failed to get symbol of {symbolName}");
                return null;
            }

            var symbolValue = await _adsClient.ReadValueAsync(symbol.Value, new CancellationToken());
            if (symbolValue is null || symbolValue.Value is null || symbolValue.Succeeded == false)
            {
                _logService.Warning($"Failed to get value of {symbolName}");
                return null;
            }

            _logService.Info($"Succeeded to get value of {symbolName}");
            return symbolValue.Value;
        }
        catch (Exception ex)
        {
            _logService.Error($"Got an exception while reading; {ex}");
            return null;

        }
    }

    public async Task WriteValue(string symbolName, object value)
    {
        if (string.IsNullOrWhiteSpace(symbolName) || value is null)
        {
            _logService.Warning("Invalid variable path or value");
            return;
        }

        try
        {
            var resultValue = await _adsClient.WriteSymbolAsync(symbolName, value, new CancellationToken());
            if (resultValue is null || resultValue.Succeeded == false)
            {
                _logService.Warning($"Failed to set value of {symbolName}");
                return;
            }

            _logService.Info($"Succeeded to set value of {symbolName}");
        }
        catch (Exception ex)
        {
            _logService.Error($"Got an exception while reading; {ex}");
        }
    }

    private void OnAdsClientConnectionStateChanged(object? sender, TwinCAT.ConnectionStateChangedEventArgs e)
    {
        try
        {
            ConnectionStateChanged?.Invoke(null, e.NewState);
        }
        catch (Exception ex)
        {
            _logService.Error($"Got an exception; {ex}");
        }
    }

    private void OnHealthCheck(object? state)
    {
        var errorCode = _adsClient.TryReadState(out _);
        if (errorCode != AdsErrorCode.NoError)
        {
            _logService.Warning("Health Checking Failed");
            if (_previousCheckState == true)
            {
                _previousCheckState = false;
                HealthCheckingChanged?.Invoke(null, false);
            }
            return;
        }

        if (_previousCheckState == false)
        {
            _previousCheckState = true;
            HealthCheckingChanged?.Invoke(null, true);
        }

        //_logService.Info("Health Checking succeeded");
    }

    #region Dispose
    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects)
            }

            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
            // TODO: set large fields to null
            _timer.Change(Timeout.Infinite, Timeout.Infinite);
            _timer.Dispose();
            _adsClient.Dispose();
            _adsClient.ConnectionStateChanged -= OnAdsClientConnectionStateChanged;

            disposedValue = true;
        }
    }

    // // TODO: override finalizer only if 'Dispose(bool disposing)' has code to free unmanaged resources
    // ~AdsClientService()
    // {
    //     // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
    //     Dispose(disposing: false);
    // }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
    #endregion
}
