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

import (
	"io"
	"math/big"
	"math/bits"

	"github.com/cockroachdb/apd/v3"

	"cuelang.org/go/cue"
	"cuelang.org/go/internal/core/adt"
	"cuelang.org/go/internal/value"
)

// CallCtxt is passed to builtin implementations that need to use a cue.Value. This is an internal type. Its interface may change.
type CallCtxt struct {
	adt.BuiltinCallContext
	ctx     *adt.OpContext
	builtin *Builtin
	Err     any
	Ret     any
}

// Do returns whether the call should be done.
func (c *CallCtxt) Do() bool {
	return c.Err == nil
}

// Schema returns the ith argument as is, without converting it to a cue.Value.
//
// TODO: Schema should use [adt.BuiltinCallContext.Expr] to capture cycle information.
// However, this only makes sense if functions also use the same OpContext for
// further evaluation. We should enforce as we port the old calls.
func (c *CallCtxt) Schema(i int) Schema {
	v := c.Arg(i)
	return value.Make(c.ctx, v)
}

// Value returns a finalized cue.Value for the ith argument.
func (c *CallCtxt) Value(i int) cue.Value {
	v := value.Make(c.ctx, c.BuiltinCallContext.Value(i))
	if c.builtin.NonConcrete {
		// In case NonConcrete is false, the concreteness is already checked
		// at call time. We may want to use finalize semantics in both cases,
		// though.
		_, f := value.ToInternal(v)
		f = f.ToDataAll(c.ctx)
		v = value.Make(c.ctx, f)
	}
	if !v.IsConcrete() {
		c.errcf(adt.IncompleteError, "non-concrete argument %d", i)
	}
	return v
}

func (c *CallCtxt) Struct(i int) Struct {
	x := c.BuiltinCallContext.Value(i)
	if c.builtin.NonConcrete {
		x = adt.Default(x)
	}
	switch v, ok := x.(*adt.Vertex); {
	case ok && !v.IsList():
		v.CompleteArcs(c.ctx)
		return Struct{v}

	case v != nil:
		x = v.Value()
	}
	if x.Kind()&adt.StructKind == 0 {
		var err error
		if b, ok := x.(*adt.Bottom); ok {
			err = &callError{b}
		}
		c.invalidArgType(x, i, "struct", err)
	} else {
		err := c.ctx.NewErrf("non-concrete struct for argument %d", i)
		err.Code = adt.IncompleteError
		c.Err = &callError{err}
	}
	return Struct{}
}

func (c *CallCtxt) Int(i int) int     { return int(c.intValue(i, 64, "int64")) }
func (c *CallCtxt) Int8(i int) int8   { return int8(c.intValue(i, 8, "int8")) }
func (c *CallCtxt) Int16(i int) int16 { return int16(c.intValue(i, 16, "int16")) }
func (c *CallCtxt) Int32(i int) int32 { return int32(c.intValue(i, 32, "int32")) }
func (c *CallCtxt) Rune(i int) rune   { return rune(c.intValue(i, 32, "rune")) }
func (c *CallCtxt) Int64(i int) int64 { return c.intValue(i, 64, "int64") }

func (c *CallCtxt) intValue(i, bitLen int, typ string) int64 {
	arg := c.BuiltinCallContext.Value(i)
	if num, _ := c.ctx.EvaluateKeepState(arg).(*adt.Num); num != nil {
		// In the happy path, avoid converting to the public [cue.Value] API, which is wasteful.
		if n, err := num.X.Int64(); err == nil && bits.Len64(uint64(n)) <= bitLen {
			return n
		}
	}
	x := value.Make(c.ctx, arg)
	n, err := x.Int(nil)
	if err != nil {
		c.invalidArgType(arg, i, typ, err)
		return 0
	}
	if n.BitLen() > bitLen {
		c.errf(err, "int %s overflows %s in argument %d",
			n, typ, i)
	}
	res, _ := x.Int64()
	return res
}

func (c *CallCtxt) Uint(i int) uint     { return uint(c.uintValue(i, 64, "uint64")) }
func (c *CallCtxt) Uint8(i int) uint8   { return uint8(c.uintValue(i, 8, "uint8")) }
func (c *CallCtxt) Byte(i int) uint8    { return byte(c.uintValue(i, 8, "byte")) }
func (c *CallCtxt) Uint16(i int) uint16 { return uint16(c.uintValue(i, 16, "uint16")) }
func (c *CallCtxt) Uint32(i int) uint32 { return uint32(c.uintValue(i, 32, "uint32")) }
func (c *CallCtxt) Uint64(i int) uint64 { return c.uintValue(i, 64, "uint64") }

