using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Mono.Cecil;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Upsilon.BaseTypes.UserData;
using Upsilon.Binder;
using Upsilon.Binder.VariableSymbols;
using Upsilon.BoundTypes;
using Upsilon.StandardLibraries;
using Type = Upsilon.BaseTypes.Type;

namespace UpsilonLanguageServer.ReferenceHandler
{
    public static class LoadReferenceLibraries
    {
        private static List<string> References { get; set; } = new List<string>();

        public static void ReloadReferences(string workSpace, string jsonString)
        {
            var obj = JObject.Parse(jsonString);
            if (!obj.TryGetValue("references", StringComparison.InvariantCultureIgnoreCase, out var references))
                return;
            if (!(references is JArray referencesArray))
                return;

            var values = referencesArray.Values<string>();

            foreach (var reference in values)
            {
                var path = Path.Combine(workSpace, reference);
                if (!File.Exists(path))
                {
                    throw new Exception($"Can't read file {path}. It doesn't exist.");
                }
                if (Path.GetExtension(path) != ".dll")
                    continue;
                References.Add(path);
            }

            References = References.Distinct().ToList();
            LoadAssemblies(References);
        }

        private static Dictionary<string, string> _translatedType;

        private static void LoadAssemblies(IReadOnlyCollection<string> locations)
        {
            var resolver = new DefaultAssemblyResolver();
            foreach (var location in locations)
            {
                var dir = Directory.GetParent(location);
                resolver.AddSearchDirectory(dir.FullName);
            }

            _translatedType = new Dictionary<string, string>();
            var types = locations.Select(x =>
                ModuleDefinition.ReadModule(x, new ReaderParameters() {AssemblyResolver = resolver}))
                .SelectMany(y => y.Types)
                .ToArray();

            var requiredUserDataAttribute = typeof(UpsilonUserDataAttribute).FullName;
            var userDataTypesEnumerable = types
                .SelectMany(x => GetDefinitions(x, requiredUserDataAttribute));

            var userDataTypes = new Dictionary<string, TypeDefinition>();
            foreach (var tuple in userDataTypesEnumerable)
            {
                if (!userDataTypes.ContainsKey(tuple.Item1))
                    userDataTypes.Add(tuple.Item1, tuple.Item2);
            }

            foreach (var definition in userDataTypes)
            {
                _translatedType.Add(definition.Value.FullName, definition.Key);
            }

            var result = new Dictionary<string, Data>();
            foreach (var definition in userDataTypes)
            {
                var name     = definition.Key;
                var type     = definition.Value;
                var typeData = ExtractData(type);
                result.Add(name, typeData);

                if (typeData is EnumData enumData)
                {
                    BoundTypeHandler.LoadUserDataTypeDefinition(new UserDataBoundEnumDefinition(enumData.Values, name));
                }
                else
                {
                    var dic = new Dictionary<string, UserDataBoundProperty>();
                    foreach (var property in typeData.Properties)
                    {
                        var propertyName = property.Key;
                        var propertyValue = property.Value;

                        var scriptType = BoundTypeParser.ParseType(propertyValue.Type);
                        if (propertyValue is MethodData method)
                        {
                            dic.Add(propertyName.ToLowerInvariant(), new UserDataBoundMethod(propertyName,
                                method.Options.Select(
                                    x =>
                                    {
                                        var returnType = BoundTypeParser.ParseType(x.Returns);
                                        return new UserDataBoundMethodOption(returnType, x.Parameters.Select(y =>
                                            new UserDataBoundFunctionParameter()
                                            {
                                                Name       = y.Key,
                                                Type       = BoundTypeParser.ParseType(y.Value.Type),
                                                Comment    = y.Value.Comment,
                                                ActualType = y.Value.Type,
                                                IsOptional = y.Value.IsOptional
                                            }).ToArray(), false);
                                    }).ToList())
                            {
                                Type = Type.Function
                            });
                        }
                        else
                        {
                            dic.Add(propertyName.ToLowerInvariant(), new UserDataBoundProperty()
                            {
                                Name       = propertyName,
                                ActualType = propertyValue.Type,
                                Comment    = propertyValue.Comment,
                                Type       = scriptType
                            });
                        }
                    }
                    BoundTypeHandler.LoadUserDataTypeDefinition(new UserDataBoundTypeDefinition(name, dic));
                }
            }

            var requiredStaticAttribute = typeof(UpsilonCreateStaticAttribute).FullName;
            var staticUserDataTypes = types
                .SelectMany(x => GetDefinitions(x, requiredStaticAttribute))
                .ToDictionary(x => x.Item1, x => x.Item2);

            foreach (var staticUserDataType in staticUserDataTypes)
            {
                var variableSymbol = CreateVariableSymbol(staticUserDataType.Key, staticUserDataType.Value);
                StaticScope.BoundScope.AssignToNearest(variableSymbol);
            }
        }

