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

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"
)

type SdkContext interface {
	// SdkVersion returns SdkSpec that corresponds to the sdk_version property of the current module
	SdkVersion(ctx ConfigContext) SdkSpec
	// SystemModules returns the system_modules property of the current module, or an empty string if it is not set.
	SystemModules() string
	// MinSdkVersion returns ApiLevel that corresponds to the min_sdk_version property of the current module,
	// or from sdk_version if it is not set.
	MinSdkVersion(ctx MinSdkVersionFromValueContext) ApiLevel
	// ReplaceMaxSdkVersionPlaceholder returns Apilevel to replace the maxSdkVersion property of permission and
	// uses-permission tags if it is set.
	ReplaceMaxSdkVersionPlaceholder(ctx EarlyModuleContext) ApiLevel
	// TargetSdkVersion returns the ApiLevel that corresponds to the target_sdk_version property of the current module,
	// or from sdk_version if it is not set.
	TargetSdkVersion(ctx EarlyModuleContext) ApiLevel
}

// SdkKind represents a particular category of an SDK spec like public, system, test, etc.
type SdkKind int

// These are generally ordered from the narrower sdk version to the wider sdk version,
// but not all entries have a strict subset/superset relationship.
// For example, SdkTest and SdkModule do not have a strict subset/superset relationship but both
// are supersets of SdkSystem.
// The general trend should be kept when an additional sdk kind is added.
const (
	SdkInvalid SdkKind = iota
	SdkNone
	SdkToolchain // API surface provided by ART to compile other API domains
	SdkCore
	SdkCorePlatform
	SdkIntraCore // API surface provided by one core module to another
	SdkPublic
	SdkSystem
	SdkTest
	SdkTestFrameworksCore
	SdkModule
	SdkSystemServer
	SdkPrivate
)

// String returns the string representation of this SdkKind
func (k SdkKind) String() string {
	switch k {
	case SdkPrivate:
		return "private"
	case SdkNone:
		return "none"
	case SdkPublic:
		return "public"
	case SdkSystem:
		return "system"
	case SdkTest:
		return "test"
	case SdkTestFrameworksCore:
		return "test_frameworks_core"
	case SdkCore:
		return "core"
	case SdkCorePlatform:
		return "core_platform"
	case SdkIntraCore:
		return "intracore"
	case SdkModule:
		return "module-lib"
	case SdkSystemServer:
		return "system-server"
	case SdkToolchain:
		return "toolchain"
	default:
		return "invalid"
	}
}

func ToSdkKind(s string) SdkKind {
	for kind := SdkNone; kind <= SdkPrivate; kind++ {
		if s == kind.String() {
			return kind
		}
	}
	return SdkInvalid
}

func (k SdkKind) DefaultJavaLibraryName() string {
	switch k {
	case SdkPublic:
		return "android_stubs_current"
	case SdkSystem:
		return "android_system_stubs_current"
	case SdkTest:
		return "android_test_stubs_current"
	case SdkTestFrameworksCore:
		return "android_test_frameworks_core_stubs_current"
	case SdkCore:
		return "core.current.stubs"
	case SdkModule:
		return "android_module_lib_stubs_current"
	case SdkSystemServer:
		return "android_system_server_stubs_current"
	default:
		panic(fmt.Errorf("APIs of API surface %v cannot be provided by a single Soong module\n", k))
	}
}

func JavaLibraryNameToSdkKind(name string) (SdkKind, bool) {
	if name == SdkPublic.DefaultJavaLibraryName() {
		return SdkPublic, true
	}
	if name == SdkSystem.DefaultJavaLibraryName() {
		return SdkSystem, true
	}
	if name == SdkTest.DefaultJavaLibraryName() {
		return SdkTest, true
	}
	if name == SdkTestFrameworksCore.DefaultJavaLibraryName() {
		return SdkTestFrameworksCore, true
	}
	if name == SdkCore.DefaultJavaLibraryName() {
		return SdkCore, true
	}
	if name == SdkModule.DefaultJavaLibraryName() {
		return SdkModule, true
	}
	if name == SdkSystemServer.DefaultJavaLibraryName() {
		return SdkSystemServer, true
	}
	return SdkInvalid, false
}

