// Copyright 2020 The Android Open Source Project
//
// 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
//
//     http://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 rust

import (
	"strings"

	"github.com/google/blueprint"
	"github.com/google/blueprint/proptools"

	"android/soong/android"
	"android/soong/cc"
	cc_config "android/soong/cc/config"
	"android/soong/rust/config"
)

var (
	defaultBindgenFlags = []string{
		"--blocklist-type",
		"max_align_t",
	}

	// bindgen should specify its own Clang revision so updating Clang isn't potentially blocked on bindgen failures.
	bindgenClangVersion = "clang-r563880"

	_ = pctx.VariableFunc("bindgenClangVersion", func(ctx android.PackageVarContext) string {
		if override := ctx.Config().Getenv("LLVM_BINDGEN_PREBUILTS_VERSION"); override != "" {
			return override
		}
		return bindgenClangVersion
	})

	//TODO(b/160803703) Use a prebuilt bindgen instead of the built bindgen.
	_ = pctx.HostBinToolVariable("bindgenCmd", "bindgen")
	_ = pctx.VariableFunc("bindgenHostPrebuiltTag", func(ctx android.PackageVarContext) string {
		if ctx.Config().UseHostMusl() {
			// This is a hack to use the glibc bindgen binary until we have a musl version checked in.
			return "linux-x86"
		} else {
			return "${config.HostPrebuiltTag}"
		}
	})
	_ = pctx.VariableFunc("bindgenClangLibdir", func(ctx android.PackageVarContext) string {
		if ctx.Config().UseHostMusl() {
			return "musl/lib/"
		} else {
			return "lib/"
		}
	})
	_ = pctx.SourcePathVariable("bindgenClang",
		"${cc_config.ClangBase}/${bindgenHostPrebuiltTag}/${bindgenClangVersion}/bin/clang")
	_ = pctx.SourcePathVariable("bindgenLibClang",
		"${cc_config.ClangBase}/${bindgenHostPrebuiltTag}/${bindgenClangVersion}/${bindgenClangLibdir}")

	//TODO(ivanlozano) Switch this to RuleBuilder
	//
	//TODO Pass the flag files directly to bindgen e.g. with @file when it supports that.
	//See https://github.com/rust-lang/rust-bindgen/issues/2508.
	bindgen = pctx.AndroidStaticRule("bindgen",
		blueprint.RuleParams{
			Command: "CLANG_PATH=$bindgenClang LIBCLANG_PATH=$bindgenLibClang RUSTFMT=${config.RustBin}/rustfmt " +
				"$cmd $flags $$(cat $flagfiles) $in -o $out -- -MD -MF $out.d $cflags",
			CommandDeps: []string{"$cmd"},
			Deps:        blueprint.DepsGCC,
			Depfile:     "$out.d",
		},
		"cmd", "flags", "flagfiles", "cflags")
)

func init() {
	android.RegisterModuleType("rust_bindgen", RustBindgenFactory)
	android.RegisterModuleType("rust_bindgen_host", RustBindgenHostFactory)
}

var _ SourceProvider = (*bindgenDecorator)(nil)

type BindgenProperties struct {
	// The wrapper header file. By default this is assumed to be a C header unless the extension is ".hh" or ".hpp".
	// This is used to specify how to interpret the header and determines which '-std' flag to use by default.
	//
	// If your C++ header must have some other extension, then the default behavior can be overridden by setting the
	// cpp_std property.
	Wrapper_src *string `android:"path,arch_variant"`

	// list of bindgen-specific flags and options
	Bindgen_flags []string `android:"arch_variant"`

	// list of files containing extra bindgen flags
	Bindgen_flag_files []string `android:"arch_variant"`

	// module name of a custom binary/script which should be used instead of the 'bindgen' binary. This custom
	// binary must expect arguments in a similar fashion to bindgen, e.g.
	//
	// "my_bindgen [flags] wrapper_header.h -o [output_path] -- [clang flags]"
	Custom_bindgen string

	// flag to indicate if bindgen should handle `static inline` functions (default is false).
	// If true, Static_inline_library must be set.
	Handle_static_inline *bool

	// module name of the corresponding cc_library_static which includes the static_inline wrapper
	// generated functions from bindgen. Must be used together with handle_static_inline.
	//
	// If there are no static inline functions provided through the header file,
	// then bindgen (as of 0.69.2) will silently fail to output a .c file, and
	// the cc_library_static depending on this module will fail compilation.
	Static_inline_library *string
}

