using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Gamecraft.Tweaks;
using RobocraftX.Common;
using Svelto.ECS;
using Techblox.EngineBlock;

namespace CodeGenerator
{
    public class BlockClassGenerator
    {
        public void Generate(string name, string group = null, Dictionary<string, string> renames = null, params Type[] types)
        {
            if (group is null)
            {
                group = GetGroup(name) + "_BLOCK_GROUP";
                if (typeof(CommonExclusiveGroups).GetFields().All(field => field.Name != group))
                    group = GetGroup(name) + "_BLOCK_BUILD_GROUP";
            }

            if (!group.Contains("."))
                group = "CommonExclusiveGroups." + group;

            var codeUnit = new CodeCompileUnit();
            var ns = new CodeNamespace("TechbloxModdingAPI.Blocks");
            ns.Imports.Add(new CodeNamespaceImport("RobocraftX.Common"));
            ns.Imports.Add(new CodeNamespaceImport("Svelto.ECS"));
            var cl = new CodeTypeDeclaration(name);
            //cl.BaseTypes.Add(baseClass != null ? new CodeTypeReference(baseClass) : new CodeTypeReference("Block"));
            cl.BaseTypes.Add(new CodeTypeReference("SignalingBlock"));
            cl.Members.Add(new CodeConstructor
            {
                Parameters = {new CodeParameterDeclarationExpression("EGID", "egid")},
                Comments =
                {
                    _start, new CodeCommentStatement($"Constructs a(n) {name} object representing an existing block.", true), _end
                },
                BaseConstructorArgs = {new CodeVariableReferenceExpression("egid")},
                Attributes = MemberAttributes.Public | MemberAttributes.Final
            });
            cl.Members.Add(new CodeConstructor
            {
                Parameters =
                {
                    new CodeParameterDeclarationExpression(typeof(uint), "id")
                },
                Comments =
                {
                    _start, new CodeCommentStatement($"Constructs a(n) {name} object representing an existing block.", true), _end
                },
                BaseConstructorArgs =
                {
                    new CodeObjectCreateExpression("EGID", new CodeVariableReferenceExpression("id"),
                        new CodeVariableReferenceExpression(group))
                },
                Attributes = MemberAttributes.Public | MemberAttributes.Final
            });
            foreach (var type in types)
            {
                GenerateProperties(cl, type, name, renames);
            }
            ns.Types.Add(cl);
            codeUnit.Namespaces.Add(ns);

            var provider = CodeDomProvider.CreateProvider("CSharp");
            var path = $@"..\..\..\TechbloxModdingAPI\Blocks\{name}.cs";
            using (var sw = new StreamWriter(path))
            {
                provider.GenerateCodeFromCompileUnit(codeUnit, sw, new CodeGeneratorOptions {BracingStyle = "C"});
            }

            File.WriteAllLines(path,
                File.ReadAllLines(path).SkipWhile(line => line.StartsWith("//") || line.Length == 0));
        }

        private static string GetGroup(string name)
        {
            var ret = "";
            foreach (var ch in name)
            {
                if (char.IsUpper(ch) && ret.Length > 0)
                    ret += "_" + ch;
                else
                    ret += char.ToUpper(ch);
            }

            return ret;
        }

        private void GenerateProperties(CodeTypeDeclaration cl, Type type, string baseClass,
            Dictionary<string, string> renames)
        {
            if (!typeof(IEntityComponent).IsAssignableFrom(type))
                throw new ArgumentException("Type must be an entity component");
            foreach (var field in type.GetFields())
            {
                var attr = field.GetCustomAttribute<TweakableStatAttribute>();
                if (renames == null || !renames.TryGetValue(field.Name, out var propName))
                {
                    propName = field.Name;
                    if (attr != null)
                        propName = attr.propertyName;
                }

                propName = char.ToUpper(propName[0]) + propName.Substring(1);
                var structFieldReference = new CodeFieldReferenceExpression(new CodeMethodInvokeExpression(
                    new CodeMethodReferenceExpression(new CodeSnippetExpression("BlockEngine"),
                        "GetBlockInfo", new CodeTypeReference(type)),
                    new CodeThisReferenceExpression()), field.Name);
                cl.Members.Add(new CodeMemberProperty
                {
                    Name = propName,
                    HasGet = true,
                    HasSet = true,
                    GetStatements =
                    {
                        new CodeMethodReturnStatement(structFieldReference)
                    },
                    SetStatements =
                    {
                        new CodeAssignStatement(structFieldReference, new CodePropertySetValueReferenceExpression())
                    },
                    Type = new CodeTypeReference(field.FieldType),
                    Attributes = MemberAttributes.Public | MemberAttributes.Final,
                    Comments =
                    {
                        _start, new CodeCommentStatement($"Gets or sets the {baseClass}'s {propName} property." +
                                                         $" {(attr != null ? "Tweakable stat." : "May not be saved.")}", true), _end
                    }
                });
            }
        }

        private static readonly CodeCommentStatement _start = new CodeCommentStatement("<summary>", true);
        private static readonly CodeCommentStatement _end = new CodeCommentStatement("</summary>", true);
    }
}