/*
 * Copyright (C) 2021 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 java

import (
	"fmt"
	"strings"

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

	"android/soong/android"
)

// Build rules and utilities to generate individual packages/modules/common/proto/classpaths.proto
// config files based on build configuration to embed into /system and /apex on a device.
//
// See `derive_classpath` service that reads the configs at runtime and defines *CLASSPATH variables
// on the device.

type classpathType int

const (
	// Matches definition in packages/modules/common/proto/classpaths.proto
	BOOTCLASSPATH classpathType = iota
	DEX2OATBOOTCLASSPATH
	SYSTEMSERVERCLASSPATH
	STANDALONE_SYSTEMSERVER_JARS
)

func (c classpathType) String() string {
	return [...]string{"BOOTCLASSPATH", "DEX2OATBOOTCLASSPATH", "SYSTEMSERVERCLASSPATH", "STANDALONE_SYSTEMSERVER_JARS"}[c]
}

type classpathFragmentProperties struct {
	// Whether to generated classpaths.proto config instance for the fragment. If the config is not
	// generated, then relevant boot jars are added to platform classpath, i.e. platform_bootclasspath
	// or platform_systemserverclasspath. This is useful for non-updatable APEX boot jars, to keep
	// them as part of dexopt on device. Defaults to true.
	Generate_classpaths_proto *bool
}

// classpathFragment interface is implemented by a module that contributes jars to a *CLASSPATH
// variables at runtime.
type classpathFragment interface {
	android.Module

	classpathFragmentBase() *ClasspathFragmentBase
}

// ClasspathFragmentBase is meant to be embedded in any module types that implement classpathFragment;
// such modules are expected to call initClasspathFragment().
type ClasspathFragmentBase struct {
	properties classpathFragmentProperties

	classpathType classpathType

	outputFilepath android.OutputPath
	installDirPath android.InstallPath
}

func (c *ClasspathFragmentBase) classpathFragmentBase() *ClasspathFragmentBase {
	return c
}

// Initializes ClasspathFragmentBase struct. Must be called by all modules that include ClasspathFragmentBase.
func initClasspathFragment(c classpathFragment, classpathType classpathType) {
	base := c.classpathFragmentBase()
	base.classpathType = classpathType
	c.AddProperties(&base.properties)
}

// Matches definition of Jar in packages/modules/SdkExtensions/proto/classpaths.proto
type classpathJar struct {
	path          string
	classpath     classpathType
	minSdkVersion string
	maxSdkVersion string
}

// gatherPossibleApexModuleNamesAndStems returns a set of module and stem names from the
// supplied contents that may be in the apex boot jars.
//
// The module names are included because sometimes the stem is set to just change the name of
// the installed file and it expects the configuration to still use the actual module name.
//
// The stem names are included because sometimes the stem is set to change the effective name of the
// module that is used in the configuration as well,e .g. when a test library is overriding an
// actual boot jar
func gatherPossibleApexModuleNamesAndStems(ctx android.ModuleContext, contents []string, tag blueprint.DependencyTag) []string {
	set := map[string]struct{}{}
	for _, name := range contents {
		dep := ctx.GetDirectDepWithTag(name, tag)
		if dep == nil && ctx.Config().AllowMissingDependencies() {
			// Ignore apex boot jars from dexpreopt if it does not exist, and missing deps are allowed.
			continue
		}
		set[ModuleStemForDeapexing(dep)] = struct{}{}
		if m, ok := dep.(ModuleWithStem); ok {
			set[m.Stem()] = struct{}{}
		} else {
			ctx.PropertyErrorf("contents", "%v is not a ModuleWithStem", name)
		}
	}
	return android.SortedKeys(set)
}

// Converts android.ConfiguredJarList into a list of classpathJars for each given classpathType.
func configuredJarListToClasspathJars(ctx android.ModuleContext, configuredJars android.ConfiguredJarList, classpaths ...classpathType) []classpathJar {
	paths := configuredJars.DevicePaths(ctx.Config(), android.Android)
	jars := make([]classpathJar, 0, len(paths)*len(classpaths))
	for i := 0; i < len(paths); i++ {
		for _, classpathType := range classpaths {
			jar := classpathJar{
				classpath: classpathType,
				path:      paths[i],
			}
			ctx.VisitDirectDepsIf(func(m android.Module) bool {
				return m.Name() == configuredJars.Jar(i)
			}, func(m android.Module) {
				if s, ok := m.(*SdkLibrary); ok {
					minSdkVersion := s.MinSdkVersion(ctx)
					maxSdkVersion := s.MaxSdkVersion(ctx)
					// TODO(208456999): instead of mapping "current" to latest, min_sdk_version should never be set to "current"
					if minSdkVersion.Specified() {
						if minSdkVersion.IsCurrent() {
							jar.minSdkVersion = ctx.Config().DefaultAppTargetSdk(ctx).String()
						} else {
							jar.minSdkVersion = minSdkVersion.String()
						}
					}
					if maxSdkVersion.Specified() {
						if maxSdkVersion.IsCurrent() {
							jar.maxSdkVersion = ctx.Config().DefaultAppTargetSdk(ctx).String()
						} else {
							jar.maxSdkVersion = maxSdkVersion.String()
						}
					}
				}
			})
			jars = append(jars, jar)
		}
	}
	return jars
}

func (c *ClasspathFragmentBase) outputFilename() string {
	return strings.ToLower(c.classpathType.String()) + ".pb"
}

func (c *ClasspathFragmentBase) generateClasspathProtoBuildActions(ctx android.ModuleContext, configuredJars android.ConfiguredJarList, jars []classpathJar) {
	generateProto := proptools.BoolDefault(c.properties.Generate_classpaths_proto, true)
	if generateProto {
		outputFilename := c.outputFilename()
		c.outputFilepath = android.PathForModuleOut(ctx, outputFilename).OutputPath
		c.installDirPath = android.PathForModuleInstall(ctx, "etc", "classpaths")

		generatedTextproto := android.PathForModuleOut(ctx, outputFilename+".textproto")
		writeClasspathsTextproto(ctx, generatedTextproto, jars)

		rule := android.NewRuleBuilder(pctx, ctx)
		rule.Command().
			BuiltTool("conv_classpaths_proto").
			Flag("encode").
			Flag("--format=textproto").
			FlagWithInput("--input=", generatedTextproto).
			FlagWithOutput("--output=", c.outputFilepath)

		rule.Build("classpath_fragment", "Compiling "+c.outputFilepath.String())
	}

	classpathProtoInfo := ClasspathFragmentProtoContentInfo{
		ClasspathFragmentProtoGenerated:  generateProto,
		ClasspathFragmentProtoContents:   configuredJars,
		ClasspathFragmentProtoInstallDir: c.installDirPath,
		ClasspathFragmentProtoOutput:     c.outputFilepath,
	}
	android.SetProvider(ctx, ClasspathFragmentProtoContentInfoProvider, classpathProtoInfo)
}

func (c *ClasspathFragmentBase) installClasspathProto(ctx android.ModuleContext) android.InstallPath {
	return ctx.InstallFile(c.installDirPath, c.outputFilename(), c.outputFilepath)
}

func writeClasspathsTextproto(ctx android.ModuleContext, output android.WritablePath, jars []classpathJar) {
	var content strings.Builder

	for _, jar := range jars {
		fmt.Fprintf(&content, "jars {\n")
		fmt.Fprintf(&content, "path: \"%s\"\n", jar.path)
		fmt.Fprintf(&content, "classpath: %s\n", jar.classpath)
		fmt.Fprintf(&content, "min_sdk_version: \"%s\"\n", jar.minSdkVersion)
		fmt.Fprintf(&content, "max_sdk_version: \"%s\"\n", jar.maxSdkVersion)
		fmt.Fprintf(&content, "}\n")
	}

	android.WriteFileRule(ctx, output, content.String())
}

// Returns AndroidMkEntries objects to install generated classpath.proto.
// Do not use this to install into APEXes as the injection of the generated files happen separately for APEXes.
func (c *ClasspathFragmentBase) androidMkEntries() []android.AndroidMkEntries {
	return []android.AndroidMkEntries{{
		Class:      "ETC",
		OutputFile: android.OptionalPathForPath(c.outputFilepath),
		ExtraEntries: []android.AndroidMkExtraEntriesFunc{
			func(ctx android.AndroidMkExtraEntriesContext, entries *android.AndroidMkEntries) {
				entries.SetString("LOCAL_MODULE_PATH", c.installDirPath.String())
				entries.SetString("LOCAL_INSTALLED_MODULE_STEM", c.outputFilepath.Base())
			},
		},
	}}
}

var ClasspathFragmentProtoContentInfoProvider = blueprint.NewProvider[ClasspathFragmentProtoContentInfo]()

type ClasspathFragmentProtoContentInfo struct {
	// Whether the classpaths.proto config is generated for the fragment.
	ClasspathFragmentProtoGenerated bool

	// ClasspathFragmentProtoContents contains a list of jars that are part of this classpath fragment.
	ClasspathFragmentProtoContents android.ConfiguredJarList

	// ClasspathFragmentProtoOutput is an output path for the generated classpaths.proto config of this module.
	//
	// The file should be copied to a relevant place on device, see ClasspathFragmentProtoInstallDir
	// for more details.
	ClasspathFragmentProtoOutput android.OutputPath

	// ClasspathFragmentProtoInstallDir contains information about on device location for the generated classpaths.proto file.
	//
	// The path encodes expected sub-location within partitions, i.e. etc/classpaths/<proto-file>,
	// for ClasspathFragmentProtoOutput. To get sub-location, instead of the full output / make path
	// use android.InstallPath#Rel().
	//
	// This is only relevant for APEX modules as they perform their own installation; while regular
	// system files are installed via ClasspathFragmentBase#androidMkEntries().
	ClasspathFragmentProtoInstallDir android.InstallPath
}