func (c *CallCtxt) uintValue(i, bitLen int, typ string) uint64 {
	arg := c.BuiltinCallContext.Value(i)
	if num, _ := c.ctx.EvaluateKeepState(arg).(*adt.Num); num != nil {
		// In the happy path, avoid converting to the public [cue.Value] API, which is wasteful.
		// Note that [apd.Decimal] has an Int64 method, but no Uint64 method,
		// so any uint64 value that doesn't fit in an int64 skips the shortcut.
		if n, err := num.X.Int64(); err == nil && n >= 0 && bits.Len64(uint64(n)) <= bitLen {
			return uint64(n)
		}
	}
	x := value.Make(c.ctx, arg)
	n, err := x.Int(nil)
	if err != nil || n.Sign() < 0 {
		c.invalidArgType(arg, i, typ, err)
		return 0
	}
	if n.BitLen() > bitLen {
		c.errf(err, "int %s overflows %s in argument %d",
			n, typ, i)
	}
	res, _ := x.Uint64()
	return res
}

func (c *CallCtxt) Decimal(i int) *apd.Decimal {
	arg := c.BuiltinCallContext.Value(i)
	if num, _ := c.ctx.EvaluateKeepState(arg).(*adt.Num); num != nil {
		// In the happy path, avoid converting to the public [cue.Value] API, which is wasteful.
		return &num.X
	}
	x := value.Make(c.ctx, arg)
	res, err := x.Decimal()
	if err != nil {
		c.invalidArgType(arg, i, "Decimal", err)
		return nil
	}
	return res
}

func (c *CallCtxt) Float64(i int) float64 {
	arg := c.BuiltinCallContext.Value(i)
	if num, _ := c.ctx.EvaluateKeepState(arg).(*adt.Num); num != nil {
		// In the happy path, avoid converting to the public [cue.Value] API, which is wasteful.
		if f, err := num.X.Float64(); err == nil {
			return f
		}
	}
	x := value.Make(c.ctx, arg)
	res, err := x.Float64()
	if err != nil {
		c.invalidArgType(arg, i, "float64", err)
		return 0
	}
	return res
}

func (c *CallCtxt) BigInt(i int) *big.Int {
	arg := c.BuiltinCallContext.Value(i)
	if num, _ := c.ctx.EvaluateKeepState(arg).(*adt.Num); num != nil {
		// In the happy path, avoid converting to the public [cue.Value] API, which is wasteful.
		return num.BigInt(nil)
	}
	x := value.Make(c.ctx, arg)
	n, err := x.Int(nil)
	if err != nil {
		c.invalidArgType(arg, i, "int", err)
		return nil
	}
	return n
}

var ten = big.NewInt(10)

func (c *CallCtxt) BigFloat(i int) *big.Float {
	arg := c.BuiltinCallContext.Value(i)
	x := value.Make(c.ctx, arg)
	var mant big.Int
	exp, err := x.MantExp(&mant)
	if err != nil {
		c.invalidArgType(arg, i, "float", err)
		return nil
	}
	f := &big.Float{}
	f.SetInt(&mant)
	if exp != 0 {
		var g big.Float
		e := big.NewInt(int64(exp))
		f.Mul(f, g.SetInt(e.Exp(ten, e, nil)))
	}
	return f
}

func (c *CallCtxt) String(i int) string {
	arg := c.BuiltinCallContext.Value(i)
	if str, _ := c.ctx.EvaluateKeepState(arg).(*adt.String); str != nil {
		// In the happy path, avoid converting to the public [cue.Value] API, which is wasteful.
		return str.Str
	}
	x := value.Make(c.ctx, arg)
	v, err := x.String()
	if err != nil {
		c.invalidArgType(arg, i, "string", err)
		return ""
	}
	return v
}

func (c *CallCtxt) Bytes(i int) []byte {
	arg := c.BuiltinCallContext.Value(i)
	if bs, _ := c.ctx.EvaluateKeepState(arg).(*adt.Bytes); bs != nil {
		// In the happy path, avoid converting to the public [cue.Value] API, which is wasteful.
		return bs.B
	}
	x := value.Make(c.ctx, arg)
	v, err := x.Bytes()
	if err != nil {
		c.invalidArgType(arg, i, "bytes", err)
		return nil
	}
	return v
}