func (k SdkKind) DefaultExportableJavaLibraryName() string {
	switch k {
	case SdkPublic, SdkSystem, SdkTest, SdkModule, SdkSystemServer:
		return k.DefaultJavaLibraryName() + "_exportable"
	case SdkCore:
		return k.DefaultJavaLibraryName() + ".exportable"
	default:
		panic(fmt.Errorf("API surface %v does not provide exportable stubs", k))
	}
}

// SdkSpec represents the kind and the version of an SDK for a module to build against
type SdkSpec struct {
	Kind     SdkKind
	ApiLevel ApiLevel
	Raw      string
}

func (s SdkSpec) String() string {
	return fmt.Sprintf("%s_%s", s.Kind, s.ApiLevel)
}

// Valid checks if this SdkSpec is well-formed. Note however that true doesn't mean that the
// specified SDK actually exists.
func (s SdkSpec) Valid() bool {
	return s.Kind != SdkInvalid
}

// Specified checks if this SdkSpec is well-formed and is not "".
func (s SdkSpec) Specified() bool {
	return s.Valid() && s.Kind != SdkPrivate
}

// whether the API surface is managed and versioned, i.e. has .txt file that
// get frozen on SDK freeze and changes get reviewed by API council.
func (s SdkSpec) Stable() bool {
	if !s.Specified() {
		return false
	}
	switch s.Kind {
	case SdkNone:
		// there is nothing to manage and version in this case; de facto stable API.
		return true
	case SdkCore, SdkPublic, SdkSystem, SdkModule, SdkSystemServer:
		return true
	case SdkCorePlatform, SdkTest, SdkTestFrameworksCore, SdkPrivate:
		return false
	default:
		panic(fmt.Errorf("unknown SdkKind=%v", s.Kind))
	}
	return false
}

// PrebuiltSdkAvailableForUnbundledBuild tells whether this SdkSpec can have a prebuilt SDK
// that can be used for unbundled builds.
func (s SdkSpec) PrebuiltSdkAvailableForUnbundledBuild() bool {
	// "", "none", and "core_platform" are not available for unbundled build
	// as we don't/can't have prebuilt stub for the versions
	return s.Kind != SdkPrivate && s.Kind != SdkNone && s.Kind != SdkCorePlatform
}

func (s SdkSpec) ForVendorPartition(ctx EarlyModuleContext) SdkSpec {
	// If BOARD_CURRENT_API_LEVEL_FOR_VENDOR_MODULES has a numeric value,
	// use it instead of "current" for the vendor partition.
	currentSdkVersion := ctx.DeviceConfig().CurrentApiLevelForVendorModules()
	// b/314011075: special case for Java modules in vendor partition. They can no longer use
	// SDK 35 or later. Their maximum API level is limited to 34 (Android U). This is to
	// discourage the use of Java APIs in the vendor partition which hasn't been officially
	// supported since the Project Treble back in Android 10. We would like to eventually
	// evacuate all Java modules from the partition, but that shall be done progressively.
	// Note that the check for the availability of SDK 34 is to not break existing tests where
	// any of the frozen SDK version is unavailable.
	if isJava(ctx.Module()) && isSdkVersion34AvailableIn(ctx.Config()) {
		currentSdkVersion = "34"
	}

	if currentSdkVersion == "current" {
		return s
	}

	if s.Kind == SdkPublic || s.Kind == SdkSystem {
		if s.ApiLevel.IsCurrent() {
			if i, err := strconv.Atoi(currentSdkVersion); err == nil {
				apiLevel := UncheckedFinalApiLevel(i)
				return SdkSpec{s.Kind, apiLevel, s.Raw}
			}
			panic(fmt.Errorf("BOARD_CURRENT_API_LEVEL_FOR_VENDOR_MODULES must be either \"current\" or a number, but was %q", currentSdkVersion))
		}
	}
	return s
}

