using Common.Attributes;
using NModbus;
using NModbus.Serial;
using System;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;

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

        private static SerialPort CreatePort(
            string portName,
            int baudRate,
            int dataBits,
            StopBits stopBits,
            Parity parity,
            int readTimeout,
            int writeTimeout)
        {
            return new SerialPort(portName, baudRate, parity, dataBits, stopBits)
            {
                ReadTimeout = readTimeout > 0 ? readTimeout : SerialPort.InfiniteTimeout,
                WriteTimeout = writeTimeout > 0 ? writeTimeout : SerialPort.InfiniteTimeout
            };
        }

        private static IModbusMaster CreateMaster(SerialPort port)
        {
            return new ModbusFactory().CreateRtuMaster(port);
        }

        #region Holding Register

        public static async Task<ushort[]> ReadHoldingRegistersAsync(
            string portName,
            int baudRate,
            byte slaveAddress,
            ushort startAddress,
            ushort numberOfPoints,
            int dataBits = 8,
            StopBits stopBits = StopBits.One,
            Parity parity = Parity.None,
            int readTimeout = 3000,
            int writeTimeout = 3000,
            CancellationToken ct = default)
        {
            await _commLock.WaitAsync(ct);
            try
            {
                using var port = CreatePort(
                    portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);

                port.Open();

                var master = CreateMaster(port);

                using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
                if (readTimeout > 0)
                    cts.CancelAfter(readTimeout);

                return await master
                    .ReadHoldingRegistersAsync(slaveAddress, startAddress, numberOfPoints)
                    .WaitAsync(TimeSpan.FromMilliseconds(readTimeout),cts.Token);
            }
            finally
            {
                _commLock.Release();
            }
        }

        public static async Task WriteSingleRegisterAsync(
            string portName,
            int baudRate,
            byte slaveAddress,
            ushort registerAddress,
            ushort value,
            int dataBits = 8,
            StopBits stopBits = StopBits.One,
            Parity parity = Parity.None,
            int readTimeout = 3000,
            int writeTimeout = 3000,
            CancellationToken ct = default)
        {
            await _commLock.WaitAsync(ct);
            try
            {
                using var port = CreatePort(
                    portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);

                port.Open();

                var master = CreateMaster(port);

                using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
                if (writeTimeout > 0)
                    cts.CancelAfter(writeTimeout);

                await master
                    .WriteSingleRegisterAsync(slaveAddress, registerAddress, value)
                    .WaitAsync(TimeSpan.FromMilliseconds(writeTimeout),cts.Token);
            }
            finally
            {
                _commLock.Release();
            }
        }

        #endregion

        #region Coil

        public static async Task<bool[]> ReadCoilsAsync(
            string portName,
            int baudRate,
            byte slaveAddress,
            ushort startAddress,
            ushort numberOfPoints,
            int dataBits = 8,
            StopBits stopBits = StopBits.One,
            Parity parity = Parity.None,
            int readTimeout = 3000,
            int writeTimeout = 3000,
            CancellationToken ct = default)
        {
            await _commLock.WaitAsync(ct);
            try
            {
                using var port = CreatePort(
                    portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);

                port.Open();

                var master = CreateMaster(port);

                using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
                if (readTimeout > 0)
                    cts.CancelAfter(readTimeout);

                return await master
                    .ReadCoilsAsync(slaveAddress, startAddress, numberOfPoints)
                    .WaitAsync(TimeSpan.FromMilliseconds(readTimeout),cts.Token);
            }
            finally
            {
                _commLock.Release();
            }
        }

        public static async Task WriteSingleCoilAsync(
            string portName,
            int baudRate,
            byte slaveAddress,
            ushort coilAddress,
            bool value,
            int dataBits = 8,
            StopBits stopBits = StopBits.One,
            Parity parity = Parity.None,
            int readTimeout = 3000,
            int writeTimeout = 3000,
            CancellationToken ct = default)
        {
            await _commLock.WaitAsync(ct);
            try
            {
                using var port = CreatePort(
                    portName, baudRate, dataBits, stopBits, parity, readTimeout, writeTimeout);

                port.Open();

                var master = CreateMaster(port);

                using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
                if (writeTimeout > 0)
                    cts.CancelAfter(writeTimeout);

                await master
                    .WriteSingleCoilAsync(slaveAddress, coilAddress, value)
                    .WaitAsync(TimeSpan.FromMilliseconds(writeTimeout), cts.Token);
            }
            finally
            {
                _commLock.Release();
            }
        }

        #endregion
    }
}
