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

import (
	"fmt"
	"math"
	"sort"
	"strings"
)

const (
	// ArchType names in arch.go
	archArm     = "arm"
	archArm64   = "arm64"
	archRiscv64 = "riscv64"
	archX86     = "x86"
	archX86_64  = "x86_64"

	// OsType names in arch.go
	OsAndroid     = "android"
	osDarwin      = "darwin"
	osLinux       = "linux_glibc"
	osLinuxMusl   = "linux_musl"
	osLinuxBionic = "linux_bionic"
	osWindows     = "windows"

	// Targets in arch.go
	osArchAndroidArm        = "android_arm"
	osArchAndroidArm64      = "android_arm64"
	osArchAndroidRiscv64    = "android_riscv64"
	osArchAndroidX86        = "android_x86"
	osArchAndroidX86_64     = "android_x86_64"
	osArchDarwinArm64       = "darwin_arm64"
	osArchDarwinX86_64      = "darwin_x86_64"
	osArchLinuxX86          = "linux_glibc_x86"
	osArchLinuxX86_64       = "linux_glibc_x86_64"
	osArchLinuxMuslArm      = "linux_musl_arm"
	osArchLinuxMuslArm64    = "linux_musl_arm64"
	osArchLinuxMuslX86      = "linux_musl_x86"
	osArchLinuxMuslX86_64   = "linux_musl_x86_64"
	osArchLinuxBionicArm64  = "linux_bionic_arm64"
	osArchLinuxBionicX86_64 = "linux_bionic_x86_64"
	osArchWindowsX86        = "windows_x86"
	osArchWindowsX86_64     = "windows_x86_64"

	// This is the string representation of the default condition wherever a
	// configurable attribute is used in a select statement, i.e.
	// //conditions:default for Bazel.
	//
	// This is consistently named "conditions_default" to mirror the Soong
	// config variable default key in an Android.bp file, although there's no
	// integration with Soong config variables (yet).
	ConditionsDefaultConfigKey = "conditions_default"

	ConditionsDefaultSelectKey = "//conditions:default"

	productVariableBazelPackage = "//build/bazel/product_variables"

	AndroidAndInApex  = "android-in_apex"
	AndroidAndNonApex = "android-non_apex"

	InApex  = "in_apex"
	NonApex = "non_apex"
)

func PowerSetWithoutEmptySet[T any](items []T) [][]T {
	resultSize := int(math.Pow(2, float64(len(items))))
	powerSet := make([][]T, 0, resultSize-1)
	for i := 1; i < resultSize; i++ {
		combination := make([]T, 0)
		for j := 0; j < len(items); j++ {
			if (i>>j)%2 == 1 {
				combination = append(combination, items[j])
			}
		}
		powerSet = append(powerSet, combination)
	}
	return powerSet
}

func createPlatformArchMap() map[string]string {
	// Copy of archFeatures from android/arch_list.go because the bazel
	// package can't access the android package
	archFeatures := map[string][]string{
		"arm": {
			"neon",
		},
		"arm64": {
			"dotprod",
		},
		"riscv64": {},
		"x86": {
			"ssse3",
			"sse4",
			"sse4_1",
			"sse4_2",
			"aes_ni",
			"avx",
			"avx2",
			"avx512",
			"popcnt",
			"movbe",
		},
		"x86_64": {
			"ssse3",
			"sse4",
			"sse4_1",
			"sse4_2",
			"aes_ni",
			"avx",
			"avx2",
			"avx512",
			"popcnt",
		},
	}
	result := make(map[string]string)
	for arch, allFeatures := range archFeatures {
		result[arch] = "//build/bazel/platforms/arch:" + arch
		// Sometimes we want to select on multiple features being active, so
		// add the power set of all possible features to the map. More details
		// in android.ModuleBase.GetArchVariantProperties
		for _, features := range PowerSetWithoutEmptySet(allFeatures) {
			sort.Strings(features)
			archFeaturesName := arch + "-" + strings.Join(features, "-")
			result[archFeaturesName] = "//build/bazel/platforms/arch/variants:" + archFeaturesName
		}
	}
	result[ConditionsDefaultConfigKey] = ConditionsDefaultSelectKey
	return result
}

