// Copyright 2020 CUE Authors
//
// 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 adt

// This file implements the closedness algorithm.

// Outline of algorithm
//
// To compute closedness each Vertex is associated with a tree which has
// leaf nodes with sets of allowed labels, and interior nodes that describe
// how these sets may be combines: Or, for embedding, or And for definitions.
//
// Each conjunct of a Vertex is associated with such a leaf node. Each
// conjunct that evaluates to a struct is added to the list of Structs, which
// in the end forms this tree. If a conjunct is embedded, or references another
// struct or definition, it adds interior node to reflect this.
//
// To test whether a feature is allowed, it must satisfy the resulting
// expression tree.
//
// In order to avoid having to copy the tree for each node, the tree is linked
// from leaf node to root, rather than the other way around. This allows
// parent nodes to be shared as the tree grows and ensures that the growth
// of the tree is bounded by the number of conjuncts. As a consequence, this
// requires a two-pass algorithm:
//
//    - walk up to mark which nodes are required and count the number of
//      child nodes that need to be satisfied.
//    - verify fields in leaf structs and mark parent leafs as satisfied
//      when appropriate.
//
// A label is allowed if all required root nodes are marked as accepted after
// these two passes.
//

// A note on embeddings: it is important to keep track which conjuncts originate
// from an embedding, as an embedded value may eventually turn into a closed
// struct. Consider
//
//    a: {
//       b
//       d: e: int
//    }
//    b: d: {
//       #A & #B
//    }
//
// At the point of evaluating `a`, the struct is not yet closed. However,
// descending into `d` will trigger the inclusion of definitions which in turn
// causes the struct to be closed. At this point, it is important to know that
// `b` originated from an embedding, as otherwise `e` may not be allowed.

// TODO(perf):
// - less nodes
// - disable StructInfo nodes that can no longer pass a feature
// - sort StructInfos active ones first.

// TODO(errors): return a dedicated ConflictError that can track original
// positions on demand.

// IsInOneOf reports whether any of the Structs associated with v is contained
// within any of the span types in the given mask.
func (v *Vertex) IsInOneOf(mask SpanType) bool {
	for _, s := range v.Structs {
		if s.CloseInfo.IsInOneOf(mask) {
			return true
		}
	}
	return false
}

// IsRecursivelyClosed returns true if this value is either a definition or unified
// with a definition.
func (v *Vertex) IsRecursivelyClosed() bool {
	return v.Closed || v.IsInOneOf(DefinitionSpan)
}

type closeNodeType uint8

const (
	// a closeRef node is created when there is a non-definition reference.
	// These nodes are not necessary for computing results, but may be
	// relevant down the line to group closures through embedded values and
	// to track position information for failures.
	closeRef closeNodeType = iota

	// closeDef indicates this node was introduced as a result of referencing
	// a definition.
	closeDef

	// closeEmbed indicates this node was added as a result of an embedding.
	closeEmbed

	_ = closeRef // silence the linter
)

// TODO: merge with closeInfo: this is a leftover of the refactoring.
type CloseInfo struct {
	*closeInfo               // old implementation (TODO: remove)
	cc         *closeContext // new implementation (TODO: rename field to closeCtx)

	// IsClosed is true if this conjunct represents a single level of closing
	// as indicated by the closed builtin.
	IsClosed bool

	// FromEmbed indicates whether this conjunct was inserted because of an
	// embedding.  This flag is sticky: it will be set for conjuncts created
	// from fields defined by this conjunct.
	// NOTE: only used when using closeContext.
	FromEmbed bool

	// FromDef indicates whether this conjunct was inserted because of a
	// definition. This flag is sticky: it will be set for conjuncts created
	// from fields defined by this conjunct.
	// NOTE: only used when using closeContext.
	FromDef bool

	// FieldTypes indicates which kinds of fields (optional, dynamic, patterns,
	// etc.) are contained in this conjunct.
	FieldTypes OptionalType

	CycleInfo
}

func (c CloseInfo) Location() Node {
	if c.closeInfo == nil {
		return nil
	}
	return c.closeInfo.location
}

func (c CloseInfo) span() SpanType {
	if c.closeInfo == nil {
		return 0
	}
	return c.closeInfo.span
}

func (c CloseInfo) RootSpanType() SpanType {
	if c.closeInfo == nil {
		return 0
	}
	return c.root
}

// IsInOneOf reports whether c is contained within any of the span types in the
// given mask.
func (c CloseInfo) IsInOneOf(t SpanType) bool {
	return c.span()&t != 0
}