        private static VariableSymbol CreateVariableSymbol(string name, TypeDefinition def)
        {
            var type = BoundTypeParser.ParseType(name);
            if (type == Type.UserData)
            {
                var boundTypeName = _translatedType[def.FullName];
                var boundType = BoundTypeHandler.GetTypeDefinition(boundTypeName);
                return new UserDataVariableSymbol(name, boundType, true);
            }
            return new VariableSymbol(name, type, true);
        }

        private static List<(string, TypeDefinition)> GetDefinitions(TypeDefinition typeDefinition,
            string requiredAttribute)
        {
            var ls = new List<(string, TypeDefinition)>();
            foreach (var nestedType in typeDefinition.NestedTypes)
            {
                var definitions = GetDefinitions(nestedType, requiredAttribute);
                if (definitions.Count > 0)
                {
                    ls.AddRange(definitions);
                }
            }

            if (!typeDefinition.HasCustomAttributes) return ls;
            foreach (var customAttribute in typeDefinition.CustomAttributes)
            {
                if (!string.Equals(customAttribute.AttributeType.FullName, requiredAttribute))
                {
                    continue;
                }
                var name = customAttribute.ConstructorArguments[0].Value.ToString();
                ls.Add((name, typeDefinition));
            }

            return ls;
        }

        private static Data ExtractData(TypeDefinition type)
        {
            if (type.IsEnum)
            {
                var enumTypeData = new EnumData()
                {
                    Values = type.Fields.Where(x => x.IsPublic && !x.IsSpecialName).Select(x => x.Name).ToList()
                };
                return enumTypeData;
            }


            var typeData = new Data(){Properties = new Dictionary<string, Property>()};

            if (type.BaseType != null)
            {
                try
                {
                    var resolved = type.BaseType.Resolve();
                    if (resolved.FullName != "System.Object")
                    {
                        var b = ExtractData(type.BaseType.Resolve());
                        foreach (var property in b.Properties)
                        {
                            typeData.Properties.Add(property.Key, property.Value);
                        }
                    }
                }
                catch
                {
                    //ignore
                }
            }

            if (type.HasInterfaces)
            {
                var interfaces = type.Interfaces;
                foreach (var @interface in interfaces)
                {
                    try
                    {
                        var extract = ExtractData(@interface.InterfaceType.Resolve());
                        foreach (var property in extract.Properties)
                        {
                            if (!typeData.Properties.ContainsKey(property.Key))
                                typeData.Properties.Add(property.Key, property.Value);
                        }
                    }
                    catch
                    {
                        // ignore
                    }
                }
            }

            foreach (var fieldDefinition in type.Fields)
            {
                if (!fieldDefinition.IsPublic)
                    continue;
                var parsedFieldType = GetParsedTypeName(fieldDefinition.FieldType);
                if (typeData.Properties.ContainsKey(fieldDefinition.Name))
                {
                    typeData.Properties[fieldDefinition.Name] = new Property()
                    {
                        Type = parsedFieldType
                    };
                }
                else
                {
                    typeData.Properties.Add(fieldDefinition.Name, new Property()
                    {
                        Type = parsedFieldType
                    });
                }
            }
            foreach (var propertyDefinition in type.Properties)
            {
                if (!propertyDefinition.GetMethod.IsPublic)
                    continue;
                var parsedFieldType = GetParsedTypeName(propertyDefinition.PropertyType);
                if (typeData.Properties.ContainsKey(propertyDefinition.Name))
                {
                    typeData.Properties[propertyDefinition.Name] = new Property()
                    {
                        Type = parsedFieldType
                    };
                }
                else
                {
                    typeData.Properties.Add(propertyDefinition.Name, new Property()
                    {
                        Type = parsedFieldType
                    });
                }
            }

            var methodDic = new Dictionary<string, MethodData>();

            foreach (var methodDefinition in type.Methods)
            {
                if (!methodDefinition.IsPublic)
                    continue;
                if (methodDefinition.IsConstructor)
                    continue;
                if (typeData.Properties.ContainsKey(methodDefinition.Name))
                    continue;
                if (methodDefinition.IsGetter || methodDefinition.IsSetter)
                    continue;
                var returnType = GetParsedTypeName(methodDefinition.ReturnType);
                var parameters = new Dictionary<string, MethodDataOption.MethodParameter>();
                foreach (var parameter in methodDefinition.Parameters)
                {
                    var parameterType = GetParsedTypeName(parameter.ParameterType);
                    var optional = parameter.IsOptional;
                    parameters.Add(parameter.Name, new MethodDataOption.MethodParameter()
                    {
                        Type = parameterType,
                        IsOptional = optional
                    });
                }
                var option = new MethodDataOption()
                {
                    Returns    = returnType,
                    Parameters = parameters
                };
                if (methodDic.TryGetValue(methodDefinition.Name, out var data))
                {
                    data.Options.Add(option);
                }
                else
                {
                    methodDic.Add(methodDefinition.Name, new MethodData()
                    {
                        Type = "function",
                        Options = new List<MethodDataOption>(){option}
                    });
                }
            }

            foreach (var methodData in methodDic)
            {
                typeData.Properties.Add(methodData.Key, methodData.Value);
            }

            return typeData;
        }