// UsePrebuilt determines whether prebuilt SDK should be used for this SdkSpec with the given context.
func (s SdkSpec) UsePrebuilt(ctx EarlyModuleContext) bool {
	switch s {
	case SdkSpecNone, SdkSpecCorePlatform, SdkSpecPrivate:
		return false
	}

	if s.ApiLevel.IsCurrent() {
		// "current" can be built from source and be from prebuilt SDK
		return ctx.Config().AlwaysUsePrebuiltSdks()
	} else if !s.ApiLevel.IsPreview() {
		// validation check
		if s.Kind != SdkPublic && s.Kind != SdkSystem && s.Kind != SdkTest &&
			s.Kind != SdkTestFrameworksCore && s.Kind != SdkModule && s.Kind != SdkSystemServer {
			panic(fmt.Errorf("prebuilt SDK is not not available for SdkKind=%q", s.Kind))
			return false
		}
		// numbered SDKs are always from prebuilt
		return true
	}
	return false
}

// EffectiveVersion converts an SdkSpec into the concrete ApiLevel that the module should use. For
// modules targeting an unreleased SDK (meaning it does not yet have a number) it returns
// FutureApiLevel(10000).
func (s SdkSpec) EffectiveVersion(ctx EarlyModuleContext) (ApiLevel, error) {
	if !s.Valid() {
		return s.ApiLevel, fmt.Errorf("invalid sdk version %q", s.Raw)
	}

	if ctx.DeviceSpecific() || ctx.SocSpecific() {
		s = s.ForVendorPartition(ctx)
	}
	return s.ApiLevel.EffectiveVersion(ctx)
}

// EffectiveVersionString converts an SdkSpec into the concrete version string that the module
// should use. For modules targeting an unreleased SDK (meaning it does not yet have a number)
// it returns the codename (P, Q, R, etc.)
func (s SdkSpec) EffectiveVersionString(ctx EarlyModuleContext) (string, error) {
	if !s.Valid() {
		return s.ApiLevel.String(), fmt.Errorf("invalid sdk version %q", s.Raw)
	}

	if ctx.DeviceSpecific() || ctx.SocSpecific() {
		s = s.ForVendorPartition(ctx)
	}
	return s.ApiLevel.EffectiveVersionString(ctx)
}

var (
	SdkSpecNone         = SdkSpec{SdkNone, NoneApiLevel, "(no version)"}
	SdkSpecPrivate      = SdkSpec{SdkPrivate, PrivateApiLevel, ""}
	SdkSpecCorePlatform = SdkSpec{SdkCorePlatform, FutureApiLevel, "core_platform"}
)

func SdkSpecFrom(ctx ConfigContext, str string) SdkSpec {
	return SdkSpecFromWithConfig(ctx.Config(), str)
}

func SdkSpecFromWithConfig(config Config, str string) SdkSpec {
	switch str {
	// special cases first
	case "":
		return SdkSpecPrivate
	case "none":
		return SdkSpecNone
	case "core_platform":
		return SdkSpecCorePlatform
	default:
		// the syntax is [kind_]version
		sep := strings.LastIndex(str, "_")

		var kindString string
		if sep == 0 {
			return SdkSpec{SdkInvalid, NewInvalidApiLevel(str), str}
		} else if sep == -1 {
			kindString = ""
		} else {
			kindString = str[0:sep]
		}
		versionString := str[sep+1 : len(str)]

		var kind SdkKind
		switch kindString {
		case "":
			kind = SdkPublic
		case "core":
			kind = SdkCore
		case "system":
			kind = SdkSystem
		case "test":
			kind = SdkTest
		case "test_frameworks_core":
			kind = SdkTestFrameworksCore
		case "module":
			kind = SdkModule
		case "system_server":
			kind = SdkSystemServer
		default:
			return SdkSpec{SdkInvalid, NoneApiLevel, str}
		}

		apiLevel, err := ApiLevelFromUserWithConfig(config, versionString)
		if err != nil {
			return SdkSpec{SdkInvalid, NewInvalidApiLevel(versionString), str}
		}
		return SdkSpec{kind, apiLevel, str}
	}
}

