// Copyright (C) 2024 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 libbpf_prog

import (
	"fmt"
	"io"
	"runtime"
	"strings"

	"android/soong/android"
	"android/soong/cc"
	"android/soong/genrule"

	"github.com/google/blueprint"
)

type libbpfProgDepType struct {
	blueprint.BaseDependencyTag
}

func init() {
	registerLibbpfProgBuildComponents(android.InitRegistrationContext)
	pctx.Import("android/soong/cc/config")
	pctx.StaticVariable("relPwd", cc.PwdPrefix())
}

var (
	pctx = android.NewPackageContext("android/soong/bpf/libbpf_prog")

	libbpfProgCcRule = pctx.AndroidStaticRule("libbpfProgCcRule",
		blueprint.RuleParams{
			Depfile:     "${out}.d",
			Deps:        blueprint.DepsGCC,
			Command:     "$relPwd $ccCmd --target=bpf -c $cFlags -MD -MF ${out}.d -o $out $in",
			CommandDeps: []string{"$ccCmd"},
		},
		"ccCmd", "cFlags")

	libbpfProgStripRule = pctx.AndroidStaticRule("libbpfProgStripRule",
		blueprint.RuleParams{
			Command: `$stripCmd --strip-unneeded --remove-section=.rel.BTF ` +
				`--remove-section=.rel.BTF.ext --remove-section=.BTF.ext $in -o $out`,
			CommandDeps: []string{"$stripCmd"},
		},
		"stripCmd")

	libbpfProgDepTag = libbpfProgDepType{}
)

func registerLibbpfProgBuildComponents(ctx android.RegistrationContext) {
	ctx.RegisterModuleType("libbpf_defaults", defaultsFactory)
	ctx.RegisterModuleType("libbpf_prog", LibbpfProgFactory)
}

var PrepareForTestWithLibbpfProg = android.GroupFixturePreparers(
	android.FixtureRegisterWithContext(registerLibbpfProgBuildComponents),
	android.FixtureAddFile("libbpf_headers/Foo.h", nil),
	android.FixtureAddFile("libbpf_headers/Android.bp", []byte(`
		genrule {
			name: "libbpf_headers",
			out: ["foo.h",],
		}
	`)),
	genrule.PrepareForTestWithGenRuleBuildComponents,
)

type LibbpfProgProperties struct {
	// source paths to the files.
	Srcs []string `android:"path"`

	// additional cflags that should be used to build the libbpf variant of
	// the C/C++ module.
	Cflags []string `android:"arch_variant"`

	// list of directories relative to the Blueprint file that will
	// be added to the include path using -I
	Local_include_dirs []string `android:"arch_variant"`

	Header_libs []string `android:"arch_variant"`

	// optional subdirectory under which this module is installed into.
	Relative_install_path string
}

type libbpfProg struct {
	android.ModuleBase
	android.DefaultableModuleBase
	properties LibbpfProgProperties
	objs       android.Paths
}

var _ android.ImageInterface = (*libbpfProg)(nil)

func (libbpf *libbpfProg) ImageMutatorBegin(ctx android.ImageInterfaceContext) {}

func (libbpf *libbpfProg) VendorVariantNeeded(ctx android.ImageInterfaceContext) bool {
	return false
}

func (libbpf *libbpfProg) ProductVariantNeeded(ctx android.ImageInterfaceContext) bool {
	return false
}

func (libbpf *libbpfProg) CoreVariantNeeded(ctx android.ImageInterfaceContext) bool {
	return true
}

func (libbpf *libbpfProg) RamdiskVariantNeeded(ctx android.ImageInterfaceContext) bool {
	return false
}

func (libbpf *libbpfProg) VendorRamdiskVariantNeeded(ctx android.ImageInterfaceContext) bool {
	return false
}

func (libbpf *libbpfProg) DebugRamdiskVariantNeeded(ctx android.ImageInterfaceContext) bool {
	return false
}

func (libbpf *libbpfProg) RecoveryVariantNeeded(ctx android.ImageInterfaceContext) bool {
	return false
}

func (libbpf *libbpfProg) ExtraImageVariations(ctx android.ImageInterfaceContext) []string {
	return nil
}

func (libbpf *libbpfProg) SetImageVariation(ctx android.ImageInterfaceContext, variation string) {
}

func (libbpf *libbpfProg) DepsMutator(ctx android.BottomUpMutatorContext) {
	ctx.AddDependency(ctx.Module(), libbpfProgDepTag, "libbpf_headers")
	ctx.AddVariationDependencies(nil, cc.HeaderDepTag(), libbpf.properties.Header_libs...)
}

