// Copyright 2026 Aaron R Robinson
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is furnished
// to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;

namespace DNNE
{
    internal static class C99Emitter
    {
        private const string SafeMacroRegEx = "[^a-zA-Z0-9_]";
        private static readonly C99TypeProvider s_typeProvider = new C99TypeProvider();

        public static void Emit(TextWriter outputStream, string assemblyName, IEnumerable<ExportedMethod> exports, IEnumerable<string> additionalCodeStatements)
        {
            // Convert the assembly name into a supported string for C99 macros.
            var assemblyNameMacroSafe = Regex.Replace(assemblyName, SafeMacroRegEx, "_");
            var generatedHeaderDefine = $"__DNNE_GENERATED_HEADER_{assemblyNameMacroSafe.ToUpperInvariant()}__";
            var compileAsSourceDefine = "DNNE_COMPILE_AS_SOURCE";

            // Emit declaration preamble
            outputStream.WriteLine(
$@"//
// Auto-generated by dnne-gen
//
// .NET Assembly: {assemblyName}
//

//
// Declare exported functions
//
#ifndef {generatedHeaderDefine}
#define {generatedHeaderDefine}

#include <stddef.h>
#include <stdint.h>
#ifdef {compileAsSourceDefine}
    #include <dnne.h>
#else
    // When used as a header file, the assumption is
    // dnne.h will be next to this file.
    #include ""dnne.h""
#endif // !{compileAsSourceDefine}
");

            // Emit additional code statements
            if (additionalCodeStatements.Any())
            {
                outputStream.WriteLine(
$@"//
// Additional code provided by user
//");
                foreach (var stmt in additionalCodeStatements)
                {
                    outputStream.WriteLine(stmt);
                }

                outputStream.WriteLine();
            }

            using var implStream = new StringWriter();

            // Emit definition preamble
            implStream.WriteLine(
$@"//
// Define exported functions
//
#ifdef {compileAsSourceDefine}

#ifdef DNNE_WINDOWS
    #ifdef _WCHAR_T_DEFINED
        typedef wchar_t char_t;
    #else
        typedef unsigned short char_t;
    #endif
#else
    typedef char char_t;
#endif

//
// Forward declarations
//

extern void* get_callable_managed_function(
    const char_t* dotnet_type,
    const char_t* dotnet_type_method,
    const char_t* dotnet_delegate_type);

extern void* get_fast_callable_managed_function(
    const char_t* dotnet_type,
    const char_t* dotnet_type_method);
");

            // Emit string table
            implStream.WriteLine(
@"//
// String constants
//
");
            int count = 1;
            var map = new StringDictionary();
            foreach (var method in exports)
            {
                if (map.ContainsKey(method.EnclosingTypeName))
                {
                    continue;
                }

                string id = $"t{count++}_name";
                implStream.WriteLine(
$@"#ifdef DNNE_TARGET_NET_FRAMEWORK
    static const char_t* {id} = DNNE_STR(""{method.EnclosingTypeName}"");
#else
    static const char_t* {id} = DNNE_STR(""{method.EnclosingTypeName}, {assemblyName}"");
#endif // !DNNE_TARGET_NET_FRAMEWORK
");
                map.Add(method.EnclosingTypeName, id);
            }

            // Emit the exports
            implStream.WriteLine(
@"
//
// Exports
//
");
            foreach (var export in exports)
            {
                (var preguard, var postguard) = GetPlatformGuards(export.Platforms);

                // Create declaration and call signature.
                string delim = "";
                var declsig = new StringBuilder();
                var callsig = new StringBuilder();
                for (int i = 0; i < export.ArgumentTypes.Length; ++i)
                {
                    var argName = export.ArgumentNames[i] ?? $"arg{i}";
                    declsig.AppendFormat("{0}{1} {2}", delim, export.ArgumentTypes[i], argName);
                    callsig.AppendFormat("{0}{1}", delim, argName);
                    delim = ", ";
                }

                // Special casing for void signature.
                if (declsig.Length == 0)
                {
                    declsig.Append("void");
                }

                // Special casing for void return.
                string returnStatementKeyword = "return ";
                if (export.ReturnType.Equals("void"))
                {
                    returnStatementKeyword = string.Empty;
                }

                string callConv = s_typeProvider.MapCallConv(export.CallingConvention);

                string classNameConstant = map[export.EnclosingTypeName];
                Debug.Assert(!string.IsNullOrEmpty(classNameConstant));

                // Generate the acquire managed function based on the export type.
                string acquireManagedFunction;
                if (export.Type == ExportType.Export)
                {
                    acquireManagedFunction =
$@"const char_t* methodName = DNNE_STR(""{export.MethodName}"");
        const char_t* delegateType = DNNE_STR(""{export.EnclosingTypeName}+{export.MethodName}Delegate, {assemblyName}"");
        {export.ExportName}_ptr = ({export.ReturnType}({callConv}*)({declsig}))get_callable_managed_function({classNameConstant}, methodName, delegateType);";

                }
                else
                {
                    Debug.Assert(export.Type == ExportType.UnmanagedCallersOnly);
                    acquireManagedFunction =
$@"const char_t* methodName = DNNE_STR(""{export.MethodName}"");
        {export.ExportName}_ptr = ({export.ReturnType}({callConv}*)({declsig}))get_fast_callable_managed_function({classNameConstant}, methodName);";
                }

                // Declare export
                outputStream.WriteLine(
$@"{preguard}// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName}{export.XmlDoc}
DNNE_EXTERN_C DNNE_API {export.ReturnType} {callConv} {export.ExportName}({declsig});
{postguard}");

                // Define export in implementation stream
                implStream.WriteLine(
$@"{preguard}// Computed from {export.EnclosingTypeName}{Type.Delimiter}{export.MethodName}
static {export.ReturnType} ({callConv}* {export.ExportName}_ptr)({declsig});
DNNE_EXTERN_C DNNE_API {export.ReturnType} {callConv} {export.ExportName}({declsig})
{{
    if ({export.ExportName}_ptr == NULL)
    {{
        {acquireManagedFunction}
    }}
    {returnStatementKeyword}{export.ExportName}_ptr({callsig});
}}
{postguard}");
            }

            // Emit implementation closing
            implStream.Write($"#endif // {compileAsSourceDefine}");

            // Emit output closing for header and merge in implementation
            outputStream.WriteLine(
$@"#endif // {generatedHeaderDefine}

{implStream}");
        }

        private static (string preguard, string postguard) GetPlatformGuards(in PlatformSupport platformSupport)
        {
            var pre = new StringBuilder();
            var post = new StringBuilder();

            var postAssembly = ConvertScope(platformSupport.Assembly, ref pre);
            var postModule = ConvertScope(platformSupport.Module, ref pre);
            var postType = ConvertScope(platformSupport.Type, ref pre);
            var postMethod = ConvertScope(platformSupport.Method, ref pre);

            // Append the post guards in reverse order
            post.Append(postMethod);
            post.Append(postType);
            post.Append(postModule);
            post.Append(postAssembly);

            return (pre.ToString(), post.ToString());

            static string ConvertScope(in Scope scope, ref StringBuilder pre)
            {
                (string pre_support, string post_support) = ConvertCollection(scope.Support, "(", ")");
                (string pre_nosupport, string post_nosupport) = ConvertCollection(scope.NoSupport, "!(", ")");

                var post = new StringBuilder();
                if (!string.IsNullOrEmpty(pre_support)
                    || !string.IsNullOrEmpty(pre_nosupport))
                {
                    // Add the preamble for the guard
                    pre.Append("#if ");
                    post.Append("#endif // ");

                    // Append the "support" clauses because if they don't exist they are string.Empty
                    pre.Append(pre_support);
                    post.Append(post_support);

                    // Check if we need to chain the clauses
                    if (!string.IsNullOrEmpty(pre_support) && !string.IsNullOrEmpty(pre_nosupport))
                    {
                        pre.Append(" && ");
                        post.Append(" && ");
                    }

                    // Append the "nosupport" clauses because if they don't exist they are string.Empty
                    pre.Append($"{pre_nosupport}");
                    post.Append($"{post_nosupport}");

                    pre.Append('\n');
                    post.Append('\n');
                }

                return post.ToString();
            }

            static (string pre, string post) ConvertCollection(in IEnumerable<OSPlatform> platforms, in string prefix, in string suffix)
            {
                var pre = new StringBuilder();
                var post = new StringBuilder();

                var delim = prefix;
                foreach (OSPlatform os in platforms)
                {
                    if (pre.Length != 0)
                    {
                        delim = " || ";
                    }

                    var platformMacroSafe = Regex.Replace(os.ToString(), SafeMacroRegEx, "_").ToUpperInvariant();
                    pre.Append($"{delim}defined({platformMacroSafe})");
                    post.Append($"{post}{delim}{platformMacroSafe}");
                }

                if (pre.Length != 0)
                {
                    pre.Append(suffix);
                    post.Append(suffix);
                }

                return (pre.ToString(), post.ToString());
            }
        }
    }
}