var (
	// These are the list of OSes and architectures with a Bazel config_setting
	// and constraint value equivalent. These exist in arch.go, but the android
	// package depends on the bazel package, so a cyclic dependency prevents
	// using those variables here.

	// A map of architectures to the Bazel label of the constraint_value
	// for the @platforms//cpu:cpu constraint_setting
	platformArchMap = createPlatformArchMap()

	// A map of target operating systems to the Bazel label of the
	// constraint_value for the @platforms//os:os constraint_setting
	platformOsMap = map[string]string{
		OsAndroid:                  "//build/bazel/platforms/os:android",
		osDarwin:                   "//build/bazel/platforms/os:darwin",
		osLinux:                    "//build/bazel/platforms/os:linux",
		osLinuxMusl:                "//build/bazel/platforms/os:linux_musl",
		osLinuxBionic:              "//build/bazel/platforms/os:linux_bionic",
		osWindows:                  "//build/bazel/platforms/os:windows",
		ConditionsDefaultConfigKey: ConditionsDefaultSelectKey, // The default condition of an os select map.
	}

	platformOsArchMap = map[string]string{
		osArchAndroidArm:           "//build/bazel/platforms/os_arch:android_arm",
		osArchAndroidArm64:         "//build/bazel/platforms/os_arch:android_arm64",
		osArchAndroidRiscv64:       "//build/bazel/platforms/os_arch:android_riscv64",
		osArchAndroidX86:           "//build/bazel/platforms/os_arch:android_x86",
		osArchAndroidX86_64:        "//build/bazel/platforms/os_arch:android_x86_64",
		osArchDarwinArm64:          "//build/bazel/platforms/os_arch:darwin_arm64",
		osArchDarwinX86_64:         "//build/bazel/platforms/os_arch:darwin_x86_64",
		osArchLinuxX86:             "//build/bazel/platforms/os_arch:linux_glibc_x86",
		osArchLinuxX86_64:          "//build/bazel/platforms/os_arch:linux_glibc_x86_64",
		osArchLinuxMuslArm:         "//build/bazel/platforms/os_arch:linux_musl_arm",
		osArchLinuxMuslArm64:       "//build/bazel/platforms/os_arch:linux_musl_arm64",
		osArchLinuxMuslX86:         "//build/bazel/platforms/os_arch:linux_musl_x86",
		osArchLinuxMuslX86_64:      "//build/bazel/platforms/os_arch:linux_musl_x86_64",
		osArchLinuxBionicArm64:     "//build/bazel/platforms/os_arch:linux_bionic_arm64",
		osArchLinuxBionicX86_64:    "//build/bazel/platforms/os_arch:linux_bionic_x86_64",
		osArchWindowsX86:           "//build/bazel/platforms/os_arch:windows_x86",
		osArchWindowsX86_64:        "//build/bazel/platforms/os_arch:windows_x86_64",
		ConditionsDefaultConfigKey: ConditionsDefaultSelectKey, // The default condition of an os select map.
	}

	// Map where keys are OsType names, and values are slices containing the archs
	// that that OS supports.
	// These definitions copied from arch.go.
	// TODO(cparsons): Source from arch.go; this task is nontrivial, as it currently results
	// in a cyclic dependency.
	osToArchMap = map[string][]string{
		OsAndroid:     {archArm, archArm64, archRiscv64, archX86, archX86_64},
		osLinux:       {archX86, archX86_64},
		osLinuxMusl:   {archX86, archX86_64},
		osDarwin:      {archArm64, archX86_64},
		osLinuxBionic: {archArm64, archX86_64},
		// TODO(cparsons): According to arch.go, this should contain archArm, archArm64, as well.
		osWindows: {archX86, archX86_64},
	}

	osAndInApexMap = map[string]string{
		AndroidAndInApex:           "//build/bazel/rules/apex:android-in_apex",
		AndroidAndNonApex:          "//build/bazel/rules/apex:android-non_apex",
		osDarwin:                   "//build/bazel/platforms/os:darwin",
		osLinux:                    "//build/bazel/platforms/os:linux",
		osLinuxMusl:                "//build/bazel/platforms/os:linux_musl",
		osLinuxBionic:              "//build/bazel/platforms/os:linux_bionic",
		osWindows:                  "//build/bazel/platforms/os:windows",
		ConditionsDefaultConfigKey: ConditionsDefaultSelectKey,
	}

	inApexMap = map[string]string{
		InApex:                     "//build/bazel/rules/apex:in_apex",
		NonApex:                    "//build/bazel/rules/apex:non_apex",
		ConditionsDefaultConfigKey: ConditionsDefaultSelectKey,
	}
)

