// Copyright 2021 Google LLC
//
// 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 mk2rbc

import (
	"fmt"
	"strings"
)

type variable interface {
	name() string
	emitGet(gctx *generationContext)
	emitSet(gctx *generationContext, asgn *assignmentNode)
	valueType() starlarkType
	setValueType(t starlarkType)
	defaultValueString() string
	isPreset() bool
}

type baseVariable struct {
	nam    string
	typ    starlarkType
	preset bool // true if it has been initialized at startup
}

func (v baseVariable) name() string {
	return v.nam
}

func (v baseVariable) valueType() starlarkType {
	return v.typ
}

func (v *baseVariable) setValueType(t starlarkType) {
	v.typ = t
}

func (v baseVariable) isPreset() bool {
	return v.preset
}

var defaultValuesByType = map[starlarkType]string{
	starlarkTypeUnknown: `""`,
	starlarkTypeList:    "[]",
	starlarkTypeString:  `""`,
	starlarkTypeInt:     "0",
	starlarkTypeBool:    "False",
	starlarkTypeVoid:    "None",
}

func (v baseVariable) defaultValueString() string {
	if v, ok := defaultValuesByType[v.valueType()]; ok {
		return v
	}
	panic(fmt.Errorf("%s has unknown type %q", v.name(), v.valueType()))
}

type productConfigVariable struct {
	baseVariable
}

func (pcv productConfigVariable) emitSet(gctx *generationContext, asgn *assignmentNode) {
	emitAssignment := func() {
		gctx.writef("cfg[%q] = ", pcv.nam)
		asgn.value.emitListVarCopy(gctx)
	}
	emitAppend := func() {
		gctx.writef("cfg[%q] += ", pcv.nam)
		value := asgn.value
		if pcv.valueType() == starlarkTypeString {
			gctx.writef(`" " + `)
			value = &toStringExpr{expr: value}
		}
		value.emit(gctx)
	}
	emitSetDefault := func() {
		if pcv.typ == starlarkTypeList {
			gctx.writef("%s(handle, %q)", cfnSetListDefault, pcv.name())
		} else {
			gctx.writef("cfg.setdefault(%q, %s)", pcv.name(), pcv.defaultValueString())
		}
		gctx.newLine()
	}

	// If we are not sure variable has been assigned before, emit setdefault
	needsSetDefault := !gctx.hasBeenAssigned(&pcv) && !pcv.isPreset() && asgn.isSelfReferential()

	switch asgn.flavor {
	case asgnSet:
		if needsSetDefault {
			emitSetDefault()
		}
		emitAssignment()
	case asgnAppend:
		if needsSetDefault {
			emitSetDefault()
		}
		emitAppend()
	case asgnMaybeSet:
		gctx.writef("if cfg.get(%q) == None:", pcv.nam)
		gctx.indentLevel++
		gctx.newLine()
		if needsSetDefault {
			emitSetDefault()
		}
		emitAssignment()
		gctx.indentLevel--
	}

	gctx.setHasBeenAssigned(&pcv)
}

func (pcv productConfigVariable) emitGet(gctx *generationContext) {
	if gctx.hasBeenAssigned(&pcv) || pcv.isPreset() {
		gctx.writef("cfg[%q]", pcv.nam)
	} else {
		gctx.writef("cfg.get(%q, %s)", pcv.nam, pcv.defaultValueString())
	}
}

type otherGlobalVariable struct {
	baseVariable
}

func (scv otherGlobalVariable) emitSet(gctx *generationContext, asgn *assignmentNode) {
	emitAssignment := func() {
		gctx.writef("g[%q] = ", scv.nam)
		asgn.value.emitListVarCopy(gctx)
	}

	emitAppend := func() {
		gctx.writef("g[%q] += ", scv.nam)
		value := asgn.value
		if scv.valueType() == starlarkTypeString {
			gctx.writef(`" " + `)
			value = &toStringExpr{expr: value}
		}
		value.emit(gctx)
	}

	// If we are not sure variable has been assigned before, emit setdefault
	needsSetDefault := !gctx.hasBeenAssigned(&scv) && !scv.isPreset() && asgn.isSelfReferential()

	switch asgn.flavor {
	case asgnSet:
		if needsSetDefault {
			gctx.writef("g.setdefault(%q, %s)", scv.name(), scv.defaultValueString())
			gctx.newLine()
		}
		emitAssignment()
	case asgnAppend:
		if needsSetDefault {
			gctx.writef("g.setdefault(%q, %s)", scv.name(), scv.defaultValueString())
			gctx.newLine()
		}
		emitAppend()
	case asgnMaybeSet:
		gctx.writef("if g.get(%q) == None:", scv.nam)
		gctx.indentLevel++
		gctx.newLine()
		if needsSetDefault {
			gctx.writef("g.setdefault(%q, %s)", scv.name(), scv.defaultValueString())
			gctx.newLine()
		}
		emitAssignment()
		gctx.indentLevel--
	}

	gctx.setHasBeenAssigned(&scv)
}

func (scv otherGlobalVariable) emitGet(gctx *generationContext) {
	if gctx.hasBeenAssigned(&scv) || scv.isPreset() {
		gctx.writef("g[%q]", scv.nam)
	} else {
		gctx.writef("g.get(%q, %s)", scv.nam, scv.defaultValueString())
	}
}

