/*
 * 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"
	"strings"

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

/*
identifierRuntime is the runtime component for identifiers.
*/
type identifierRuntime struct {
	*baseRuntime
}

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

/*
Eval evaluate this runtime component.
*/
func (rt *identifierRuntime) Eval(vs parser.Scope, is map[string]interface{}) (interface{}, error) {
	var res interface{}
	_, err := rt.baseRuntime.Eval(vs, is)
	if err == nil {
		res, err = rt.resolveValue(vs, is, rt.node)
	}
	return res, err
}

/*
resolveValue resolves the value of this identifier.
*/
func (rt *identifierRuntime) resolveValue(vs parser.Scope, is map[string]interface{}, node *parser.ASTNode) (interface{}, error) {
	var anode *parser.ASTNode
	var astring string
	var result interface{}
	var err error

	functionResolved := func(astring string, rnode *parser.ASTNode) *parser.ASTNode {

		res := &parser.ASTNode{ // Create a dummy identifier which models the value evaluation so far
			Name: parser.NodeIDENTIFIER,
			Token: &parser.LexToken{
				ID:         node.Token.ID,
				Identifier: node.Token.Identifier,
				Lline:      node.Token.Lline,
				Lpos:       node.Token.Lpos,
				Pos:        node.Token.Pos,
				Val:        strings.Replace(astring, ".", ">", -1),
			},
			Children: nil,
		}

		for i, c := range rnode.Children {
			if c.Name == parser.NodeFUNCCALL {
				res.Children = rnode.Children[i+1:]
			}
		}

		return res
	}

	anode, astring, err = buildAccessString(rt.erp, vs, is, node, node.Token.Val)

	if len(node.Children) == 0 {

		// Simple case we just have a variable

		result, _, err = vs.GetValue(node.Token.Val)

	} else if cval, ok := stdlib.GetStdlibConst(astring); ok {

		result = cval

	} else {

		if rerr, ok := err.(*util.RuntimeError); err == nil || ok && rerr.Type == util.ErrInvalidConstruct {
			funcCallInAccessStringExecuted := ok && rerr.Type == util.ErrInvalidConstruct

			if result, _, err = vs.GetValue(astring); err == nil {

				if funcCallInAccessStringExecuted {

					result, err = rt.resolveFunction(astring, vs, is, rerr.Node, result, err)

					node = functionResolved(astring, anode)

					if len(node.Children) > 0 {

						// We have more identifiers after the func call - there is more to do ...

						vs = scope.NewScope("funcresult")
						vs.SetValue(node.Token.Val, result)

						result, err = rt.resolveValue(vs, is, node)
					}
				} else {

					result, err = rt.resolveFunction(astring, vs, is, node, result, err)
				}
			}
		}
	}

	return result, err
}

/*
resolveFunction execute function calls and return the result.
*/
func (rt *identifierRuntime) resolveFunction(astring string, vs parser.Scope, is map[string]interface{},
	node *parser.ASTNode, result interface{}, err error) (interface{}, error) {

	is["erp"] = rt.erp      // All functions have access to the ECAL Runtime Provider
	is["astnode"] = rt.node // ... and the AST node

	for _, funccall := range node.Children {

		if funccall.Name == parser.NodeFUNCCALL {
			var funcObj util.ECALFunction

			ok := astring == "log" || astring == "error" || astring == "debug"

			if !ok {

				funcObj, ok = result.(util.ECALFunction)

				if !ok {

					// Check for stdlib function

					funcObj, ok = stdlib.GetStdlibFunc(astring)

					if !ok {

						// Check for inbuild function

						funcObj, ok = InbuildFuncMap[astring]
					}
				}
			}

			if ok {
				var args []interface{}

				// Collect the parameter values

				for _, c := range funccall.Children {
					var val interface{}

					if err == nil {
						val, err = c.Runtime.Eval(vs, make(map[string]interface{}))
						args = append(args, val)
					}
				}

				if err == nil {

					if astring == "log" || astring == "error" || astring == "debug" {

						// Convert non-string structures

						for i, a := range args {
							if _, ok := args[i].(string); !ok {
								args[i] = stringutil.ConvertToPrettyString(a)
							}
						}

						if astring == "log" {
							rt.erp.Logger.LogInfo(args...)
						} else if astring == "error" {
							rt.erp.Logger.LogError(args...)
						} else if astring == "debug" {
							rt.erp.Logger.LogDebug(args...)
						}

					} else {

						// Execute the function and

						result, err = funcObj.Run(rt.instanceID, vs, is, args)

						_, ok1 := err.(*util.RuntimeErrorWithDetail)
						_, ok2 := err.(*util.RuntimeErrorWithDetail)

						if err != nil && !ok1 && !ok2 {

							// Convert into a proper runtime error if necessary

							rerr := rt.erp.NewRuntimeError(util.ErrRuntimeError,
								err.Error(), node).(*util.RuntimeError)

							if err == util.ErrIsIterator || err == util.ErrEndOfIteration || err == util.ErrContinueIteration {
								rerr.Type = err
							}

							err = rerr
						}
					}
				}

			} else {

				err = rt.erp.NewRuntimeError(util.ErrUnknownConstruct,
					fmt.Sprintf("Unknown function: %v", node.Token.Val), node)
			}

			break
		}
	}

	return result, err
}

/*
Set sets a value to this identifier.
*/
func (rt *identifierRuntime) Set(vs parser.Scope, is map[string]interface{}, value interface{}) error {
	var err error

	if len(rt.node.Children) == 0 {

		// Simple case we just have a variable

		err = vs.SetValue(rt.node.Token.Val, value)

	} else {
		var as string

		_, as, err = buildAccessString(rt.erp, vs, is, rt.node, rt.node.Token.Val)

		if err == nil {

			// Collect all the children and find the right spot

			err = vs.SetValue(as, value)
		}
	}

	return err
}

/*
buildAccessString builds an access string using a given node and a prefix.
*/
func buildAccessString(erp *ECALRuntimeProvider, vs parser.Scope, is map[string]interface{},
	node *parser.ASTNode, prefix string) (*parser.ASTNode, string, error) {

	var err error
	res := prefix

	for i, c := range node.Children {

		if err == nil {

			// The unexpected construct error is used in two ways:
			// 1. Error message when a function call is used on the left hand of
			// an assignment.
			// 2. Signalling there is a function call involved on the right hand
			// of an assignment.

			if c.Name == parser.NodeCOMPACCESS {
				var val interface{}
				val, err = c.Children[0].Runtime.Eval(vs, is)
				res = fmt.Sprintf("%v.%v", res, val)

				if len(node.Children) > i+1 && node.Children[i+1].Name == parser.NodeFUNCCALL {

					err = erp.NewRuntimeError(util.ErrInvalidConstruct,
						"Unexpected construct", node)
					break
				}

			} else if c.Name == parser.NodeIDENTIFIER {

				res = fmt.Sprintf("%v.%v", res, c.Token.Val)

				if len(c.Children) > 0 && c.Children[0].Name == parser.NodeFUNCCALL {
					node = c

					err = erp.NewRuntimeError(util.ErrInvalidConstruct,
						"Unexpected construct", node)
					break
				}

				node, res, err = buildAccessString(erp, vs, is, c, res)
			}
		}
	}

	return node, res, err
}
