// Copyright 2024 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
//
//     https://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 compile

// This file contains validator and other non-monotonic builtins.

import (
	"cuelang.org/go/internal/core/adt"
)

// matchN is a validator that checks that the number of schemas in the given
// list that unify with "self" matches the number passed as the first argument
// of the validator. Note that this number may itself be a number constraint
// and does not have to be a concrete number.
var matchNBuiltin = &adt.Builtin{
	Name:        "matchN",
	Params:      []adt.Param{topParam, intParam, listParam}, // varargs
	Result:      adt.BoolKind,
	NonConcrete: true,
	Func: func(call adt.BuiltinCallContext) adt.Expr {
		c := call.OpContext()

		if !c.IsValidator {
			return c.NewErrf("matchN is a validator and should not be used as a function")
		}

		self := finalizeSelf(c, call.Value(0))
		if err := bottom(c, self); err != nil {
			return adt.StaticBoolFalse
		}

		// A self-referential matchN, e.g. _x: matchN(2, [_x, _x]), would loop
		// forever; the cycle detector flags the re-entry, so report it.
		if c.InStructuralCycle() {
			return &adt.Bottom{
				Code: adt.StructuralCycleError,
				Err:  c.NewPosf(call.Pos(), "structural cycle"),
			}
		}

		var errs []*adt.Bottom
		var count, possibleCount int64
		for check := range c.Elems(call.Value(2)) {
			v := adt.Unify(c, self, check)
			if err := adt.Validate(c, v, finalCfg); err == nil {
				// TODO: is it always true that the lack of an error signifies
				// success?
				count++
			} else {
				errs = append(errs, err)
				if err.IsIncomplete() {
					possibleCount++
				}
			}
		}

		bound := call.Value(1)
		// TODO: consider a mode to require "all" to pass, for instance by
		// supporting the value null or "all".

		b := checkNum(c, bound, count, count+possibleCount)
		if b != nil {
			// Only show errors related to incomplete schema if there is still
			// a possibility that we can resolve it.
			isIncomplete := b.IsIncomplete()
			for _, err := range errs {
				if !isIncomplete || err.IsIncomplete() {
					c.AddBottom(err)
				}
			}
			return b
		}
		return adt.StaticBoolTrue
	},
}

// matchIf is a validator that checks that if the first argument unifies with
// self, the second argument also unifies with self, otherwise the third
// argument unifies with self.
// The same finalization heuristics are applied to self as are applied
// in matchN.
var matchIfBuiltin = &adt.Builtin{
	Name:        "matchIf",
	Params:      []adt.Param{topParam, topParam, topParam, topParam},
	Result:      adt.BoolKind,
	NonConcrete: true,
	Func: func(call adt.BuiltinCallContext) adt.Expr {
		c := call.OpContext()

		if !c.IsValidator {
			return c.NewErrf("matchIf is a validator and should not be used as a function")
		}

		self := finalizeSelf(c, call.Value(0))
		if err := bottom(c, self); err != nil {
			return adt.StaticBoolFalse
		}

		// As with matchN, report a self-referential matchIf, e.g.
		// _y: matchIf(_y, _y, _y), rather than looping forever. schemaArg keeps
		// the references so the cycle detector flags the re-entry.
		if c.InStructuralCycle() {
			return &adt.Bottom{
				Code: adt.StructuralCycleError,
				Err:  c.NewPosf(call.Pos(), "structural cycle"),
			}
		}
		ifSchema := schemaArg(c, call, 1)
		thenSchema := schemaArg(c, call, 2)
		elseSchema := schemaArg(c, call, 3)
		v := adt.Unify(c, self, ifSchema)
		var chosenSchema adt.Value
		if err := adt.Validate(c, v, finalCfg); err == nil {
			chosenSchema = thenSchema
		} else {
			chosenSchema = elseSchema
		}
		v = adt.Unify(c, self, chosenSchema)
		err := adt.Validate(c, v, finalCfg)
		if err == nil {
			return adt.StaticBoolTrue
		}
		// TODO should we also include in the error something about the fact that
		// the if condition passed or failed?
		return err
	},
}

// Explicitly disallow incomplete errors.
var finalCfg = &adt.ValidateConfig{ReportIncomplete: true, Final: true}

// schemaArg returns matchIf's i-th argument as a vertex that retains the
// argument expression, rather than its resolved value. Unifying against this
// re-resolves references each iteration, letting the cycle detector spot a
// self-referential matchIf just as it does for matchN's list of schemas.
func schemaArg(c *adt.OpContext, call adt.BuiltinCallContext, i int) adt.Value {
	v := &adt.Vertex{}
	v.AddConjunct(adt.MakeConjunct(c.Env(0), call.Expr(i), c.CloseInfo()))
	return v
}

// finalizeSelf ensures a value is fully evaluated and then strips it of any
// of its validators or default values.
func finalizeSelf(c *adt.OpContext, self adt.Value) adt.Value {
	if x, ok := self.(*adt.Vertex); ok {
		self = x.ToDataAll(c)
	}
	return self
}

// TODO: use adt.Unify instead.
func unifyScalar(c *adt.OpContext, self, check adt.Value) *adt.Vertex {
	v := &adt.Vertex{}
	closeInfo := c.CloseInfo()
	v.AddConjunct(adt.MakeConjunct(nil, self, closeInfo))
	v.AddConjunct(adt.MakeConjunct(nil, check, closeInfo))
	v.Finalize(c)
	return v
}

func checkNum(ctx *adt.OpContext, bound adt.Value, count, maxCount int64) *adt.Bottom {
	cnt := ctx.NewInt64(count)
	n := unifyScalar(ctx, bound, cnt)
	b, _ := n.BaseValue.(*adt.Bottom)
	if b != nil {
		b := ctx.NewErrf("%d matched, expected %v", count, bound)

		// By default we should mark the error as incomplete, but check if we
		// know for sure it will fail.
		switch bound := bound.(type) {
		case *adt.Num:
			if i, err := bound.X.Int64(); err == nil && i > count && i <= maxCount {
				b.Code = adt.IncompleteError
			}

		case *adt.BoundValue:
			v := &adt.Vertex{}
			v.AddConjunct(ctx.MakeConjunct(bound))
			v.AddConjunct(ctx.MakeConjunct(&adt.BoundValue{
				Op:    adt.GreaterEqualOp,
				Value: cnt,
			}))
			v.AddConjunct(ctx.MakeConjunct(&adt.BoundValue{
				Op:    adt.LessEqualOp,
				Value: ctx.NewInt64(maxCount),
			}))
			v.Finalize(ctx)
			if _, ok := v.BaseValue.(*adt.Bottom); !ok {
				b.Code = adt.IncompleteError
			}

		default:
			b.Code = adt.IncompleteError
		}

		return b
	}
	return nil
}

func bottom(c *adt.OpContext, v adt.Value) *adt.Bottom {
	switch x := v.(type) {
	case *adt.Vertex:
		return x.Err(c)
	case *adt.Bottom:
		return x
	}
	return nil
}
