/*
 * ECAL
 *
 * Copyright 2020 Matthias Ladkau. All rights reserved.
 *
 * This Source Code Form is subject to the terms of the MIT
 * License, If a copy of the MIT License was not distributed with this
 * file, You can obtain one at https://opensource.org/licenses/MIT.
 */

package interpreter

import (
	"fmt"

	"devt.de/krotik/common/errorutil"
	"devt.de/krotik/ecal/parser"
	"devt.de/krotik/ecal/scope"
	"devt.de/krotik/ecal/util"
)

// Base Runtime
// ============

/*
baseRuntime models a base runtime component which provides the essential fields and functions.
*/
type baseRuntime struct {
	instanceID string               // Unique identifier (should be used when instance state is stored)
	erp        *ECALRuntimeProvider // Runtime provider
	node       *parser.ASTNode      // AST node which this runtime component is servicing
}

var instanceCounter uint64 // Global instance counter to create unique identifiers for every runtime component instance

/*
Validate this node and all its child nodes.
*/
func (rt *baseRuntime) Validate() error {

	// Validate all children

	for _, child := range rt.node.Children {
		if err := child.Runtime.Validate(); err != nil {
			return err
		}
	}

	return nil
}

/*
Eval evaluate this runtime component.
*/
func (rt *baseRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
	return nil, nil
}

/*
newBaseRuntime returns a new instance of baseRuntime.
*/
func newBaseRuntime(erp *ECALRuntimeProvider, node *parser.ASTNode) *baseRuntime {
	instanceCounter++
	return &baseRuntime{fmt.Sprint(instanceCounter), erp, node}
}

// Void Runtime
// ============

/*
voidRuntime is a special runtime for constructs which are only evaluated as part
of other components.
*/
type voidRuntime struct {
	*baseRuntime
}

/*
voidRuntimeInst returns a new runtime component instance.
*/
func voidRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
	return &voidRuntime{newBaseRuntime(erp, node)}
}

/*
Validate this node and all its child nodes.
*/
func (rt *voidRuntime) Validate() error {
	return rt.baseRuntime.Validate()
}

/*
Eval evaluate this runtime component.
*/
func (rt *voidRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
	return rt.baseRuntime.Eval(vs, is)
}

// Import Runtime
// ==============

/*
importRuntime handles import statements.
*/
type importRuntime struct {
	*baseRuntime
}

/*
importRuntimeInst returns a new runtime component instance.
*/
func importRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
	return &importRuntime{newBaseRuntime(erp, node)}
}

/*
Validate this node and all its child nodes.
*/
func (rt *importRuntime) Validate() error {
	return rt.baseRuntime.Validate()
}

/*
Eval evaluate this runtime component.
*/
func (rt *importRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
	_, err := rt.baseRuntime.Eval(vs, is)

	if rt.erp.ImportLocator == nil {
		err = rt.erp.NewRuntimeError(util.ErrRuntimeError, "No import locator was specified", rt.node)
	}

	if err == nil {

		var importPath interface{}
		if importPath, err = rt.node.Children[0].Runtime.Eval(vs, is); err == nil {

			var codeText string
			if codeText, err = rt.erp.ImportLocator.Resolve(fmt.Sprint(importPath)); err == nil {
				var ast *parser.ASTNode

				if ast, err = parser.ParseWithRuntime(fmt.Sprint(importPath), codeText, rt.erp); err == nil {
					if err = ast.Runtime.Validate(); err == nil {

						ivs := scope.NewScope(scope.GlobalScope)
						if _, err = ast.Runtime.Eval(ivs, make(map[string]interface{})); err == nil {
							irt := rt.node.Children[1].Runtime.(*identifierRuntime)
							irt.Set(vs, is, scope.ToObject(ivs))
						}
					}
				}
			}
		}
	}

	return nil, err
}

// Not Implemented Runtime
// =======================