// basic configuration types
type configurationType int

const (
	noConfig configurationType = iota
	arch
	os
	osArch
	productVariables
	osAndInApex
	inApex
)

func osArchString(os string, arch string) string {
	return fmt.Sprintf("%s_%s", os, arch)
}

func (ct configurationType) String() string {
	return map[configurationType]string{
		noConfig:         "no_config",
		arch:             "arch",
		os:               "os",
		osArch:           "arch_os",
		productVariables: "product_variables",
		osAndInApex:      "os_in_apex",
		inApex:           "in_apex",
	}[ct]
}

func (ct configurationType) validateConfig(config string) {
	switch ct {
	case noConfig:
		if config != "" {
			panic(fmt.Errorf("Cannot specify config with %s, but got %s", ct, config))
		}
	case arch:
		if _, ok := platformArchMap[config]; !ok {
			panic(fmt.Errorf("Unknown arch: %s", config))
		}
	case os:
		if _, ok := platformOsMap[config]; !ok {
			panic(fmt.Errorf("Unknown os: %s", config))
		}
	case osArch:
		if _, ok := platformOsArchMap[config]; !ok {
			panic(fmt.Errorf("Unknown os+arch: %s", config))
		}
	case productVariables:
		// do nothing
	case osAndInApex:
		if _, ok := osAndInApexMap[config]; !ok {
			panic(fmt.Errorf("Unknown os+in_apex config: %s", config))
		}
	case inApex:
		if _, ok := inApexMap[config]; !ok {
			panic(fmt.Errorf("Unknown in_apex config: %s", config))
		}
	default:
		panic(fmt.Errorf("Unrecognized ConfigurationType %d", ct))
	}
}

// SelectKey returns the Bazel select key for a given configurationType and config string.
func (ca ConfigurationAxis) SelectKey(config string) string {
	ca.validateConfig(config)
	switch ca.configurationType {
	case noConfig:
		panic(fmt.Errorf("SelectKey is unnecessary for noConfig ConfigurationType "))
	case arch:
		return platformArchMap[config]
	case os:
		return platformOsMap[config]
	case osArch:
		return platformOsArchMap[config]
	case productVariables:
		if strings.HasSuffix(config, ConditionsDefaultConfigKey) {
			// e.g. "acme__feature1__conditions_default" or "android__board__conditions_default"
			return ConditionsDefaultSelectKey
		}
		return fmt.Sprintf("%s:%s", productVariableBazelPackage, config)
	case osAndInApex:
		return osAndInApexMap[config]
	case inApex:
		return inApexMap[config]
	default:
		panic(fmt.Errorf("Unrecognized ConfigurationType %d", ca.configurationType))
	}
}

var (
	// Indicating there is no configuration axis
	NoConfigAxis = ConfigurationAxis{configurationType: noConfig}
	// An axis for architecture-specific configurations
	ArchConfigurationAxis = ConfigurationAxis{configurationType: arch}
	// An axis for os-specific configurations
	OsConfigurationAxis = ConfigurationAxis{configurationType: os}
	// An axis for arch+os-specific configurations
	OsArchConfigurationAxis = ConfigurationAxis{configurationType: osArch}
	// An axis for os+in_apex-specific configurations
	OsAndInApexAxis = ConfigurationAxis{configurationType: osAndInApex}
	// An axis for in_apex-specific configurations
	InApexAxis = ConfigurationAxis{configurationType: inApex}
)

// ProductVariableConfigurationAxis returns an axis for the given product variable
func ProductVariableConfigurationAxis(variable string, outerAxis ConfigurationAxis) ConfigurationAxis {
	return ConfigurationAxis{
		configurationType: productVariables,
		subType:           variable,
		outerAxisType:     outerAxis.configurationType,
	}
}

// ConfigurationAxis is an independent axis for configuration, there should be no overlap between
// elements within an axis.
type ConfigurationAxis struct {
	configurationType
	// some configuration types (e.g. productVariables) have multiple independent axes, subType helps
	// distinguish between them without needing to list all 17 product variables.
	subType string
	// used to keep track of which product variables are arch variant
	outerAxisType configurationType
}

func (ca *ConfigurationAxis) less(other ConfigurationAxis) bool {
	if ca.configurationType < other.configurationType {
		return true
	}
	return ca.subType < other.subType
}
