package script

import (
	"context"
	"fmt"

	"github.com/d5/tengo/compiler"
	"github.com/d5/tengo/compiler/parser"
	"github.com/d5/tengo/compiler/source"
	"github.com/d5/tengo/objects"
	"github.com/d5/tengo/runtime"
)

// Script can simplify compilation and execution of embedded scripts.
type Script struct {
	variables        map[string]*Variable
	builtinFuncs     []objects.Object
	builtinModules   map[string]*objects.Object
	userModuleLoader compiler.ModuleLoader
	input            []byte
}

// New creates a Script instance with an input script.
func New(input []byte) *Script {
	return &Script{
		variables: make(map[string]*Variable),
		input:     input,
	}
}

// Add adds a new variable or updates an existing variable to the script.
func (s *Script) Add(name string, value interface{}) error {
	obj, err := objects.FromInterface(value)
	if err != nil {
		return err
	}

	s.variables[name] = &Variable{
		name:  name,
		value: &obj,
	}

	return nil
}

// Remove removes (undefines) an existing variable for the script.
// It returns false if the variable name is not defined.
func (s *Script) Remove(name string) bool {
	if _, ok := s.variables[name]; !ok {
		return false
	}

	delete(s.variables, name)

	return true
}

// SetBuiltinFunctions allows to define builtin functions.
func (s *Script) SetBuiltinFunctions(funcs []*objects.BuiltinFunction) {
	if funcs != nil {
		s.builtinFuncs = make([]objects.Object, len(funcs))
		for idx, fn := range funcs {
			s.builtinFuncs[idx] = fn
		}
	} else {
		s.builtinFuncs = []objects.Object{}
	}
}

// SetBuiltinModules allows to define builtin modules.
func (s *Script) SetBuiltinModules(modules map[string]*objects.ImmutableMap) {
	if modules != nil {
		s.builtinModules = make(map[string]*objects.Object, len(modules))
		for k, mod := range modules {
			s.builtinModules[k] = objectPtr(mod)
		}
	} else {
		s.builtinModules = map[string]*objects.Object{}
	}
}

// SetUserModuleLoader sets the user module loader for the compiler.
func (s *Script) SetUserModuleLoader(loader compiler.ModuleLoader) {
	s.userModuleLoader = loader
}

// Compile compiles the script with all the defined variables, and, returns Compiled object.
func (s *Script) Compile() (*Compiled, error) {
	symbolTable, builtinModules, globals, err := s.prepCompile()
	if err != nil {
		return nil, err
	}

	fileSet := source.NewFileSet()
	srcFile := fileSet.AddFile("(main)", -1, len(s.input))

	p := parser.NewParser(srcFile, s.input, nil)
	file, err := p.ParseFile()
	if err != nil {
		return nil, err
	}

	c := compiler.NewCompiler(srcFile, symbolTable, nil, builtinModules, nil)

	if s.userModuleLoader != nil {
		c.SetModuleLoader(s.userModuleLoader)
	}

	if err := c.Compile(file); err != nil {
		return nil, err
	}

	return &Compiled{
		symbolTable: symbolTable,
		machine:     runtime.NewVM(c.Bytecode(), globals, s.builtinFuncs, s.builtinModules),
	}, nil
}

// Run compiles and runs the scripts.
// Use returned compiled object to access global variables.
func (s *Script) Run() (compiled *Compiled, err error) {
	compiled, err = s.Compile()
	if err != nil {
		return
	}

	err = compiled.Run()

	return
}

// RunContext is like Run but includes a context.
func (s *Script) RunContext(ctx context.Context) (compiled *Compiled, err error) {
	compiled, err = s.Compile()
	if err != nil {
		return
	}

	err = compiled.RunContext(ctx)

	return
}

func (s *Script) prepCompile() (symbolTable *compiler.SymbolTable, builtinModules map[string]bool, globals []*objects.Object, err error) {
	var names []string
	for name := range s.variables {
		names = append(names, name)
	}

	symbolTable = compiler.NewSymbolTable()

	if s.builtinFuncs == nil {
		s.builtinFuncs = make([]objects.Object, len(objects.Builtins))
		for idx, fn := range objects.Builtins {
			s.builtinFuncs[idx] = &objects.BuiltinFunction{
				Name:  fn.Name,
				Value: fn.Value,
			}
		}
	}

	if s.builtinModules == nil {
		s.builtinModules = make(map[string]*objects.Object)
	}

	for idx, fn := range s.builtinFuncs {
		f := fn.(*objects.BuiltinFunction)
		symbolTable.DefineBuiltin(idx, f.Name)
	}

	builtinModules = make(map[string]bool)
	for name := range s.builtinModules {
		builtinModules[name] = true
	}

	globals = make([]*objects.Object, runtime.GlobalsSize, runtime.GlobalsSize)

	for idx, name := range names {
		symbol := symbolTable.Define(name)
		if symbol.Index != idx {
			panic(fmt.Errorf("wrong symbol index: %d != %d", idx, symbol.Index))
		}

		globals[symbol.Index] = s.variables[name].value
	}

	return
}

func (s *Script) copyVariables() map[string]*Variable {
	vars := make(map[string]*Variable)
	for n, v := range s.variables {
		vars[n] = v
	}

	return vars
}

func objectPtr(o objects.Object) *objects.Object {
	return &o
}