/*
invalidRuntime is a special runtime for not implemented constructs.
*/
type invalidRuntime struct {
	*baseRuntime
}

/*
invalidRuntimeInst returns a new runtime component instance.
*/
func invalidRuntimeInst(erp *ECALRuntimeProvider, node *parser.ASTNode) parser.Runtime {
	return &invalidRuntime{newBaseRuntime(erp, node)}
}

/*
Validate this node and all its child nodes.
*/
func (rt *invalidRuntime) Validate() error {
	err := rt.baseRuntime.Validate()
	if err == nil {
		err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct,
			fmt.Sprintf("Unknown node: %s", rt.node.Name), rt.node)
	}
	return err
}

/*
Eval evaluate this runtime component.
*/
func (rt *invalidRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
	_, err := rt.baseRuntime.Eval(vs, is)
	if err == nil {
		err = rt.erp.NewRuntimeError(util.ErrInvalidConstruct, fmt.Sprintf("Unknown node: %s", rt.node.Name), rt.node)
	}
	return nil, err
}

// General Operator Runtime
// ========================

/*
operatorRuntime is a general operator operation. Used for embedding.
*/
type operatorRuntime struct {
	*baseRuntime
}

/*
errorDetailString produces a detail string for errors.
*/
func (rt *operatorRuntime) errorDetailString(token *parser.LexToken, opVal interface{}) string {
	if !token.Identifier {
		return token.Val
	}

	if opVal == nil {
		opVal = "NULL"
	}

	return fmt.Sprintf("%v=%v", token.Val, opVal)
}

/*
numVal returns a transformed number value.
*/
func (rt *operatorRuntime) numVal(op func(float64) interface{}, vs parser.Scope,
	is map[string]interface{}) (interface{}, error) {

	var ret interface{}

	errorutil.AssertTrue(len(rt.node.Children) == 1,
		fmt.Sprint("Operation requires 1 operand", rt.node))

	res, err := rt.node.Children[0].Runtime.Eval(vs, is)
	if err == nil {

		// Check if the value is a number

		resNum, ok := res.(float64)

		if !ok {

			// Produce a runtime error if the value is not a number

			return nil, rt.erp.NewRuntimeError(util.ErrNotANumber,
				rt.errorDetailString(rt.node.Children[0].Token, res), rt.node.Children[0])
		}

		ret = op(resNum)
	}

	return ret, err
}

/*
boolVal returns a transformed boolean value.
*/
func (rt *operatorRuntime) boolVal(op func(bool) interface{},
	vs parser.Scope, is map[string]interface{}) (interface{}, error) {

	var ret interface{}

	errorutil.AssertTrue(len(rt.node.Children) == 1,
		fmt.Sprint("Operation requires 1 operand", rt.node))

	res, err := rt.node.Children[0].Runtime.Eval(vs, is)
	if err == nil {

		resBool, ok := res.(bool)

		if !ok {
			return nil, rt.erp.NewRuntimeError(util.ErrNotABoolean,
				rt.errorDetailString(rt.node.Children[0].Token, res), rt.node.Children[0])
		}

		ret = op(resBool)
	}

	return ret, err
}

/*
numOp executes an operation on two number values.
*/
func (rt *operatorRuntime) numOp(op func(float64, float64) interface{},
	vs parser.Scope, is map[string]interface{}) (interface{}, error) {
	var ok bool
	var res1, res2 interface{}
	var err error

	errorutil.AssertTrue(len(rt.node.Children) == 2,
		fmt.Sprint("Operation requires 2 operands", rt.node))

	if res1, err = rt.node.Children[0].Runtime.Eval(vs, is); err == nil {
		if res2, err = rt.node.Children[1].Runtime.Eval(vs, is); err == nil {
			var res1Num, res2Num float64

			if res1Num, ok = res1.(float64); !ok {
				err = rt.erp.NewRuntimeError(util.ErrNotANumber,
					rt.errorDetailString(rt.node.Children[0].Token, res1), rt.node.Children[0])

			} else {
				if res2Num, ok = res2.(float64); !ok {
					err = rt.erp.NewRuntimeError(util.ErrNotANumber,
						rt.errorDetailString(rt.node.Children[1].Token, res2), rt.node.Children[1])

				} else {

					return op(res1Num, res2Num), err
				}
			}
		}
	}

	return nil, err
}