type localVariable struct {
	baseVariable
}

func (lv localVariable) String() string {
	return "_" + lv.nam
}

func (lv localVariable) emitSet(gctx *generationContext, asgn *assignmentNode) {
	switch asgn.flavor {
	case asgnSet, asgnMaybeSet:
		gctx.writef("%s = ", lv)
		asgn.value.emitListVarCopy(gctx)
	case asgnAppend:
		gctx.writef("%s += ", lv)
		value := asgn.value
		if lv.valueType() == starlarkTypeString {
			gctx.writef(`" " + `)
			value = &toStringExpr{expr: value}
		}
		value.emit(gctx)
	}
}

func (lv localVariable) emitGet(gctx *generationContext) {
	gctx.writef("%s", lv)
}

type predefinedVariable struct {
	baseVariable
	value starlarkExpr
}

func (pv predefinedVariable) emitGet(gctx *generationContext) {
	pv.value.emit(gctx)
}

func (pv predefinedVariable) emitSet(gctx *generationContext, asgn *assignmentNode) {
	if expectedValue, ok1 := maybeString(pv.value); ok1 {
		actualValue, ok2 := maybeString(asgn.value)
		if ok2 {
			if actualValue == expectedValue {
				return
			}
			gctx.emitConversionError(asgn.location,
				fmt.Sprintf("cannot set predefined variable %s to %q, its value should be %q",
					pv.name(), actualValue, expectedValue))
			gctx.starScript.hasErrors = true
			return
		}
	}
	panic(fmt.Errorf("cannot set predefined variable %s to %q", pv.name(), asgn.mkValue.Dump()))
}

var localProductConfigVariables = map[string]string{
	"LOCAL_AUDIO_PRODUCT_PACKAGE":         "PRODUCT_PACKAGES",
	"LOCAL_AUDIO_PRODUCT_COPY_FILES":      "PRODUCT_COPY_FILES",
	"LOCAL_AUDIO_DEVICE_PACKAGE_OVERLAYS": "DEVICE_PACKAGE_OVERLAYS",
	"LOCAL_DUMPSTATE_PRODUCT_PACKAGE":     "PRODUCT_PACKAGES",
	"LOCAL_GATEKEEPER_PRODUCT_PACKAGE":    "PRODUCT_PACKAGES",
	"LOCAL_HEALTH_PRODUCT_PACKAGE":        "PRODUCT_PACKAGES",
	"LOCAL_SENSOR_PRODUCT_PACKAGE":        "PRODUCT_PACKAGES",
	"LOCAL_KEYMASTER_PRODUCT_PACKAGE":     "PRODUCT_PACKAGES",
	"LOCAL_KEYMINT_PRODUCT_PACKAGE":       "PRODUCT_PACKAGES",
}

var presetVariables = map[string]bool{
	"BUILD_ID":                  true,
	"HOST_ARCH":                 true,
	"HOST_OS":                   true,
	"HOST_BUILD_TYPE":           true,
	"OUT_DIR":                   true,
	"PLATFORM_VERSION_CODENAME": true,
	"PLATFORM_VERSION":          true,
	"TARGET_ARCH":               true,
	"TARGET_ARCH_VARIANT":       true,
	"TARGET_BUILD_TYPE":         true,
	"TARGET_BUILD_VARIANT":      true,
	"TARGET_PRODUCT":            true,
}

// addVariable returns a variable with a given name. A variable is
// added if it does not exist yet.
func (ctx *parseContext) addVariable(name string) variable {
	// Get the hintType before potentially changing the variable name
	var hintType starlarkType
	var ok bool
	if hintType, ok = ctx.typeHints[name]; !ok {
		hintType = starlarkTypeUnknown
	}
	// Heuristics: if variable's name is all lowercase, consider it local
	// string variable.
	isLocalVariable := name == strings.ToLower(name)
	// Local variables can't have special characters in them, because they
	// will be used as starlark identifiers
	if isLocalVariable {
		name = strings.ReplaceAll(strings.TrimSpace(name), "-", "_")
	}
	v, found := ctx.variables[name]
	if !found {
		if vi, found := KnownVariables[name]; found {
			_, preset := presetVariables[name]
			switch vi.class {
			case VarClassConfig:
				v = &productConfigVariable{baseVariable{nam: name, typ: vi.valueType, preset: preset}}
			case VarClassSoong:
				v = &otherGlobalVariable{baseVariable{nam: name, typ: vi.valueType, preset: preset}}
			}
		} else if isLocalVariable {
			v = &localVariable{baseVariable{nam: name, typ: hintType}}
		} else {
			vt := hintType
			// Heuristics: local variables that contribute to corresponding config variables
			if cfgVarName, found := localProductConfigVariables[name]; found && vt == starlarkTypeUnknown {
				vi, found2 := KnownVariables[cfgVarName]
				if !found2 {
					panic(fmt.Errorf("unknown config variable %s for %s", cfgVarName, name))
				}
				vt = vi.valueType
			}
			if strings.HasSuffix(name, "_LIST") && vt == starlarkTypeUnknown {
				// Heuristics: Variables with "_LIST" suffix are lists
				vt = starlarkTypeList
			}
			v = &otherGlobalVariable{baseVariable{nam: name, typ: vt}}
		}
		ctx.variables[name] = v
	}
	return v
}
