// 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 fsgen

import (
	"fmt"
	"slices"
	"strings"
	"sync"

	"android/soong/android"

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

func RegisterCollectFileSystemDepsMutators(ctx android.RegisterMutatorsContext) {
	ctx.BottomUp("fs_collect_deps", collectDepsMutator).MutatesGlobalState()
	ctx.BottomUp("fs_set_deps", setDepsMutator)
}

var fsGenStateOnceKey = android.NewOnceKey("FsGenState")
var fsGenRemoveOverridesOnceKey = android.NewOnceKey("FsGenRemoveOverrides")

// Map of partition module name to its partition that may be generated by Soong.
// Note that it is not guaranteed that all modules returned by this function are successfully
// created.
func getAllSoongGeneratedPartitionNames(config android.Config, partitions []string) map[string]string {
	ret := map[string]string{}
	for _, partition := range partitions {
		ret[generatedModuleNameForPartition(config, partition)] = partition
	}
	return ret
}

type depCandidateProps struct {
	Namespace string
	Multilib  string
	Arch      []android.ArchType
}

// Map of module name to depCandidateProps
type multilibDeps map[string]*depCandidateProps

// Information necessary to generate the filesystem modules, including details about their
// dependencies
type FsGenState struct {
	// List of modules in `PRODUCT_PACKAGES` and `PRODUCT_PACKAGES_DEBUG`
	depCandidates []string
	// Map of names of partition to the information of modules to be added as deps
	fsDeps map[string]*multilibDeps
	// List of name of partitions to be generated by the filesystem_creator module
	soongGeneratedPartitions []string
	// Mutex to protect the fsDeps
	fsDepsMutex sync.Mutex
	// Map of _all_ soong module names to their corresponding installation properties
	moduleToInstallationProps map[string]installationProperties
	// List of prebuilt_* modules that are autogenerated.
	generatedPrebuiltEtcModuleNames []string
	// Mapping from a path to an avb key to the name of a filegroup module that contains it
	avbKeyFilegroups map[string]string
}

type installationProperties struct {
	Required  []string
	Overrides []string
}

func defaultDepCandidateProps(config android.Config) *depCandidateProps {
	return &depCandidateProps{
		Namespace: ".",
		Arch:      []android.ArchType{config.BuildArch},
	}
}

func createFsGenState(ctx android.LoadHookContext, generatedPrebuiltEtcModuleNames []string, avbpubkeyGenerated bool) *FsGenState {
	return ctx.Config().Once(fsGenStateOnceKey, func() interface{} {
		partitionVars := ctx.Config().ProductVariables().PartitionVarsForSoongMigrationOnlyDoNotUse
		candidates := android.FirstUniqueStrings(android.Concat(partitionVars.ProductPackages, partitionVars.ProductPackagesDebug))
		candidates = android.Concat(candidates, generatedPrebuiltEtcModuleNames)

		fsGenState := FsGenState{
			depCandidates: candidates,
			fsDeps: map[string]*multilibDeps{
				// These additional deps are added according to the cuttlefish system image bp.
				"system": {
					// keep-sorted start
					"com.android.apex.cts.shim.v1_prebuilt":     defaultDepCandidateProps(ctx.Config()),
					"dex_bootjars":                              defaultDepCandidateProps(ctx.Config()),
					"framework_compatibility_matrix.device.xml": defaultDepCandidateProps(ctx.Config()),
					"init.environ.rc-soong":                     defaultDepCandidateProps(ctx.Config()),
					"libcompiler_rt":                            defaultDepCandidateProps(ctx.Config()),
					"libdmabufheap":                             defaultDepCandidateProps(ctx.Config()),
					"libgsi":                                    defaultDepCandidateProps(ctx.Config()),
					"llndk.libraries.txt":                       defaultDepCandidateProps(ctx.Config()),
					"logpersist.start":                          defaultDepCandidateProps(ctx.Config()),
					"update_engine_sideload":                    defaultDepCandidateProps(ctx.Config()),
					// keep-sorted end
				},
				"vendor": {
					"fs_config_files_vendor":                               defaultDepCandidateProps(ctx.Config()),
					"fs_config_dirs_vendor":                                defaultDepCandidateProps(ctx.Config()),
					generatedModuleName(ctx.Config(), "vendor-build.prop"): defaultDepCandidateProps(ctx.Config()),
				},
				"odm": {
					// fs_config_* files are automatically installed for all products with odm partitions.
					// https://cs.android.com/android/_/android/platform/build/+/e4849e87ab660b59a6501b3928693db065ee873b:tools/fs_config/Android.mk;l=34;drc=8d6481b92c4b4e9b9f31a61545b6862090fcc14b;bpv=1;bpt=0
					"fs_config_files_odm": defaultDepCandidateProps(ctx.Config()),
					"fs_config_dirs_odm":  defaultDepCandidateProps(ctx.Config()),
				},
				"product": {},
				"system_ext": {
					// VNDK apexes are automatically included.
					// This hardcoded list will need to be updated if `PRODUCT_EXTRA_VNDK_VERSIONS` is updated.
					// https://cs.android.com/android/_/android/platform/build/+/adba533072b00c53ac0f198c550a3cbd7a00e4cd:core/main.mk;l=984;bpv=1;bpt=0;drc=174db7b179592cf07cbfd2adb0119486fda911e7
					"com.android.vndk.v30": defaultDepCandidateProps(ctx.Config()),
					"com.android.vndk.v31": defaultDepCandidateProps(ctx.Config()),
					"com.android.vndk.v32": defaultDepCandidateProps(ctx.Config()),
					"com.android.vndk.v33": defaultDepCandidateProps(ctx.Config()),
					"com.android.vndk.v34": defaultDepCandidateProps(ctx.Config()),
				},
				"userdata": {},
				"system_dlkm": {
					// these are phony required deps of the phony fs_config_dirs_nonsystem
					"fs_config_dirs_system_dlkm":  defaultDepCandidateProps(ctx.Config()),
					"fs_config_files_system_dlkm": defaultDepCandidateProps(ctx.Config()),
					// build props are automatically added to `ALL_DEFAULT_INSTALLED_MODULES`
					"system_dlkm-build.prop": defaultDepCandidateProps(ctx.Config()),
				},
				"vendor_dlkm": {
					"fs_config_dirs_vendor_dlkm":  defaultDepCandidateProps(ctx.Config()),
					"fs_config_files_vendor_dlkm": defaultDepCandidateProps(ctx.Config()),
					"vendor_dlkm-build.prop":      defaultDepCandidateProps(ctx.Config()),
				},
				"odm_dlkm": {
					"fs_config_dirs_odm_dlkm":  defaultDepCandidateProps(ctx.Config()),
					"fs_config_files_odm_dlkm": defaultDepCandidateProps(ctx.Config()),
					"odm_dlkm-build.prop":      defaultDepCandidateProps(ctx.Config()),
				},
				"ramdisk":        {},
				"vendor_ramdisk": {},
				"recovery":       {},
			},
			fsDepsMutex:                     sync.Mutex{},
			moduleToInstallationProps:       map[string]installationProperties{},
			generatedPrebuiltEtcModuleNames: generatedPrebuiltEtcModuleNames,
			avbKeyFilegroups:                map[string]string{},
		}

		if avbpubkeyGenerated {
			(*fsGenState.fsDeps["product"])["system_other_avbpubkey"] = defaultDepCandidateProps(ctx.Config())
		}

		if len(ctx.Config().DeviceManifestFiles()) > 0 {
			(*fsGenState.fsDeps["vendor"])["vendor_manifest.xml"] = defaultDepCandidateProps(ctx.Config())
		}

		// Add common resources `prebuilt_res` module as dep of recovery partition
		(*fsGenState.fsDeps["recovery"])[fmt.Sprintf("recovery-resources-common-%s", getDpi(ctx))] = defaultDepCandidateProps(ctx.Config())

		return &fsGenState
	}).(*FsGenState)
}

func checkDepModuleInMultipleNamespaces(mctx android.BottomUpMutatorContext, foundDeps multilibDeps, module string, partitionName string) {
	otherNamespace := mctx.Namespace().Path
	if val, found := foundDeps[module]; found && otherNamespace != "." && !android.InList(val.Namespace, []string{".", otherNamespace}) {
		mctx.ModuleErrorf("found in multiple namespaces(%s and %s) when including in %s partition", val.Namespace, otherNamespace, partitionName)
	}
}

func appendDepIfAppropriate(mctx android.BottomUpMutatorContext, deps *multilibDeps, installPartition string) {
	moduleName := mctx.ModuleName()
	checkDepModuleInMultipleNamespaces(mctx, *deps, moduleName, installPartition)
	if _, ok := (*deps)[moduleName]; ok {
		// Prefer the namespace-specific module over the platform module
		if mctx.Namespace().Path != "." {
			(*deps)[moduleName].Namespace = mctx.Namespace().Path
		}
		(*deps)[moduleName].Arch = append((*deps)[moduleName].Arch, mctx.Module().Target().Arch.ArchType)
	} else {
		multilib, _ := mctx.Module().DecodeMultilib(mctx)
		(*deps)[moduleName] = &depCandidateProps{
			Namespace: mctx.Namespace().Path,
			Multilib:  multilib,
			Arch:      []android.ArchType{mctx.Module().Target().Arch.ArchType},
		}
	}
}

func collectDepsMutator(mctx android.BottomUpMutatorContext) {
	m := mctx.Module()
	if m.Target().Os.Class != android.Device {
		return
	}
	fsGenState := mctx.Config().Get(fsGenStateOnceKey).(*FsGenState)

	fsGenState.fsDepsMutex.Lock()
	defer fsGenState.fsDepsMutex.Unlock()

	if slices.Contains(fsGenState.depCandidates, mctx.ModuleName()) {
		installPartition := m.PartitionTag(mctx.DeviceConfig())
		// Only add the module as dependency when:
		// - its enabled
		// - its namespace is included in PRODUCT_SOONG_NAMESPACES
		if m.Enabled(mctx) && m.ExportedToMake() {
			appendDepIfAppropriate(mctx, fsGenState.fsDeps[installPartition], installPartition)
		}
	}
	// store the map of module to (required,overrides) even if the module is not in PRODUCT_PACKAGES.
	// the module might be installed transitively.
	if m.Enabled(mctx) && m.ExportedToMake() {
		fsGenState.moduleToInstallationProps[m.Name()] = installationProperties{
			Required:  m.RequiredModuleNames(mctx),
			Overrides: m.Overrides(),
		}
	}
}

type depsStruct struct {
	Deps []string
}

type multilibDepsStruct struct {
	Common   depsStruct
	Lib32    depsStruct
	Lib64    depsStruct
	Both     depsStruct
	Prefer32 depsStruct
}

type packagingPropsStruct struct {
	High_priority_deps []string
	Deps               []string
	Multilib           multilibDepsStruct
}

func fullyQualifiedModuleName(moduleName, namespace string) string {
	if namespace == "." {
		return moduleName
	}
	return fmt.Sprintf("//%s:%s", namespace, moduleName)
}

func getBitness(archTypes []android.ArchType) (ret []string) {
	for _, archType := range archTypes {
		if archType.Multilib == "" {
			ret = append(ret, android.COMMON_VARIANT)
		} else {
			ret = append(ret, archType.Bitness())
		}
	}
	return ret
}

func setDepsMutator(mctx android.BottomUpMutatorContext) {
	removeOverriddenDeps(mctx)
	fsGenState := mctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
	fsDeps := fsGenState.fsDeps
	soongGeneratedPartitionMap := getAllSoongGeneratedPartitionNames(mctx.Config(), fsGenState.soongGeneratedPartitions)
	m := mctx.Module()
	if partition, ok := soongGeneratedPartitionMap[m.Name()]; ok {
		depsStruct := generateDepStruct(*fsDeps[partition], fsGenState.generatedPrebuiltEtcModuleNames)
		if err := proptools.AppendMatchingProperties(m.GetProperties(), depsStruct, nil); err != nil {
			mctx.ModuleErrorf(err.Error())
		}
	}
}

// removeOverriddenDeps collects PRODUCT_PACKAGES and (transitive) required deps.
// it then removes any modules which appear in `overrides` of the above list.
func removeOverriddenDeps(mctx android.BottomUpMutatorContext) {
	mctx.Config().Once(fsGenRemoveOverridesOnceKey, func() interface{} {
		fsGenState := mctx.Config().Get(fsGenStateOnceKey).(*FsGenState)
		fsDeps := fsGenState.fsDeps
		overridden := map[string]bool{}
		allDeps := []string{}

		// Step 1: Initialization: Append PRODUCT_PACKAGES to the queue
		for _, fsDep := range fsDeps {
			for depName, _ := range *fsDep {
				allDeps = append(allDeps, depName)
			}
		}

		// Step 2: Process the queue, and add required modules to the queue.
		i := 0
		for {
			if i == len(allDeps) {
				break
			}
			depName := allDeps[i]
			for _, overrides := range fsGenState.moduleToInstallationProps[depName].Overrides {
				overridden[overrides] = true
			}
			// add required dep to the queue.
			allDeps = append(allDeps, fsGenState.moduleToInstallationProps[depName].Required...)
			i += 1
		}

		// Step 3: Delete all the overridden modules.
		for overridden, _ := range overridden {
			for partition, _ := range fsDeps {
				delete(*fsDeps[partition], overridden)
			}
		}
		return nil
	})
}

var HighPriorityDeps = []string{}

func isHighPriorityDep(depName string) bool {
	for _, highPriorityDeps := range HighPriorityDeps {
		if strings.HasPrefix(depName, highPriorityDeps) {
			return true
		}
	}
	return false
}

func generateDepStruct(deps map[string]*depCandidateProps, highPriorityDeps []string) *packagingPropsStruct {
	depsStruct := packagingPropsStruct{}
	for depName, depProps := range deps {
		bitness := getBitness(depProps.Arch)
		fullyQualifiedDepName := fullyQualifiedModuleName(depName, depProps.Namespace)
		if android.InList(depName, highPriorityDeps) {
			depsStruct.High_priority_deps = append(depsStruct.High_priority_deps, fullyQualifiedDepName)
		} else if android.InList("32", bitness) && android.InList("64", bitness) {
			// If both 32 and 64 bit variants are enabled for this module
			switch depProps.Multilib {
			case string(android.MultilibBoth):
				depsStruct.Multilib.Both.Deps = append(depsStruct.Multilib.Both.Deps, fullyQualifiedDepName)
			case string(android.MultilibCommon), string(android.MultilibFirst):
				depsStruct.Deps = append(depsStruct.Deps, fullyQualifiedDepName)
			case "32":
				depsStruct.Multilib.Lib32.Deps = append(depsStruct.Multilib.Lib32.Deps, fullyQualifiedDepName)
			case "64", "darwin_universal":
				depsStruct.Multilib.Lib64.Deps = append(depsStruct.Multilib.Lib64.Deps, fullyQualifiedDepName)
			case "prefer32", "first_prefer32":
				depsStruct.Multilib.Prefer32.Deps = append(depsStruct.Multilib.Prefer32.Deps, fullyQualifiedDepName)
			default:
				depsStruct.Multilib.Both.Deps = append(depsStruct.Multilib.Both.Deps, fullyQualifiedDepName)
			}
		} else if android.InList("64", bitness) {
			// If only 64 bit variant is enabled
			depsStruct.Multilib.Lib64.Deps = append(depsStruct.Multilib.Lib64.Deps, fullyQualifiedDepName)
		} else if android.InList("32", bitness) {
			// If only 32 bit variant is enabled
			depsStruct.Multilib.Lib32.Deps = append(depsStruct.Multilib.Lib32.Deps, fullyQualifiedDepName)
		} else {
			// If only common variant is enabled
			depsStruct.Multilib.Common.Deps = append(depsStruct.Multilib.Common.Deps, fullyQualifiedDepName)
		}
	}
	depsStruct.Deps = android.SortedUniqueStrings(depsStruct.Deps)
	depsStruct.Multilib.Lib32.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Lib32.Deps)
	depsStruct.Multilib.Lib64.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Lib64.Deps)
	depsStruct.Multilib.Prefer32.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Prefer32.Deps)
	depsStruct.Multilib.Both.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Both.Deps)
	depsStruct.Multilib.Common.Deps = android.SortedUniqueStrings(depsStruct.Multilib.Common.Deps)
	depsStruct.High_priority_deps = android.SortedUniqueStrings(depsStruct.High_priority_deps)

	return &depsStruct
}
