using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace com.itac.mes.imsapi.protocol
{
    internal sealed class IhapReader
    {
        private readonly Stream _stream;
        private IhapProtocolMode _mode;
        private readonly List<object> _objectRefs=new List<object>();
        private readonly List<string> _stringRefs=new List<string>();
        public IhapReader(Stream stream) { _stream=stream; }

        public object ReadReply()
        {
            if(ReadRawByte()!=IhapConstants.PROTOCOL_IDENTIFIER) throw new IhapCommunicationException("Invalid IHAP protocol identifier.");
            int version=ReadRawByte();
            if(version!=1 && version!=17) throw new IhapCommunicationException("Unsupported IHAP protocol version "+version+".");
            _mode=(IhapProtocolMode)version;
            int rpc=ReadRawByte();
            if(rpc==IhapConstants.RPC_REPLY) return ReadObject();
            if(rpc==IhapConstants.RPC_FAULTREPLY)
            {
                int code=ReadInt(); string message=ReadString();
                try { ReadObject(); } catch { }
                throw new IhapRemoteException(code,message);
            }
            throw new IhapCommunicationException("Unexpected IHAP reply type "+rpc+".");
        }

        public object ReadObject()
        {
            int code=ReadRawByte();
            if(code==IhapConstants.NULL_VALUE) return null;
            if(code==IhapConstants.BOOL_FALSE) return false;
            if(code==IhapConstants.BOOL_TRUE) return true;
            if(code==IhapConstants.BYTE) return (byte)ReadRawByte();
            if(code==IhapConstants.CHAR) return (char)ReadRawUInt16();
            if(IsIntCode(code)) return ReadIntAfterCode(code);
            if(IsLongCode(code)) return ReadLongAfterCode(code);
            if(code==IhapConstants.SHORT) return unchecked((short)ReadRawUInt16());
            if(IsFloatCode(code)) return ReadFloatAfterCode(code);
            if(IsDoubleCode(code)) return ReadDoubleAfterCode(code);
            if(IsReferenceCode(code)) return ReadReferenceAfterCode(code);
            if(IsStringCode(code)) return ReadStringAfterCode(code);
            if(code==IhapConstants.DATE) { long ms=ReadRawInt64(); var dt=new DateTime(1970,1,1,0,0,0,DateTimeKind.Utc).AddMilliseconds(ms); _objectRefs.Add(dt); return dt; }
            if(IsArrayCode(code)) return ReadArrayAfterCode(code);
            if(code==IhapConstants.OBJECT) return ReadComplexObject();
            throw new IhapCommunicationException("Unsupported IHAP type code "+code+".");
        }

        public int ReadInt() { int c=ReadRawByte(); return ReadIntAfterCode(c); }
        private int ReadIntAfterCode(int c)
        {
            if(c>=IhapConstants.INT_1BYTE_BASE+IhapConstants.INT_1BYTE_MIN && c<=IhapConstants.INT_1BYTE_BASE+IhapConstants.INT_1BYTE_MAX) return c-IhapConstants.INT_1BYTE_BASE;
            if(c>=IhapConstants.INT_2BYTE_BASE-8 && c<=IhapConstants.INT_2BYTE_BASE+7) return SignExtend(((c-IhapConstants.INT_2BYTE_BASE)<<8) + ReadRawByte(),12);
            if(c>=IhapConstants.INT_3BYTE_BASE-4 && c<=IhapConstants.INT_3BYTE_BASE+3) return SignExtend(((c-IhapConstants.INT_3BYTE_BASE)<<16) + (ReadRawByte()<<8) + ReadRawByte(),20);
            if(c==IhapConstants.INT) return ReadRawInt32();
            throw new IhapCommunicationException("Expected int, found IHAP code "+c+".");
        }
        public long ReadLong() { int c=ReadRawByte(); return ReadLongAfterCode(c); }
        private long ReadLongAfterCode(int c)
        {
            if(c>=IhapConstants.LONG_1BYTE_BASE+IhapConstants.LONG_1BYTE_MIN && c<=IhapConstants.LONG_1BYTE_BASE+IhapConstants.LONG_1BYTE_MAX) return c-IhapConstants.LONG_1BYTE_BASE;
            if(c>=IhapConstants.LONG_2BYTE_BASE-8 && c<=IhapConstants.LONG_2BYTE_BASE+7)
                return SignExtend(((c-(int)IhapConstants.LONG_2BYTE_BASE)<<8) + ReadRawByte(),12);
            if(c>=IhapConstants.LONG_3BYTE_BASE-4 && c<=IhapConstants.LONG_3BYTE_BASE+3)
                return SignExtend(((c-(int)IhapConstants.LONG_3BYTE_BASE)<<16) + (ReadRawByte()<<8) + ReadRawByte(),20);
            if(c==IhapConstants.LONG_4BYTE) return ReadRawInt32();
            if(c==IhapConstants.LONG) return ReadRawInt64();
            // int compact codes are legal when a long value was encoded compactly only in the long range above.
            throw new IhapCommunicationException("Expected long, found IHAP code "+c+".");
        }
        private static int SignExtend(int value,int bits) { int s=32-bits; return (value<<s)>>s; }
        private bool IsIntCode(int c) => c==IhapConstants.INT || (c>=80 && c<=167);
        private bool IsLongCode(int c) => c==IhapConstants.LONG_4BYTE || c==IhapConstants.LONG || (c>=176 && c<=215);
        private bool IsFloatCode(int c) => c==IhapConstants.FLOAT_1BYTE || c==IhapConstants.FLOAT_2BYTE || c==IhapConstants.FLOAT;
        private float ReadFloatAfterCode(int c)
        {
            if(c==IhapConstants.FLOAT_1BYTE) return unchecked((sbyte)ReadRawByte());
            if(c==IhapConstants.FLOAT_2BYTE) return unchecked((short)ReadRawUInt16());
            if(c==IhapConstants.FLOAT){ var b=ReadExact(4); if(BitConverter.IsLittleEndian) Array.Reverse(b); return BitConverter.ToSingle(b,0); }
            throw new IhapCommunicationException("Expected float.");
        }
        private bool IsDoubleCode(int c) => c>=IhapConstants.DOUBLE_0 && c<=IhapConstants.DOUBLE;
        private double ReadDoubleAfterCode(int c)
        {
            if(c==IhapConstants.DOUBLE_0) return 0d; if(c==IhapConstants.DOUBLE_1) return 1d;
            if(c==IhapConstants.DOUBLE_1BYTE) return unchecked((sbyte)ReadRawByte());
            if(c==IhapConstants.DOUBLE_2BYTE) return unchecked((short)ReadRawUInt16());
            if(c==IhapConstants.DOUBLE_4BYTE){var b=ReadExact(4);if(BitConverter.IsLittleEndian)Array.Reverse(b);return BitConverter.ToSingle(b,0);}
            if(c==IhapConstants.DOUBLE){var b=ReadExact(8);if(BitConverter.IsLittleEndian)Array.Reverse(b);return BitConverter.ToDouble(b,0);}
            throw new IhapCommunicationException("Expected double.");
        }

        public string ReadString() { return ReadStringAfterCode(ReadRawByte()); }
        private bool IsStringCode(int c) => c==IhapConstants.NULL_VALUE || c==IhapConstants.STRING_NFINAL || c==IhapConstants.STRING_FINAL || (c>=IhapConstants.SHORT_STRING_BASE && c<=IhapConstants.SHORT_STRING_BASE+IhapConstants.SHORT_STRING_MAX);
        private string ReadStringAfterCode(int c)
        {
            if(c==IhapConstants.NULL_VALUE) return null;
            if(c==IhapConstants.REF_VALUE_1BYTE || c==IhapConstants.REF_VALUE_2BYTE || c==IhapConstants.REF_VALUE_4BYTE) return (string)ReadReferenceAfterCode(c);
            if(c==IhapConstants.REF_STRING_1BYTE || c==IhapConstants.REF_STRING_2BYTE || c==IhapConstants.REF_STRING_4BYTE) return (string)ReadReferenceAfterCode(c);
            var sb=new StringBuilder();
            int cur=c;
            while(true)
            {
                int count; bool final;
                if(cur>=IhapConstants.SHORT_STRING_BASE && cur<=IhapConstants.SHORT_STRING_BASE+IhapConstants.SHORT_STRING_MAX){count=cur-IhapConstants.SHORT_STRING_BASE;final=true;}
                else if(cur==IhapConstants.STRING_FINAL){count=ReadRawUInt16();final=true;}
                else if(cur==IhapConstants.STRING_NFINAL){count=IhapConstants.BLOCK_SIZE;final=false;}
                else throw new IhapCommunicationException("Expected string, found IHAP code "+cur+".");
                sb.Append(ReadUtf8CodePoints(count));
                if(final) break; cur=ReadRawByte();
            }
            string s=sb.ToString(); _objectRefs.Add(s); _stringRefs.Add(s); return s;
        }
        private string ReadUtf8CodePoints(int count)
        {
            var sb=new StringBuilder();
            for(int i=0;i<count;i++)
            {
                int b1=ReadRawByte(); int cp;
                if((b1&0x80)==0) cp=b1;
                else if((b1&0xE0)==0xC0) cp=((b1&0x1F)<<6)|(ReadRawByte()&0x3F);
                else if((b1&0xF0)==0xE0) cp=((b1&0x0F)<<12)|((ReadRawByte()&0x3F)<<6)|(ReadRawByte()&0x3F);
                else if((b1&0xF8)==0xF0) cp=((b1&0x07)<<18)|((ReadRawByte()&0x3F)<<12)|((ReadRawByte()&0x3F)<<6)|(ReadRawByte()&0x3F);
                else throw new IhapCommunicationException("Invalid UTF-8 in IHAP string.");
                sb.Append(char.ConvertFromUtf32(cp));
            }
            return sb.ToString();
        }

        private bool IsReferenceCode(int c) => c>=IhapConstants.REF_VALUE_1BYTE && c<=IhapConstants.REF_STRING_4BYTE;
        private object ReadReferenceAfterCode(int c)
        {
            int index;
            if(c==IhapConstants.REF_VALUE_1BYTE || c==IhapConstants.REF_STRING_1BYTE) index=ReadRawByte();
            else if(c==IhapConstants.REF_VALUE_2BYTE || c==IhapConstants.REF_STRING_2BYTE) index=ReadRawUInt16();
            else index=ReadRawInt32();
            if(c>=IhapConstants.REF_STRING_1BYTE) { if(index<0||index>=_stringRefs.Count) throw new IhapCommunicationException("Invalid IHAP string reference."); return _stringRefs[index]; }
            if(index<0||index>=_objectRefs.Count) throw new IhapCommunicationException("Invalid IHAP object reference."); return _objectRefs[index];
        }

        private bool IsArrayCode(int c) => (c>=IhapConstants.BOOL_ARRAY && c<=IhapConstants.OBJECT_ARRAY) || (c>=IhapConstants.SHORT_BYTE_ARRAY_LENGTH_BASE && c<=IhapConstants.SHORT_BYTE_ARRAY_LENGTH_BASE+IhapConstants.SHORT_BYTE_ARRAY_MAX_LENGTH);
        private object ReadArrayAfterCode(int c)
        {
            if(c==IhapConstants.BYTE_ARRAY_NFINAL || c==IhapConstants.BYTE_ARRAY_FINAL || (c>=64 && c<=79)) return ReadByteArrayAfterCode(c);
            if(c==IhapConstants.CHAR_ARRAY){ string s=ReadString(); var a=s==null?null:s.ToCharArray(); if(a!=null)_objectRefs.Add(a); return a; }
            int n=ReadInt();
            if(c==IhapConstants.BOOL_ARRAY){var a=new bool[n];_objectRefs.Add(a);for(int i=0;i<n;i++)a[i]=(bool)ReadObject();return a;}
            if(c==IhapConstants.DOUBLE_ARRAY){var a=new double[n];_objectRefs.Add(a);for(int i=0;i<n;i++)a[i]=(double)ReadObject();return a;}
            if(c==IhapConstants.FLOAT_ARRAY){var a=new float[n];_objectRefs.Add(a);for(int i=0;i<n;i++)a[i]=(float)ReadObject();return a;}
            if(c==IhapConstants.INT_ARRAY){var a=new int[n];_objectRefs.Add(a);for(int i=0;i<n;i++)a[i]=Convert.ToInt32(ReadObject());return a;}
            if(c==IhapConstants.LONG_ARRAY){var a=new long[n];_objectRefs.Add(a);for(int i=0;i<n;i++)a[i]=Convert.ToInt64(ReadObject());return a;}
            if(c==IhapConstants.SHORT_ARRAY){var a=new short[n];_objectRefs.Add(a);for(int i=0;i<n;i++)a[i]=Convert.ToInt16(ReadObject());return a;}
            if(c==IhapConstants.STRING_ARRAY){var a=new string[n];_objectRefs.Add(a);for(int i=0;i<n;i++)a[i]=ReadString();return a;}
            if(c==IhapConstants.OBJECT_ARRAY)
            {
                string arrayWire=ReadString(); Type t=WireRegistry.ResolveWireType(arrayWire); Type e=t.IsArray?t.GetElementType():typeof(object);
                Array a=Array.CreateInstance(e,n); _objectRefs.Add(a); for(int i=0;i<n;i++) a.SetValue(Coerce(ReadObject(),e),i); return a;
            }
            throw new IhapCommunicationException("Unsupported array code "+c+".");
        }
        private byte[] ReadByteArrayAfterCode(int first)
        {
            using(var ms=new MemoryStream())
            {
                int c=first;
                while(true)
                {
                    int n; bool final;
                    if(c>=64 && c<=79){n=c-64;final=true;}
                    else if(c==IhapConstants.BYTE_ARRAY_FINAL){n=ReadRawUInt16();final=true;}
                    else if(c==IhapConstants.BYTE_ARRAY_NFINAL){n=IhapConstants.BLOCK_SIZE;final=false;}
                    else throw new IhapCommunicationException("Invalid byte-array code.");
                    var b=ReadExact(n); ms.Write(b,0,b.Length); if(final)break; c=ReadRawByte();
                }
                var a=ms.ToArray(); _objectRefs.Add(a); return a;
            }
        }

        private object ReadComplexObject()
        {
            string javaName=ReadString(); var info=WireRegistry.ForName(javaName);
            long hash=0; int fieldCount=-1;
            if(_mode==IhapProtocolMode.BinaryHash) hash=ReadLong(); else fieldCount=ReadInt();
            if(info==null)
            {
                if(_mode==IhapProtocolMode.BinaryHash) throw new IhapCommunicationException("Unknown BINARY_HASH class "+javaName+".");
                var map=new Dictionary<string,object>(StringComparer.Ordinal); _objectRefs.Add(map);
                for(int i=0;i<fieldCount;i++){string key=ReadString();map[key]=ReadObject();} return map;
            }
            if(_mode==IhapProtocolMode.BinaryHash && hash!=info.ClassHash) throw new IhapCommunicationException("Class hash mismatch for "+javaName+": server="+hash+", client="+info.ClassHash+".");
            object obj=Activator.CreateInstance(info.ClrType); _objectRefs.Add(obj);
            if(_mode==IhapProtocolMode.BinaryHash)
            {
                foreach(var f in info.Fields) f.Field.SetValue(obj,Coerce(ReadByWireType(f.WireType),f.Field.FieldType));
            }
            else
            {
                var byKey=new Dictionary<string,WireFieldInfo>(StringComparer.Ordinal); foreach(var f in info.Fields)byKey[f.Key]=f;
                for(int i=0;i<fieldCount;i++)
                {
                    string key=ReadString(); WireFieldInfo f;
                    if(byKey.TryGetValue(key,out f)) f.Field.SetValue(obj,Coerce(ReadByWireType(f.WireType),f.Field.FieldType));
                    else ReadObject();
                }
            }
            return obj;
        }
        private object ReadByWireType(string wt)
        {
            // The wire stream is self-describing; specialized dispatch remains important only to coerce CLR field types.
            return ReadObject();
        }
        private static object Coerce(object value,Type t)
        {
            if(value==null) return null; if(t.IsInstanceOfType(value))return value;
            if(t.IsArray && value is Array)
            {
                var s=(Array)value; var e=t.GetElementType(); var d=Array.CreateInstance(e,s.Length); for(int i=0;i<s.Length;i++)d.SetValue(Coerce(s.GetValue(i),e),i); return d;
            }
            return Convert.ChangeType(value,t,System.Globalization.CultureInfo.InvariantCulture);
        }
        private int ReadRawByte(){int b=_stream.ReadByte();if(b<0)throw new EndOfStreamException();return b;}
        private ushort ReadRawUInt16()=>unchecked((ushort)((ReadRawByte()<<8)|ReadRawByte()));
        private int ReadRawInt32()=>unchecked((ReadRawByte()<<24)|(ReadRawByte()<<16)|(ReadRawByte()<<8)|ReadRawByte());
        private long ReadRawInt64(){unchecked{ulong v=0;for(int i=0;i<8;i++)v=(v<<8)|(byte)ReadRawByte();return (long)v;}}
        private byte[] ReadExact(int n){var b=new byte[n];int off=0;while(off<n){int r=_stream.Read(b,off,n-off);if(r<=0)throw new EndOfStreamException();off+=r;}return b;}
    }
}
