// Copyright © 2010 Xamasoft

using Humanizer;
using Json2CSharpCodeGenerator.Lib.CodeWriters;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System.Text;

namespace Json2CSharpCodeGenerator.Lib;

public class JsonClassGenerator : IJsonClassGeneratorConfig
{
    #region IJsonClassGeneratorConfig

    public OutputTypes OutputType { get; set; } = OutputTypes.MutableClass;
    public OutputCollectionType CollectionType { get; set; } = OutputCollectionType.MutableList;
    public MutableClassConfig MutableClasses { get; } = new MutableClassConfig();
    public JsonLibrary AttributeLibrary { get; set; } = JsonLibrary.NewtonsoftJson;
    public JsonPropertyAttributeUsage AttributeUsage { get; set; } = JsonPropertyAttributeUsage.OnlyWhenNecessary;

    public string Namespace { get; set; }
    public string SecondaryNamespace { get; set; }

    public bool InternalVisibility { get; set; }
    public bool NoHelperClass { get; set; }
    public string MainClass { get; set; }
    public bool UseThisKeyWord { get; set; }
    public bool UsePascalCase { get; set; }
    public bool UseNestedClasses { get; set; }
    public bool ApplyObfuscationAttributes { get; set; }
    public bool SingleFile { get; set; }
    public ICodeBuilder CodeWriter { get; set; }
    public bool AlwaysUseNullableValues { get; set; }
    public bool ExamplesInDocumentation { get; set; }
    public bool RemoveToJson { get; set; }
    public bool RemoveFromJson { get; set; }
    public bool RemoveConstructors { get; set; }

    #endregion

    public TextWriter OutputStream { get; set; }

    //private readonly PluralizationService pluralizationService = PluralizationService.CreateService(new CultureInfo("en-US"));

    public StringBuilder GenerateClasses(string jsonInput, out string errorMessage)
    {
        JObject[] examples = null;
        bool rootWasArray = false;
        try
        {
            using StringReader sr = new(jsonInput);
            using JsonTextReader reader = new(sr);
            JToken json = JToken.ReadFrom(reader);
            if (json is JArray jArray && (jArray.Count == 0 || jArray.All(el => el is JObject)))
            {
                rootWasArray = true;
                examples = jArray.Cast<JObject>().ToArray();
            }
            else if (json is JObject jObject)
            {
                examples = new[] { jObject };
            }
        }
        catch (Exception ex)
        {
            errorMessage = "Exception: " + ex.Message;
            return new StringBuilder();
        }

        try
        {
            if (CodeWriter == null)
                CodeWriter = new CSharpCodeWriter();

            Types = new List<JsonType>();
            _ = this._Names.Add("Root");
            JsonType rootType = new(this, examples[0])
            {
                IsRoot = true
            };
            rootType.AssignName("Root");
            GenerateClass(examples, rootType);

            Types = HandleDuplicateClasses(Types);

            StringBuilder builder = new();
            WriteClassesToFile(builder, Types, rootWasArray);

            errorMessage = string.Empty;
            return builder;
        }
        catch (Exception ex)
        {
            errorMessage = ex.ToString();
            return new StringBuilder();
        }
    }

    private void WriteClassesToFile(StringBuilder sw, IEnumerable<JsonType> types, bool rootIsArray = false)
    {
        bool inNamespace = false;
        bool rootNamespace = false;

        CodeWriter.WriteFileStart(this, sw);
        CodeWriter.WriteDeserializationComment(this, sw, rootIsArray);

        foreach (JsonType type in types)
        {
            if (this.HasNamespace() && inNamespace && rootNamespace != type.IsRoot && SecondaryNamespace != null)
            {
                CodeWriter.WriteNamespaceEnd(this, sw, rootNamespace);
                inNamespace = false;
            }

            if (this.HasNamespace() && !inNamespace)
            {
                CodeWriter.WriteNamespaceStart(this, sw, type.IsRoot);
                inNamespace = true;
                rootNamespace = type.IsRoot;
            }

            CodeWriter.WriteClass(this, sw, type);
        }

        if (this.HasNamespace() && inNamespace)
        {
            CodeWriter.WriteNamespaceEnd(this, sw, rootNamespace);
        }

        CodeWriter.WriteFileEnd(this, sw);
    }