type bindgenDecorator struct {
	*BaseSourceProvider

	Properties      BindgenProperties
	ClangProperties cc.RustBindgenClangProperties
}

func (b *bindgenDecorator) getStdVersion(ctx ModuleContext, src android.Path) (string, bool) {
	// Assume headers are C headers
	isCpp := false
	stdVersion := ""

	switch src.Ext() {
	case ".hpp", ".hh":
		isCpp = true
	}

	if String(b.ClangProperties.Cpp_std) != "" && String(b.ClangProperties.C_std) != "" {
		ctx.PropertyErrorf("c_std", "c_std and cpp_std cannot both be defined at the same time.")
	}

	if b.ClangProperties.Cpp_std != nil {
		isCpp = true
		if String(b.ClangProperties.Cpp_std) == "experimental" {
			stdVersion = cc_config.ExperimentalCppStdVersion
		} else if String(b.ClangProperties.Cpp_std) == "default" || String(b.ClangProperties.Cpp_std) == "" {
			stdVersion = cc_config.CppStdVersion
		} else {
			stdVersion = String(b.ClangProperties.Cpp_std)
		}
	} else if b.ClangProperties.C_std != nil {
		isCpp = false
		if String(b.ClangProperties.C_std) == "experimental" {
			stdVersion = cc_config.ExperimentalCStdVersion
		} else if String(b.ClangProperties.C_std) == "default" || String(b.ClangProperties.C_std) == "" {
			stdVersion = cc_config.CStdVersion
		} else {
			stdVersion = String(b.ClangProperties.C_std)
		}
	} else if isCpp {
		stdVersion = cc_config.CppStdVersion
	} else {
		stdVersion = cc_config.CStdVersion
	}

	return stdVersion, isCpp
}

