// Copyright (c) 2024 Celestino Amoroso (celestino.amoroso@gmail.com).
// All rights reserved.

// func-import.go
package expr

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"strings"
)

const ENV_EXPR_PATH = "EXPR_PATH"

func importFunc(ctx ExprContext, name string, args []any) (result any, err error) {
	return importGeneral(ctx, name, args)
}

func includeFunc(ctx ExprContext, name string, args []any) (result any, err error) {
	enable(ctx, control_export_all)
	return importGeneral(ctx, name, args)
}

func importGeneral(ctx ExprContext, name string, args []any) (result any, err error) {
	var dirList []string

	dirList = addEnvImportDirs(dirList)
	dirList = addPresetImportDirs(ctx, dirList)
	result, err = doImport(ctx, name, dirList, NewFlatArrayIterator(args))
	return
}

func checkStringParamExpected(funcName string, paramValue any, paramPos int) (err error) {
	if !(isString(paramValue) /*|| isList(paramValue)*/) {
		err = fmt.Errorf("%s(): param nr %d has wrong type %T, string expected", funcName, paramPos+1, paramValue)
	}
	return
}

func addEnvImportDirs(dirList []string) []string {
	if dirSpec, exists := os.LookupEnv(ENV_EXPR_PATH); exists {
		dirs := strings.Split(dirSpec, ":")
		if dirList == nil {
			dirList = dirs
		} else {
			dirList = append(dirList, dirs...)
		}
	}
	return dirList
}

func addPresetImportDirs(ctx ExprContext, dirList []string) []string {
	if dirSpec, exists := getControlString(ctx, ControlImportPath); exists {
		dirs := strings.Split(dirSpec, ":")
		if dirList == nil {
			dirList = dirs
		} else {
			dirList = append(dirList, dirs...)
		}
	}
	return dirList
}

func isFile(filePath string) bool {
	info, err := os.Stat(filePath)
	return (err == nil || errors.Is(err, os.ErrExist)) && info.Mode().IsRegular()
}

func searchAmongPath(filename string, dirList []string) (filePath string) {
	for _, dir := range dirList {
		if fullPath := path.Join(dir, filename); isFile(fullPath) {
			filePath = fullPath
			break
		}
	}
	return
}

func isPathRelative(filePath string) bool {
	unixPath := filepath.ToSlash(filePath)
	return strings.HasPrefix(unixPath, "./") || strings.HasPrefix(unixPath, "../")
}

func makeFilepath(filename string, dirList []string) (filePath string, err error) {
	if path.IsAbs(filename) || isPathRelative(filename) {
		if isFile(filename) {
			filePath = filename
		}
	} else {
		filePath = searchAmongPath(filename, dirList)
	}
	if len(filePath) == 0 {
		err = fmt.Errorf("source file %q not found", filename)
	}
	return
}

func doImport(ctx ExprContext, name string, dirList []string, it Iterator) (result any, err error) {
	var v any
	var sourceFilepath string

	for v, err = it.Next(); err == nil; v, err = it.Next() {
		if err = checkStringParamExpected(name, v, it.Index()); err != nil {
			break
		}
		if sourceFilepath, err = makeFilepath(v.(string), dirList); err != nil {
			break
		}
		var file *os.File
		if file, err = os.Open(sourceFilepath); err == nil {
			defer file.Close()
			var expr *ast
			scanner := NewScanner(file, DefaultTranslations())
			parser := NewParser(ctx)
			if expr, err = parser.parseGeneral(scanner, true, true); err == nil {
				result, err = expr.eval(ctx, false)
			}
			if err != nil {
				break
			}
		} else {
			break
		}
	}
	if err != nil {
		if err == io.EOF {
			err = nil
		} else {
			result = nil
		}
	}
	return
}

func ImportImportFuncs(ctx ExprContext) {
	ctx.RegisterFunc("import", &simpleFunctor{f: importFunc}, 1, -1)
	ctx.RegisterFunc("include", &simpleFunctor{f: includeFunc}, 1, -1)
}