using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;

namespace com.itac.mes.imsapi.protocol
{
    internal sealed class ReferenceComparer : IEqualityComparer<object>
    {
        public static readonly ReferenceComparer Instance = new ReferenceComparer();
        public new bool Equals(object x, object y) => ReferenceEquals(x, y);
        public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
    }

    internal sealed class IhapWriter
    {
        private readonly Stream _stream;
        private readonly IhapProtocolMode _mode;
        private readonly Dictionary<object,int> _objectRefs = new Dictionary<object,int>(ReferenceComparer.Instance);
        private readonly Dictionary<string,int> _stringRefs = new Dictionary<string,int>(StringComparer.Ordinal);

        public IhapWriter(Stream stream, IhapProtocolMode mode) { _stream=stream; _mode=mode; }

        public void WriteCall(string wireMethodName, object[] args)
        {
            WriteRawByte(IhapConstants.PROTOCOL_IDENTIFIER);
            WriteRawByte((byte)_mode);
            WriteRawByte(IhapConstants.RPC_CALL);
            WriteString(wireMethodName);
            WriteInt(args == null ? 0 : args.Length);
            if (args != null) foreach (var arg in args) WriteObject(arg);
        }

        private bool WriteObjectReference(object value)
        {
            int n;
            if (_objectRefs.TryGetValue(value, out n)) { WriteReference(n,false); return true; }
            _objectRefs[value]=_objectRefs.Count; return false;
        }
        private bool WriteStringReference(string value)
        {
            int n;
            if (_stringRefs.TryGetValue(value, out n)) { WriteReference(n,true); return true; }
            _stringRefs[value]=_stringRefs.Count; return false;
        }
        private void WriteReference(int n, bool str)
        {
            if (n < 256) { WriteRawByte(str ? IhapConstants.REF_STRING_1BYTE : IhapConstants.REF_VALUE_1BYTE); WriteRawByte((byte)n); }
            else if (n < 65536) { WriteRawByte(str ? IhapConstants.REF_STRING_2BYTE : IhapConstants.REF_VALUE_2BYTE); WriteRawUInt16((ushort)n); }
            else { WriteRawByte(str ? IhapConstants.REF_STRING_4BYTE : IhapConstants.REF_VALUE_4BYTE); WriteRawInt32(n); }
        }

        public void WriteObject(object value)
        {
            if (value == null) { WriteRawByte(IhapConstants.NULL_VALUE); return; }
            if (value is bool) { WriteRawByte((bool)value ? IhapConstants.BOOL_TRUE : IhapConstants.BOOL_FALSE); return; }
            if (value is byte) { WriteRawByte(IhapConstants.BYTE); WriteRawByte((byte)value); return; }
            if (value is char) { WriteRawByte(IhapConstants.CHAR); WriteRawUInt16((ushort)(char)value); return; }
            if (value is int) { WriteInt((int)value); return; }
            if (value is long) { WriteLong((long)value); return; }
            if (value is short) { WriteShort((short)value); return; }
            if (value is float) { WriteFloat((float)value); return; }
            if (value is double) { WriteDouble((double)value); return; }
            if (value is string) { WriteString((string)value); return; }
            if (value is DateTime) { WriteDate((DateTime)value); return; }
            var a=value as Array;
            if (a != null) { WriteArray(a); return; }
            WriteComplexObject(value);
        }