func (b *bindgenDecorator) GenerateSource(ctx ModuleContext, deps PathDeps) android.Path {
	ccToolchain := ctx.RustModule().ccToolchain(ctx)

	var cflags []string
	var implicits android.Paths
	var implicitOutputs android.WritablePaths
	var validations android.Paths

	if Bool(b.Properties.Handle_static_inline) && b.Properties.Static_inline_library == nil {
		ctx.PropertyErrorf("handle_static_inline",
			"requires declaring static_inline_library to the corresponding cc_library module that includes the generated C source from bindgen.")
	}

	if b.Properties.Static_inline_library != nil && !Bool(b.Properties.Handle_static_inline) {
		ctx.PropertyErrorf("static_inline_library",
			"requires declaring handle_static_inline.")
	}

	implicits = append(implicits, deps.depGeneratedHeaders...)

	// Default clang flags
	cflags = append(cflags, "${cc_config.CommonGlobalCflags}")
	if ctx.Device() {
		cflags = append(cflags, "${cc_config.DeviceGlobalCflags}", "-nostdlibinc")
	}

	// Toolchain clang flags
	cflags = append(cflags, "-target "+ccToolchain.ClangTriple())
	cflags = append(cflags, strings.ReplaceAll(ccToolchain.Cflags(), "${config.", "${cc_config."))
	cflags = append(cflags, strings.ReplaceAll(ccToolchain.ToolchainCflags(), "${config.", "${cc_config."))

	if ctx.RustModule().InVendorOrProduct() {
		cflags = append(cflags, "-D__ANDROID_VNDK__")
		if ctx.RustModule().InVendor() {
			cflags = append(cflags, "-D__ANDROID_VENDOR__")
		} else if ctx.RustModule().InProduct() {
			cflags = append(cflags, "-D__ANDROID_PRODUCT__")
		}

		// Define __ANDROID_VENDOR_API__ for both product and vendor variants
		// because they both use the same LLNDK libraries.
		vendorApiLevel := ctx.Config().VendorApiLevel()
		if vendorApiLevel == "" {
			// TODO(b/314036847): This is a fallback for UDC targets.
			// This must be a build failure when UDC is no longer built
			// from this source tree.
			vendorApiLevel = ctx.Config().PlatformSdkVersion().String()
		}
		cflags = append(cflags, "-D__ANDROID_VENDOR_API__="+vendorApiLevel)
	}

	if ctx.RustModule().InRecovery() {
		cflags = append(cflags, "-D__ANDROID_RECOVERY__")
	}

	if mctx, ok := ctx.(*moduleContext); ok && mctx.apexVariationName() != "" {
		cflags = append(cflags, "-D__ANDROID_APEX__")
	}

	if ctx.Target().NativeBridge == android.NativeBridgeEnabled {
		cflags = append(cflags, "-D__ANDROID_NATIVE_BRIDGE__")
	}

	// Dependency clang flags and include paths
	cflags = append(cflags, deps.depClangFlags...)
	for _, include := range deps.depIncludePaths {
		cflags = append(cflags, "-I"+include.String())
	}
	for _, include := range deps.depSystemIncludePaths {
		cflags = append(cflags, "-isystem "+include.String())
	}

	esc := proptools.NinjaAndShellEscapeList

	// Filter out invalid cflags
	cflagsProp := b.ClangProperties.Cflags.GetOrDefault(ctx, nil)
	for _, flag := range cflagsProp {
		if flag == "-x c++" || flag == "-xc++" {
			ctx.PropertyErrorf("cflags",
				"-x c++ should not be specified in cflags; setting cpp_std specifies this is a C++ header, or change the file extension to '.hpp' or '.hh'")
		}
		if strings.HasPrefix(flag, "-std=") {
			ctx.PropertyErrorf("cflags",
				"-std should not be specified in cflags; instead use c_std or cpp_std")
		}
	}

	// Module defined clang flags and include paths
	cflags = append(cflags, esc(cflagsProp)...)
	for _, include := range b.ClangProperties.Local_include_dirs.GetOrDefault(ctx, nil) {
		cflags = append(cflags, "-I"+android.PathForModuleSrc(ctx, include).String())
		implicits = append(implicits, android.PathForModuleSrc(ctx, include))
	}

	bindgenFlags := defaultBindgenFlags
	bindgenFlags = append(bindgenFlags, "--rust-target", config.GetRustVersion(ctx))
	bindgenFlags = append(bindgenFlags, esc(b.Properties.Bindgen_flags)...)
	if Bool(b.Properties.Handle_static_inline) {
		outputStaticFnsFile := android.PathForModuleOut(ctx, b.getStem(ctx)+".c")
		implicitOutputs = append(implicitOutputs, outputStaticFnsFile)
		validations = append(validations, outputStaticFnsFile)
		bindgenFlags = append(bindgenFlags, []string{"--experimental", "--wrap-static-fns", "--wrap-static-fns-path=" + outputStaticFnsFile.String()}...)
	}

	// cat reads from stdin if its command line is empty,
	// so we pass in /dev/null if there are no other flag files
	bindgenFlagFiles := []string{"/dev/null"}
	for _, flagFile := range b.Properties.Bindgen_flag_files {
		bindgenFlagFiles = append(bindgenFlagFiles, android.PathForModuleSrc(ctx, flagFile).String())
		implicits = append(implicits, android.PathForModuleSrc(ctx, flagFile))
	}

	wrapperFile := android.OptionalPathForModuleSrc(ctx, b.Properties.Wrapper_src)
	if !wrapperFile.Valid() {
		ctx.PropertyErrorf("wrapper_src", "invalid path to wrapper source")
	}

	// Add C std version flag
	stdVersion, isCpp := b.getStdVersion(ctx, wrapperFile.Path())
	cflags = append(cflags, "-std="+stdVersion)

	// Specify the header source language to avoid ambiguity.
	if isCpp {
		cflags = append(cflags, "-x c++")
		// Add any C++ only flags.
		cflags = append(cflags, esc(b.ClangProperties.Cppflags.GetOrDefault(ctx, nil))...)
	} else {
		cflags = append(cflags, "-x c")
	}

	// clang-r468909b complains about the -x c in the flags in clang-sys parse_search_paths:
	// clang: error: '-x c' after last input file has no effect [-Werror,-Wunused-command-line-argument]
	cflags = append(cflags, "-Wno-unused-command-line-argument")

	// The Clang version used by CXX can be newer than the one used by Bindgen, and uses warning related flags that
	// it cannot recognize. Turn off unknown warning flags warning.
	cflags = append(cflags, "-Wno-unknown-warning-option")

	// The main file for bindgen usually is header where #pragma once is actually best practice.
	// Don't pollute the build log with unnecessary warnings.
	cflags = append(cflags, "-Wno-pragma-once-outside-header")

	// Suppress warnings while testing a new compiler.
	if ctx.Config().IsEnvTrue("LLVM_NEXT") {
		cflags = append(cflags, "-Wno-everything")
	}

	outputFile := android.PathForModuleOut(ctx, b.getStem(ctx)+".rs")

	var cmd, cmdDesc string
	if b.Properties.Custom_bindgen != "" {
		m := ctx.GetDirectDepProxyWithTag(b.Properties.Custom_bindgen, customBindgenDepTag)
		cmd = android.OtherModuleProviderOrDefault(ctx, m, android.HostToolProviderInfoProvider).HostToolPath.String()
		cmdDesc = b.Properties.Custom_bindgen
	} else {
		cmd = "$bindgenCmd"
		cmdDesc = "bindgen"
	}

	ctx.Build(pctx, android.BuildParams{
		Rule:            bindgen,
		Description:     strings.Join([]string{cmdDesc, wrapperFile.Path().Rel()}, " "),
		Output:          outputFile,
		Input:           wrapperFile.Path(),
		Implicits:       implicits,
		ImplicitOutputs: implicitOutputs,
		Validations:     validations,
		Args: map[string]string{
			"cmd":       cmd,
			"flags":     strings.Join(bindgenFlags, " "),
			"flagfiles": strings.Join(bindgenFlagFiles, " "),
			"cflags":    strings.Join(cflags, " "),
		},
	})

	b.OutputFiles = android.Paths{outputFile}

	// Append any additional implicit outputs after the entry point source.
	// We append any generated .c file here so it can picked up by cc_library_static modules.
	// Those CC modules need to be sure not to pass any included .rs files to Clang.
	// We don't have to worry about the additional .c files for Rust modules as only the entry point
	// is passed to rustc.
	b.OutputFiles = append(b.OutputFiles, implicitOutputs.Paths()...)

	return outputFile
}

