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

import (
	"strconv"

	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/token"
	"cuelang.org/go/internal/core/adt"
)

func predeclared(n *ast.Ident) adt.Expr {
	// TODO: consider supporting GraphQL-style names:
	// String, Bytes, Boolean, Integer, Number.
	// These names will not conflict with idiomatic camel-case JSON.
	switch n.Name {
	case "string", "__string":
		return &adt.BasicType{Src: n, K: adt.StringKind}
	case "bytes", "__bytes":
		return &adt.BasicType{Src: n, K: adt.BytesKind}
	case "bool", "__bool":
		return &adt.BasicType{Src: n, K: adt.BoolKind}
	case "int", "__int":
		return &adt.BasicType{Src: n, K: adt.IntKind}
	case "float", "__float":
		return &adt.BasicType{Src: n, K: adt.FloatKind}
	case "number", "__number":
		return &adt.BasicType{Src: n, K: adt.NumberKind}

	case "len", "__len":
		return lenBuiltin
	case "close", "__close":
		return closeBuiltin
	case "matchIf", "__matchIf":
		return matchIfBuiltin
	case "matchN", "__matchN":
		return matchNBuiltin
	case "and", "__and":
		return andBuiltin
	case "or", "__or":
		return orBuiltin
	case "div", "__div":
		return divBuiltin
	case "mod", "__mod":
		return modBuiltin
	case "quo", "__quo":
		return quoBuiltin
	case "rem", "__rem":
		return remBuiltin

	case "__no_sharing":
		return adt.NoShareSentinel
	}

	if r, ok := predefinedRanges[n.Name]; ok {
		return r
	}

	return nil
}

// LookupRange returns a CUE expressions for the given predeclared identifier
// representing a range, such as uint8, int128, and float64.
func LookupRange(name string) adt.Expr {
	return predefinedRanges[name]
}

var predefinedRanges = map[string]adt.Expr{
	"rune":  mkIntRange("0", strconv.Itoa(0x10FFFF)),
	"int8":  mkIntRange("-128", "127"),
	"int16": mkIntRange("-32768", "32767"),
	"int32": mkIntRange("-2147483648", "2147483647"),
	"int64": mkIntRange("-9223372036854775808", "9223372036854775807"),
	"int128": mkIntRange(
		"-170141183460469231731687303715884105728",
		"170141183460469231731687303715884105727"),

	// Do not include an alias for "byte", as it would be too easily confused
	// with the builtin "bytes".
	"uint":    mkUint(),
	"uint8":   mkIntRange("0", "255"),
	"uint16":  mkIntRange("0", "65535"),
	"uint32":  mkIntRange("0", "4294967295"),
	"uint64":  mkIntRange("0", "18446744073709551615"),
	"uint128": mkIntRange("0", "340282366920938463463374607431768211455"),

	// 2**127 * (2**24 - 1) / 2**23
	"float32": mkFloatRange(
		"-3.40282346638528859811704183484516925440e+38",
		"3.40282346638528859811704183484516925440e+38",
	),
	// 2**1023 * (2**53 - 1) / 2**52
	"float64": mkFloatRange(
		"-1.797693134862315708145274237317043567981e+308",
		"1.797693134862315708145274237317043567981e+308",
	),
}

func init() {
	for k, v := range predefinedRanges {
		predefinedRanges["__"+k] = v
	}
}

// TODO: use an adt.BoundValue here. and conjunctions here.

func mkUint() adt.Expr {
	from := newBound(adt.GreaterEqualOp, adt.IntKind, parseInt("0"))
	ident := ast.NewIdent("__int")
	src := ast.NewBinExpr(token.AND, ident, from.Src)
	return &adt.Conjunction{
		Src: src,
		Values: []adt.Value{
			&adt.BasicType{Src: ident, K: adt.IntKind}, from,
		},
	}
}

func mkIntRange(a, b string) adt.Expr {
	from := newBound(adt.GreaterEqualOp, adt.IntKind, parseInt(a))
	to := newBound(adt.LessEqualOp, adt.IntKind, parseInt(b))
	ident := ast.NewIdent("__int")
	src := ast.NewBinExpr(token.AND, ident, from.Src, to.Src)
	return &adt.Conjunction{
		Src: src,
		Values: []adt.Value{
			&adt.BasicType{Src: ident, K: adt.IntKind}, from, to,
		},
	}
}

func mkFloatRange(a, b string) adt.Expr {
	from := newBound(adt.GreaterEqualOp, adt.NumberKind, parseFloat(a))
	to := newBound(adt.LessEqualOp, adt.NumberKind, parseFloat(b))
	src := ast.NewBinExpr(token.AND, from.Src, to.Src)
	return &adt.Conjunction{Src: src, Values: []adt.Value{from, to}}
}

func newBound(op adt.Op, k adt.Kind, v adt.Value) *adt.BoundValue {
	src := &ast.UnaryExpr{Op: op.Token(), X: v.Source().(ast.Expr)}
	return &adt.BoundValue{Src: src, Op: op, Value: v}
}

func parseInt(s string) *adt.Num {
	n := parseNum(adt.IntKind, s)
	n.Src = &ast.BasicLit{Kind: token.INT, Value: s}
	return n
}

func parseFloat(s string) *adt.Num {
	n := parseNum(adt.FloatKind, s)
	n.Src = &ast.BasicLit{Kind: token.FLOAT, Value: s}
	return n
}

func parseNum(k adt.Kind, s string) *adt.Num {
	num := &adt.Num{K: k}
	_, _, err := num.X.SetString(s)
	if err != nil {
		panic(err)
	}
	return num
}
