package android

import (
	"path"
	"path/filepath"

	"android/soong/android/team_proto"

	"google.golang.org/protobuf/proto"
)

const ownershipDirectory = "ownership"
const allTeamsFile = "all_teams.pb"

func AllTeamsFactory() Singleton {
	return &allTeamsSingleton{}
}

func init() {
	registerAllTeamBuildComponents(InitRegistrationContext)
}

func registerAllTeamBuildComponents(ctx RegistrationContext) {
	ctx.RegisterParallelSingletonType("all_teams", AllTeamsFactory)
}

// For each module, list the team or the bpFile the module is defined in.
type moduleTeamAndTestInfo struct {
	// Name field from bp file for the team
	teamName string
	// Blueprint file the module is located in.
	bpFile string
	// Is this module only used by tests.
	testOnly bool
	// Is this a directly testable target by running the module directly
	// or via tradefed.
	topLevelTestTarget bool
	// String name indicating the module, like `java_library` for reporting.
	kind string
}

type allTeamsSingleton struct {
	// Path where the collected metadata is stored after successful validation.
	outputPath OutputPath

	// Map of all package modules we visit during GenerateBuildActions
	packages map[string]packageProperties
	// Map of all team modules we visit during GenerateBuildActions
	teams map[string]teamProperties
	// Keeps track of team information or bp file for each module we visit.
	teams_for_mods map[string]moduleTeamAndTestInfo
}

// See if there is a package module for the given bpFilePath with a team defined, if so return the team.
// If not ascend up to the parent directory and do the same.
func (t *allTeamsSingleton) lookupDefaultTeam(bpFilePath string) (teamProperties, bool) {
	// return the Default_team listed in the package if is there.
	if p, ok := t.packages[bpFilePath]; ok {
		if defaultTeam := p.Default_team; defaultTeam != nil {
			return t.teams[*defaultTeam], true
		}
	}
	// Strip a directory and go up.
	// Does android/paths.go basePath,SourcePath help?
	current, base := filepath.Split(bpFilePath)
	current = filepath.Clean(current) // removes trailing slash, convert "" -> "."
	parent, _ := filepath.Split(current)
	if current == "." {
		return teamProperties{}, false
	}
	return t.lookupDefaultTeam(filepath.Join(parent, base))
}

// Visit all modules and collect all teams and use WriteFileRuleVerbatim
// to write it out.
func (t *allTeamsSingleton) GenerateBuildActions(ctx SingletonContext) {
	t.packages = make(map[string]packageProperties)
	t.teams = make(map[string]teamProperties)
	t.teams_for_mods = make(map[string]moduleTeamAndTestInfo)

	ctx.VisitAllModuleProxies(func(module ModuleProxy) {
		bpFile := ctx.BlueprintFile(module)

		// Package Modules and Team Modules are stored in a map so we can look them up by name for
		// modules without a team.
		if pack, ok := OtherModuleProvider(ctx, module, PackageInfoProvider); ok {
			// Packages don't have names, use the blueprint file as the key. we can't get qualifiedModuleId in t context.
			pkgKey := bpFile
			t.packages[pkgKey] = pack.Properties
			return
		}
		if team, ok := OtherModuleProvider(ctx, module, TeamInfoProvider); ok {
			t.teams[module.Name()] = team.Properties
			return
		}

		testModInfo := TestModuleInformation{}
		if tmi, ok := OtherModuleProvider(ctx, module, TestOnlyProviderKey); ok {
			testModInfo = tmi
		}

		// Some modules, like java_test_host don't set the provider when the module isn't enabled:
		//                                                test_only, top_level
		//     AVFHostTestCases{os:linux_glibc,arch:common} {true true}
		//     AVFHostTestCases{os:windows,arch:common} {false false}
		// Generally variant information of true override false or unset.
		if testModInfo.TestOnly == false {
			if prevValue, exists := t.teams_for_mods[module.Name()]; exists {
				if prevValue.testOnly == true {
					return
				}
			}
		}
		entry := moduleTeamAndTestInfo{
			bpFile:             bpFile,
			testOnly:           testModInfo.TestOnly,
			topLevelTestTarget: testModInfo.TopLevelTarget,
			kind:               ctx.ModuleType(module),
			teamName:           OtherModulePointerProviderOrDefault(ctx, module, CommonModuleInfoProvider).Team,
		}
		t.teams_for_mods[module.Name()] = entry

	})

	// Visit all modules again and lookup the team name in the package or parent package if the team
	// isn't assignged at the module level.
	allTeams := t.lookupTeamForAllModules()

	t.outputPath = PathForOutput(ctx, ownershipDirectory, allTeamsFile)
	data, err := proto.Marshal(allTeams)
	if err != nil {
		ctx.Errorf("Unable to marshal team data. %s", err)
	}

	WriteFileRuleVerbatim(ctx, t.outputPath, string(data))
	ctx.Phony("all_teams", t.outputPath)
	ctx.DistForGoal("all_teams", t.outputPath)
}

// Visit every (non-package, non-team) module and write out a proto containing
// either the declared team data for that module or the package default team data for that module.
func (t *allTeamsSingleton) lookupTeamForAllModules() *team_proto.AllTeams {
	teamsProto := make([]*team_proto.Team, len(t.teams_for_mods))
	for i, moduleName := range SortedKeys(t.teams_for_mods) {
		m, _ := t.teams_for_mods[moduleName]
		teamName := m.teamName
		var teamProperties teamProperties
		found := false
		if teamName != "" {
			teamProperties, found = t.teams[teamName]
		} else {
			teamProperties, found = t.lookupDefaultTeam(m.bpFile)
		}
		// Deal with one blueprint file including another by looking up the default
		// in the main Android.bp rather than one listed with "build = [My.bp]"
		if !found {
			teamProperties, found = t.lookupDefaultTeam(path.Join(path.Dir(m.bpFile), "Android.bp"))
		}

		trendy_team_id := ""
		if found {
			trendy_team_id = *teamProperties.Trendy_team_id
		}

		teamData := new(team_proto.Team)
		*teamData = team_proto.Team{
			TargetName:     proto.String(moduleName),
			Path:           proto.String(m.bpFile),
			TestOnly:       proto.Bool(m.testOnly),
			TopLevelTarget: proto.Bool(m.topLevelTestTarget),
			Kind:           proto.String(m.kind),
		}
		if trendy_team_id != "" {
			teamData.TrendyTeamId = proto.String(trendy_team_id)
		} else {
			// Clients rely on the TrendyTeamId optional field not being set.
		}
		teamsProto[i] = teamData
	}
	return &team_proto.AllTeams{Teams: teamsProto}
}