func (libbpf *libbpfProg) GenerateAndroidBuildActions(ctx android.ModuleContext) {
	var cFlagsDeps android.Paths
	cflags := []string{
		"-nostdlibinc",

		// Make paths in deps files relative
		"-no-canonical-prefixes",

		"-O2",
		"-Wall",
		"-Werror",
		"-Wextra",
		// Flag to assist with the transition to libbpf
		"-DENABLE_LIBBPF",
		"-isystem bionic/libc/include",
		"-isystem bionic/libc/kernel/uapi",
		// The architecture doesn't matter here, but asm/types.h is included by linux/types.h.
		"-isystem bionic/libc/kernel/uapi/asm-arm64",
		"-isystem bionic/libc/kernel/android/uapi",
		"-I " + ctx.ModuleDir(),
		"-g", //Libbpf builds require BTF data
	}

	if runtime.GOOS != "darwin" {
		cflags = append(cflags, "-fdebug-prefix-map=/proc/self/cwd=")
	}

	ctx.VisitDirectDeps(func(dep android.Module) {
		depTag := ctx.OtherModuleDependencyTag(dep)
		if depTag == libbpfProgDepTag {
			if genRule, ok := dep.(genrule.SourceFileGenerator); ok {
				cFlagsDeps = append(cFlagsDeps, genRule.GeneratedDeps()...)
				dirs := genRule.GeneratedHeaderDirs()
				for _, dir := range dirs {
					cflags = append(cflags, "-I "+dir.String())
				}
			} else {
				depName := ctx.OtherModuleName(dep)
				ctx.ModuleErrorf("module %q is not a genrule", depName)
			}
		} else if depTag == cc.HeaderDepTag() {
			depExporterInfo, _ := android.OtherModuleProvider(ctx, dep, cc.FlagExporterInfoProvider)
			for _, dir := range depExporterInfo.IncludeDirs {
				cflags = append(cflags, "-I "+dir.String())
			}
		}
	})

	for _, dir := range android.PathsForModuleSrc(ctx, libbpf.properties.Local_include_dirs) {
		cflags = append(cflags, "-I "+dir.String())
	}

	cflags = append(cflags, libbpf.properties.Cflags...)

	srcs := android.PathsForModuleSrc(ctx, libbpf.properties.Srcs)

	for _, src := range srcs {
		if strings.ContainsRune(src.Base(), '_') {
			ctx.ModuleErrorf("invalid character '_' in source name")
		}
		obj := android.ObjPathWithExt(ctx, "unstripped", src, "bpf")

		ctx.Build(pctx, android.BuildParams{
			Rule:      libbpfProgCcRule,
			Input:     src,
			Implicits: cFlagsDeps,
			Output:    obj,
			Args: map[string]string{
				"cFlags": strings.Join(cflags, " "),
				"ccCmd":  "${config.ClangBin}/clang",
			},
		})

		objStripped := android.ObjPathWithExt(ctx, "", src, "bpf")
		ctx.Build(pctx, android.BuildParams{
			Rule:   libbpfProgStripRule,
			Input:  obj,
			Output: objStripped,
			Args: map[string]string{
				"stripCmd": "${config.ClangBin}/llvm-strip",
			},
		})
		libbpf.objs = append(libbpf.objs, objStripped.WithoutRel())
	}

	installDir := android.PathForModuleInstall(ctx, "etc", "bpf")
	if len(libbpf.properties.Relative_install_path) > 0 {
		installDir = installDir.Join(ctx, libbpf.properties.Relative_install_path)
	}
	for _, obj := range libbpf.objs {
		ctx.PackageFile(installDir, obj.Base(), obj)
	}

	android.SetProvider(ctx, blueprint.SrcsFileProviderKey, blueprint.SrcsFileProviderData{SrcPaths: srcs.Strings()})

	ctx.SetOutputFiles(libbpf.objs, "")
}

func (libbpf *libbpfProg) AndroidMk() android.AndroidMkData {
	return android.AndroidMkData{
		Custom: func(w io.Writer, name, prefix, moduleDir string, data android.AndroidMkData) {
			var names []string
			fmt.Fprintln(w)
			fmt.Fprintln(w, "LOCAL_PATH :=", moduleDir)
			fmt.Fprintln(w)
			var localModulePath string
			localModulePath = "LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/bpf"
			if len(libbpf.properties.Relative_install_path) > 0 {
				localModulePath += "/" + libbpf.properties.Relative_install_path
			}
			for _, obj := range libbpf.objs {
				objName := name + "_" + obj.Base()
				names = append(names, objName)
				fmt.Fprintln(w, "include $(CLEAR_VARS)", " # libbpf.libbpf.obj")
				fmt.Fprintln(w, "LOCAL_MODULE := ", objName)
				fmt.Fprintln(w, "LOCAL_PREBUILT_MODULE_FILE :=", obj.String())
				fmt.Fprintln(w, "LOCAL_MODULE_STEM :=", obj.Base())
				fmt.Fprintln(w, "LOCAL_MODULE_CLASS := ETC")
				fmt.Fprintln(w, localModulePath)
				// AconfigUpdateAndroidMkData may have added elements to Extra.  Process them here.
				for _, extra := range data.Extra {
					extra(w, nil)
				}
				fmt.Fprintln(w, "include $(BUILD_PREBUILT)")
				fmt.Fprintln(w)
			}
			fmt.Fprintln(w, "include $(CLEAR_VARS)", " # libbpf.libbpf")
			fmt.Fprintln(w, "LOCAL_MODULE := ", name)
			android.AndroidMkEmitAssignList(w, "LOCAL_REQUIRED_MODULES", names)
			fmt.Fprintln(w, "include $(BUILD_PHONY_PACKAGE)")
		},
	}
}

type Defaults struct {
	android.ModuleBase
	android.DefaultsModuleBase
}

func defaultsFactory() android.Module {
	return DefaultsFactory()
}

func DefaultsFactory(props ...interface{}) android.Module {
	module := &Defaults{}

	module.AddProperties(props...)
	module.AddProperties(&LibbpfProgProperties{})

	android.InitDefaultsModule(module)

	return module
}

func LibbpfProgFactory() android.Module {
	module := &libbpfProg{}

	module.AddProperties(&module.properties)
	android.InitAndroidArchModule(module, android.DeviceSupported, android.MultilibFirst)
	android.InitDefaultableModule(module)

	return module
}