    private void GenerateClass(JObject[] examples, JsonType type)
    {
        Dictionary<string, JsonType> jsonFields = new();
        Dictionary<string, List<object>> fieldExamples = new();

        bool first = true;

        foreach (JObject obj in examples)
        {
            foreach (JProperty prop in obj.Properties())
            {
                JsonType fieldType;
                JsonType currentType = new(this, prop.Value);
                string propName = prop.Name;

                if (jsonFields.TryGetValue(propName, out fieldType))
                {
                    JsonType commonType = fieldType.GetCommonType(currentType);

                    jsonFields[propName] = commonType;
                }
                else
                {
                    JsonType commonType = currentType;
                    if (first)
                        commonType = commonType.MaybeMakeNullable(this);
                    else
                        commonType = commonType.GetCommonType(JsonType.GetNull(this));

                    jsonFields.Add(propName, commonType);
                    fieldExamples[propName] = new List<object>();
                }

                List<object> fe = fieldExamples[propName];
                JToken val = prop.Value;
                if (val.Type is JTokenType.Null or JTokenType.Undefined)
                {
                    if (!fe.Contains(null))
                    {
                        fe.Insert(0, null);
                    }
                }
                else
                {
                    object v = val.Type is JTokenType.Array or JTokenType.Object ? val : val.Value<object>();
                    if (!fe.Any(x => v.Equals(x)))
                    {
                        fe.Add(v);
                    }
                }
            }
            first = false;
        }

        if (UseNestedClasses)
        {
            foreach (KeyValuePair<string, JsonType> field in jsonFields)
            {
                _ = this._Names.Add(field.Key.ToLower());
            }
        }

        foreach (KeyValuePair<string, JsonType> field in jsonFields)
        {
            JsonType fieldType = field.Value;
            if (fieldType.Type == JsonTypeEnum.Object)
            {
                List<JObject> subexamples = new(examples.Length);
                foreach (JObject obj in examples)
                {
                    JToken value;
                    if (obj.TryGetValue(field.Key, out value))
                    {
                        if (value.Type == JTokenType.Object)
                        {
                            subexamples.Add((JObject)value);
                        }
                    }
                }

                fieldType.AssignOriginalName(field.Key);
                fieldType.AssignName(CreateUniqueClassName(field.Key));
                fieldType.AssignNewAssignedName(ToTitleCase(field.Key));

                GenerateClass(subexamples.ToArray(), fieldType);
            }

            if (fieldType.InternalType != null && fieldType.InternalType.Type == JsonTypeEnum.Object)
            {
                List<JObject> subexamples = new(examples.Length);
                foreach (JObject obj in examples)
                {
                    JToken value;
                    if (obj.TryGetValue(field.Key, out value))
                    {
                        if (value is JArray jArray)
                        {
                            const int MAX_JSON_ARRAY_ITEMS = 50; // Take like 30 items from the array this will increase the chance of getting all the objects accuralty while not analyzing all the data

                            subexamples.AddRange(jArray.OfType<JObject>().Take(MAX_JSON_ARRAY_ITEMS));
                        }
                        else if (value is JObject jObject) //TODO J2C : ONLY LOOP OVER 50 OBJECT AND NOT THE WHOLE THING
                        {
                            foreach (KeyValuePair<string, JToken> jsonObjectProperty in jObject)
                            {
                                // if (!(item.Value is JObject)) throw new NotSupportedException("Arrays of non-objects are not supported yet.");
                                if (jsonObjectProperty.Value is JObject innerObject)
                                {
                                    subexamples.Add(innerObject);
                                }
                            }
                        }
                    }
                }

                field.Value.InternalType.AssignOriginalName(field.Key);
                field.Value.InternalType.AssignName(CreateUniqueClassNameFromPlural(field.Key));
                field.Value.InternalType.AssignNewAssignedName(ToTitleCase(field.Key).Singularize(inputIsKnownToBePlural: false));

                GenerateClass(subexamples.ToArray(), field.Value.InternalType);
            }
        }

        type.Fields = jsonFields
            .Select(x => new FieldInfo(
                generator: this,
                jsonMemberName: x.Key,
                type: x.Value,
                usePascalCase: UsePascalCase || AttributeUsage == JsonPropertyAttributeUsage.Always,
                examples: fieldExamples[x.Key])
            )
            .ToList();

        if (!string.IsNullOrEmpty(type.AssignedName))
        {
            Types.Add(type);
        }
    }

    /// <summary>Checks if there are any duplicate classes in the input, and merges its corresponding properties (TEST CASE 7)</summary>
    private IList<JsonType> HandleDuplicateClasses(IList<JsonType> types)
    {
        // TODO: This is currently O(n*n) because it iterates through List<T> on every loop iteration. This can be optimized.

        List<JsonType> typesWithNoDuplicates = new();
        types = types.OrderBy(p => p.AssignedName).ToList();
        foreach (JsonType type in types)
        {
            if (!typesWithNoDuplicates.Exists(p => p.OriginalName == type.OriginalName))
            {
                typesWithNoDuplicates.Add(type);
            }
            else
            {
                JsonType duplicatedType = typesWithNoDuplicates.FirstOrDefault(p => p.OriginalName == type.OriginalName);

                // Rename all references of this type to the original assigned name
                foreach (FieldInfo field in type.Fields)
                {
                    if (!duplicatedType.Fields.ToList().Exists(x => x.JsonMemberName == field.JsonMemberName))
                    {
                        duplicatedType.Fields.Add(field);
                    }
                }
            }
        }

        return typesWithNoDuplicates;
    }

    public IList<JsonType> Types { get; private set; }
    private readonly HashSet<string> _Names = new();

    private string CreateUniqueClassName(string name)
    {
        name = ToTitleCase(name);

        string finalName = name;
        int i = 2;
        while (this._Names.Any(x => x.Equals(finalName, StringComparison.OrdinalIgnoreCase)))
        {

            finalName = name + i.ToString();
            i++;
        }

        _ = this._Names.Add(finalName);
        return finalName;
    }

    private string CreateUniqueClassNameFromPlural(string plural)
    {
        plural = ToTitleCase(plural);
        string singular = plural.Singularize(inputIsKnownToBePlural: false);
        return CreateUniqueClassName(singular);
    }

    internal static string ToTitleCase(string str)
    {
        StringBuilder sb = new(str.Length);
        bool flag = true;

        for (int i = 0; i < str.Length; i++)
        {
            char c = str[i];
            string specialCaseFirstCharIsNumber = string.Empty;

            // Handle the case where the first character is a number
            if (i == 0 && char.IsDigit(c))
                specialCaseFirstCharIsNumber = "_" + c;

            if (char.IsLetterOrDigit(c))
            {
                if (string.IsNullOrEmpty(specialCaseFirstCharIsNumber))
                {
                    _ = sb.Append(flag ? char.ToUpper(c) : c);
                }
                else
                {
                    _ = sb.Append(flag ? specialCaseFirstCharIsNumber.ToUpper() : specialCaseFirstCharIsNumber);
                }

                flag = false;
            }
            else
            {
                flag = true;
            }
        }

        return sb.ToString();
    }
}