// Copyright 2018 Google LLC
//
// 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 decls provides helpers for creating variable and function declarations.
package decls

import (
	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
	emptypb "google.golang.org/protobuf/types/known/emptypb"
	structpb "google.golang.org/protobuf/types/known/structpb"
)

var (
	// Error type used to communicate issues during type-checking.
	Error = &exprpb.Type{
		TypeKind: &exprpb.Type_Error{
			Error: &emptypb.Empty{}}}

	// Dyn is a top-type used to represent any value.
	Dyn = &exprpb.Type{
		TypeKind: &exprpb.Type_Dyn{
			Dyn: &emptypb.Empty{}}}
)

// Commonly used types.
var (
	Bool   = NewPrimitiveType(exprpb.Type_BOOL)
	Bytes  = NewPrimitiveType(exprpb.Type_BYTES)
	Double = NewPrimitiveType(exprpb.Type_DOUBLE)
	Int    = NewPrimitiveType(exprpb.Type_INT64)
	Null   = &exprpb.Type{
		TypeKind: &exprpb.Type_Null{
			Null: structpb.NullValue_NULL_VALUE}}
	String = NewPrimitiveType(exprpb.Type_STRING)
	Uint   = NewPrimitiveType(exprpb.Type_UINT64)
)

// Well-known types.
// TODO: Replace with an abstract type registry.
var (
	Any       = NewWellKnownType(exprpb.Type_ANY)
	Duration  = NewWellKnownType(exprpb.Type_DURATION)
	Timestamp = NewWellKnownType(exprpb.Type_TIMESTAMP)
)

// NewAbstractType creates an abstract type declaration which references a proto
// message name and may also include type parameters.
func NewAbstractType(name string, paramTypes ...*exprpb.Type) *exprpb.Type {
	return &exprpb.Type{
		TypeKind: &exprpb.Type_AbstractType_{
			AbstractType: &exprpb.Type_AbstractType{
				Name:           name,
				ParameterTypes: paramTypes}}}
}

// NewOptionalType constructs an abstract type indicating that the parameterized type
// may be contained within the object.
func NewOptionalType(paramType *exprpb.Type) *exprpb.Type {
	return NewAbstractType("optional_type", paramType)
}

// NewFunctionType creates a function invocation contract, typically only used
// by type-checking steps after overload resolution.
func NewFunctionType(resultType *exprpb.Type,
	argTypes ...*exprpb.Type) *exprpb.Type {
	return &exprpb.Type{
		TypeKind: &exprpb.Type_Function{
			Function: &exprpb.Type_FunctionType{
				ResultType: resultType,
				ArgTypes:   argTypes}}}
}

// NewFunction creates a named function declaration with one or more overloads.
func NewFunction(name string,
	overloads ...*exprpb.Decl_FunctionDecl_Overload) *exprpb.Decl {
	return &exprpb.Decl{
		Name: name,
		DeclKind: &exprpb.Decl_Function{
			Function: &exprpb.Decl_FunctionDecl{
				Overloads: overloads}}}
}

// NewFunctionWithDoc creates a named function declaration with a description and one or more overloads.
func NewFunctionWithDoc(name, doc string,
	overloads ...*exprpb.Decl_FunctionDecl_Overload) *exprpb.Decl {
	return &exprpb.Decl{
		Name: name,
		DeclKind: &exprpb.Decl_Function{
			Function: &exprpb.Decl_FunctionDecl{
				// Doc: desc,
				Overloads: overloads}}}
}

// NewIdent creates a named identifier declaration with an optional literal
// value.
//
// Literal values are typically only associated with enum identifiers.
//
// Deprecated: Use NewVar or NewConst instead.
func NewIdent(name string, t *exprpb.Type, v *exprpb.Constant) *exprpb.Decl {
	return newIdent(name, t, v, "")
}

func newIdent(name string, t *exprpb.Type, v *exprpb.Constant, desc string) *exprpb.Decl {
	return &exprpb.Decl{
		Name: name,
		DeclKind: &exprpb.Decl_Ident{
			Ident: &exprpb.Decl_IdentDecl{
				Type:  t,
				Value: v,
				Doc:   desc}}}
}

// NewConst creates a constant identifier with a CEL constant literal value.
func NewConst(name string, t *exprpb.Type, v *exprpb.Constant) *exprpb.Decl {
	return newIdent(name, t, v, "")
}

// NewVar creates a variable identifier.
func NewVar(name string, t *exprpb.Type) *exprpb.Decl {
	return newIdent(name, t, nil, "")
}