func (b *bindgenDecorator) SourceProviderProps() []any {
	return append(b.BaseSourceProvider.SourceProviderProps(),
		&b.Properties, &b.ClangProperties)
}

// rust_bindgen generates Rust FFI bindings to C libraries using bindgen given a wrapper header as the primary input.
// Bindgen has a number of flags to control the generated source, and additional flags can be passed to clang to ensure
// the header and generated source is appropriately handled. It is recommended to add it as a dependency in the
// rlibs or rustlibs property. It may also be added in the srcs property for external crates, using the ":"
// prefix.
func RustBindgenFactory() android.Module {
	module, _ := NewRustBindgen(android.HostAndDeviceSupported)
	return module.Init()
}

func RustBindgenHostFactory() android.Module {
	module, _ := NewRustBindgen(android.HostSupported)
	return module.Init()
}

func NewRustBindgen(hod android.HostOrDeviceSupported) (*Module, *bindgenDecorator) {
	bindgen := &bindgenDecorator{
		BaseSourceProvider: NewSourceProvider(),
		Properties:         BindgenProperties{},
		ClangProperties:    cc.RustBindgenClangProperties{},
	}

	module := NewSourceProviderModule(hod, bindgen, false, false)

	android.AddLoadHook(module, func(ctx android.LoadHookContext) {
		type stub_props struct {
			Visibility []string
		}
		props := &stub_props{[]string{":__subpackages__"}}
		ctx.PrependProperties(props)
	})

	return module, bindgen
}

func (b *bindgenDecorator) SourceProviderDeps(ctx DepsContext, deps Deps) Deps {
	deps = b.BaseSourceProvider.SourceProviderDeps(ctx, deps)
	if ctx.toolchain().Bionic() && !ctx.RustModule().compiler.noStdlibs() {
		deps = bionicDeps(ctx, deps, false)
	} else if ctx.Os() == android.LinuxMusl {
		deps = muslDeps(ctx, deps, false)
	}

	if !ctx.RustModule().Source() && b.Properties.Static_inline_library != nil {
		// This is not the source variant, so add the static inline library as a dependency.
		//
		// This is necessary to avoid a circular dependency between the source variant and the
		// dependent cc module.
		deps.WholeStaticLibs = append(deps.WholeStaticLibs, String(b.Properties.Static_inline_library))
	}

	deps.SharedLibs = append(deps.SharedLibs, b.ClangProperties.Shared_libs.GetOrDefault(ctx, nil)...)
	deps.StaticLibs = append(deps.StaticLibs, b.ClangProperties.Static_libs.GetOrDefault(ctx, nil)...)
	deps.HeaderLibs = append(deps.HeaderLibs, b.ClangProperties.Header_libs.GetOrDefault(ctx, nil)...)
	return deps
}