// TODO(perf): remove: error positions should always be computed on demand
// in dedicated error types.
func (c *CloseInfo) AddPositions(ctx *OpContext) {
	for s := c.closeInfo; s != nil; s = s.parent {
		if loc := s.location; loc != nil {
			ctx.AddPosition(loc)
		}
	}
}

// TODO(perf): use on StructInfo. Then if parent and expression are the same
// it is possible to use cached value.
func (c CloseInfo) SpawnEmbed(x Node) CloseInfo {
	c.closeInfo = &closeInfo{
		parent:   c.closeInfo,
		location: x,
		mode:     closeEmbed,
		root:     EmbeddingSpan,
		span:     c.span() | EmbeddingSpan,
	}
	return c
}

// SpawnGroup is used for structs that contain embeddings that may end up
// closing the struct. This is to force that `b` is not allowed in
//
//	a: {#foo} & {b: int}
func (c CloseInfo) SpawnGroup(x Expr) CloseInfo {
	c.closeInfo = &closeInfo{
		parent:   c.closeInfo,
		location: x,
		span:     c.span(),
	}
	return c
}

// SpawnSpan is used to track that a value is introduced by a comprehension
// or constraint. Definition and embedding spans are introduced with SpawnRef
// and SpawnEmbed, respectively.
func (c CloseInfo) SpawnSpan(x Node, t SpanType) CloseInfo {
	c.closeInfo = &closeInfo{
		parent:   c.closeInfo,
		location: x,
		root:     t,
		span:     c.span() | t,
	}
	return c
}

func (c CloseInfo) SpawnRef(arc *Vertex, isDef bool, x Expr) CloseInfo {
	span := c.span()
	found := false
	if !isDef {
		xnode := Node(x) // Optimization so we're comparing identical interface types.
		// TODO: make this work for non-definitions too.
		for p := c.closeInfo; p != nil; p = p.parent {
			if p.span == span && p.location == xnode {
				found = true
				break
			}
		}
	}
	if !found {
		c.closeInfo = &closeInfo{
			parent:   c.closeInfo,
			location: x,
			span:     span,
		}
	}
	if isDef {
		c.mode = closeDef
		c.closeInfo.root = DefinitionSpan
		c.closeInfo.span |= DefinitionSpan
	}
	return c
}

// IsDef reports whether an expressions is a reference that references a
// definition anywhere in its selection path.
//
// TODO(performance): this should be merged with resolve(). But for now keeping
// this code isolated makes it easier to see what it is for.
func IsDef(x Expr) bool {
	switch r := x.(type) {
	case *FieldReference:
		return r.Label.IsDef()

	case *SelectorExpr:
		if r.Sel.IsDef() {
			return true
		}
		return IsDef(r.X)

	case *IndexExpr:
		return IsDef(r.X)
	}
	return false
}

// A SpanType is used to indicate whether a CUE value is within the scope of
// a certain CUE language construct, the span type.
type SpanType uint8

const (
	// EmbeddingSpan means that this value was embedded at some point and should
	// not be included as a possible root node in the todo field of OpContext.
	EmbeddingSpan SpanType = 1 << iota
	ConstraintSpan
	ComprehensionSpan
	DefinitionSpan
)

type closeInfo struct {
	// location records the expression that led to this node's introduction.
	location Node

	// The parent node in the tree.
	parent *closeInfo

	// TODO(performance): if references are chained, we could have a separate
	// parent pointer to skip the chain.

	// mode indicates whether this node was added as part of an embedding,
	// definition or non-definition reference.
	mode closeNodeType

	// noCheck means this struct is irrelevant for closedness checking. This can
	// happen when:
	//  - it is a sibling of a new definition.
	noCheck bool // don't process for inclusion info

	root SpanType
	span SpanType
}

// closeStats holds the administrative fields for a closeInfo value. Each
// closeInfo is associated with a single closeStats value per unification
// operator. This association is done through an OpContext. This allows the
// same value to be used in multiple concurrent unification operations.
// NOTE: there are other parts of the algorithm that are not thread-safe yet.
type closeStats struct {
	// the other fields of this closeStats value are only valid if generation
	// is equal to the generation in OpContext. This allows for lazy
	// initialization of closeStats.
	generation int

	// These counts keep track of how many required child nodes need to be
	// completed before this node is accepted.
	requiredCount int
	acceptedCount int

	// accepted is set if this node is accepted.
	accepted bool

	required bool

	inTodoList bool // true if added to todo list.
	next       *closeStats
}

func (c *closeInfo) isClosed() bool {
	return c.mode == closeDef
}

