package expr

import (
	"errors"
	"fmt"
	"reflect"
	"time"

	"github.com/expr-lang/expr/ast"
	"github.com/expr-lang/expr/builtin"
	"github.com/expr-lang/expr/checker"
	"github.com/expr-lang/expr/compiler"
	"github.com/expr-lang/expr/conf"
	"github.com/expr-lang/expr/file"
	"github.com/expr-lang/expr/optimizer"
	"github.com/expr-lang/expr/patcher"
	"github.com/expr-lang/expr/vm"
)

// Option for configuring config.
type Option func(c *conf.Config)

// Env specifies expected input of env for type checks.
// If struct is passed, all fields will be treated as variables,
// as well as all fields of embedded structs and struct itself.
// If map is passed, all items will be treated as variables.
// Methods defined on this type will be available as functions.
func Env(env any) Option {
	return func(c *conf.Config) {
		c.WithEnv(env)
	}
}

// AllowUndefinedVariables allows to use undefined variables inside expressions.
// This can be used with expr.Env option to partially define a few variables.
func AllowUndefinedVariables() Option {
	return func(c *conf.Config) {
		c.Strict = false
	}
}

// Operator allows to replace a binary operator with a function.
func Operator(operator string, fn ...string) Option {
	return func(c *conf.Config) {
		p := &patcher.OperatorOverloading{
			Operator:  operator,
			Overloads: fn,
			Types:     c.Types,
			Functions: c.Functions,
		}
		c.Visitors = append(c.Visitors, p)
	}
}

// ConstExpr defines func expression as constant. If all argument to this function is constants,
// then it can be replaced by result of this func call on compile step.
func ConstExpr(fn string) Option {
	return func(c *conf.Config) {
		c.ConstExpr(fn)
	}
}

// AsAny tells the compiler to expect any result.
func AsAny() Option {
	return func(c *conf.Config) {
		c.ExpectAny = true
	}
}

// AsKind tells the compiler to expect kind of the result.
func AsKind(kind reflect.Kind) Option {
	return func(c *conf.Config) {
		c.Expect = kind
		c.ExpectAny = true
	}
}

// AsBool tells the compiler to expect a boolean result.
func AsBool() Option {
	return func(c *conf.Config) {
		c.Expect = reflect.Bool
		c.ExpectAny = true
	}
}

// AsInt tells the compiler to expect an int result.
func AsInt() Option {
	return func(c *conf.Config) {
		c.Expect = reflect.Int
		c.ExpectAny = true
	}
}

// AsInt64 tells the compiler to expect an int64 result.
func AsInt64() Option {
	return func(c *conf.Config) {
		c.Expect = reflect.Int64
		c.ExpectAny = true
	}
}

// AsFloat64 tells the compiler to expect a float64 result.
func AsFloat64() Option {
	return func(c *conf.Config) {
		c.Expect = reflect.Float64
		c.ExpectAny = true
	}
}

// WarnOnAny tells the compiler to warn if expression return any type.
func WarnOnAny() Option {
	return func(c *conf.Config) {
		if c.Expect == reflect.Invalid {
			panic("WarnOnAny() works only with combination with AsInt(), AsBool(), etc. options")
		}
		c.ExpectAny = false
	}
}

// Optimize turns optimizations on or off.
func Optimize(b bool) Option {
	return func(c *conf.Config) {
		c.Optimize = b
	}
}

// Patch adds visitor to list of visitors what will be applied before compiling AST to bytecode.
func Patch(visitor ast.Visitor) Option {
	return func(c *conf.Config) {
		c.Visitors = append(c.Visitors, visitor)
	}
}

// Function adds function to list of functions what will be available in expressions.
func Function(name string, fn func(params ...any) (any, error), types ...any) Option {
	return func(c *conf.Config) {
		ts := make([]reflect.Type, len(types))
		for i, t := range types {
			t := reflect.TypeOf(t)
			if t.Kind() == reflect.Ptr {
				t = t.Elem()
			}
			if t.Kind() != reflect.Func {
				panic(fmt.Sprintf("expr: type of %s is not a function", name))
			}
			ts[i] = t
		}
		c.Functions[name] = &builtin.Function{
			Name:  name,
			Func:  fn,
			Types: ts,
		}
	}
}

// DisableAllBuiltins disables all builtins.
func DisableAllBuiltins() Option {
	return func(c *conf.Config) {
		for name := range c.Builtins {
			c.Disabled[name] = true
		}
	}
}

// DisableBuiltin disables builtin function.
func DisableBuiltin(name string) Option {
	return func(c *conf.Config) {
		c.Disabled[name] = true
	}
}

// EnableBuiltin enables builtin function.
func EnableBuiltin(name string) Option {
	return func(c *conf.Config) {
		delete(c.Disabled, name)
	}
}

// WithContext passes context to all functions calls with a context.Context argument.
func WithContext(name string) Option {
	return Patch(patcher.WithContext{
		Name: name,
	})
}

// Timezone sets default timezone for date() and now() builtin functions.
func Timezone(name string) Option {
	tz, err := time.LoadLocation(name)
	if err != nil {
		panic(err)
	}
	return Patch(patcher.WithTimezone{
		Location: tz,
	})
}

// Compile parses and compiles given input expression to bytecode program.
func Compile(input string, ops ...Option) (*vm.Program, error) {
	config := conf.CreateNew()
	for _, op := range ops {
		op(config)
	}
	for name := range config.Disabled {
		delete(config.Builtins, name)
	}
	config.Check()

	tree, err := checker.ParseCheck(input, config)
	if err != nil {
		return nil, err
	}

	if config.Optimize {
		err = optimizer.Optimize(&tree.Node, config)
		if err != nil {
			var fileError *file.Error
			if errors.As(err, &fileError) {
				return nil, fileError.Bind(tree.Source)
			}
			return nil, err
		}
	}

	program, err := compiler.Compile(tree, config)
	if err != nil {
		return nil, err
	}

	return program, nil
}

// Run evaluates given bytecode program.
func Run(program *vm.Program, env any) (any, error) {
	return vm.Run(program, env)
}

// Eval parses, compiles and runs given input.
func Eval(input string, env any) (any, error) {
	if _, ok := env.(Option); ok {
		return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
	}

	program, err := Compile(input)
	if err != nil {
		return nil, err
	}

	output, err := Run(program, env)
	if err != nil {
		return nil, err
	}

	return output, nil
}