// Checks if the use of this SDK `s` is valid for the given module context `ctx`.
func (s SdkSpec) ValidateSystemSdk(ctx EarlyModuleContext) bool {
	// Do some early checks. This check is currently only for Java modules. And our only concern
	// is the use of "system" SDKs.
	if !isJava(ctx.Module()) || s.Kind != SdkSystem || ctx.DeviceConfig().BuildBrokenDontCheckSystemSdk() {
		return true
	}

	inVendor := ctx.DeviceSpecific() || ctx.SocSpecific()
	inProduct := ctx.ProductSpecific()
	isProductUnbundled := ctx.Config().EnforceProductPartitionInterface()
	inApex := false
	if am, ok := ctx.Module().(ApexModule); ok {
		inApex = am.InAnyApex()
	}
	isUnbundled := inVendor || (inProduct && isProductUnbundled) || inApex

	// Bundled modules can use any SDK
	if !isUnbundled {
		return true
	}

	// Unbundled modules are allowed to use BOARD_SYSTEMSDK_VERSIONS
	supportedVersions := ctx.DeviceConfig().SystemSdkVersions()

	// b/314011075: special case for vendor modules. Java modules in the vendor partition can
	// not use SDK 35 or later. This is to discourage the use of Java APIs in the vendor
	// partition which hasn't been officially supported since the Project Treble back in Android
	// 10. We would like to eventually evacuate all Java modules from the partition, but that
	// shall be done progressively.
	if inVendor {
		// 28 was the API when BOARD_SYSTEMSDK_VERSIONS was introduced, so that's the oldest
		// we should allow.
		supportedVersions = []string{}
		for v := 28; v <= 34; v++ {
			supportedVersions = append(supportedVersions, strconv.Itoa(v))
		}
	}

	// APEXes in the system partition are still considered as part of the platform, thus can use
	// more SDKs from PLATFORM_SYSTEMSDK_VERSIONS
	if inApex && !inVendor {
		supportedVersions = ctx.DeviceConfig().PlatformSystemSdkVersions()
	}

	thisVer, err := s.EffectiveVersion(ctx)
	if err != nil {
		ctx.PropertyErrorf("sdk_version", "invalid sdk version %q", s.Raw)
		return false
	}

	thisVerString := strconv.Itoa(thisVer.FinalOrPreviewInt())
	if thisVer.IsPreview() {
		thisVerString = *ctx.Config().productVariables.Platform_sdk_version_or_codename
	}

	if !InList(thisVerString, supportedVersions) {
		ctx.PropertyErrorf("sdk_version", "incompatible sdk version %q. System SDK version should be one of %q",
			s.Raw, supportedVersions)
		return false
	}
	return true
}

func isJava(m Module) bool {
	moduleType := reflect.TypeOf(m).String()
	return strings.HasPrefix(moduleType, "*java.")
}

func isSdkVersion34AvailableIn(c Config) bool {
	return c.PlatformSdkVersion().FinalInt() >= 34
}

func init() {
	RegisterMakeVarsProvider(pctx, javaSdkMakeVars)
}

// Export the name of the soong modules representing the various Java API surfaces.
func javaSdkMakeVars(ctx MakeVarsContext) {
	ctx.Strict("ANDROID_PUBLIC_STUBS", SdkPublic.DefaultJavaLibraryName())
	ctx.Strict("ANDROID_PUBLIC_EXPORTABLE_STUBS", SdkPublic.DefaultExportableJavaLibraryName())
	ctx.Strict("ANDROID_SYSTEM_STUBS", SdkSystem.DefaultJavaLibraryName())
	ctx.Strict("ANDROID_TEST_STUBS", SdkTest.DefaultJavaLibraryName())
	ctx.Strict("ANDROID_MODULE_LIB_STUBS", SdkModule.DefaultJavaLibraryName())
	ctx.Strict("ANDROID_SYSTEM_SERVER_STUBS", SdkSystemServer.DefaultJavaLibraryName())
	ctx.Strict("ANDROID_CORE_STUBS", SdkCore.DefaultJavaLibraryName())
}
