using System.Text;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;

namespace Umbraco.Cms.Infrastructure.ModelsBuilder.Building;

/// <summary>
///     Implements a builder that works by writing text.
/// </summary>
public class TextBuilder : Builder
{
    private static readonly IDictionary<string, string> _typesMap =
        new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            { "System.Int16", "short" },
            { "System.Int32", "int" },
            { "System.Int64", "long" },
            { "System.String", "string" },
            { "System.Object", "object" },
            { "System.Boolean", "bool" },
            { "System.Void", "void" },
            { "System.Char", "char" },
            { "System.Byte", "byte" },
            { "System.UInt16", "ushort" },
            { "System.UInt32", "uint" },
            { "System.UInt64", "ulong" },
            { "System.SByte", "sbyte" },
            { "System.Single", "float" },
            { "System.Double", "double" },
            { "System.Decimal", "decimal" },
        };

    /// <summary>
    ///     Initializes a new instance of the <see cref="TextBuilder" /> class with a list of models to generate
    ///     and the result of code parsing.
    /// </summary>
    /// <param name="typeModels">The list of models to generate.</param>
    public TextBuilder(ModelsBuilderSettings config, IList<TypeModel> typeModels)
        : base(config, typeModels)
    {
    }

    // internal for unit tests only
    public TextBuilder()
    {
    }

    /// <summary>
    ///     Outputs an "auto-generated" header to a string builder.
    /// </summary>
    /// <param name="sb">The string builder.</param>
    public static void WriteHeader(StringBuilder sb) => TextHeaderWriter.WriteHeader(sb);

    /// <summary>
    ///     Outputs a generated model to a string builder.
    /// </summary>
    /// <param name="sb">The string builder.</param>
    /// <param name="typeModel">The model to generate.</param>
    public void Generate(StringBuilder sb, TypeModel typeModel)
    {
        WriteHeader(sb);

        foreach (var t in TypesUsing)
        {
            sb.AppendFormat("using {0};\n", t);
        }

        sb.Append("\n");
        sb.AppendFormat("namespace {0}\n", GetModelsNamespace());
        sb.Append("{\n");

        WriteContentType(sb, typeModel);

        sb.Append("}\n");
    }

    /// <summary>
    ///     Outputs generated models to a string builder.
    /// </summary>
    /// <param name="sb">The string builder.</param>
    /// <param name="typeModels">The models to generate.</param>
    public void Generate(StringBuilder sb, IEnumerable<TypeModel> typeModels)
    {
        WriteHeader(sb);

        foreach (var t in TypesUsing)
        {
            sb.AppendFormat("using {0};\n", t);
        }

        // assembly attributes marker
        sb.Append("\n//ASSATTR\n");

        sb.Append("\n");
        sb.AppendFormat("namespace {0}\n", GetModelsNamespace());
        sb.Append("{\n");

        foreach (TypeModel typeModel in typeModels)
        {
            WriteContentType(sb, typeModel);
            sb.Append("\n");
        }

        sb.Append("}\n");
    }

    // internal for unit tests
    public void WriteClrType(StringBuilder sb, Type type)
    {
        var s = type.ToString();

        if (type.IsGenericType)
        {
            var p = s.IndexOf('`');
            WriteNonGenericClrType(sb, s.Substring(0, p));
            sb.Append("<");
            Type[] args = type.GetGenericArguments();
            for (var i = 0; i < args.Length; i++)
            {
                if (i > 0)
                {
                    sb.Append(", ");
                }

                WriteClrType(sb, args[i]);
            }

            sb.Append(">");
        }
        else
        {
            WriteNonGenericClrType(sb, s);
        }
    }

    // writes an attribute that identifies code generated by a tool
    // (helps reduce warnings, tools such as FxCop use it)
    // see https://github.com/zpqrtbnk/Zbu.ModelsBuilder/issues/107
    // see https://docs.microsoft.com/en-us/dotnet/api/system.codedom.compiler.generatedcodeattribute
    // see https://blogs.msdn.microsoft.com/codeanalysis/2007/04/27/correct-usage-of-the-compilergeneratedattribute-and-the-generatedcodeattribute/
    //
    // note that the blog post above clearly states that "Nor should it be applied at the type level if the type being generated is a partial class."
    // and since our models are partial classes, we have to apply the attribute against the individual members, not the class itself.
    private static void WriteGeneratedCodeAttribute(StringBuilder sb, string tabs) => sb.AppendFormat(
        "{0}[global::System.CodeDom.Compiler.GeneratedCodeAttribute(\"Umbraco.ModelsBuilder.Embedded\", \"{1}\")]\n",
        tabs, ApiVersion.Current.Version);

    // writes an attribute that specifies that an output may be null.
    // (useful for consuming projects with nullable reference types enabled)
    private static void WriteMaybeNullAttribute(StringBuilder sb, string tabs, bool isReturn = false) =>
        sb.AppendFormat("{0}[{1}global::System.Diagnostics.CodeAnalysis.MaybeNull]\n", tabs,
            isReturn ? "return: " : string.Empty);

    private static string MixinStaticGetterName(string clrName) => string.Format("Get{0}", clrName);

    private static IEnumerable<string> SplitError(string error)
    {
        var p = 0;
        while (p < error.Length)
        {
            var n = p + 50;
            while (n < error.Length && error[n] != ' ')
            {
                n++;
            }

            if (n >= error.Length)
            {
                break;
            }

            yield return error.Substring(p, n - p);
            p = n + 1;
        }

        if (p < error.Length)
        {
            yield return error[p..];
        }
    }

    private void WriteContentType(StringBuilder sb, TypeModel type)
    {
        string sep;

        if (type.IsMixin)
        {
            // write the interface declaration
            sb.AppendFormat("\t// Mixin Content Type with alias \"{0}\"\n", type.Alias);
            if (!string.IsNullOrWhiteSpace(type.Name))
            {
                sb.AppendFormat("\t/// <summary>{0}</summary>\n", XmlCommentString(type.Name));
            }

            sb.AppendFormat("\tpublic partial interface I{0}", type.ClrName);
            var implements = type.BaseType == null
                ? type.HasBase ? null : type.IsElement ? "PublishedElement" : "PublishedContent"
                : type.BaseType.ClrName;
            if (implements != null)
            {
                sb.AppendFormat(" : I{0}", implements);
            }

            // write the mixins
            sep = implements == null ? ":" : ",";
            foreach (TypeModel mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName))
            {
                sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName);
                sep = ",";
            }

            sb.Append("\n\t{\n");

            // write the properties - only the local (non-ignored) ones, we're an interface
            var more = false;
            foreach (PropertyModel prop in type.Properties.OrderBy(x => x.ClrName))
            {
                if (more)
                {
                    sb.Append("\n");
                }

                more = true;
                WriteInterfaceProperty(sb, prop);
            }

            sb.Append("\t}\n\n");
        }

        // write the class declaration
        if (!string.IsNullOrWhiteSpace(type.Name))
        {
            sb.AppendFormat("\t/// <summary>{0}</summary>\n", XmlCommentString(type.Name));
        }

        // cannot do it now. see note in ImplementContentTypeAttribute
        // if (!type.HasImplement)
        //    sb.AppendFormat("\t[ImplementContentType(\"{0}\")]\n", type.Alias);
        sb.AppendFormat("\t[PublishedModel(\"{0}\")]\n", type.Alias);
        sb.AppendFormat("\tpublic partial class {0}", type.ClrName);
        var inherits = type.HasBase
            ? null // has its own base already
            : type.BaseType == null
                ? GetModelsBaseClassName(type)
                : type.BaseType.ClrName;
        if (inherits != null)
        {
            sb.AppendFormat(" : {0}", inherits);
        }

        sep = inherits == null ? ":" : ",";
        if (type.IsMixin)
        {
            // if it's a mixin it implements its own interface
            sb.AppendFormat("{0} I{1}", sep, type.ClrName);
        }
        else
        {
            // write the mixins, if any, as interfaces
            // only if not a mixin because otherwise the interface already has them already
            foreach (TypeModel mixinType in type.DeclaringInterfaces.OrderBy(x => x.ClrName))
            {
                sb.AppendFormat("{0} I{1}", sep, mixinType.ClrName);
                sep = ",";
            }
        }

        // begin class body
        sb.Append("\n\t{\n");

        // write the constants & static methods
        // as 'new' since parent has its own - or maybe not - disable warning
        sb.Append("\t\t// helpers\n");
        sb.Append("#pragma warning disable 0109 // new is redundant\n");
        WriteGeneratedCodeAttribute(sb, "\t\t");
        sb.AppendFormat(
            "\t\tpublic new const string ModelTypeAlias = \"{0}\";\n",
            type.Alias);
        TypeModel.ItemTypes itemType = type.IsElement ? TypeModel.ItemTypes.Content : type.ItemType; // fixme
        WriteGeneratedCodeAttribute(sb, "\t\t");
        sb.AppendFormat(
            "\t\tpublic new const PublishedItemType ModelItemType = PublishedItemType.{0};\n",
            itemType);
        WriteGeneratedCodeAttribute(sb, "\t\t");
        WriteMaybeNullAttribute(sb, "\t\t", true);
        sb.Append(
            "\t\tpublic new static IPublishedContentType GetModelContentType(IPublishedSnapshotAccessor publishedSnapshotAccessor)\n");
        sb.Append(
            "\t\t\t=> PublishedModelUtility.GetModelContentType(publishedSnapshotAccessor, ModelItemType, ModelTypeAlias);\n");
        WriteGeneratedCodeAttribute(sb, "\t\t");
        WriteMaybeNullAttribute(sb, "\t\t", true);
        sb.AppendFormat(
            "\t\tpublic static IPublishedPropertyType GetModelPropertyType<TValue>(IPublishedSnapshotAccessor publishedSnapshotAccessor, Expression<Func<{0}, TValue>> selector)\n",
            type.ClrName);
        sb.Append(
            "\t\t\t=> PublishedModelUtility.GetModelPropertyType(GetModelContentType(publishedSnapshotAccessor), selector);\n");
        sb.Append("#pragma warning restore 0109\n\n");
        sb.Append("\t\tprivate IPublishedValueFallback _publishedValueFallback;");

        // write the ctor
        sb.AppendFormat(
            "\n\n\t\t// ctor\n\t\tpublic {0}(IPublished{1} content, IPublishedValueFallback publishedValueFallback)\n\t\t\t: base(content, publishedValueFallback)\n\t\t{{\n\t\t\t_publishedValueFallback = publishedValueFallback;\n\t\t}}\n\n",
            type.ClrName, type.IsElement ? "Element" : "Content");

        // write the properties
        sb.Append("\t\t// properties\n");
        WriteContentTypeProperties(sb, type);

        // close the class declaration
        sb.Append("\t}\n");
    }

    private void WriteContentTypeProperties(StringBuilder sb, TypeModel type)
    {
        var staticMixinGetters = true;

        // write the properties
        foreach (PropertyModel prop in type.Properties.OrderBy(x => x.ClrName))
        {
            WriteProperty(sb, type, prop, staticMixinGetters && type.IsMixin ? type.ClrName : null);
        }

        // no need to write the parent properties since we inherit from the parent
        // and the parent defines its own properties. need to write the mixins properties
        // since the mixins are only interfaces and we have to provide an implementation.

        // write the mixins properties
        foreach (TypeModel mixinType in type.ImplementingInterfaces.OrderBy(x => x.ClrName))
        {
            foreach (PropertyModel prop in mixinType.Properties.OrderBy(x => x.ClrName))
            {
                if (staticMixinGetters)
                {
                    WriteMixinProperty(sb, prop, mixinType.ClrName);
                }
                else
                {
                    WriteProperty(sb, mixinType, prop);
                }
            }
        }
    }

    private void WriteMixinProperty(StringBuilder sb, PropertyModel property, string mixinClrName)
    {
        sb.Append("\n");

        // Adds xml summary to each property containing
        // property name and property description
        if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description))
        {
            sb.Append("\t\t///<summary>\n");

            if (!string.IsNullOrWhiteSpace(property.Description))
            {
                sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name),
                    XmlCommentString(property.Description));
            }
            else
            {
                sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name));
            }

            sb.Append("\t\t///</summary>\n");
        }

        WriteGeneratedCodeAttribute(sb, "\t\t");

        if (!property.ModelClrType.IsValueType)
        {
            WriteMaybeNullAttribute(sb, "\t\t");
        }

        sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);

        sb.Append("\t\tpublic virtual ");
        WriteClrType(sb, property.ClrTypeName);

        sb.AppendFormat(
            " {0} => ",
            property.ClrName);
        WriteNonGenericClrType(sb, GetModelsNamespace() + "." + mixinClrName);
        sb.AppendFormat(
            ".{0}(this, _publishedValueFallback);\n",
            MixinStaticGetterName(property.ClrName));
    }

    private void WriteProperty(StringBuilder sb, TypeModel type, PropertyModel property, string? mixinClrName = null)
    {
        var mixinStatic = mixinClrName != null;

        sb.Append("\n");

        if (property.Errors != null)
        {
            sb.Append("\t\t/*\n");
            sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n");
            sb.Append("\t\t *\n");
            var first = true;
            foreach (var error in property.Errors)
            {
                if (first)
                {
                    first = false;
                }
                else
                {
                    sb.Append("\t\t *\n");
                }

                foreach (var s in SplitError(error))
                {
                    sb.Append("\t\t * ");
                    sb.Append(s);
                    sb.Append("\n");
                }
            }

            sb.Append("\t\t *\n");
            sb.Append("\n");
        }

        // Adds xml summary to each property containing
        // property name and property description
        if (!string.IsNullOrWhiteSpace(property.Name) || !string.IsNullOrWhiteSpace(property.Description))
        {
            sb.Append("\t\t///<summary>\n");

            if (!string.IsNullOrWhiteSpace(property.Description))
            {
                sb.AppendFormat("\t\t/// {0}: {1}\n", XmlCommentString(property.Name),
                    XmlCommentString(property.Description));
            }
            else
            {
                sb.AppendFormat("\t\t/// {0}\n", XmlCommentString(property.Name));
            }

            sb.Append("\t\t///</summary>\n");
        }

        WriteGeneratedCodeAttribute(sb, "\t\t");
        if (!property.ModelClrType.IsValueType)
        {
            WriteMaybeNullAttribute(sb, "\t\t");
        }

        sb.AppendFormat("\t\t[ImplementPropertyType(\"{0}\")]\n", property.Alias);

        if (mixinStatic)
        {
            sb.Append("\t\tpublic virtual ");
            WriteClrType(sb, property.ClrTypeName);
            sb.AppendFormat(
                " {0} => {1}(this, _publishedValueFallback);\n",
                property.ClrName, MixinStaticGetterName(property.ClrName));
        }
        else
        {
            sb.Append("\t\tpublic virtual ");
            WriteClrType(sb, property.ClrTypeName);
            sb.AppendFormat(
                " {0} => this.Value",
                property.ClrName);
            if (property.ModelClrType != typeof(object))
            {
                sb.Append("<");
                WriteClrType(sb, property.ClrTypeName);
                sb.Append(">");
            }

            sb.AppendFormat(
                "(_publishedValueFallback, \"{0}\");\n",
                property.Alias);
        }

        if (property.Errors != null)
        {
            sb.Append("\n");
            sb.Append("\t\t *\n");
            sb.Append("\t\t */\n");
        }

        if (!mixinStatic)
        {
            return;
        }

        var mixinStaticGetterName = MixinStaticGetterName(property.ClrName);

        // if (type.StaticMixinMethods.Contains(mixinStaticGetterName)) return;
        sb.Append("\n");

        if (!string.IsNullOrWhiteSpace(property.Name))
        {
            sb.AppendFormat("\t\t/// <summary>Static getter for {0}</summary>\n", XmlCommentString(property.Name));
        }

        WriteGeneratedCodeAttribute(sb, "\t\t");
        if (!property.ModelClrType.IsValueType)
        {
            WriteMaybeNullAttribute(sb, "\t\t", true);
        }

        sb.Append("\t\tpublic static ");
        WriteClrType(sb, property.ClrTypeName);
        sb.AppendFormat(
            " {0}(I{1} that, IPublishedValueFallback publishedValueFallback) => that.Value",
            mixinStaticGetterName, mixinClrName);
        if (property.ModelClrType != typeof(object))
        {
            sb.Append("<");
            WriteClrType(sb, property.ClrTypeName);
            sb.Append(">");
        }

        sb.AppendFormat(
            "(publishedValueFallback, \"{0}\");\n",
            property.Alias);
    }

    private void WriteInterfaceProperty(StringBuilder sb, PropertyModel property)
    {
        if (property.Errors != null)
        {
            sb.Append("\t\t/*\n");
            sb.Append("\t\t * THIS PROPERTY CANNOT BE IMPLEMENTED, BECAUSE:\n");
            sb.Append("\t\t *\n");
            var first = true;
            foreach (var error in property.Errors)
            {
                if (first)
                {
                    first = false;
                }
                else
                {
                    sb.Append("\t\t *\n");
                }

                foreach (var s in SplitError(error))
                {
                    sb.Append("\t\t * ");
                    sb.Append(s);
                    sb.Append("\n");
                }
            }

            sb.Append("\t\t *\n");
            sb.Append("\n");
        }

        if (!string.IsNullOrWhiteSpace(property.Name))
        {
            sb.AppendFormat("\t\t/// <summary>{0}</summary>\n", XmlCommentString(property.Name));
        }

        WriteGeneratedCodeAttribute(sb, "\t\t");
        if (!property.ModelClrType.IsValueType)
        {
            WriteMaybeNullAttribute(sb, "\t\t");
        }

        sb.Append("\t\t");
        WriteClrType(sb, property.ClrTypeName);
        sb.AppendFormat(
            " {0} {{ get; }}\n",
            property.ClrName);

        if (property.Errors != null)
        {
            sb.Append("\n");
            sb.Append("\t\t *\n");
            sb.Append("\t\t */\n");
        }
    }

    internal void WriteClrType(StringBuilder sb, string type)
    {
        var p = type.IndexOf('<');
        if (type.Contains('<'))
        {
            WriteNonGenericClrType(sb, type[..p]);
            sb.Append("<");
            var args = type[(p + 1)..].TrimEnd(Constants.CharArrays.GreaterThan)
                .Split(Constants.CharArrays.Comma); // fixme will NOT work with nested generic types
            for (var i = 0; i < args.Length; i++)
            {
                if (i > 0)
                {
                    sb.Append(", ");
                }

                WriteClrType(sb, args[i]);
            }

            sb.Append(">");
        }
        else
        {
            WriteNonGenericClrType(sb, type);
        }
    }

    private static string XmlCommentString(string s) =>
        s.Replace('<', '{').Replace('>', '}').Replace('\r', ' ').Replace('\n', ' ');

    private void WriteNonGenericClrType(StringBuilder sb, string s)
    {
        // map model types
        s = Regex.Replace(s, @"\{(.*)\}\[\*\]", m => ModelsMap[m.Groups[1].Value + "[]"]);

        // takes care eg of "System.Int32" vs. "int"
        if (_typesMap.TryGetValue(s, out var typeName))
        {
            sb.Append(typeName);
            return;
        }

        // if full type name matches a using clause, strip
        // so if we want Umbraco.Core.Models.IPublishedContent
        // and using Umbraco.Core.Models, then we just need IPublishedContent
        typeName = s;
        string? typeUsing = null;
        var p = typeName.LastIndexOf('.');
        if (p > 0)
        {
            var x = typeName.Substring(0, p);
            if (Using.Contains(x))
            {
                typeName = typeName.Substring(p + 1);
                typeUsing = x;
            }
            else if (x == ModelsNamespace) // that one is used by default
            {
                typeName = typeName.Substring(p + 1);
                typeUsing = ModelsNamespace;
            }
        }

        // nested types *after* using
        typeName = typeName.Replace("+", ".");

        // symbol to test is the first part of the name
        // so if type name is Foo.Bar.Nil we want to ensure that Foo is not ambiguous
        p = typeName.IndexOf('.');
        var symbol = p > 0 ? typeName.Substring(0, p) : typeName;

        // what we should find - WITHOUT any generic <T> thing - just the type
        // no 'using' = the exact symbol
        // a 'using' = using.symbol
        var match = typeUsing == null ? symbol : typeUsing + "." + symbol;

        // if not ambiguous, be happy
        if (!IsAmbiguousSymbol(symbol, match))
        {
            sb.Append(typeName);
            return;
        }

        // symbol is ambiguous
        // if no 'using', must prepend global::
        if (typeUsing == null)
        {
            sb.Append("global::");
            sb.Append(s.Replace("+", "."));
            return;
        }

        // could fullname be non-ambiguous?
        // note: all-or-nothing, not trying to segment the using clause
        typeName = s.Replace("+", ".");
        p = typeName.IndexOf('.');
        symbol = typeName.Substring(0, p);
        match = symbol;

        // still ambiguous, must prepend global::
        if (IsAmbiguousSymbol(symbol, match))
        {
            sb.Append("global::");
        }

        sb.Append(typeName);
    }
}