// isClosed reports whether v is closed at this level (so not recursively).
func isClosed(v *Vertex) bool {
	// We could have used IsRecursivelyClosed here, but (effectively)
	// implementing it again here allows us to only have to iterate over
	// Structs once.
	if v.Closed {
		return true
	}
	for _, s := range v.Structs {
		if s.IsClosed || s.IsInOneOf(DefinitionSpan) {
			return true
		}
	}
	return false
}

// Accept determines whether f is allowed in n. It uses the OpContext for
// caching administrative fields.
func Accept(ctx *OpContext, n *Vertex, f Feature) (found, required bool) {
	ctx.generation++
	ctx.todo = nil

	var optionalTypes OptionalType

	// TODO(perf): more aggressively determine whether a struct is open or
	// closed: open structs do not have to be checked, yet they can particularly
	// be the ones with performance issues, for instanced as a result of
	// embedded for comprehensions.
	for _, s := range n.Structs {
		if !s.useForAccept() {
			continue
		}
		markCounts(ctx, s.CloseInfo)
		optionalTypes |= s.types
	}

	var str Value
	if f.Index() == MaxIndex {
		f &= fTypeMask
	} else if optionalTypes&(HasComplexPattern|HasDynamic) != 0 && f.IsString() {
		str = f.ToValue(ctx)
	}

	for _, s := range n.Structs {
		if !s.useForAccept() {
			continue
		}
		if verifyArc(ctx, s, f, str) {
			// Beware: don't add to below expression: this relies on the
			// side effects of markUp.
			ok := markUp(ctx, s.closeInfo, 0)
			found = found || ok
		}
	}

	// Reject if any of the roots is not accepted.
	for x := ctx.todo; x != nil; x = x.next {
		if !x.accepted {
			return false, true
		}
	}

	return found, ctx.todo != nil
}

func markCounts(ctx *OpContext, info CloseInfo) {
	if info.IsClosed {
		markRequired(ctx, info.closeInfo)
		return
	}
	for s := info.closeInfo; s != nil; s = s.parent {
		if s.isClosed() {
			markRequired(ctx, s)
			return
		}
	}
}

func markRequired(ctx *OpContext, info *closeInfo) {
	count := 0
	for ; ; info = info.parent {
		var s closeInfo
		if info != nil {
			s = *info
		}

		x := getScratch(ctx, info)

		x.requiredCount += count

		if x.required {
			return
		}

		if s.span&EmbeddingSpan == 0 && !x.inTodoList {
			x.next = ctx.todo
			ctx.todo = x
			x.inTodoList = true
		}

		x.required = true

		if info == nil {
			return
		}

		count = 0
		if s.mode != closeEmbed {
			count = 1
		}
	}
}

func markUp(ctx *OpContext, info *closeInfo, count int) bool {
	for ; ; info = info.parent {
		var s closeInfo
		if info != nil {
			s = *info
		}

		x := getScratch(ctx, info)

		x.acceptedCount += count

		if x.acceptedCount < x.requiredCount {
			return false
		}

		x.accepted = true

		if info == nil {
			return true
		}

		count = 0
		if x.required && s.mode != closeEmbed {
			count = 1
		}
	}
}

// getScratch: explain generation.
func getScratch(ctx *OpContext, s *closeInfo) *closeStats {
	m := ctx.closed
	if m == nil {
		m = map[*closeInfo]*closeStats{}
		ctx.closed = m
	}

	x := m[s]
	if x == nil {
		x = &closeStats{}
		m[s] = x
	}

	if x.generation != ctx.generation {
		*x = closeStats{generation: ctx.generation}
	}

	return x
}

func verifyArc(ctx *OpContext, s *StructInfo, f Feature, label Value) bool {
	isRegular := f.IsString()

	o := s.StructLit
	env := s.Env

	if len(o.Additional) > 0 || o.IsOpen {
		return true
	}

	for _, g := range o.Fields {
		if f == g.Label {
			return true
		}
	}

	if !isRegular {
		return false
	}

	// Do not record errors during this validation.
	errs := ctx.errs
	defer func() { ctx.errs = errs }()

	if len(o.Dynamic) > 0 && f.IsString() && label != nil {
		for _, b := range o.Dynamic {
			v := env.evalCached(ctx, b.Key)
			v, _ = ctx.getDefault(v)
			s, ok := Unwrap(v).(*String)
			if !ok {
				continue
			}
			if label.(*String).Str == s.Str {
				return true
			}
		}
	}

	for _, b := range o.Bulk {
		if matchBulk(ctx, env, b, f, label) {
			return true
		}
	}

	// TODO(perf): delay adding this position: create a special error type that
	// computes all necessary positions on demand.
	if ctx != nil {
		ctx.AddPosition(s.StructLit)
	}

	return false
}
