using System;
using System.Buffers;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace EonaCat.Logger.Servers.Udp
{
    // This file is part of the EonaCat project(s) which is released under the Apache License.
    // See the LICENSE file or go to https://EonaCat.com/License for full license details.

    public class Udp : IDisposable
    {
        private const int MaxUdpPacketSize = 4096;
        private string _hostname = "127.0.0.1";
        private int _port = 514;
        private UdpClient _udp;
        private readonly SemaphoreSlim _sendLock = new(1, 1);
        private bool _disposed;

        public event EventHandler<Exception> OnException;

        public bool IsConnected { get; private set; }
        public string Nickname { get; set; }
        public string IpPort => $"{_hostname}:{_port}";
        public List<ELogType> TypesToLog { get; set; }

        public Udp(string hostname, int port, string nickname = null, List<ELogType> typesToLog = null)
        {
            _hostname = hostname ?? throw new ArgumentNullException(nameof(hostname));
            _port = port >= 0 ? port : throw new ArgumentException("Port must be zero or greater.");
            TypesToLog = typesToLog;
            Nickname = nickname ?? IpPort;

            SetUdp();
        }

        public string Hostname
        {
            get => _hostname;
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new ArgumentNullException(nameof(Hostname));
                }

                _hostname = value;
                SetUdp();
            }
        }

        public int Port
        {
            get => _port;
            set
            {
                if (value < 0)
                {
                    throw new ArgumentException("Port must be zero or greater.");
                }

                _port = value;
                SetUdp();
            }
        }

        internal void SetUdp()
        {
            if (_disposed)
            {
                return;
            }

            try
            {
                DisposeUdp();
                _udp = new UdpClient(_hostname, _port)
                {
                    DontFragment = false
                };
                IsConnected = true;
            }
            catch
            {
                IsConnected = false;
            }
        }

        internal void DisposeUdp()
        {
            IsConnected = false;

            try
            {
                _udp?.Close();
                _udp?.Dispose();
            }
            catch
            {
                // Silently ignore
            }
            finally
            {
                _udp = null;
            }
        }

        public async Task WriteAsync(byte[] data, bool dontFragment = false)
        {
            if (_disposed || data == null || data.Length == 0)
            {
                return;
            }

            await _sendLock.WaitAsync();
            try
            {
                if (!IsConnected)
                {
                    SetUdp();
                }

                if (!IsConnected || _udp == null)
                {
                    return;
                }

                _udp.DontFragment = dontFragment;
                await SendChunksAsync(data);
            }
            finally
            {
                _sendLock.Release();
            }
        }

        public async Task WriteAsync(string data, bool dontFragment = false)
        {
            if (_disposed || string.IsNullOrWhiteSpace(data))
            {
                return;
            }

            var bytes = Encoding.UTF8.GetBytes(data);
            await WriteAsync(bytes, dontFragment);
        }

        private async Task SendChunksAsync(byte[] data)
        {
            if (_disposed || _udp == null)
            {
                return;
            }

            int offset = 0;
            int length = data.Length;
            byte[] buffer = ArrayPool<byte>.Shared.Rent(MaxUdpPacketSize);

            try
            {
                while (offset < length)
                {
                    int chunkSize = Math.Min(MaxUdpPacketSize, length - offset);
                    Buffer.BlockCopy(data, offset, buffer, 0, chunkSize);
                    await _udp.SendAsync(buffer, chunkSize);
                    offset += chunkSize;
                }
            }
            catch (Exception ex)
            {
                IsConnected = false;
                OnException?.Invoke(this, ex);
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(buffer);
            }
        }

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

        protected virtual void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            _disposed = true;

            if (disposing)
            {
                DisposeUdp();
                _sendLock.Dispose();
            }
        }

        ~Udp()
        {
            Dispose(false);
        }
    }
}
