using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Horizon.Common;
using Ryujinx.Horizon.LogManager.Types;
using Ryujinx.Horizon.Sdk.Lm;
using Ryujinx.Horizon.Sdk.Sf;
using Ryujinx.Horizon.Sdk.Sf.Hipc;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;

namespace Ryujinx.Horizon.LogManager.Ipc
{
    partial class LmLogger : ILmLogger
    {
        private const int MessageLengthLimit = 5000;

        private readonly LogService _log;
        private readonly ulong _pid;

        private LogPacket _logPacket;

        public LmLogger(LogService log, ulong pid)
        {
            _log = log;
            _pid = pid;

            _logPacket = new LogPacket();
        }

        [CmifCommand(0)]
        public Result Log([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] Span<byte> message)
        {
            if (!SetProcessId(message, _pid))
            {
                return Result.Success;
            }

            if (LogImpl(message))
            {
                Logger.Guest?.Print(LogClass.ServiceLm, _logPacket.ToString());

                _logPacket = new LogPacket();
            }

            return Result.Success;
        }

        [CmifCommand(1)] // 3.0.0+
        public Result SetDestination(LogDestination destination)
        {
            _log.LogDestination = destination;

            return Result.Success;
        }

        private static bool SetProcessId(Span<byte> message, ulong processId)
        {
            ref LogPacketHeader header = ref MemoryMarshal.Cast<byte, LogPacketHeader>(message)[0];

            uint expectedMessageSize = (uint)Unsafe.SizeOf<LogPacketHeader>() + header.PayloadSize;
            if (expectedMessageSize != (uint)message.Length)
            {
                Logger.Warning?.Print(LogClass.ServiceLm, $"Invalid message size (expected 0x{expectedMessageSize:X} but got 0x{message.Length:X}).");

                return false;
            }

            header.ProcessId = processId;

            return true;
        }

        private bool LogImpl(ReadOnlySpan<byte> message)
        {
            SpanReader reader = new(message);

            if (!reader.TryRead(out LogPacketHeader header))
            {
                return true;
            }

            bool isHeadPacket = (header.Flags & LogPacketFlags.IsHead) != 0;
            bool isTailPacket = (header.Flags & LogPacketFlags.IsTail) != 0;

            _logPacket.Severity = header.Severity;

            while (reader.Length > 0)
            {
                if (!TryReadUleb128(ref reader, out int type) || !TryReadUleb128(ref reader, out int size))
                {
                    return true;
                }

                LogDataChunkKey key = (LogDataChunkKey)type;

                if (key == LogDataChunkKey.Start)
                {
                    reader.Skip(size);

                    continue;
                }
                else if (key == LogDataChunkKey.Stop)
                {
                    break;
                }
                else if (key == LogDataChunkKey.Line)
                {
                    if (!reader.TryRead<int>(out _logPacket.Line))
                    {
                        return true;
                    }
                }
                else if (key == LogDataChunkKey.DropCount)
                {
                    if (!reader.TryRead<long>(out _logPacket.DropCount))
                    {
                        return true;
                    }
                }
                else if (key == LogDataChunkKey.Time)
                {
                    if (!reader.TryRead<long>(out _logPacket.Time))
                    {
                        return true;
                    }
                }
                else if (key == LogDataChunkKey.Message)
                {
                    string text = Encoding.UTF8.GetString(reader.GetSpanSafe(size)).TrimEnd();

                    if (isHeadPacket && isTailPacket)
                    {
                        _logPacket.Message = text;
                    }
                    else
                    {
                        _logPacket.Message += text;

                        if (_logPacket.Message.Length >= MessageLengthLimit)
                        {
                            isTailPacket = true;
                        }
                    }
                }
                else if (key == LogDataChunkKey.Filename)
                {
                    _logPacket.Filename = Encoding.UTF8.GetString(reader.GetSpanSafe(size)).TrimEnd();
                }
                else if (key == LogDataChunkKey.Function)
                {
                    _logPacket.Function = Encoding.UTF8.GetString(reader.GetSpanSafe(size)).TrimEnd();
                }
                else if (key == LogDataChunkKey.Module)
                {
                    _logPacket.Module = Encoding.UTF8.GetString(reader.GetSpanSafe(size)).TrimEnd();
                }
                else if (key == LogDataChunkKey.Thread)
                {
                    _logPacket.Thread = Encoding.UTF8.GetString(reader.GetSpanSafe(size)).TrimEnd();
                }
                else if (key == LogDataChunkKey.ProgramName)
                {
                    _logPacket.ProgramName = Encoding.UTF8.GetString(reader.GetSpanSafe(size)).TrimEnd();
                }
            }

            return isTailPacket;
        }

        private static bool TryReadUleb128(ref SpanReader reader, out int result)
        {
            result = 0;
            int count = 0;
            byte encoded;

            do
            {
                if (!reader.TryRead<byte>(out encoded))
                {
                    return false;
                }

                result += (encoded & 0x7F) << (7 * count);

                count++;
            } while ((encoded & 0x80) != 0);

            return true;
        }
    }
}