        public void WriteInt(int v)
        {
            if (v>=IhapConstants.INT_1BYTE_MIN && v<=IhapConstants.INT_1BYTE_MAX) { WriteRawByte((byte)(v+IhapConstants.INT_1BYTE_BASE)); }
            else if (v>=IhapConstants.INT_2BYTE_MIN && v<=IhapConstants.INT_2BYTE_MAX) { WriteRawByte((byte)(IhapConstants.INT_2BYTE_BASE+(v>>8))); WriteRawByte((byte)v); }
            else if (v>=IhapConstants.INT_3BYTE_MIN && v<=IhapConstants.INT_3BYTE_MAX) { WriteRawByte((byte)(IhapConstants.INT_3BYTE_BASE+(v>>16))); WriteRawByte((byte)(v>>8)); WriteRawByte((byte)v); }
            else { WriteRawByte(IhapConstants.INT); WriteRawInt32(v); }
        }
        public void WriteLong(long v)
        {
            if (v>=IhapConstants.LONG_1BYTE_MIN && v<=IhapConstants.LONG_1BYTE_MAX) { WriteRawByte((byte)(v+IhapConstants.LONG_1BYTE_BASE)); }
            else if (v>=IhapConstants.LONG_2BYTE_MIN && v<=IhapConstants.LONG_2BYTE_MAX) { WriteRawByte((byte)(IhapConstants.LONG_2BYTE_BASE+(v>>8))); WriteRawByte((byte)v); }
            else if (v>=IhapConstants.LONG_3BYTE_MIN && v<=IhapConstants.LONG_3BYTE_MAX) { WriteRawByte((byte)(IhapConstants.LONG_3BYTE_BASE+(v>>16))); WriteRawByte((byte)(v>>8)); WriteRawByte((byte)v); }
            else if (v>=int.MinValue && v<=int.MaxValue) { WriteRawByte(IhapConstants.LONG_4BYTE); WriteRawInt32((int)v); }
            else { WriteRawByte(IhapConstants.LONG); WriteRawInt64(v); }
        }
        public void WriteShort(short v) { WriteRawByte(IhapConstants.SHORT); WriteRawUInt16(unchecked((ushort)v)); }
        public void WriteFloat(float v)
        {
            if (!float.IsNaN(v) && !float.IsInfinity(v) && v==Math.Truncate(v) && v>=sbyte.MinValue && v<=sbyte.MaxValue) { WriteRawByte(IhapConstants.FLOAT_1BYTE); WriteRawByte(unchecked((byte)(sbyte)v)); }
            else if (!float.IsNaN(v) && !float.IsInfinity(v) && v==Math.Truncate(v) && v>=short.MinValue && v<=short.MaxValue) { WriteRawByte(IhapConstants.FLOAT_2BYTE); WriteRawUInt16(unchecked((ushort)(short)v)); }
            else { WriteRawByte(IhapConstants.FLOAT); var b=BitConverter.GetBytes(v); if(BitConverter.IsLittleEndian) Array.Reverse(b); _stream.Write(b,0,b.Length); }
        }
        public void WriteDouble(double v)
        {
            if (v==0d) { WriteRawByte(IhapConstants.DOUBLE_0); return; }
            if (v==1d) { WriteRawByte(IhapConstants.DOUBLE_1); return; }
            if (!double.IsNaN(v) && !double.IsInfinity(v) && v==Math.Truncate(v) && v>=sbyte.MinValue && v<=sbyte.MaxValue) { WriteRawByte(IhapConstants.DOUBLE_1BYTE); WriteRawByte(unchecked((byte)(sbyte)v)); return; }
            if (!double.IsNaN(v) && !double.IsInfinity(v) && v==Math.Truncate(v) && v>=short.MinValue && v<=short.MaxValue) { WriteRawByte(IhapConstants.DOUBLE_2BYTE); WriteRawUInt16(unchecked((ushort)(short)v)); return; }
            float f=(float)v;
            if ((double)f==v) { WriteRawByte(IhapConstants.DOUBLE_4BYTE); var bf=BitConverter.GetBytes(f); if(BitConverter.IsLittleEndian) Array.Reverse(bf); _stream.Write(bf,0,bf.Length); return; }
            WriteRawByte(IhapConstants.DOUBLE); var b=BitConverter.GetBytes(v); if(BitConverter.IsLittleEndian) Array.Reverse(b); _stream.Write(b,0,b.Length);
        }
        public void WriteString(string s)
        {
            if (s==null) { WriteRawByte(IhapConstants.NULL_VALUE); return; }
            if (WriteObjectReference(s)) return;
            if (WriteStringReference(s)) return;
            int start=0;
            while (true)
            {
                int remaining=CountCodePoints(s,start);
                int count=Math.Min(remaining,IhapConstants.BLOCK_SIZE);
                string part=SubstringByCodePoints(s,start,count,out int consumedChars);
                start += consumedChars;
                bool final = remaining <= IhapConstants.BLOCK_SIZE;
                if (final && count<=IhapConstants.SHORT_STRING_MAX) WriteRawByte((byte)(IhapConstants.SHORT_STRING_BASE+count));
                else if (final) { WriteRawByte(IhapConstants.STRING_FINAL); WriteRawUInt16((ushort)count); }
                else { WriteRawByte(IhapConstants.STRING_NFINAL); } // non-final blocks are implicitly 65535 code points
                var bytes=Encoding.UTF8.GetBytes(part); _stream.Write(bytes,0,bytes.Length);
                if (final) break;
            }
        }
        private static int CountCodePoints(string s,int start)
        {
            int n=0; for(int i=start;i<s.Length;i++,n++) if(char.IsHighSurrogate(s[i]) && i+1<s.Length && char.IsLowSurrogate(s[i+1])) i++; return n;
        }
        private static string SubstringByCodePoints(string s,int start,int count,out int consumed)
        {
            int i=start,n=0; while(i<s.Length && n<count){ if(char.IsHighSurrogate(s[i]) && i+1<s.Length && char.IsLowSurrogate(s[i+1])) i+=2; else i++; n++; }
            consumed=i-start; return s.Substring(start,consumed);
        }
        private void WriteDate(DateTime dt)
        {
            if (WriteObjectReference(dt)) return;
            WriteRawByte(IhapConstants.DATE);
            var utc=dt.Kind==DateTimeKind.Utc?dt:dt.ToUniversalTime();
            long ms=(long)(utc-new DateTime(1970,1,1,0,0,0,DateTimeKind.Utc)).TotalMilliseconds;
            WriteRawInt64(ms);
        }
        private void WriteArray(Array a)
        {
            if (a is byte[]) { WriteByteArray((byte[])a); return; }
            Type e=a.GetType().GetElementType();

            // Java ArrayDeSerializer registers object arrays only after the array wire-type
            // string has been written. Character arrays are also registered after their
            // String representation. All other primitive arrays are registered up front.
            if (e!=typeof(bool) && e!=typeof(char) && e!=typeof(double) && e!=typeof(float) &&
                e!=typeof(int) && e!=typeof(long) && e!=typeof(short) && e!=typeof(string))
            {
                int existing;
                if (_objectRefs.TryGetValue(a,out existing)) { WriteReference(existing,false); return; }
                WriteRawByte(IhapConstants.OBJECT_ARRAY); WriteInt(a.Length); WriteString(WireRegistry.JavaNameForType(a.GetType()));
                _objectRefs[a]=_objectRefs.Count;
                foreach(var x in a) WriteObject(x);
                return;
            }
            if (e==typeof(char))
            {
                int existing;
                if (_objectRefs.TryGetValue(a,out existing)) { WriteReference(existing,false); return; }
                WriteRawByte(IhapConstants.CHAR_ARRAY); WriteString(new string((char[])a)); _objectRefs[a]=_objectRefs.Count; return;
            }

            if (WriteObjectReference(a)) return;
            if (e==typeof(bool)) { WriteRawByte(IhapConstants.BOOL_ARRAY); WriteInt(a.Length); foreach(bool x in a) WriteRawByte(x?IhapConstants.BOOL_TRUE:IhapConstants.BOOL_FALSE); return; }
            if (e==typeof(double)) { WriteRawByte(IhapConstants.DOUBLE_ARRAY); WriteInt(a.Length); foreach(double x in a) WriteDouble(x); return; }
            if (e==typeof(float)) { WriteRawByte(IhapConstants.FLOAT_ARRAY); WriteInt(a.Length); foreach(float x in a) WriteFloat(x); return; }
            if (e==typeof(int)) { WriteRawByte(IhapConstants.INT_ARRAY); WriteInt(a.Length); foreach(int x in a) WriteInt(x); return; }
            if (e==typeof(long)) { WriteRawByte(IhapConstants.LONG_ARRAY); WriteInt(a.Length); foreach(long x in a) WriteLong(x); return; }
            if (e==typeof(short)) { WriteRawByte(IhapConstants.SHORT_ARRAY); WriteInt(a.Length); foreach(short x in a) WriteShort(x); return; }
            if (e==typeof(string)) { WriteRawByte(IhapConstants.STRING_ARRAY); WriteInt(a.Length); foreach(string x in a) WriteString(x); return; }
            throw new InvalidOperationException("Unsupported IMSAPI array type: "+a.GetType().FullName);
        }
        private void WriteByteArray(byte[] a)
        {
            if (WriteObjectReference(a)) return;
            int off=0;
            while (a.Length-off>IhapConstants.BLOCK_SIZE) { WriteRawByte(IhapConstants.BYTE_ARRAY_NFINAL); _stream.Write(a,off,IhapConstants.BLOCK_SIZE); off+=IhapConstants.BLOCK_SIZE; }
            int n=a.Length-off;
            if (n<=IhapConstants.SHORT_BYTE_ARRAY_MAX_LENGTH) WriteRawByte((byte)(IhapConstants.SHORT_BYTE_ARRAY_LENGTH_BASE+n));
            else { WriteRawByte(IhapConstants.BYTE_ARRAY_FINAL); WriteRawUInt16((ushort)n); }
            _stream.Write(a,off,n);
        }
        private void WriteComplexObject(object value)
        {
            // Java ObjectDeSerializer writes class name/hash first and only then registers
            // the object identity. This order matters because class-name strings themselves
            // consume reference slots.
            int existing;
            if (_objectRefs.TryGetValue(value, out existing)) { WriteReference(existing,false); return; }
            var info=WireRegistry.ForType(value.GetType());
            if (info==null) throw new InvalidOperationException("Type is not an IMSAPI wire type: "+value.GetType().FullName);
            WriteRawByte(IhapConstants.OBJECT); WriteString(info.JavaName);
            if (_mode==IhapProtocolMode.BinaryHash) WriteLong(info.ClassHash);
            _objectRefs[value]=_objectRefs.Count;
            if (_mode==IhapProtocolMode.BinaryField) WriteInt(info.Fields.Length);
            foreach(var f in info.Fields)
            {
                if (_mode==IhapProtocolMode.BinaryField) WriteString(f.Key);
                WriteByWireType(f.WireType, f.Field.GetValue(value));
            }
        }
        private void WriteByWireType(string wt, object v) { WriteObject(v); }
        private void WriteRawByte(byte b) => _stream.WriteByte(b);
        private void WriteRawUInt16(ushort v) { WriteRawByte((byte)(v>>8)); WriteRawByte((byte)v); }
        private void WriteRawInt32(int v) { unchecked { WriteRawByte((byte)(v>>24)); WriteRawByte((byte)(v>>16)); WriteRawByte((byte)(v>>8)); WriteRawByte((byte)v); } }
        private void WriteRawInt64(long v) { unchecked { for(int s=56;s>=0;s-=8) WriteRawByte((byte)(v>>s)); } }
    }

    public static class IhapProtocol
    {
        public static byte[] SerializeCall(string wireMethodName, IhapProtocolMode mode, params object[] args)
        {
            using(var ms=new MemoryStream()) { new IhapWriter(ms,mode).WriteCall(wireMethodName,args); return ms.ToArray(); }
        }
        public static byte[] SerializeObjectsForDiagnostics(IhapProtocolMode mode, params object[] values)
        {
            using(var ms=new MemoryStream()) { var w=new IhapWriter(ms,mode); foreach(var value in values) w.WriteObject(value); return ms.ToArray(); }
        }
        public static object DeserializeReply(byte[] bytes)
        {
            if (bytes == null) throw new ArgumentNullException(nameof(bytes));
            using(var ms=new MemoryStream(bytes,false)) return new IhapReader(ms).ReadReply();
        }
    }
}
