using Log;
using ProtocalGeneral;
using System.Collections.Concurrent;
using TwinCAT.Ads.SumCommand;
using TwinCAT.TypeSystem;

namespace AdsCommunicatorNet8;

//this Implementation has removed notification function, only get value and set value available
public class AdsCommunicator(ILog log) : IDisposable
{
    private bool _isConnected;
    private string? _netID = null;
    private int _port = 0;


    private AdsClient? _adsClient;
    private List<string[]>? _adsCsvInfo;
    private ITcpConnectNority? _connectionNotify;
    private bool disposedValue;
    private readonly ConcurrentDictionary<string, IAdsSymbol> _symbols = [];


    public bool IsConnected { get { return _isConnected; } }

    public bool Initialize(ITcpConnectNority connectionNotify)
    {
        if (this._adsClient is not null)
            return false;


        this._adsClient = new AdsClient();
        this._adsCsvInfo = [];
        this._connectionNotify = connectionNotify;

        return true;
    }

    public bool Open(string netID, int port, int retry=10)
    {
        log?.Info($"Try Open {netID} {port}");
        if (this._adsClient is null)
        {
            log?.Warning("Not initialized before Open");
            return false;
        }

        if (this._adsClient.IsConnected)
        {
            log?.Warning($"Ads {netID} {port} already connected");
            return false;
        }

        this._adsClient.ConnectionStateChanged += AdsClient_ConnectionStateChanged;


        for (int i = 1; i <= retry; i++)
        {
            log?.Info($"Try Connected {i}");

            try
            {
                this._adsClient.Connect(netID, port);
                if (this._adsClient.TryReadState(out _) == AdsErrorCode.NoError)
                    break;
            }
            catch
            {

            }

            if (i == retry)
            {
                log?.Error($"Reach Max Retry {i}, connect failed");
                return false;
            }
            Thread.Sleep(300);

        }

        this._netID = netID;
        this._port = port;

        log?.Info($"Connect success");
        this._isConnected = true;
        return true;
    }

    public bool ReConnect()
    {
        if (string.IsNullOrEmpty(this._netID) || this._port == 0)
            return false;
        this._adsClient ??= new();
        if (this._adsClient.IsConnected)
            return true;

        for (; ; )
        {
            log?.Info($"Try ReConnected");

            try
            {
                this._adsClient.Connect(_netID, _port);
                if (this._adsClient.TryReadState(out _) == AdsErrorCode.NoError)
                    break;
            }
            catch
            {
                Thread.Sleep(300);
            }
        }

        return true;
    }

    public bool GetValue<T>(string symbolName, out T? value)
    {
        value = default;

        if (string.IsNullOrEmpty(symbolName))
        {
            log?.Warning("Symbol Empty");
            return false;
        }

        log?.Info($"Try get value {symbolName}");

        if (this._adsClient is null)
        {
            log?.Error("PLC not connected");
            return false;
        }

        if (!this._symbols.TryGetValue(symbolName, out IAdsSymbol? adsSymbol) || adsSymbol is null)
        {
            log?.Info($"{symbolName} not Created before try create symbol");
            if (this._adsClient.TryReadSymbol(symbolName, out adsSymbol) != AdsErrorCode.NoError || adsSymbol is null)
            {
                log?.Error($"{symbolName} not exist in PLC");
                return false;
            }

            this._symbols[symbolName] = adsSymbol;
        }

        try
        {
            value = (T)this._adsClient.ReadValue(adsSymbol);
            log?.Info($"{symbolName} value {value}");
        }
        catch (Exception ex)
        {
            log?.Error($"{symbolName} Read failed: {ex.Message}");
            return false;
        }

        return true;
    }

    public bool SetValue<T>(string symbolName, T value) where T : notnull
    {
        if (string.IsNullOrEmpty(symbolName))
        {
            log?.Warning("Set Value Symbol empty");
            return false;
        }

        log?.Info($"Try set value {symbolName}");

        if (this._adsClient is null)
        {
            log?.Error("PLC not connected");
            return false;
        }

        if (value is null)
        {
            log?.Warning("Value Empty");
            return false;
        }

        log?.Info($"Value {value}");

        if (!this._symbols.TryGetValue(symbolName, out IAdsSymbol? adsSymbol) || adsSymbol is null)
        {
            log?.Info($"{symbolName} not Created before try create symbol");
            if (this._adsClient.TryReadSymbol(symbolName, out adsSymbol) != AdsErrorCode.NoError || adsSymbol is null)
            {
                log?.Error($"{symbolName} not exist in PLC");
                return false;
            }

            this._symbols[symbolName] = adsSymbol;
        }

        try
        {
            this._adsClient.WriteValue(adsSymbol, value);
            log?.Info($"Set {symbolName} value {value} succeeded");
        }
        catch (Exception ex)
        {
            log?.Error($"{symbolName} Set failed: {ex.Message}");
            return false;
        }

        return true;
    }

    public bool SetBatchValue(IEnumerable<string> symbolCollection, IList<object> valueCollection)
    {
        if (this._adsClient is null)
            return false;

        if (symbolCollection is null || valueCollection is null)
            return false;

        if (symbolCollection.Count() != symbolCollection.Count())
            return false;

        List<ISymbol> symbols = [];
        foreach (var item in symbolCollection)
        {
            if (!this._symbols.TryGetValue(item, out IAdsSymbol? symbol) || symbol is null)
            {
                log?.Warning($"BatchSend: {item} not Registered before");
                return false;
            }

            symbols.Add(symbol);
        }

        SumSymbolWrite writes = new(this._adsClient, symbols);
        try
        {
            writes.Write([.. valueCollection]);
        }
        catch (Exception ex)
        {
            log?.Warning(ex.Message);
            return false;
        }

        return true;
    }

    private void AdsClient_ConnectionStateChanged(object? sender, TwinCAT.ConnectionStateChangedEventArgs e)
    {
        if (this._adsClient is null)
            return;

        log?.Info($"{e.NewState} {this._adsClient.Address.NetId} {this._adsClient.Address.Port}");

        switch (e.NewState)
        {
            case TwinCAT.ConnectionState.Connected:
                this._isConnected = true;
                this._connectionNotify?.Connect(this._adsClient.Address.NetId.ToString(), this._adsClient.Address.Port);
                break;
            case TwinCAT.ConnectionState.Disconnected:
            case TwinCAT.ConnectionState.Lost:
            default:
                this._isConnected = false;
                this._connectionNotify?.DisConnect(this._adsClient.Address.NetId.ToString(), this._adsClient.Address.Port);
                break;
        }
    }


    #region Dispose
    protected virtual void Dispose(bool disposing)
    {
        log?.Info("Disposing AdsCommunication");
        if (!disposedValue)
        {
            if (disposing)
            {
                this._adsClient?.Dispose();
                this._adsClient = null;
                this._adsCsvInfo?.Clear();
            }

            disposedValue = true;
        }
    }

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    #endregion
}