// Copyright 2026 The BoringSSL Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"bytes"
	"fmt"
	"maps"
	"os"
	"os/exec"
	"path/filepath"
	"slices"
	"strings"

	"boringssl.googlesource.com/boringssl.git/util/idextractor"
)

// platformDependentRedefineExtnameSymbols is the list of symbols in the public
// headers that are not enabled on all platforms, and that can use redefine_extname.
//
// They will always be included in the prefixing headers.
var platformDependentRedefineExtnameSymbols = []string{
	"CRYPTO_has_broken_NEON",
	"CRYPTO_needs_hwcap2_workaround",
	"CRYPTO_set_fuzzer_mode",
	"RAND_enable_fork_unsafe_buffering",
	"RAND_disable_fork_unsafe_buffering",
	"RAND_reset_for_fuzzing",
}

// platformDependentRedefineExtnameSymbols is the list of symbols in the public
// headers that are not enabled on all platforms, and that must be renamed using macros.
//
// They will always be included in the prefixing headers.
var platformDependentMacroSymbols = []string{}

// isClangCL returns whether the program given is likely the `clang-cl` driver.
func isClangCL(clang string) (bool, error) {
	// We probably could be smarter here than just using the binary name.
	return strings.TrimSuffix(strings.ToLower(filepath.Base(clang)), ".exe") == "clang-cl", nil
}

// cSymbolData is data for generating the C renaming includes.
type cSymbolData struct {
	inlineDefinitions  map[string]struct{}
	externDeclarations map[string]struct{}
}

// CollectCSymbols calls Clang to extract the AST of the headers, then processes them to extract the symbols.
//
// It returns data for generating C and Rust includes.
func CollectCSymbols(headers []string) (syms cSymbolData, err error) {
	cmd := *clangPath
	if cmd == "" {
		return cSymbolData{}, fmt.Errorf("%w: clang has been disabled by flag", TaskSkipped)
	}

	defer func() {
		if err != nil {
			err = fmt.Errorf("%w; note that this step can be turned off by passing -clang=", err)
		}
	}()

	isCL, err := isClangCL(cmd)
	if err != nil {
		return cSymbolData{}, err
	}

	var args []string
	if isCL {
		// If using clang-cl.exe, args need to be in CL form.
		args = []string{
			"/TP",
			"/std:c++17",
			"/Zs",
			"-Xclang", "-ast-dump=json",
			"/I", "include",
			"-",
		}
	} else {
		// Standard Clang args.
		args = []string{
			"-x", "c++",
			"-std=c++17",
			"-fsyntax-only",
			"-Xclang", "-ast-dump=json",
			"-Iinclude",
			"-",
		}
	}

	var stdin bytes.Buffer
	for _, header := range headers {
		fmt.Fprintf(&stdin, "#include <%s>\n", strings.TrimPrefix(filepath.ToSlash(header), "include/"))
	}

	c := exec.Command(cmd, args...)
	c.Stdin = &stdin
	c.Stderr = os.Stderr

	stdout, err := c.StdoutPipe()
	if err != nil {
		return cSymbolData{}, err
	}
	defer stdout.Close()

	err = c.Start()
	if err != nil {
		return cSymbolData{}, err
	}

	syms.externDeclarations = map[string]struct{}{}
	for _, sym := range platformDependentRedefineExtnameSymbols {
		syms.externDeclarations[sym] = struct{}{}
	}

	syms.inlineDefinitions = map[string]struct{}{}
	for _, sym := range platformDependentMacroSymbols {
		syms.inlineDefinitions[sym] = struct{}{}
	}

	report := func(id idextractor.IdentifierInfo) error {
		switch id.Symbol {
		case "begin", "end":
			// Template specializations for STL use, namespaced in template arguments.
			return nil
		case id.Identifier:
			// So it's not namespaced. Proceed.
		default:
			// Already in a namespace.
			return nil
		}
		var isInline bool
		switch id.Linkage {
		case "", "static", "static inline":
			// Definitely not linked.
			return nil
		case `extern "C" inline`, `extern "C++" inline`:
			// Sorry, can't redefine_extname inline functions:
			// error: #pragma redefine_extname is applicable to external C declarations only; not applied to function
			isInline = true
		case `extern "C"`:
			// Link those.
			isInline = false
		default:
			return fmt.Errorf("unexpected linkage: %q", id.Linkage)
		}
		switch id.Tag {
		case "enumerator", "typedef", "using":
			// These never create any symbols and are safe to ignore.
			return nil
		case "class", "enum", "struct", "union":
			// These may create symbols when used as a template argument,
			// however cannot be namespaced as known callers forward declare them.
			return nil
		case "function", "var":
			if isInline {
				syms.inlineDefinitions[id.Symbol] = struct{}{}
			} else {
				syms.externDeclarations[id.Symbol] = struct{}{}
			}
			return nil
		default:
			return fmt.Errorf("unexpected tag in %+v", id)
		}
	}

	for sym := range syms.inlineDefinitions {
		if _, found := syms.externDeclarations[sym]; found {
			return cSymbolData{}, fmt.Errorf("symbol %q both marked as extern and inline type symbol renaming; please fix", sym)
		}
	}

	err = idextractor.New(report, idextractor.Options{Language: "C++"}).Parse(stdout)
	if err != nil {
		c.Process.Kill()
		return cSymbolData{}, err
	}

	err = c.Wait()
	if err != nil {
		return cSymbolData{}, err
	}

	return syms, nil
}

