using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;
using Upsilon.BaseTypes;
using Upsilon.Binder.VariableSymbols;
using Upsilon.BoundTypes;
using Upsilon.StandardLibraries;
using Type = Upsilon.BaseTypes.Type;

namespace UpsilonLanguageServer
{
    public static class BoundTypeParser
    {
        public static void LoadBoundTypes(string fileContent)
        {
            BoundTypeHandler.Reset();
            var json = JObject.Parse(fileContent);
            foreach (var prop in json.Properties())
            {
                var typeName = prop.Name;
                if (!(prop.Value is JObject obj))
                    continue;

                var dic = new Dictionary<string, UserDataBoundProperty>();
                var specialMod = obj.GetValue("__type", StringComparison.InvariantCultureIgnoreCase)?.ToString();
                if (string.Equals(specialMod, "enum", StringComparison.InvariantCultureIgnoreCase))
                {
                    var values = obj.GetValue("values", StringComparison.InvariantCultureIgnoreCase)?.ToObject<string[]>();
                    if (values == null)
                        continue;
                    BoundTypeHandler.LoadUserDataTypeDefinition(new UserDataBoundEnumDefinition(values, typeName));
                    continue;
                }
                var innerProperties = (JObject)obj.GetValue("Properties", StringComparison.InvariantCultureIgnoreCase);
                if (innerProperties != null)
                {
                    foreach (var innerProperty in innerProperties.Properties())
                    {
                        var propertyName = innerProperty.Name;
                        if (!(innerProperty.Value is JObject value))
                            continue;
                        var type    = value.GetValue("type", StringComparison.InvariantCultureIgnoreCase)?.ToString();
                        var comment = value.GetValue("comment", StringComparison.InvariantCultureIgnoreCase)?.ToString();
                        if (string.Equals("function", type, StringComparison.InvariantCultureIgnoreCase))
                        {
                            ParseMethod(value, dic, propertyName, type, comment);
                        }
                        else
                        {
                            dic.Add(propertyName.ToLowerInvariant(), new UserDataBoundProperty()
                            {
                                Name       = propertyName,
                                ActualType = type,
                                Comment    = comment,
                                Type       = ParseType(type)
                            });
                        }
                    }
                }

                BoundTypeHandler.LoadUserDataTypeDefinition(new UserDataBoundTypeDefinition(typeName, dic));
            }
        }

        private static void ParseMethod(JObject value, IDictionary<string, UserDataBoundProperty> dic,
            string propertyName, string type, string comment)
        {
            var options = value.GetValue("options", StringComparison.InvariantCultureIgnoreCase);
            var listOptions = new List<UserDataBoundMethodOption>();
            foreach (var jToken in options)
            {
                if (!(jToken is JObject option)) continue;
                var returnType = option.GetValue("returns", StringComparison.InvariantCultureIgnoreCase)?.ToString();
                var parameters = new List<UserDataBoundFunctionParameter>();
                if (option.GetValue("Parameters", StringComparison.InvariantCultureIgnoreCase) is JObject parameterJson)
                {
                    var properties = parameterJson.Properties();
                    foreach (var property in properties)
                    {
                        var parameterName  = property.Name;
                        var parameterValue = (JObject) property.Value;
                        var parType = parameterValue.GetValue("type", StringComparison.InvariantCultureIgnoreCase)
                            ?.ToString();
                        var parComment = parameterValue.GetValue("comment", StringComparison.InvariantCultureIgnoreCase)
                            ?.ToString();
                        var isOptional = parameterValue.GetValue("IsOptional", StringComparison.InvariantCultureIgnoreCase)
                            ?.ToObject<bool>();
                        parameters.Add(new UserDataBoundFunctionParameter()
                        {
                            Name       = parameterName,
                            Type       = ParseType(parType),
                            Comment    = parComment,
                            IsOptional = isOptional.HasValue && isOptional.Value,
                            ActualType = parType
                        });
                    }
                }
                listOptions.Add(new UserDataBoundMethodOption(ParseType(returnType), parameters.ToArray(), false));
            }

            var t = value.GetValue("type", StringComparison.InvariantCultureIgnoreCase).Value<string>();

            dic.Add(propertyName.ToLowerInvariant(), new UserDataBoundMethod(propertyName, listOptions)
            {
                Type = ParseType(t)
            });
        }

