using Common.Attributes;
using NModbus;
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace DeviceCommand.Flexible
{
    [ACPCommand]
    public static class FModbusTCP
    {
        private static readonly SemaphoreSlim _commLock = new(1, 1);

        private static  async Task<IModbusMaster> ConnectAsync(string ipAddress, int port, int sendTimeout, int receiveTimeout, CancellationToken ct)
        {
            var tcpClient = new TcpClient();
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            if (sendTimeout > 0)
                cts.CancelAfter(sendTimeout);

            await tcpClient.ConnectAsync(ipAddress, port, cts.Token);
            return new ModbusFactory().CreateMaster(tcpClient);
        }

        #region Holding Registers

        public static async Task<ushort[]> ReadHoldingRegistersAsync(
            string ipAddress,
            int port,
            byte slaveAddress,
            ushort startAddress,
            ushort numberOfPoints,
            int sendTimeout = 3000,
            int receiveTimeout = 3000,
            CancellationToken ct = default)
        {
            await _commLock.WaitAsync(ct);
            try
            {
                using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
                var result = await ((IModbusMaster)master)
                    .ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints)
                    .WaitAsync(TimeSpan.FromMilliseconds(receiveTimeout), ct);
                return result;
            }
            finally
            {
                _commLock.Release();
            }
        }

        public static async Task WriteSingleRegisterAsync(
            string ipAddress,
            int port,
            byte slaveAddress,
            ushort registerAddress,
            ushort value,
            int sendTimeout = 3000,
            int receiveTimeout = 3000,
            CancellationToken ct = default)
        {
            await _commLock.WaitAsync(ct);
            try
            {
                using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
                await ((IModbusMaster)master)
                    .WriteSingleRegisterAsync(slaveAddress, registerAddress, value)
                    .WaitAsync(TimeSpan.FromMilliseconds(sendTimeout),ct);
            }
            finally
            {
                _commLock.Release();
            }
        }

        #endregion

        #region Coils

        public static async Task<bool[]> ReadCoilsAsync(
            string ipAddress,
            int port,
            byte slaveAddress,
            ushort startAddress,
            ushort numberOfPoints,
            int sendTimeout = 3000,
            int receiveTimeout = 3000,
            CancellationToken ct = default)
        {
            await _commLock.WaitAsync(ct);
            try
            {
                using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
                var result = await ((IModbusMaster)master)
                    .ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints)
                    .WaitAsync(TimeSpan.FromMilliseconds(receiveTimeout), ct);
                return result;
            }
            finally
            {
                _commLock.Release();
            }
        }

        public static async Task WriteSingleCoilAsync(
            string ipAddress,
            int port,
            byte slaveAddress,
            ushort coilAddress,
            bool value,
            int sendTimeout = 3000,
            int receiveTimeout = 3000,
            CancellationToken ct = default)
        {
            await _commLock.WaitAsync(ct);
            try
            {
                using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
                await ((IModbusMaster)master)
                    .WriteSingleCoilAsync(slaveAddress, coilAddress, value)
                    .WaitAsync(TimeSpan.FromMilliseconds(sendTimeout),ct);
            }
            finally
            {
                _commLock.Release();
            }
        }

        #endregion
    }
}