func BuildCRenamingInclude(syms cSymbolData) []byte {
	var output bytes.Buffer
	writeHeader(&output, "//")
	output.WriteString(`
#ifndef OPENSSL_HEADER_PREFIX_SYMBOLS_H
#define OPENSSL_HEADER_PREFIX_SYMBOLS_H

#include <openssl/opensslconf.h>  // For BORINGSSL_ALWAYS_USE_STATIC_INLINE.


#if defined(BORINGSSL_PREFIX)

#if defined(__USER_LABEL_PREFIX__)
#define BORINGSSL_USER_LABEL_PREFIX __USER_LABEL_PREFIX__
#else
#define BORINGSSL_USER_LABEL_PREFIX
#endif

#define BORINGSSL_CONCAT_INNER(a, b) a##b
#define BORINGSSL_CONCAT(a, b) BORINGSSL_CONCAT_INNER(a, b)
#define BORINGSSL_ADD_PREFIX(s) BORINGSSL_CONCAT(BORINGSSL_PREFIX, BORINGSSL_CONCAT(_, s))
#define BORINGSSL_ADD_USER_LABEL_AND_PREFIX(s) BORINGSSL_CONCAT(BORINGSSL_USER_LABEL_PREFIX, BORINGSSL_ADD_PREFIX(s))
`)

	// Extern functions: use #pragma redefine_extname if supported, macros if not.
	// They may be called by asm code.
	output.WriteString("\n")
	output.WriteString("#if defined(__PRAGMA_REDEFINE_EXTNAME) && !defined(__ASSEMBLER__)\n")
	output.WriteString("\n")
	for _, sym := range slices.Sorted(maps.Keys(syms.externDeclarations)) {
		fmt.Fprintf(&output, "#pragma redefine_extname %s BORINGSSL_ADD_USER_LABEL_AND_PREFIX(%s)\n", sym, sym)
	}
	output.WriteString("\n")
	output.WriteString("#else  // __PRAGMA_REDEFINE_EXTNAME && !__ASSEMBLER__\n")
	output.WriteString("\n")
	for _, sym := range slices.Sorted(maps.Keys(syms.externDeclarations)) {
		fmt.Fprintf(&output, "#define %s BORINGSSL_ADD_PREFIX(%s)\n", sym, sym)
	}
	output.WriteString("\n")
	output.WriteString("#endif  // __PRAGMA_REDEFINE_EXTNAME && !__ASSEMBLER__\n")

	// Inline functions: they only need renaming in C++ when using inline (nothing to do when using static inline).
	// In that case, use #pragma redefine_extname if supported (only clang supports it on inline functions), macros if not.
	// They may not be called by asm code as they usually aren't even generated.
	output.WriteString("\n")
	output.WriteString("#if !defined(BORINGSSL_ALWAYS_USE_STATIC_INLINE) && !defined(__ASSEMBLER__)\n")
	output.WriteString("\n")
	// Extern functions: use #pragma redefine_extname.
	output.WriteString("#if defined(__PRAGMA_REDEFINE_EXTNAME) && defined(__clang__)\n")
	output.WriteString("\n")
	for _, sym := range slices.Sorted(maps.Keys(syms.inlineDefinitions)) {
		fmt.Fprintf(&output, "#pragma redefine_extname %s BORINGSSL_ADD_USER_LABEL_AND_PREFIX(%s)\n", sym, sym)
	}
	output.WriteString("\n")
	output.WriteString("#else  // __PRAGMA_REDEFINE_EXTNAME && __clang__\n")
	output.WriteString("\n")
	for _, sym := range slices.Sorted(maps.Keys(syms.inlineDefinitions)) {
		fmt.Fprintf(&output, "#define %s BORINGSSL_ADD_PREFIX(%s)\n", sym, sym)
	}
	output.WriteString("\n")
	output.WriteString("#endif  // __PRAGMA_REDEFINE_EXTNAME && __clang__\n")
	output.WriteString("\n")
	output.WriteString("#endif  // !BORINGSSL_ALWAYS_USE_STATIC_INLINE && !__ASSEMBLER__\n")

	// In theory there could be a third category for types etc. that always uses #define.
	// However, such a category is not necessary yet.

	output.WriteString(`
#endif  // BORINGSSL_PREFIX

#endif  // OPENSSL_HEADER_PREFIX_SYMBOLS_H
`)
	return output.Bytes()
}