        public static void LoadStaticVariables(string fileContent)
        {
            // We call the evaluation scope so that it initializes, and won't overwrite the bound scope later on
            // DONT REMOVE
            var _ = StaticScope.Scope;
            StaticScope.BoundScope = StaticScope.CreateStandardLibrary().Item2;
            var json = JObject.Parse(fileContent);
            foreach (var property in json.Properties())
            {
                var name = property.Name;
                var obj = (JObject)property.Value;
                var stringType = obj.GetValue("type", StringComparison.InvariantCultureIgnoreCase).ToString().ToLowerInvariant();
                var comments = obj.GetValue("comment", StringComparison.InvariantCultureIgnoreCase)?.ToString()
                    .Split('\n');
                var type = ParseType(stringType);
                if (type == Type.UserData)
                {
                    var boundType = BoundTypeHandler.GetTypeDefinition(stringType);
                    if (boundType != null)
                    {
                        StaticScope.BoundScope.AssignToNearest(new UserDataVariableSymbol(name, boundType, true){CommentValue = comments});
                    }
                    else
                    {
                        StaticScope.BoundScope.AssignToNearest(new UserDataVariableSymbol(name, Type.Unknown, true){CommentValue = comments});
                    }
                }
                else if (type == Type.Function)
                {
                    var options = obj.GetValue("options", StringComparison.InvariantCultureIgnoreCase).Children();
                    var listOptions = new List<FunctionVariableSymbolOption>();
                    foreach (var jToken in options)
                    {
                        if (!(jToken is JObject option)) continue;
                        var returnType = option.GetValue("returns", StringComparison.InvariantCultureIgnoreCase).ToString().ToLowerInvariant();
                        var parameters = new List<InternalFunctionVariableSymbol.InternalFunctionParameter>();
                        if (option.GetValue("Parameters", StringComparison.InvariantCultureIgnoreCase) is JObject parameterJson)
                        {
                            var properties = parameterJson.Properties();
                            foreach (var prop in properties)
                            {
                                var parameterValue = (JObject) prop.Value;
                                var parName = parameterValue.GetValue("name", StringComparison.InvariantCultureIgnoreCase)
                                    ?.ToString();
                                var parType = parameterValue.GetValue("type", StringComparison.InvariantCultureIgnoreCase)
                                    ?.ToString();
                                var parsedType = ParseType(parType);
                                var isOptional = parameterValue.GetValue("IsOptional", StringComparison.InvariantCultureIgnoreCase)
                                    ?.ToObject<bool>();
                                parameters.Add(new InternalFunctionVariableSymbol.InternalFunctionParameter(parName, parsedType,
                                    isOptional.HasValue && isOptional.Value));
                            }
                        }
                        var parsedReturnType = ParseType(returnType);
                        listOptions.Add(new InternalFunctionVariableOption(parsedReturnType, parameters.ToArray(), null));
                    }
                    StaticScope.BoundScope.AssignToNearest(
                        new InternalFunctionVariableSymbol(name, false, listOptions, false)
                    {
                        CommentValue = comments
                    });
                }
                else
                {
                    StaticScope.BoundScope.AssignToNearest(new VariableSymbol(property.Name,
                        type, false)
                    {
                        CommentValue = comments
                    });
                }
            }
        }

        private static readonly Regex TableMatcher = new Regex(@"table *\((?'catch'.*)\)", RegexOptions.IgnoreCase);

        private static readonly Regex TableKeyValueMatcher =
            new Regex(
                @"(?'keyType'(?:\w| )+) *(?:\((?'keyInternalType'.*)\))* *, *(?'valueType'(?:\w| )+)(?:\((?'valueInternalType'.*)\))*",
                RegexOptions.IgnoreCase);

        public static TypeContainer ParseType(string input)
        {
            input = input.Trim();
            if (string.Equals(input, "string", StringComparison.InvariantCultureIgnoreCase))
                return Type.String;
            if (string.Equals(input, "number", StringComparison.InvariantCultureIgnoreCase))
                return Type.Number;
            if (string.Equals(input, "bool", StringComparison.InvariantCultureIgnoreCase))
                return Type.Boolean;
            if (string.Equals(input, "table", StringComparison.InvariantCultureIgnoreCase))
                return Type.Table;
            if (string.Equals(input, "function", StringComparison.InvariantCultureIgnoreCase))
                return Type.Function;
            if (string.Equals(input, "unknown", StringComparison.InvariantCultureIgnoreCase))
                return Type.Unknown;
            var match = TableMatcher.Match(input);
            if (match.Success)
            {
                var group = match.Groups["catch"].Value;
                return ParseTableKeyValue(group);
            }
            return new TypeContainer(input);
        }

        private static CompositeTypeContainer ParseTableKeyValue(string s)
        {
            var match = TableKeyValueMatcher.Match(s);
            var dictionary = match.Groups.Where(x => x.Success).ToDictionary(x => x.Name, x => x.Value);
            var keyType = ParseType(dictionary["keyType"]);
            var valueType = ParseType(dictionary["valueType"]);
            if (keyType == Type.Table && dictionary.TryGetValue("keyInternalType", out var keyInternalType))
            {
                keyType = ParseTableKeyValue(keyInternalType);
            }
            if (valueType == Type.Table && dictionary.TryGetValue("valueInternalType", out var valueInternalType))
            {
                valueType = ParseTableKeyValue(valueInternalType);
            }

            return new CompositeTypeContainer(new[] {keyType, valueType}.ToImmutableArray());
        }
    }
}