// Copyright 2019 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 java

import (
	"android/soong/android"
	"android/soong/dexpreopt"

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

type GenruleCombiner struct {
	android.ModuleBase
	android.DefaultableModuleBase

	genruleCombinerProperties GenruleCombinerProperties

	headerJars                    android.Paths
	implementationJars            android.Paths
	implementationAndResourceJars android.Paths
	resourceJars                  android.Paths
	aconfigProtoFiles             android.Paths

	srcJarArgs []string
	srcJarDeps android.Paths

	headerDirs android.Paths

	combinedHeaderJar         android.Path
	combinedImplementationJar android.Path
}

type GenruleCombinerProperties struct {
	// List of modules whose implementation (and resources) jars will be visible to modules
	// that depend on this module.
	Static_libs proptools.Configurable[[]string] `android:"arch_variant"`

	// List of modules whose header jars will be visible to modules that depend on this module.
	Headers proptools.Configurable[[]string] `android:"arch_variant"`
}

// java_genrule_combiner provides the implementation and resource jars from `static_libs`, with
// the header jars from `headers`.
//
// This is useful when a java_genrule is used to change the implementation of a java library
// without requiring a change in the header jars.
func GenruleCombinerFactory() android.Module {
	module := &GenruleCombiner{}

	module.AddProperties(&module.genruleCombinerProperties)
	InitJavaModule(module, android.HostAndDeviceSupported)
	return module
}

var genruleCombinerHeaderDepTag = dependencyTag{name: "genrule_combiner_header"}

func (j *GenruleCombiner) DepsMutator(ctx android.BottomUpMutatorContext) {
	ctx.AddVariationDependencies(nil, staticLibTag,
		j.genruleCombinerProperties.Static_libs.GetOrDefault(ctx, nil)...)
	ctx.AddVariationDependencies(nil, genruleCombinerHeaderDepTag,
		j.genruleCombinerProperties.Headers.GetOrDefault(ctx, nil)...)
}

func (j *GenruleCombiner) GenerateAndroidBuildActions(ctx android.ModuleContext) {
	if len(j.genruleCombinerProperties.Static_libs.GetOrDefault(ctx, nil)) < 1 {
		ctx.PropertyErrorf("static_libs", "at least one dependency is required")
	}

	if len(j.genruleCombinerProperties.Headers.GetOrDefault(ctx, nil)) < 1 {
		ctx.PropertyErrorf("headers", "at least one dependency is required")
	}

	var transitiveHeaderJars []depset.DepSet[android.Path]
	var transitiveImplementationJars []depset.DepSet[android.Path]
	var transitiveResourceJars []depset.DepSet[android.Path]
	var sdkVersion android.SdkSpec
	var stubsLinkType StubsLinkType
	moduleWithSdkDepInfo := &ModuleWithSdkDepInfo{}

	// Collect the headers first, so that aconfig flag values for the libraries will override
	// values from the headers (if they are different).
	ctx.VisitDirectDepsProxyWithTag(genruleCombinerHeaderDepTag, func(m android.ModuleProxy) {
		if dep, ok := android.OtherModuleProvider(ctx, m, JavaInfoProvider); ok {
			j.headerJars = append(j.headerJars, dep.HeaderJars...)

			j.srcJarArgs = append(j.srcJarArgs, dep.SrcJarArgs...)
			j.srcJarDeps = append(j.srcJarDeps, dep.SrcJarDeps...)
			j.aconfigProtoFiles = append(j.aconfigProtoFiles, dep.AconfigIntermediateCacheOutputPaths...)
			sdkVersion = dep.SdkVersion
			stubsLinkType = dep.StubsLinkType
			*moduleWithSdkDepInfo = *dep.ModuleWithSdkDepInfo

			transitiveHeaderJars = append(transitiveHeaderJars, dep.TransitiveStaticLibsHeaderJars)
		} else if dep, ok := android.OtherModuleProvider(ctx, m, android.CodegenInfoProvider); ok {
			j.aconfigProtoFiles = append(j.aconfigProtoFiles, dep.IntermediateCacheOutputPaths...)
		} else {
			ctx.PropertyErrorf("headers", "module %q cannot be used as a dependency", ctx.OtherModuleName(m))
		}
	})
	ctx.VisitDirectDepsProxyWithTag(staticLibTag, func(m android.ModuleProxy) {
		if dep, ok := android.OtherModuleProvider(ctx, m, JavaInfoProvider); ok {
			j.implementationJars = append(j.implementationJars, dep.ImplementationJars...)
			j.implementationAndResourceJars = append(j.implementationAndResourceJars, dep.ImplementationAndResourcesJars...)
			j.resourceJars = append(j.resourceJars, dep.ResourceJars...)

			transitiveImplementationJars = append(transitiveImplementationJars, dep.TransitiveStaticLibsImplementationJars)
			transitiveResourceJars = append(transitiveResourceJars, dep.TransitiveStaticLibsResourceJars)
			j.aconfigProtoFiles = append(j.aconfigProtoFiles, dep.AconfigIntermediateCacheOutputPaths...)
		} else if dep, ok := android.OtherModuleProvider(ctx, m, android.OutputFilesProvider); ok {
			// This is provided by `java_genrule` modules.
			j.implementationJars = append(j.implementationJars, dep.DefaultOutputFiles...)
			j.implementationAndResourceJars = append(j.implementationAndResourceJars, dep.DefaultOutputFiles...)
			stubsLinkType = Implementation
		} else {
			ctx.PropertyErrorf("static_libs", "module %q cannot be used as a dependency", ctx.OtherModuleName(m))
		}
	})

	jarName := ctx.ModuleName() + ".jar"

	if len(j.implementationAndResourceJars) > 1 {
		outputFile := android.PathForModuleOut(ctx, "combined", jarName)
		TransformJarsToJar(ctx, outputFile, "combine", j.implementationAndResourceJars,
			android.OptionalPath{}, false, nil, nil)
		j.combinedImplementationJar = outputFile
	} else if len(j.implementationAndResourceJars) == 1 {
		j.combinedImplementationJar = j.implementationAndResourceJars[0]
	}

	if len(j.headerJars) > 1 {
		outputFile := android.PathForModuleOut(ctx, "turbine-combined", jarName)
		TransformJarsToJar(ctx, outputFile, "turbine combine", j.headerJars,
			android.OptionalPath{}, false, nil, []string{"META-INF/TRANSITIVE"})
		j.combinedHeaderJar = outputFile
		j.headerDirs = append(j.headerDirs, android.PathForModuleOut(ctx, "turbine-combined"))
	} else if len(j.headerJars) == 1 {
		j.combinedHeaderJar = j.headerJars[0]
	}

	javaInfo := &JavaInfo{
		HeaderJars:                             android.Paths{j.combinedHeaderJar},
		LocalHeaderJars:                        android.Paths{j.combinedHeaderJar},
		TransitiveStaticLibsHeaderJars:         depset.New(depset.PREORDER, android.Paths{j.combinedHeaderJar}, transitiveHeaderJars),
		TransitiveStaticLibsImplementationJars: depset.New(depset.PREORDER, android.Paths{j.combinedImplementationJar}, transitiveImplementationJars),
		TransitiveStaticLibsResourceJars:       depset.New(depset.PREORDER, nil, transitiveResourceJars),
		GeneratedSrcjars:                       android.Paths{j.combinedImplementationJar},
		ImplementationAndResourcesJars:         android.Paths{j.combinedImplementationJar},
		ImplementationJars:                     android.Paths{j.combinedImplementationJar},
		ModuleWithSdkDepInfo:                   moduleWithSdkDepInfo,
		ResourceJars:                           j.resourceJars,
		OutputFile:                             j.combinedImplementationJar,
		SdkVersion:                             sdkVersion,
		SrcJarArgs:                             j.srcJarArgs,
		SrcJarDeps:                             j.srcJarDeps,
		StubsLinkType:                          stubsLinkType,
		AconfigIntermediateCacheOutputPaths:    j.aconfigProtoFiles,
	}
	setExtraJavaInfo(ctx, j, javaInfo)
	ctx.SetOutputFiles(android.Paths{javaInfo.OutputFile}, "")
	ctx.SetOutputFiles(android.Paths{javaInfo.OutputFile}, android.DefaultDistTag)
	ctx.SetOutputFiles(javaInfo.ImplementationAndResourcesJars, ".jar")
	ctx.SetOutputFiles(javaInfo.HeaderJars, ".hjar")
	android.SetProvider(ctx, JavaInfoProvider, javaInfo)

	moduleInfoJSON := ctx.ModuleInfoJSON()
	moduleInfoJSON.Class = []string{"JAVA_LIBRARIES"}
	if j.combinedImplementationJar != nil {
		moduleInfoJSON.ClassesJar = []string{j.combinedImplementationJar.String()}
	}
	moduleInfoJSON.SystemSharedLibs = []string{"none"}

	if ctx.Os() == android.Windows {
		// Make does not support Windows Java modules
		j.HideFromMake()
	}
}

func (j *GenruleCombiner) GeneratedSourceFiles() android.Paths {
	return append(android.Paths{}, j.combinedImplementationJar)
}

func (j *GenruleCombiner) GeneratedHeaderDirs() android.Paths {
	return append(android.Paths{}, j.headerDirs...)
}

func (j *GenruleCombiner) GeneratedDeps() android.Paths {
	return append(android.Paths{}, j.combinedImplementationJar)
}

func (j *GenruleCombiner) Srcs() android.Paths {
	return append(android.Paths{}, j.implementationAndResourceJars...)
}

func (j *GenruleCombiner) HeaderJars() android.Paths {
	return j.headerJars
}

func (j *GenruleCombiner) ImplementationAndResourcesJars() android.Paths {
	return j.implementationAndResourceJars
}

func (j *GenruleCombiner) DexJarBuildPath(ctx android.ModuleErrorfContext) android.Path {
	return nil
}

func (j *GenruleCombiner) DexJarInstallPath() android.Path {
	return nil
}

func (j *GenruleCombiner) AidlIncludeDirs() android.Paths {
	return nil
}

func (j *GenruleCombiner) ClassLoaderContexts() dexpreopt.ClassLoaderContextMap {
	return nil
}

func (j *GenruleCombiner) PrepareAndroidMKProviderInfo(config android.Config) *android.AndroidMkProviderInfo {
	info := &android.AndroidMkProviderInfo{}
	info.PrimaryInfo = android.AndroidMkInfo{
		Class:      "JAVA_LIBRARIES",
		OutputFile: android.OptionalPathForPath(j.combinedImplementationJar),
		Include:    "$(BUILD_SYSTEM)/soong_java_prebuilt.mk",
	}
	info.PrimaryInfo.SetBool("LOCAL_UNINSTALLABLE_MODULE", true)
	info.PrimaryInfo.SetPath("LOCAL_SOONG_HEADER_JAR", j.combinedHeaderJar)
	info.PrimaryInfo.SetPath("LOCAL_SOONG_CLASSES_JAR", j.combinedImplementationJar)

	return info
}

// implement the following interface for IDE completion.
var _ android.IDEInfo = (*GenruleCombiner)(nil)

func (j *GenruleCombiner) IDEInfo(ctx android.BaseModuleContext, ideInfo *android.IdeInfo) {
	ideInfo.Deps = append(ideInfo.Deps, j.genruleCombinerProperties.Static_libs.GetOrDefault(ctx, nil)...)
	ideInfo.Libs = append(ideInfo.Libs, j.genruleCombinerProperties.Static_libs.GetOrDefault(ctx, nil)...)
	ideInfo.Deps = append(ideInfo.Deps, j.genruleCombinerProperties.Headers.GetOrDefault(ctx, nil)...)
	ideInfo.Libs = append(ideInfo.Libs, j.genruleCombinerProperties.Headers.GetOrDefault(ctx, nil)...)
}
