using Newtonsoft.Json.Linq;
using System.Data;
using System.Net;

namespace Pilz.Net;

public abstract class ConnectionManagerBase(int port)
{
    private const int HEADER_LENGTH = 12;
    
    private bool listening = false;
    private readonly Dictionary<int, Dictionary<int, byte[]>> dicData = [];

    public int Port { get; private set; } = port;
    public bool UseAssemblyQualifiedName { get; set; } = false;

    public event RetriveDataEventHandler? RetriveData;

    public delegate void RetriveDataEventHandler(ConnectionManagerBase manager, string senderIP, string cmd, object content);

    public bool IsListening
    {
        get => listening;
        protected set => listening = value;
    }

    ~ConnectionManagerBase()
    {
        Stop();
    }

    public abstract void Start();
    public abstract void Stop();
    protected abstract void SendData(IPEndPoint endPoint, byte[] data);
    protected abstract int GetBufferSize();

    public virtual void Send(string empfängerIP, string cmd)
    {
        Send(empfängerIP, cmd, string.Empty);
    }

    public virtual void Send(string empfängerIP, string cmd, string info)
    {
        Send(empfängerIP, cmd, (object)info);
    }

    private void RaiseRetriveData(string senderIP, string cmd, object content)
    {
        RetriveData?.Invoke(this, senderIP, cmd, content);
    }

    protected void ProcessRetrivedData(string senderIP, byte[] buf)
    {
        int readInteger(int index) => buf[index] << 24 | buf[index + 1] << 16 | buf[index + 2] << 8 | buf[index + 3];

        int dataID = readInteger(0);
        int packageID = readInteger(4);
        int packageCount = readInteger(8);
        bool resolveData = true;

        // Remember data
        byte[] data = buf.Skip(HEADER_LENGTH).ToArray();
        Dictionary<int, byte[]> dicMyData;

        if (dicData.ContainsKey(dataID))
        {
            dicMyData = dicData[dataID];
            if (dicMyData.ContainsKey(packageID))
                dicMyData[packageID] = data;
            else
                dicMyData.Add(packageID, data);
        }
        else
        {
            dicMyData = new Dictionary<int, byte[]>() { { packageID, data } };
            dicData.Add(dataID, dicMyData);
        }

        if (dicMyData.Count < packageCount)
            resolveData = false;

        // Resolve Data
        if (resolveData)
        {
            dicMyData ??= dicData[dataID];
            var myData = new List<byte>();
            foreach (var kvp in dicMyData.OrderBy(n => n.Key))
                myData.AddRange(kvp.Value);
            dicMyData.Remove(dataID);

            object content = null;
            string cmd = string.Empty;
            try
            {
                var res = DecodeFromBytes(myData.ToArray());
                cmd = res.cmd;
                content = res.content;
            }
            catch (Exception)
            {
            }

            RaiseRetriveData(senderIP, cmd, content);
        }
    }
    private Random _Send_rnd = new();

    public void Send(string empfängerIP, string cmd, object content)
    {
        var ep = new IPEndPoint(NetworkFeatures.GetIPFromHost(empfängerIP).MapToIPv4(), Port);
        var finalBuffer = new List<byte>();
        int maxBufferSize = GetBufferSize();
        int maxDataSize = maxBufferSize - HEADER_LENGTH;
        byte[] data = EncodeToBytes(cmd, content, UseAssemblyQualifiedName);
        int dataID = _Send_rnd.Next();

        // Some methods for later user
        void send() => SendData(ep, finalBuffer.ToArray());
        void addInteger(int value)
        {
            finalBuffer.Add((byte)(value >> 24 & 0xFF));
            finalBuffer.Add((byte)(value >> 16 & 0xFF));
            finalBuffer.Add((byte)(value >> 8 & 0xFF));
            finalBuffer.Add((byte)(value & 0xFF));
        };
        void addHeader(int packageID, int packagesCount)
        {
            addInteger(dataID); // Data ID
            addInteger(packageID); // Package ID
            addInteger(packagesCount); // Packages Count
        };

        // Send data (this if statement and else content might be useless)
        if (data.Length > maxDataSize)
        {
            int curIndex = 0;
            int curID = 0;
            int packagesCount = (int)Math.Round(Math.Ceiling(data.Length / (double)maxDataSize));

            while (curIndex < data.Length)
            {
                finalBuffer.Clear();
                addHeader(curID, packagesCount);
                for (int i = 1, loopTo = maxDataSize; i <= loopTo; i++)
                {
                    if (curIndex < data.Length)
                    {
                        finalBuffer.Add(data[curIndex]);
                        curIndex += 1;
                    }
                }
                send();
                curID += 1;
            }
        }
        else
        {
            addHeader(0, 1);
            finalBuffer.AddRange(data);
            send();
        }
    }

    private static byte[] EncodeToBytes(string cmd, object content, bool useAssemblyQualifiedName)
    {
        var ms = new MemoryStream();
        var bw = new BinaryWriter(ms);
        var obj = new JObject
        {
            // Write header
            ["Cmd"] = cmd,
            ["ContentType"] = useAssemblyQualifiedName ? (content?.GetType()?.AssemblyQualifiedName) : (content?.GetType()?.ToString()),

            // Content
            ["Content"] = JToken.FromObject(content)
        };

        // Write Json to MemoryStream
        bw.Write(System.Text.Encoding.Default.GetBytes(obj.ToString()));

        // Get Buffer Bytes
        byte[] buf = ms.ToArray();
        ms.Close();

        return buf;
    }

    private static (string cmd, object content) DecodeFromBytes(byte[] buf)
    {
        string contentstring = System.Text.Encoding.Default.GetString(buf);
        object content = null;
        var contentobj = JObject.Parse(contentstring);

        string cmd = (string)contentobj["Cmd"];
        string contenttypestring = (string)contentobj["ContentType"];
        var contentlinq = contentobj["Content"];

        if (!string.IsNullOrEmpty(contenttypestring))
        {
            var contenttype = Type.GetType(contenttypestring);
            content = contentlinq.ToObject(contenttype);
        }

        return (cmd, content);
    }
}