package pp

import (
	"github.com/d5/tengo/v2"
	"github.com/d5/tengo/v2/stdlib"
	"context"
	"strings"
	"fmt"
)

type Preprocessor struct {
	// The string will be inserted in the
	// beginning of processed files.
	head string

	tags [2]string
	modules *tengo.ModuleMap
}

// Evaluate the expression and return the value
// it would print.
func (pp *Preprocessor) Eval(codes []string) ([]string, error) {
	const retHead = `
		__ret_one__ := ""
		printf := func(format, ...vals) {
			__ret_one__ += sprintf(format, vals...)
		}

		print := func(...vals) {
			__ret_one__ += sprint(vals...)
		}

		println := func(...vals) {
			__ret_one__ += sprintln(vals...)
		}
	`

	const retSeparator = `
		__Separate(__ret_one__)
		__ret_one__ = ""
	`
	rets := []string{}

	fullCode := retHead + "\n" + pp.head
	for _, code := range codes {
		fullCode += "\n" + code + retSeparator + "\n"
	}
	script := tengo.NewScript([]byte(fullCode))
	script.SetImports(pp.modules)
	err := script.Add("sprintf", &tengo.UserFunction{
			Value: func(args ...tengo.Object) (tengo.Object, error){
				if len(args) < 1 {
					return nil, tengo.ErrWrongNumArguments
				}
				format, ok := tengo.ToString(args[0])
				if !ok {
					return nil, tengo.ErrInvalidArgumentType{
						Expected: "string",
					}
				}
				gargs := make([]any, len(args) - 1)
				for i := range gargs {
					gargs[i] = tengo.ToInterface(args[i+1])
					//fmt.Printf("shit: %q\n", gargs[i])
				}
				str := fmt.Sprintf(format, gargs...)
				return tengo.FromInterface(str)
			},
		},
	)
	if err != nil {
		return nil, err
	}
	err = script.Add("sprint", &tengo.UserFunction{
			Value: func(args ...tengo.Object) (tengo.Object, error){
				gargs := make([]any, len(args))
				for i := range gargs {
					gargs[i] = tengo.ToInterface(args[i])
				}
				str := fmt.Sprint(gargs...)
				return tengo.FromInterface(str)
			},
		},
	)
	if err != nil {
		return nil, err
	}
	err = script.Add("sprintln", &tengo.UserFunction{
			Value: func(args ...tengo.Object) (tengo.Object, error){
				gargs := make([]any, len(args))
				for i := range gargs {
					gargs[i] = tengo.ToInterface(args[i])
				}
				str := fmt.Sprintln(gargs...)
				return tengo.FromInterface(str)
			},
		},
	)
	if err != nil {
		return nil, err
	}
	err = script.Add("__Separate", &tengo.UserFunction{
			Value: func(args ...tengo.Object) (tengo.Object, error){
				if len(args) < 1 {
					return nil, tengo.ErrWrongNumArguments
				}
				str, ok := tengo.ToString(args[0])
				if !ok {
					return nil, tengo.ErrInvalidArgumentType{
					}
				}
				rets = append(rets, str)
				return nil, nil
			},
		},
	)
	if err != nil {
		return nil, err
	}

	_, err = script.RunContext(context.Background())
	if err != nil {
		return nil, err
	}

	return rets, nil
}

// Get the new preprocessor with default options.
func NewPp() *Preprocessor {
	pp := &Preprocessor{
		tags: [2]string{
			"<?",
			"?>",
		},
		modules: stdlib.GetModuleMap(stdlib.AllModuleNames()...),
	}
	return pp
}

func (pp *Preprocessor) Process(data string) (string, error) {
	var b strings.Builder
	last := 0
	texts := []string{}
	codes := []string{}
	for {
		idxStart := strings.Index(data[last:], pp.tags[0])
		idxEnd := strings.Index(data[last:], pp.tags[1])
		//fmt.Printf("cock %d %d %d\n", last, idxStart, idxEnd)
		if idxStart < 0 {
			if idxEnd >= 0 {
				return "", UnexpectedError{
					What: "end tag",
				}
			}
			texts = append(texts, data[last:])
			break
		} else if idxEnd < 0 {
			return "", UnexpectedError{
				What: "start tag",
			}
		}
		text := data[last:idxStart]
		texts = append(texts, text)

		code := data[idxStart+len(pp.tags[0]):idxEnd]
		codes = append(codes, code)

		data = data[idxEnd + len(pp.tags[1]):]
		/*if len(data) > 0 && data[0] == '\n' {
			data = data[1:]
		}*/
	}
	codeRets, err := pp.Eval(codes)
	if err != nil {
		return "", err
	}

	for i, codeRet := range codeRets {
		fmt.Fprint(&b, texts[i])
		fmt.Fprintf(&b, codeRet)
	}
	fmt.Fprint(&b, texts[len(codeRets)])

	return b.String(), nil
}