package pkglint

// Var describes a variable in a Makefile snippet.
//
// It keeps track of all places where the variable is accessed or modified (see
// ReadLocations, WriteLocations) and provides information for further static
// analysis, such as:
//
// * Whether the variable value is constant, and if so, what the constant value
// is (see IsConstant, ConstantValue).
//
// * What its (approximated) value is, either including values from the pkgsrc
// infrastructure (see ValueInfra) or excluding them (Value).
//
// * On which other variables this variable depends (see IsConditional,
// ConditionalVars).
//
// TODO: Decide how to handle OPSYS-specific variables, such as LDFLAGS.SunOS.
//
// TODO: Decide how to handle parameterized variables, such as SUBST_MESSAGE.doc.
type Var struct {
	Name string

	//  0 = neither written nor read
	//  1 = constant
	//  2 = constant and read; further writes will make it non-constant
	//  3 = not constant anymore
	constantState uint8
	constantValue string

	value      string
	valueInfra string

	readLocations  []*MkLine
	writeLocations []*MkLine

	conditional     bool
	conditionalVars StringSet

	refs StringSet
}

func NewVar(name string) *Var {
	return &Var{name, 0, "", "", "", nil, nil, false, NewStringSet(), NewStringSet()}
}

// IsConditional returns whether the variable value depends on other variables.
func (v *Var) IsConditional() bool {
	return v.conditional
}

// ConditionalVars returns all variables in conditions on which the value of
// this variable depends.
//
// The returned slice must not be modified.
func (v *Var) ConditionalVars() []string {
	return v.conditionalVars.Elements
}

// Refs returns all variables on which this variable depends. These are:
//
// Variables that are referenced in the value, such as in VAR=${OTHER}.
//
// Variables that are used in conditions that enclose one of the assignments
// to this variable, such as .if ${OPSYS} == NetBSD.
//
// Variables that are used in .for loops in which this variable is assigned
// a value, such as DIRS in:
//  .for dir in ${DIRS}
//  VAR+=${dir}
//  .endfor
func (v *Var) Refs() []string {
	return v.refs.Elements
}

// AddRef marks this variable as being dependent on the given variable name.
// This can be used for the .for loops mentioned in Refs.
func (v *Var) AddRef(varname string) {
	v.refs.Add(varname)
}

// IsConstant returns whether the variable's value is a constant.
// It may reference other variables since these references are evaluated
// lazily, when the variable value is actually needed.
//
// Multiple assignments (such as VAR=1, VAR+=2, VAR+=3) are considered to
// form a single constant as well, as long as the variable is not read before
// or in-between these assignments. The definition of "read" is very strict
// here since every mention of the variable counts. This may prevent some
// essentially constant values from being detected as such, but these special
// cases may be implemented later.
//
// TODO: Simple .for loops that append to the variable are ok as well.
//  (This needs to be worded more precisely since that part potentially
//  adds a lot of complexity to the whole data structure.)
//
// Variable assignments in the pkgsrc infrastructure are taken into account
// for determining whether a variable is constant.
func (v *Var) IsConstant() bool {
	return v.constantState == 1 || v.constantState == 2
}

// ConstantValue returns the constant value of the variable.
// It is only allowed when IsConstant() returns true.
//
// Variable assignments in the pkgsrc infrastructure are taken into account
// for determining the constant value.
func (v *Var) ConstantValue() string {
	assert(v.IsConstant())
	return v.constantValue
}

// Value returns the (approximated) value of the variable, taking into account
// all variable assignments that happen outside the pkgsrc infrastructure.
//
// For variables that are conditionally assigned (as in .if/.else), the
// returned value is not reliable. It may be the value from either branch, or
// even the combined value of both branches.
//
// See IsConstant and ConstantValue for more reliable information.
func (v *Var) Value() string {
	return v.value
}

// ValueInfra returns the (approximated) value of the variable, taking into
// account all variable assignments from the package, the user and the pkgsrc
// infrastructure.
//
// For variables that are conditionally assigned (as in .if/.else), the
// returned value is not reliable. It may be the value from either branch, or
// even the combined value of both branches.
//
// See IsConstant and ConstantValue for more reliable information, but these
// ignore assignments from the infrastructure.
func (v *Var) ValueInfra() string {
	return v.valueInfra
}

