using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using VCommon.Reflection;
using VCommon.VApplication.DataAnnotations;

namespace VCommon.VOpenApi.Docgen
{
    internal class SchemaCrawler
    {
        public static IDictionary<string, Schema> Crawl(IEnumerable<Type> modelsToCrawl, string refPath = "#/definitions/")
        {
            var dic = new Dictionary<string, Schema>();

            foreach (var item in modelsToCrawl) CrawlInternal(dic, item, refPath);

            return dic;
        }

        private static void CrawlInternal(IDictionary<string, Schema> dic, Type toCrawl, string refPath)
        {
            if (toCrawl.IsPrimitiveType())
            {
                var primitiveTypeName = toCrawl.GetPrimitiveTypeName();
                if (dic.ContainsKey(primitiveTypeName)) return;
                dic[primitiveTypeName] = CreatePrimitiveSchema(toCrawl);
            }
            else
            {
                var fullName = toCrawl.GetFriendlyTypeName();

                if (dic.ContainsKey(fullName)) return;

                if (toCrawl.IsArray())
                {
                    var arrayElementType = toCrawl.GetArrayElementType();
                    dic[fullName] = CreateArrayTypeSchema(refPath, arrayElementType.GetFriendlyTypeName());
                    CrawlInternal(dic, arrayElementType, refPath);
                }
                else
                {
                    var typeSchema = dic[fullName] = new Schema
                    {
                        type = "object",
                        properties = new Dictionary<string, Schema>(),
                        required = new List<string>()
                    };

                    foreach (var propertyInfo in toCrawl.GetPublicInstanceProperties())
                    {
                        if (propertyInfo.IsDefined(typeof(JsonIgnoreAttribute))) continue;
                        if (propertyInfo.IsDefined(typeof(OutputIgnoreAttribute))) continue;
                        if (propertyInfo.IsDefined(typeof(IgnoreSchema))) continue;

                        var propertySchema = new Schema();
                        var propertyName = propertyInfo.GetCustomAttribute<JsonPropertyAttribute>()?.PropertyName ?? propertyInfo.Name;

                        if (propertyInfo.IsDefined(typeof(RequiredAttribute))) typeSchema.required.Add(propertyInfo.Name);

                        if (propertyInfo.IsDefinedIncludeInterface(typeof(ExampleValueAttribute))) propertySchema.example = propertyInfo.GetCustomAttributeIncludeInterface<ExampleValueAttribute>().First().Value;

                        var ptype = propertyInfo.PropertyType;

                        if (ptype.IsPrimitiveType())
                        {
                            propertySchema.@ref = refPath + ptype.GetPrimitiveTypeName();
                            CrawlInternal(dic, ptype, refPath);
                        }
                        else if (ptype.IsArray())
                        {
                            var arrayElementType = ptype.GetArrayElementType();
                            propertySchema = CreateArrayTypeSchema(refPath, arrayElementType.GetFriendlyTypeName());
                            CrawlInternal(dic, arrayElementType, refPath);
                        }
                        else
                        {
                            propertySchema.@ref = refPath + ptype.GetFriendlyTypeName();
                            CrawlInternal(dic, ptype, refPath);
                        }

                        typeSchema.properties[propertyName] = propertySchema;
                    }
                }
            }
        }

        private static Schema CreateArrayTypeSchema(string refPath, string fullName)
        {
            return new Schema
            {
                type = "array",
                items = new Schema
                {
                    @ref = refPath + fullName
                }
            };
        }

        private static Schema CreatePrimitiveSchema(Type type)
        {
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                type = type.GetGenericArguments()[0];
            }

            if (type.IsEnum)
                return CreateEnumSchema(type);

            switch (type.FullName)
            {
                case "System.Boolean":
                    return new Schema { type = "boolean" };

                case "System.Byte":
                case "System.SByte":
                case "System.Int16":
                case "System.UInt16":
                case "System.Int32":
                case "System.UInt32":
                    return new Schema { type = "integer", format = "int32" };

                case "System.Int64":
                case "System.UInt64":
                    return new Schema { type = "integer", format = "int64" };

                case "System.Single":
                    return new Schema { type = "number", format = "float" };

                case "System.Double":
                case "System.Decimal":
                    return new Schema { type = "number", format = "double" };

                case "System.Byte[]":
                    return new Schema { type = "string", format = "byte" };

                case "System.DateTime":
                case "System.DateTimeOffset":
                    return new Schema { type = "string", format = "date-time" };

                case "System.Guid": return new Schema { type = "string", format = "uuid", example = Guid.Empty };
                case "System.Object": return new Schema { type = "object" };

                default: return new Schema { type = "string" };
            }
        }

        private static Schema CreateEnumSchema(IReflect enumType)
        {
            return new Schema
            {
                type = "string",
                @enum = enumType.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
                    .Select(fieldInfo => (object)fieldInfo.Name)
                    .ToArray()
            };
        }
    }
}
