// Copyright 2021 Google Inc. All rights reserved.
//
// 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 cc

import (
	"fmt"
	"strings"

	"github.com/google/blueprint/proptools"

	"android/soong/android"
)

var (
	globalAfdoProfileProjects = []string{
		"vendor/google_data/pgo_profile/sampling/",
		"toolchain/pgo-profiles/sampling/",
	}
)

var afdoProfileProjectsConfigKey = android.NewOnceKey("AfdoProfileProjects")

const afdoCFlagsFormat = "-funique-internal-linkage-names -fprofile-sample-accurate -fprofile-sample-use=%s"

func getAfdoProfileProjects(config android.DeviceConfig) []string {
	return config.OnceStringSlice(afdoProfileProjectsConfigKey, func() []string {
		return globalAfdoProfileProjects
	})
}

func recordMissingAfdoProfileFile(ctx android.BaseModuleContext, missing string) {
	getNamedMapForConfig(ctx.Config(), modulesMissingProfileFileKey).Store(missing, true)
}

type AfdoProperties struct {
	// Afdo allows developers self-service enroll for
	// automatic feedback-directed optimization using profile data.
	Afdo bool

	AfdoTarget *string  `blueprint:"mutated"`
	AfdoDeps   []string `blueprint:"mutated"`
}

type afdo struct {
	Properties AfdoProperties
}

func (afdo *afdo) props() []interface{} {
	return []interface{}{&afdo.Properties}
}

func (afdo *afdo) AfdoEnabled() bool {
	return afdo != nil && afdo.Properties.Afdo && afdo.Properties.AfdoTarget != nil
}

// Get list of profile file names, ordered by level of specialisation. For example:
//  1. libfoo_arm64.afdo
//  2. libfoo.afdo
//
// Add more specialisation as needed.
func getProfileFiles(ctx android.BaseModuleContext, moduleName string) []string {
	var files []string
	files = append(files, moduleName+"_"+ctx.Arch().ArchType.String()+".afdo")
	files = append(files, moduleName+".afdo")
	return files
}

func (props *AfdoProperties) GetAfdoProfileFile(ctx android.BaseModuleContext, module string) android.OptionalPath {
	// Test if the profile_file is present in any of the Afdo profile projects
	for _, profileFile := range getProfileFiles(ctx, module) {
		for _, profileProject := range getAfdoProfileProjects(ctx.DeviceConfig()) {
			path := android.ExistentPathForSource(ctx, profileProject, profileFile)
			if path.Valid() {
				return path
			}
		}
	}

	// Record that this module's profile file is absent
	missing := ctx.ModuleDir() + ":" + module
	recordMissingAfdoProfileFile(ctx, missing)

	return android.OptionalPathForPath(nil)
}

func (afdo *afdo) begin(ctx BaseModuleContext) {
	if ctx.Host() {
		return
	}
	if ctx.static() && !ctx.staticBinary() {
		return
	}
	if afdo.Properties.Afdo {
		module := ctx.ModuleName()
		if afdo.Properties.GetAfdoProfileFile(ctx, module).Valid() {
			afdo.Properties.AfdoTarget = proptools.StringPtr(module)
		}
	}
}

func (afdo *afdo) flags(ctx ModuleContext, flags Flags) Flags {
	if profile := afdo.Properties.AfdoTarget; profile != nil {
		if profileFile := afdo.Properties.GetAfdoProfileFile(ctx, *profile); profileFile.Valid() {
			profileFilePath := profileFile.Path()

			profileUseFlag := fmt.Sprintf(afdoCFlagsFormat, profileFile)
			flags.Local.CFlags = append(flags.Local.CFlags, profileUseFlag)
			flags.Local.LdFlags = append(flags.Local.LdFlags, profileUseFlag)
			flags.Local.LdFlags = append(flags.Local.LdFlags, "-Wl,-mllvm,-no-warn-sample-unused=true")

			// Update CFlagsDeps and LdFlagsDeps so the module is rebuilt
			// if profileFile gets updated
			flags.CFlagsDeps = append(flags.CFlagsDeps, profileFilePath)
			flags.LdFlagsDeps = append(flags.LdFlagsDeps, profileFilePath)
		}
	}

	return flags
}

// Propagate afdo requirements down from binaries
func afdoDepsMutator(mctx android.TopDownMutatorContext) {
	if m, ok := mctx.Module().(*Module); ok && m.afdo.AfdoEnabled() {
		afdoTarget := *m.afdo.Properties.AfdoTarget
		mctx.WalkDeps(func(dep android.Module, parent android.Module) bool {
			tag := mctx.OtherModuleDependencyTag(dep)
			libTag, isLibTag := tag.(libraryDependencyTag)

			// Do not recurse down non-static dependencies
			if isLibTag {
				if !libTag.static() {
					return false
				}
			} else {
				if tag != objDepTag && tag != reuseObjTag {
					return false
				}
			}

			if dep, ok := dep.(*Module); ok {
				dep.afdo.Properties.AfdoDeps = append(dep.afdo.Properties.AfdoDeps, afdoTarget)
			}

			return true
		})
	}
}

// Create afdo variants for modules that need them
func afdoMutator(mctx android.BottomUpMutatorContext) {
	if m, ok := mctx.Module().(*Module); ok && m.afdo != nil {
		if m.afdo.AfdoEnabled() && !m.static() {
			afdoTarget := *m.afdo.Properties.AfdoTarget
			mctx.SetDependencyVariation(encodeTarget(afdoTarget))
		}

		variationNames := []string{""}
		afdoDeps := android.FirstUniqueStrings(m.afdo.Properties.AfdoDeps)
		for _, dep := range afdoDeps {
			variationNames = append(variationNames, encodeTarget(dep))
		}
		if len(variationNames) > 1 {
			modules := mctx.CreateVariations(variationNames...)
			for i, name := range variationNames {
				if name == "" {
					continue
				}
				variation := modules[i].(*Module)
				variation.Properties.PreventInstall = true
				variation.Properties.HideFromMake = true
				variation.afdo.Properties.AfdoTarget = proptools.StringPtr(decodeTarget(name))
			}
		}
	}
}

// Encode target name to variation name.
func encodeTarget(target string) string {
	if target == "" {
		return ""
	}
	return "afdo-" + target
}

// Decode target name from variation name.
func decodeTarget(variation string) string {
	if variation == "" {
		return ""
	}
	return strings.TrimPrefix(variation, "afdo-")
}