// ReadLocations returns the locations where the variable is read, such as
// in ${VAR} or defined(VAR) or empty(VAR).
//
// Uses inside conditionals are included, no matter whether they are actually
// reachable in practice.
//
// Indirect uses through other variables (such as VAR2=${VAR}, VAR3=${VAR2})
// are not listed.
//
// Variable uses in the pkgsrc infrastructure are taken into account.
func (v *Var) ReadLocations() []*MkLine {
	return v.readLocations
}

// WriteLocations returns the locations where the variable is modified.
//
// Assignments inside conditionals are included, no matter whether they are actually
// reachable in practice.
//
// Variable assignments in the pkgsrc infrastructure are taken into account.
func (v *Var) WriteLocations() []*MkLine {
	return v.writeLocations
}

func (v *Var) Read(mkline *MkLine) {
	v.readLocations = append(v.readLocations, mkline)
	v.constantState = [...]uint8{3, 2, 2, 3}[v.constantState]
}

// Write marks the variable as being assigned in the given line.
// Only standard assignments (VAR=value) are handled.
// Side-effect assignments (${VAR::=value}) are not handled here since
// they don't occur in practice.
func (v *Var) Write(mkline *MkLine, conditional bool, conditionVarnames ...string) {
	assert(mkline.Varname() == v.Name)

	v.writeLocations = append(v.writeLocations, mkline)

	if conditional {
		v.conditional = true
	}
	v.conditionalVars.AddAll(conditionVarnames)

	mkline.ForEachUsed(func(varUse *MkVarUse, time VucTime) {
		v.refs.Add(varUse.varname)
	})
	v.refs.AddAll(conditionVarnames)

	v.update(mkline, &v.valueInfra)
	if !G.Pkgsrc.IsInfra(mkline.Line.Filename()) {
		v.update(mkline, &v.value)
	}

	v.updateConstantValue(mkline)
}

func (v *Var) update(mkline *MkLine, update *string) {
	firstWrite := len(v.writeLocations) == 1
	if v.IsConditional() && !firstWrite {
		return
	}

	value := mkline.Value()
	switch mkline.Op() {
	case opAssign, opAssignEval:
		*update = value

	case opAssignDefault:
		if firstWrite {
			*update = value
		}

	case opAssignAppend:
		*update += " " + value

	default:
		// Ignore these for now.
		// Later it might be useful to parse the shell commands to
		// evaluate simple commands like "test && echo yes || echo no".
	}
}

func (v *Var) updateConstantValue(mkline *MkLine) {
	if v.constantState == 3 {
		return
	}

	// Even if the variable references other variables, this does not
	// influence whether the variable is considered constant. (Except
	// for the := operator.)
	//
	// Strictly speaking, the referenced variables must be still
	// be constant at the end of loading the complete package.
	// (And even after that, because of the ::= modifier. But luckily
	// almost no one knows that modifier.)

	if v.IsConditional() {
		v.constantState = 3
		v.constantValue = ""
		return
	}

	value := mkline.Value()
	switch mkline.Op() {
	case opAssign:
		v.constantValue = value

	case opAssignEval:
		if value != mkline.WithoutMakeVariables(value) {
			// To leave the variable in the constant state, the current value
			// of the referenced variables would need to be resolved.
			//
			// This in turn requires the proper scope for resolving variable
			// references. Furthermore, the referenced variables must be
			// constant at this point. Later changes to these variables
			// can be ignored though.
			//
			// Because this sounds complicated to implement, the variable
			// is marked as non-constant for now.
			v.constantState = 3
			v.constantValue = ""
		} else {
			v.constantValue = value
		}

	case opAssignDefault:
		if v.constantState == 0 {
			v.constantValue = value
		}

	case opAssignAppend:
		if v.constantState != 0 {
			v.constantValue += " "
		}
		v.constantValue += value

	default:
		v.constantState = 3
		v.constantValue = ""
	}

	v.constantState |= 1
}