func (c *CallCtxt) Reader(i int) io.Reader {
	arg := c.BuiltinCallContext.Value(i)
	x := value.Make(c.ctx, arg)
	// TODO: optimize for string and bytes cases
	r, err := x.Reader()
	if err != nil {
		c.invalidArgType(arg, i, "bytes|string", err)
		return nil
	}
	return r
}

func (c *CallCtxt) Bool(i int) bool {
	arg := c.BuiltinCallContext.Value(i)
	if b, _ := c.ctx.EvaluateKeepState(arg).(*adt.Bool); b != nil {
		// In the happy path, avoid converting to the public [cue.Value] API, which is wasteful.
		return b.B
	}
	x := value.Make(c.ctx, arg)
	b, err := x.Bool()
	if err != nil {
		c.invalidArgType(arg, i, "bool", err)
		return false
	}
	return b
}

func (c *CallCtxt) List(i int) (a []cue.Value) {
	arg := c.BuiltinCallContext.Value(i)
	x := value.Make(c.ctx, arg)
	v, err := x.List()
	if err != nil {
		c.invalidArgType(arg, i, "list", err)
		return a
	}
	for v.Next() {
		a = append(a, v.Value())
	}
	return a
}

func (c *CallCtxt) CueList(i int) List {
	v := c.getList(i)
	if v == nil {
		return List{}
	}
	return List{v, v.BaseValue.(*adt.ListMarker).IsOpen}
}

func (c *CallCtxt) Iter(i int) (a cue.Iterator) {
	arg := c.BuiltinCallContext.Value(i)
	x := value.Make(c.ctx, arg)
	v, err := x.List()
	if err != nil {
		c.invalidArgType(arg, i, "list", err)
	}
	return v
}

func (c *CallCtxt) getList(i int) *adt.Vertex {
	x := c.BuiltinCallContext.Value(i)
	if c.builtin.NonConcrete {
		x = adt.Default(x)
	}
	switch v, ok := x.(*adt.Vertex); {
	case ok && v.IsList():
		v.Finalize(c.ctx)
		if err := v.Bottom(); err != nil {
			c.Err = &callError{err}
			return nil
		}
		return v

	case v != nil:
		x = v.Value()
	}

	if x.Kind()&adt.ListKind == 0 {
		var err error
		if b, ok := x.(*adt.Bottom); ok {
			err = &callError{b}
		}
		c.invalidArgType(x, i, "list", err)
	} else {
		err := c.ctx.NewErrf("non-concrete list for argument %d", i)
		err.Code = adt.IncompleteError
		c.Err = &callError{err}
	}
	return nil
}

func (c *CallCtxt) DecimalList(i int) (a []*apd.Decimal) {
	v := c.getList(i)
	if v == nil {
		return nil
	}

	j := 0
	for w := range v.Elems() {
		w.Finalize(c.ctx) // defensive
		switch x := adt.Unwrap(adt.Default(w.Value())).(type) {
		case *adt.Num:
			a = append(a, &x.X)

		case *adt.Bottom:
			if x.IsIncomplete() {
				c.Err = x
				return nil
			}

		default:
			if k := w.Kind(); k&adt.NumberKind == 0 {
				err := c.ctx.NewErrf(
					"invalid list element %d in argument %d to call: cannot use value %v (%s) as number",
					j, i, w, k)
				c.Err = &callError{err}
				return a
			}

			err := c.ctx.NewErrf(
				"non-concrete value %v for element %d of number list argument %d",
				w, j, i)
			err.Code = adt.IncompleteError
			c.Err = &callError{err}
			return nil
		}
		j++
	}
	return a
}

func (c *CallCtxt) StringList(i int) (a []string) {
	v := c.getList(i)
	if v == nil {
		return nil
	}

	j := 0
	for w := range v.Elems() {
		w.Finalize(c.ctx) // defensive
		switch x := adt.Unwrap(adt.Default(w.Value())).(type) {
		case *adt.String:
			a = append(a, x.Str)

		case *adt.Bottom:
			if x.IsIncomplete() {
				c.Err = x
				return nil
			}

		default:
			if k := w.Kind(); k&adt.StringKind == 0 {
				err := c.ctx.NewErrf(
					"invalid list element %d in argument %d to call: cannot use value %v (%s) as string",
					j, i, w, k)
				c.Err = &callError{err}
				return a
			}

			err := c.ctx.NewErrf(
				"non-concrete value %v for element %d of string list argument %d",
				w, j, i)
			err.Code = adt.IncompleteError
			c.Err = &callError{err}
			return nil
		}
		j++
	}
	return a
}