// NewVarWithDoc creates a variable identifier with a type and a description string.
func NewVarWithDoc(name string, t *exprpb.Type, desc string) *exprpb.Decl {
	return newIdent(name, t, nil, desc)
}

// NewInstanceOverload creates a instance function overload contract.
// First element of argTypes is instance.
func NewInstanceOverload(id string, argTypes []*exprpb.Type, resultType *exprpb.Type) *exprpb.Decl_FunctionDecl_Overload {
	return &exprpb.Decl_FunctionDecl_Overload{
		OverloadId:         id,
		ResultType:         resultType,
		Params:             argTypes,
		IsInstanceFunction: true}
}

// NewListType generates a new list with elements of a certain type.
func NewListType(elem *exprpb.Type) *exprpb.Type {
	return &exprpb.Type{
		TypeKind: &exprpb.Type_ListType_{
			ListType: &exprpb.Type_ListType{
				ElemType: elem}}}
}

// NewMapType generates a new map with typed keys and values.
func NewMapType(key *exprpb.Type, value *exprpb.Type) *exprpb.Type {
	return &exprpb.Type{
		TypeKind: &exprpb.Type_MapType_{
			MapType: &exprpb.Type_MapType{
				KeyType:   key,
				ValueType: value}}}
}

// NewObjectType creates an object type for a qualified type name.
func NewObjectType(typeName string) *exprpb.Type {
	return &exprpb.Type{
		TypeKind: &exprpb.Type_MessageType{
			MessageType: typeName}}
}

// NewOverload creates a function overload declaration which contains a unique
// overload id as well as the expected argument and result types. Overloads
// must be aggregated within a Function declaration.
func NewOverload(id string, argTypes []*exprpb.Type, resultType *exprpb.Type) *exprpb.Decl_FunctionDecl_Overload {
	return &exprpb.Decl_FunctionDecl_Overload{
		OverloadId:         id,
		ResultType:         resultType,
		Params:             argTypes,
		IsInstanceFunction: false}
}

// NewParameterizedInstanceOverload creates a parametric function instance overload type.
func NewParameterizedInstanceOverload(id string,
	argTypes []*exprpb.Type,
	resultType *exprpb.Type,
	typeParams []string) *exprpb.Decl_FunctionDecl_Overload {
	return &exprpb.Decl_FunctionDecl_Overload{
		OverloadId:         id,
		ResultType:         resultType,
		Params:             argTypes,
		TypeParams:         typeParams,
		IsInstanceFunction: true}
}

// NewParameterizedOverload creates a parametric function overload type.
func NewParameterizedOverload(id string,
	argTypes []*exprpb.Type,
	resultType *exprpb.Type,
	typeParams []string) *exprpb.Decl_FunctionDecl_Overload {
	return &exprpb.Decl_FunctionDecl_Overload{
		OverloadId:         id,
		ResultType:         resultType,
		Params:             argTypes,
		TypeParams:         typeParams,
		IsInstanceFunction: false}
}

// NewPrimitiveType creates a type for a primitive value. See the var declarations
// for Int, Uint, etc.
func NewPrimitiveType(primitive exprpb.Type_PrimitiveType) *exprpb.Type {
	return &exprpb.Type{
		TypeKind: &exprpb.Type_Primitive{
			Primitive: primitive}}
}

// NewTypeType creates a new type designating a type.
func NewTypeType(nested *exprpb.Type) *exprpb.Type {
	if nested == nil {
		// must set the nested field for a valid oneof option
		nested = &exprpb.Type{}
	}
	return &exprpb.Type{
		TypeKind: &exprpb.Type_Type{
			Type: nested}}
}

// NewTypeParamType creates a type corresponding to a named, contextual parameter.
func NewTypeParamType(name string) *exprpb.Type {
	return &exprpb.Type{
		TypeKind: &exprpb.Type_TypeParam{
			TypeParam: name}}
}

// NewWellKnownType creates a type corresponding to a protobuf well-known type
// value.
func NewWellKnownType(wellKnown exprpb.Type_WellKnownType) *exprpb.Type {
	return &exprpb.Type{
		TypeKind: &exprpb.Type_WellKnown{
			WellKnown: wellKnown}}
}

// NewWrapperType creates a wrapped primitive type instance. Wrapped types
// are roughly equivalent to a nullable, or optionally valued type.
func NewWrapperType(wrapped *exprpb.Type) *exprpb.Type {
	primitive := wrapped.GetPrimitive()
	if primitive == exprpb.Type_PRIMITIVE_TYPE_UNSPECIFIED {
		// TODO: return an error
		panic("Wrapped type must be a primitive")
	}
	return &exprpb.Type{TypeKind: &exprpb.Type_Wrapper{Wrapper: primitive}}
}
