using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace SteamLanguageParser
{
    class CSharpGen : ICodeGen
    {
        private static readonly Dictionary<string, string> readerTypeMap = new()
        {
            {"byte", "Byte"},
            {"short", "Int16"},
            {"ushort", "UInt16"},
            {"int", "Int32"},
            {"uint", "UInt32"},
            {"long", "Int64"},
            {"ulong", "UInt64"},
            {"char", "Char"},
        };

        public void EmitNamespace(StringBuilder sb, bool end, string nspace)
        {
            if (end)
            {
                sb.AppendLine("}");
                sb.AppendLine("#pragma warning restore CS1591");
                sb.AppendLine("#pragma warning restore CS0219");
                sb.AppendLine("#pragma warning restore CA1041");
            }
            else
            {
                sb.AppendLine("// <auto-generated/>");
                sb.AppendLine("#pragma warning disable CS1591"); // this will hide "Missing XML comment for publicly visible type or member 'Type_or_Member'"
                sb.AppendLine("#pragma warning disable CS0219"); // Warning CS0219: The variable `(variable)' is assigned but its value is never used
                sb.AppendLine("#pragma warning disable CA1041"); // CA1041: Provide ObsoleteAttribute message
                sb.AppendLine("using System;");
                sb.AppendLine("using System.IO;");
                sb.AppendLine("using System.Runtime.InteropServices;");
                sb.AppendLine("using System.Text;");
                sb.AppendLine();
                sb.AppendLine(string.Format("namespace {0}", nspace));
                sb.AppendLine("{");
            }
        }

        public void EmitSerialBase(StringBuilder sb, int level, bool supportsGC)
        {
            string padding = new string('\t', level);

            sb.AppendLine(padding + "public interface ISteamSerializable");
            sb.AppendLine(padding + "{");
            sb.AppendLine(padding + "\tvoid Serialize(Stream stream);");
            sb.AppendLine(padding + "\tvoid Deserialize( Stream stream );");
            sb.AppendLine(padding + "}");

            sb.AppendLine(padding + "public interface ISteamSerializableHeader : ISteamSerializable");
            sb.AppendLine(padding + "{");
            sb.AppendLine(padding + "\tvoid SetEMsg( EMsg msg );");
            sb.AppendLine(padding + "}");

            sb.AppendLine(padding + "public interface ISteamSerializableMessage : ISteamSerializable");
            sb.AppendLine(padding + "{");
            sb.AppendLine(padding + "\tEMsg GetEMsg();");
            sb.AppendLine(padding + "}");

            if (supportsGC)
            {
                sb.AppendLine(padding + "public interface IGCSerializableHeader : ISteamSerializable");
                sb.AppendLine(padding + "{");
                sb.AppendLine(padding + "\tvoid SetEMsg( uint msg );");
                sb.AppendLine(padding + "}");

                sb.AppendLine(padding + "public interface IGCSerializableMessage : ISteamSerializable");
                sb.AppendLine(padding + "{");
                sb.AppendLine(padding + "\tuint GetEMsg();");
                sb.AppendLine(padding + "}");
            }

            sb.AppendLine();
        }

        public static string EmitType(Symbol sym)
        {
            if (sym is WeakSymbol)
            {
                WeakSymbol wsym = sym as WeakSymbol;

                return wsym.Identifier;
            }
            else if (sym is StrongSymbol)
            {
                StrongSymbol ssym = sym as StrongSymbol;

                if (ssym.Prop == null)
                {
                    return ssym.Class.Name;
                }
                else
                {
                    return ssym.Class.Name + "." + ssym.Prop.Name;
                }
            }

            return "INVALID";
        }
        public static string EmitMultipleTypes(List<Symbol> syms, string operation = "|")
        {
            var identList = syms.OfType<WeakSymbol>().Select(wsym => wsym.Identifier);
            return string.Join(" " + operation + " ", identList);
        }

        public static string GetUpperName(string name)
        {
            return name[..1].ToUpperInvariant() + name.Remove(0, 1);
        }

        public void EmitNode(Node n, StringBuilder sb, int level)
        {
            if (n is ClassNode cn)
            {
                if (cn.Emit)
                {
                    EmitClassNode(cn, sb, level);
                }
            }
            else if (n is EnumNode en)
            {
                EmitEnumNode(en, sb, level);
            }
        }

        private static void EmitEnumNode(EnumNode enode, StringBuilder sb, int level)
        {
            string padding = new string('\t', level);

            if (enode.Flags == "flags")
            {
                sb.AppendLine(padding + "[Flags]");
            }

            if (enode.Type != null)
            {
                sb.AppendLine(padding + "public enum " + enode.Name + " : " + EmitType(enode.Type));
            }
            else
            {
                sb.AppendLine(padding + "public enum " + enode.Name);
            }

            sb.AppendLine(padding + "{");

            foreach (PropNode prop in enode.ChildNodes.Cast<PropNode>())
            {
                if (!prop.Emit)
                {
                    continue;
                }

                var lastValue = EmitMultipleTypes(prop.Default);
                if (prop.Obsolete != null)
                {
                    if (prop.Obsolete.Length > 0)
                    {
                        sb.AppendLine(padding + "\t[Obsolete( \"" + prop.Obsolete + "\" )]");
                    }
                    else
                    {
                        sb.AppendLine(padding + "\t[Obsolete]");
                    }
                }
                sb.AppendLine(padding + "\t" + prop.Name + " = " + lastValue + ",");
            }

            sb.AppendLine(padding + "}");
        }

        private void EmitClassNode(ClassNode cnode, StringBuilder sb, int level)
        {
            EmitClassDef(cnode, sb, level, false);

            EmitClassIdentity(cnode, sb, level + 1);

            int baseSize = EmitClassProperties(cnode, sb, level + 1);
            EmitClassConstructor(cnode, sb, level + 1);

            EmitClassSerializer(cnode, sb, level + 1, baseSize);
            EmitClassDeserializer(cnode, sb, level + 1, baseSize);

            EmitClassDef(cnode, sb, level, true);
        }

        private static void EmitClassDef(ClassNode cnode, StringBuilder sb, int level, bool end)
        {
            string padding = new string('\t', level);

            if (end)
            {
                sb.AppendLine(padding + "}");
                sb.AppendLine();
                return;
            }

            string parent = "ISteamSerializable";

            if (cnode.Ident != null)
            {
                if (cnode.Name.Contains("MsgGC", StringComparison.Ordinal))
                {
                    parent = "IGCSerializableMessage";
                }
                else
                {
                    parent = "ISteamSerializableMessage";
                }
            }
            else if (cnode.Name.Contains("Hdr", StringComparison.Ordinal))
            {
                if (cnode.Name.Contains("MsgGC", StringComparison.Ordinal))
                {
                    parent = "IGCSerializableHeader";
                }
                else
                {
                    parent = "ISteamSerializableHeader";
                }
            }

            if (cnode.Name.Contains("Hdr", StringComparison.Ordinal))
            {
                sb.AppendLine(padding + "[StructLayout( LayoutKind.Sequential )]");
            }

            sb.AppendLine(padding + "public class " + cnode.Name + " : " + parent);
            sb.AppendLine(padding + "{");
        }

        private static void EmitClassIdentity(ClassNode cnode, StringBuilder sb, int level)
        {
            string padding = new string('\t', level);

            if (cnode.Ident != null)
            {
                var supressObsoletionWarning = false;

                if (cnode.Ident is StrongSymbol strongSymbol)
                {
                    if (strongSymbol.Prop is PropNode propNode && propNode.Obsolete != null)
                    {
                        supressObsoletionWarning = true;
                    }
                }

                if (supressObsoletionWarning)
                {
                    sb.AppendLine(padding + "#pragma warning disable 0612");
                }

                if (cnode.Name.Contains("MsgGC", StringComparison.Ordinal))
                {
                    sb.AppendLine(padding + "public uint GetEMsg() { return " + EmitType(cnode.Ident) + "; }");
                }
                else
                {
                    sb.AppendLine(padding + "public EMsg GetEMsg() { return " + EmitType(cnode.Ident) + "; }");
                }

                if (supressObsoletionWarning)
                {
                    sb.AppendLine(padding + "#pragma warning restore 0612");
                }

                sb.AppendLine();
            }
            else if (cnode.Name.Contains("Hdr", StringComparison.Ordinal))
            {
                if (cnode.Name.Contains("MsgGC", StringComparison.Ordinal))
                {
                    if (cnode.ChildNodes.Find(node => node.Name == "msg") != null)
                    {
                        sb.AppendLine(padding + "public void SetEMsg( uint msg ) { this.Msg = msg; }");
                        sb.AppendLine();
                    }
                    else
                    {
                        // this is required for a gc header which doesn't have an emsg
                        sb.AppendLine(padding + "public void SetEMsg( uint msg ) { }");
                        sb.AppendLine();
                    }
                }
                else
                {
                    sb.AppendLine(padding + "public void SetEMsg( EMsg msg ) { this.Msg = msg; }");
                    sb.AppendLine();
                }
            }
        }

        private static int EmitClassProperties(ClassNode cnode, StringBuilder sb, int level)
        {
            string padding = new string('\t', level);
            int baseClassSize = 0;

            if (cnode.Parent != null)
            {
                sb.AppendLine(padding + "public " + EmitType(cnode.Parent) + " Header { get; set; }");
            }

            foreach (PropNode prop in cnode.ChildNodes.Cast<PropNode>())
            {
                string typestr = EmitType(prop.Type);
                string propName = GetUpperName(prop.Name);

                if (prop.Flags != null && prop.Flags == "const")
                {
                    sb.AppendLine(padding + "public static readonly " + typestr + " " + propName + " = " + EmitType(prop.Default.FirstOrDefault()) + ";");
                    continue;
                }

                int size = CodeGenerator.GetTypeSize(prop);
                baseClassSize += size;

                sb.AppendLine(padding + "// Static size: " + size);

                if (prop.Flags != null && prop.Flags == "steamidmarshal" && typestr == "ulong")
                {
                    sb.AppendLine(padding + string.Format("private {0} {1};", typestr, prop.Name));
                    sb.AppendLine(padding + "public SteamID " + propName + " { get { return new SteamID( " + prop.Name + " ); } set { " + prop.Name + " = value.ConvertToUInt64(); } }");
                }
                else if (prop.Flags != null && prop.Flags == "boolmarshal" && typestr == "byte")
                {
                    sb.AppendLine(padding + string.Format("private {0} {1};", typestr, prop.Name));
                    sb.AppendLine(padding + "public bool " + propName + " { get { return ( " + prop.Name + " == 1 ); } set { " + prop.Name + " = ( byte )( value ? 1 : 0 ); } }");
                }
                else if (prop.Flags != null && prop.Flags == "gameidmarshal" && typestr == "ulong")
                {
                    sb.AppendLine(padding + string.Format("private {0} {1};", typestr, prop.Name));
                    sb.AppendLine(padding + "public GameID " + propName + " { get { return new GameID( " + prop.Name + " ); } set { " + prop.Name + " = value.ToUInt64(); } }");
                }
                else
                {
                    if (!string.IsNullOrEmpty(prop.FlagsOpt) && int.TryParse(prop.FlagsOpt, out _))
                    {
                        typestr += "[]";
                    }

                    sb.AppendLine(padding + "public " + typestr + " " + propName + " { get; set; }");
                }
            }

            sb.AppendLine();

            return baseClassSize;
        }

        private static void EmitClassConstructor(ClassNode cnode, StringBuilder sb, int level)
        {
            string padding = new string('\t', level);

            sb.AppendLine(padding + "public " + cnode.Name + "()");
            sb.AppendLine(padding + "{");

            if (cnode.Parent != null)
            {
                sb.AppendLine(padding + "\tHeader = new " + EmitType(cnode.Parent) + "();");
                sb.AppendLine(padding + "\tHeader.Msg = GetEMsg();");
            }

            foreach (PropNode prop in cnode.ChildNodes.Cast<PropNode>())
            {
                Symbol defsym = prop.Default.FirstOrDefault();
                string defflags = prop.Flags;

                string symname = GetUpperName(prop.Name);
                string ctor = EmitType(defsym);

                if (defflags != null && defflags == "proto")
                {
                    ctor = "new " + EmitType(prop.Type) + "()";
                }
                else if (defsym == null)
                {
                    if (!string.IsNullOrEmpty(prop.FlagsOpt))
                    {
                        ctor = "new " + EmitType(prop.Type) + "[" + CodeGenerator.GetTypeSize(prop) + "]";
                    }
                    else
                    {
                        ctor = "0";
                    }
                }
                if (defflags != null && (defflags == "steamidmarshal" || defflags == "gameidmarshal" || defflags == "boolmarshal"))
                {
                    symname = prop.Name;
                }

                else if (defflags != null && defflags == "const")
                {
                    continue;
                }

                sb.AppendLine(padding + "\t" + symname + " = " + ctor + ";");
            }

            sb.AppendLine(padding + "}");
        }

        private static void EmitClassSerializer(ClassNode cnode, StringBuilder sb, int level, int baseSize)
        {
            string padding = new string('\t', level);

            sb.AppendLine();
            sb.AppendLine(padding + "public void Serialize(Stream stream)");
            sb.AppendLine(padding + "{");


            // first emit variable length members
            if (cnode.Parent != null)
            {
                sb.AppendLine(padding + "\tHeader.Serialize(stream);");
                sb.AppendLine();
            }

            foreach (PropNode prop in cnode.ChildNodes.Cast<PropNode>())
            {
                string typestr = EmitType(prop.Type);
                int size = CodeGenerator.GetTypeSize(prop);

                if (size == 0)
                {
                    if (prop.Flags != null && prop.Flags == "proto")
                    {
                        if (baseSize == 0)
                        {
                            // early exit
                            sb.AppendLine(padding + "\tProtoBuf.Serializer.Serialize<" + typestr + ">(stream, " + GetUpperName(prop.Name) + ");");
                            sb.AppendLine(padding + "}");
                            return;
                        }

                        sb.AppendLine(padding + "\tusing MemoryStream ms" + GetUpperName(prop.Name) + " = new MemoryStream();");
                        sb.AppendLine(padding + "\tProtoBuf.Serializer.Serialize<" + typestr + ">(ms" + GetUpperName(prop.Name) + ", " + GetUpperName(prop.Name) + ");");

                        if (prop.FlagsOpt != null)
                        {
                            sb.AppendLine(padding + "\t" + GetUpperName(prop.FlagsOpt) + " = (int)ms" + GetUpperName(prop.Name) + ".Length;");
                        }

                        //sb.AppendLine(padding + "\tms" + GetUpperName(prop.Name) + ".Seek( 0, SeekOrigin.Begin );");
                    }
                    else
                    {
                        sb.AppendLine(padding + "\tusing MemoryStream ms" + GetUpperName(prop.Name) + " = " + GetUpperName(prop.Name) + ".serialize();");
                    }
                }
            }

            //sb.AppendLine(padding + "\tBinaryWriterEx bw = new BinaryWriterEx( stream );");
            //sb.AppendLine();
            sb.AppendLine(padding + "\tusing BinaryWriter bw = new BinaryWriter( stream, Encoding.UTF8, leaveOpen: true );");
            sb.AppendLine();

            if (cnode.Parent != null)
            {
                sb.AppendLine(padding + "\tmsHeader.CopyTo( msBuffer );");
            }

            // next emit writers
            foreach (PropNode prop in cnode.ChildNodes.Cast<PropNode>())
            {
                string typecast = "";
                string propName = GetUpperName(prop.Name);

                if (prop.Type is StrongSymbol strongSymbol && strongSymbol.Class is EnumNode enode)
                {
                    if (enode.Type is WeakSymbol weakSymbol)
                    {
                        typecast = "(" + weakSymbol.Identifier + ")";
                    }
                    else
                    {
                        typecast = "(int)";
                    }
                }

                if (prop.Flags != null)
                {
                    if (prop.Flags == "steamidmarshal" || prop.Flags == "gameidmarshal" || prop.Flags == "boolmarshal")
                    {
                        propName = prop.Name;
                    }
                    else if (prop.Flags == "proto")
                    {
                        sb.AppendLine(padding + "\tbw.Write( ms" + propName + ".ToArray() );");
                        continue;
                    }
                    else if (prop.Flags == "const")
                    {
                        continue;
                    }
                }

                if (prop.Flags == "protomask")
                {
                    propName = "MsgUtil.MakeMsg( " + propName + ", true )";
                }
                else if (prop.Flags == "protomaskgc")
                {
                    propName = "MsgUtil.MakeGCMsg( " + propName + ", true )";
                }

                sb.AppendLine(padding + "\tbw.Write( " + typecast + propName + " );");
            }

            sb.AppendLine();
            sb.AppendLine(padding + "}");
        }

        private void EmitClassDeserializer(ClassNode cnode, StringBuilder sb, int level, int baseSize)
        {
            string padding = new string('\t', level);

            sb.AppendLine();
            sb.AppendLine(padding + "public void Deserialize( Stream stream )");
            sb.AppendLine(padding + "{");

            if (baseSize > 0)
            {
                sb.AppendLine(padding + "\tusing BinaryReader br = new BinaryReader( stream, Encoding.UTF8, leaveOpen: true );");
                sb.AppendLine();
            }

            if (cnode.Parent != null)
            {
                sb.AppendLine(padding + "\tHeader.Deserialize( stream );");
            }

            foreach (PropNode prop in cnode.ChildNodes.Cast<PropNode>())
            {
                string typestr = EmitType(prop.Type);
                int size = CodeGenerator.GetTypeSize(prop);

                string defflags = prop.Flags;
                string symname = GetUpperName(prop.Name);

                if (defflags != null && (defflags == "steamidmarshal" || defflags == "gameidmarshal" || defflags == "boolmarshal"))
                {
                    symname = prop.Name;
                }
                else if (defflags != null && defflags == "const")
                {
                    continue;
                }

                if (size == 0)
                {
                    if (prop.Flags != null && prop.Flags == "proto")
                    {
                        if (prop.FlagsOpt != null)
                        {
                            sb.AppendLine(padding + "\tArgumentOutOfRangeException.ThrowIfNegative( " + GetUpperName( prop.FlagsOpt ) + " );" );
                            sb.AppendLine(padding + "\t" + GetUpperName(prop.Name) + " = ProtoBuf.Serializer.Deserialize<" + typestr + ">( stream, length: " + GetUpperName( prop.FlagsOpt ) + " );");
                        }
                        else
                        {
                            sb.AppendLine(padding + "\t" + GetUpperName(prop.Name) + " = ProtoBuf.Serializer.Deserialize<" + typestr + ">( stream );");
                        }
                    }
                    else
                    {
                        sb.AppendLine(padding + "\t" + GetUpperName(prop.Name) + ".Deserialize( stream );");
                    }
                }
                else
                {
                    string typecast = "";
                    if (!readerTypeMap.ContainsKey(typestr))
                    {
                        typecast = "(" + typestr + ")";
                        typestr = CodeGenerator.GetTypeOfSize(size, SupportsUnsignedTypes());
                    }

                    string call = "br.Read" + readerTypeMap[typestr] + "()";

                    if (!string.IsNullOrEmpty(prop.FlagsOpt))
                    {
                        call = "br.Read" + readerTypeMap[typestr] + "s( " + prop.FlagsOpt + " )";
                    }

                    if (prop.Flags == "protomask")
                    {
                        call = "MsgUtil.GetMsg( (uint)" + call + " )";
                    }
                    else if (prop.Flags == "protomaskgc")
                    {
                        call = "MsgUtil.GetGCMsg( (uint)" + call + " )";
                    }

                    sb.AppendLine(padding + "\t" + symname + " = " + typecast + call + ";");
                }
            }


            sb.AppendLine(padding + "}");
        }

        public bool SupportsUnsignedTypes()
        {
            return true;
        }

        public bool SupportsNamespace()
        {
            return true;
        }

    }
}
