package d2script

import (
	"errors"
	"fmt"
	"io/ioutil"
	"path/filepath"

	"github.com/robertkrimen/otto"
	_ "github.com/robertkrimen/otto/underscore" // This causes the runtime to support underscore.js
)

// ScriptEngine allows running JavaScript scripts
type ScriptEngine struct {
	vm            *otto.Otto
	isEvalAllowed bool
}

// CreateScriptEngine creates the script engine and returns a pointer to it.
func CreateScriptEngine() *ScriptEngine {
	vm := otto.New()
	err := vm.Set("debugPrint", func(call otto.FunctionCall) otto.Value {
		fmt.Printf("Script: %s\n", call.Argument(0).String())
		return otto.Value{}
	})

	if err != nil {
		fmt.Printf("could not bind the 'debugPrint' to the given function in script engine")
	}

	return &ScriptEngine{
		vm:            vm,
		isEvalAllowed: false,
	}
}

// AllowEval allows the evaluation of JS code.
func (s *ScriptEngine) AllowEval() {
	s.isEvalAllowed = true
}

// DisallowEval disallows the evaluation of JS code.
func (s *ScriptEngine) DisallowEval() {
	s.isEvalAllowed = false
}

// ToValue converts the given interface{} value to a otto.Value
func (s *ScriptEngine) ToValue(source interface{}) (otto.Value, error) {
	return s.vm.ToValue(source)
}

// AddFunction adds the given function to the script engine with the given name.
func (s *ScriptEngine) AddFunction(name string, value interface{}) {
	err := s.vm.Set(name, value)
	if err != nil {
		fmt.Printf("could not add the '%s' function to the script engine", name)
	}
}

// RunScript runs the script file within the given path.
func (s *ScriptEngine) RunScript(fileName string) (*otto.Value, error) {
	fileData, err := ioutil.ReadFile(filepath.Clean(fileName))
	if err != nil {
		fmt.Printf("could not read script file: %s\n", err.Error())
		return nil, err
	}

	val, err := s.vm.Run(string(fileData))
	if err != nil {
		fmt.Printf("Error running script: %s\n", err.Error())
		return nil, err
	}

	return &val, nil
}

// Eval JS code.
func (s *ScriptEngine) Eval(code string) (string, error) {
	if !s.isEvalAllowed {
		return "", errors.New("disabled")
	}

	val, err := s.vm.Eval(code)
	if err != nil {
		return "", err
	}

	return val.String(), nil
}