/*
genOp executes an operation on two general values.
*/
func (rt *operatorRuntime) genOp(op func(interface{}, interface{}) interface{},
	vs parser.Scope, is map[string]interface{}) (interface{}, error) {

	var ret interface{}

	errorutil.AssertTrue(len(rt.node.Children) == 2,
		fmt.Sprint("Operation requires 2 operands", rt.node))

	res1, err := rt.node.Children[0].Runtime.Eval(vs, is)
	if err == nil {
		var res2 interface{}

		if res2, err = rt.node.Children[1].Runtime.Eval(vs, is); err == nil {
			ret = op(res1, res2)
		}
	}

	return ret, err
}

/*
strOp executes an operation on two string values.
*/
func (rt *operatorRuntime) strOp(op func(string, string) interface{},
	vs parser.Scope, is map[string]interface{}) (interface{}, error) {

	var ret interface{}

	errorutil.AssertTrue(len(rt.node.Children) == 2,
		fmt.Sprint("Operation requires 2 operands", rt.node))

	res1, err := rt.node.Children[0].Runtime.Eval(vs, is)
	if err == nil {
		var res2 interface{}

		if res2, err = rt.node.Children[1].Runtime.Eval(vs, is); err == nil {
			ret = op(fmt.Sprint(res1), fmt.Sprint(res2))
		}
	}

	return ret, err
}

/*
boolOp executes an operation on two boolean values.
*/
func (rt *operatorRuntime) boolOp(op func(bool, bool) interface{},
	vs parser.Scope, is map[string]interface{}) (interface{}, error) {

	var res interface{}

	errorutil.AssertTrue(len(rt.node.Children) == 2,
		fmt.Sprint("Operation requires 2 operands", rt.node))

	res1, err := rt.node.Children[0].Runtime.Eval(vs, is)
	if err == nil {
		var res2 interface{}

		if res2, err = rt.node.Children[1].Runtime.Eval(vs, is); err == nil {

			res1bool, ok := res1.(bool)
			if !ok {
				return nil, rt.erp.NewRuntimeError(util.ErrNotABoolean,
					rt.errorDetailString(rt.node.Children[0].Token, res1), rt.node.Children[0])
			}

			res2bool, ok := res2.(bool)
			if !ok {
				return nil, rt.erp.NewRuntimeError(util.ErrNotABoolean,
					rt.errorDetailString(rt.node.Children[1].Token, res2), rt.node.Children[0])
			}

			res = op(res1bool, res2bool)
		}
	}

	return res, err
}

/*
listOp executes an operation on a value and a list.
*/
func (rt *operatorRuntime) listOp(op func(interface{}, []interface{}) interface{},
	vs parser.Scope, is map[string]interface{}) (interface{}, error) {

	var res interface{}

	errorutil.AssertTrue(len(rt.node.Children) == 2,
		fmt.Sprint("Operation requires 2 operands", rt.node))

	res1, err := rt.node.Children[0].Runtime.Eval(vs, is)
	if err == nil {
		var res2 interface{}

		if res2, err = rt.node.Children[1].Runtime.Eval(vs, is); err == nil {

			res2list, ok := res2.([]interface{})
			if !ok {
				err = rt.erp.NewRuntimeError(util.ErrNotAList,
					rt.errorDetailString(rt.node.Children[1].Token, res2), rt.node.Children[0])
			} else {
				res = op(res1, res2list)
			}
		}
	}

	return res, err
}