        private static string GetParsedTypeName(TypeReference type)
        {
            if (string.Equals(type.FullName, "System.Void"))
                return "nil";
            if (string.Equals(type.FullName, "System.String"))
                return "string";
            if (string.Equals(type.FullName, "System.Boolean"))
                return "bool";
            if (type.IsPrimitive)
                return "number";
            if (type.IsArray && type is ArrayType arrayType)
            {
                var t = arrayType.ElementType;
                return $"table (number, {GetParsedTypeName(t)})";
            }
            if (type is GenericInstanceType genericType)
            {
                if (type.FullName.StartsWith("System.Collections.Generic.Dictionary"))
                {
                    var generics = genericType.GenericArguments;
                    if (generics.Count != 2)
                        return "table";
                    var key   = GetParsedTypeName(generics[0]);
                    var value = GetParsedTypeName(generics[1]);
                    return $"table ({key}, {value})";
                }
                if (type.FullName.StartsWith("System.Collections.Generic.List"))
                {
                    var generics = genericType.GenericArguments;
                    if (generics.Count != 1)
                        return "table";
                    var value = GetParsedTypeName(generics[0]);
                    return $"table (number, {value})";
                }
            }
            if (_translatedType.TryGetValue(type.FullName, out var s))
            {
                return s;
            }
            return "unknown";
        }

        public class Data
        {
            public Dictionary<string, Property> Properties { get; set; }
        }

        public class EnumData : Data
        {
            [JsonProperty("__type")] public string SpecialType = "enum";
            public List<string> Values = new List<string>();
        }

        public class Property
        {
            public string Type { get; set; }
            public string Comment { get; set; }
        }

        public class MethodData : Property
        {
            public List<MethodDataOption> Options { get; set; }
        }

        public class MethodDataOption
        {
            public string                              Returns    { get; set; }
            public Dictionary<string, MethodParameter> Parameters { get; set; }
            public class MethodParameter
            {
                public string Type       { get; set; }
                public string Comment    { get; set; }
                public bool   IsOptional { get; set; }
            }
        }

    }
}