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

// main.go
package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strings"

	"git.portale-stac.it/go-pkg/expr"
	"git.portale-stac.it/go-pkg/expr/kern"
	"git.portale-stac.it/go-pkg/expr/scan"
	"git.portale-stac.it/go-pkg/expr/types"
	"git.portale-stac.it/go-pkg/expr/types/array"
	"git.portale-stac.it/go-pkg/expr/types/str"
	"git.portale-stac.it/go-pkg/utils"

	// https://pkg.go.dev/github.com/ergochat/readline#section-readme
	"github.com/ergochat/readline"
)

const (
	intro = PROGNAME + ` -- Expressions calculator ` + VERSION + `
	Based on the Expr package ` + EXPR_VERSION + ` (` + EXPR_DATE + `) 
	Type help to get the list of available commands
	See also https://git.portale-stac.it/go-pkg/expr/src/branch/main/README.adoc
`
	mainPrompt = ">>> "
	contPrompt = "... "

	historyFile = "~/.expr_history"
)

// ------

func errOptValueRequired(opt string) error {
	return fmt.Errorf("option %q requires a value", opt)
}

func about() string {
	return PROGNAME + " -- " + VERSION + "; Expr package " + EXPR_VERSION
}

func importBuiltins(ctx kern.ExprContext, opt *Options) (err error) {
	for _, spec := range opt.builtin {
		if moduleSpec, ok := spec.(string); ok {
			if moduleSpec == "all" {
				moduleSpec = "*"
			}
			_, err = expr.ImportInContextByGlobPattern(ctx, moduleSpec)
		} else if moduleSpec, ok := spec.([]string); ok {
			notFoundList := make([]string, 0)
			for _, name := range moduleSpec {
				if !expr.ImportInContext(ctx, name) {
					notFoundList = append(notFoundList, name)
				}
			}
			if len(notFoundList) > 0 {
				err = fmt.Errorf("not found modules: %s", strings.Join(notFoundList, ","))
			}
		}
	}
	return
}

func initReadlineConfig(cfg *readline.Config) {
	if histfile, err := utils.ExpandPath(historyFile); err == nil {
		cfg.HistoryFile = histfile
	}
	cfg.Undo = true
	cfg.DisableAutoSaveHistory = true

}

func goInteractiveReadline(opt *Options, ctx kern.ExprContext, r io.Reader) {
	var sb strings.Builder
	var cfg readline.Config
	initReadlineConfig(&cfg)
	rl, err := readline.NewFromConfig(&cfg)
	if err != nil {
		goInteractive(opt, ctx, r)
		return
	}
	defer rl.Close()

	fmt.Print(intro)
	rl.SetPrompt(mainPrompt)
	for line, err := rl.ReadLine(); err == nil; line, err = rl.ReadLine() {
		if continuation(&sb, line) {
			rl.SetPrompt(contPrompt)
			continue
		}

		rl.SetPrompt(mainPrompt)
		sb.WriteString(line)
		source := strings.TrimSpace(sb.String())
		if source != "" && !strings.HasPrefix(source, "//") {
			if cmd, args := cmdHandler.get(source); cmd != nil {
				rl.SaveToHistory(source)
				if err = cmd.exec(opt, ctx, args); err != nil {
					if err == io.EOF {
						err = nil
						break
					} else {
						fmt.Fprintln(os.Stderr, "Eval Error:", err)
						break
					}
				}
			} else {
				rl.SaveToHistory(source)
				r := strings.NewReader(source)
				compute(opt, ctx, r, true)
			}
		}
		sb.Reset()
	}
	fmt.Println()
}

func goInteractive(opt *Options, ctx kern.ExprContext, r io.Reader) {
	var sb strings.Builder
	fmt.Print(intro)
	fmt.Print(mainPrompt)
	reader := bufio.NewReaderSize(r, 1024)
	for line, err := reader.ReadString('\n'); err == nil && line != "exit\n"; line, err = reader.ReadString('\n') {
		if continuation(&sb, line) {
			continue
		}

		sb.WriteString(line)
		source := strings.TrimSpace(sb.String())
		// fmt.Printf("source=%q\n", source)
		if source != "" && !strings.HasPrefix(source, "//") {
			if cmd, args := cmdHandler.get(source); cmd != nil {
				if err = cmd.exec(opt, ctx, args); err != nil {
					break
				}
			} else {
				r := strings.NewReader(source)
				compute(opt, ctx, r, true)
			}
		}
		sb.Reset()
		fmt.Print(mainPrompt)
	}
	fmt.Println()
}

func continuation(sb *strings.Builder, line string) (cont bool) {
	line = strings.TrimSpace(line)
	if strings.HasSuffix(line, "\\") {
		sb.WriteString(line[0 : len(line)-1])
		cont = true
	} else if strings.HasSuffix(line, ";") {
		sb.WriteString(line)
		cont = true
	} else if len(line) > 0 {
		if scan.StringEndsWithOperator(line) {
			sb.WriteString(line)
			cont = true
		} else {
			fullInput := sb.String() + line
			if strings.Count(fullInput, "(") > strings.Count(fullInput, ")") ||
				strings.Count(fullInput, "[") > strings.Count(fullInput, "]") ||
				strings.Count(fullInput, "{") > strings.Count(fullInput, "}") {
				sb.WriteString(line)
				cont = true
			}
		}
	}

	return
}

func goBatch(opt *Options, ctx kern.ExprContext, r io.Reader, outputEnabled bool) {
	var sb strings.Builder
	reader := bufio.NewReaderSize(r, 1024)
	for line, err := reader.ReadString('\n'); err == nil && line != "exit\n"; line, err = reader.ReadString('\n') {
		if continuation(&sb, line) {
			continue
		}
		sb.WriteString(line)
		source := strings.TrimSpace(sb.String())
		// fmt.Printf("source=%q\n", source)
		if source != "" && !strings.HasPrefix(source, "//") {
			if cmd, args := cmdHandler.get(source); cmd != nil {
				if err = cmd.exec(opt, ctx, args); err != nil {
					fmt.Fprintln(os.Stderr, "Eval Error:", err)
					break
				}
			} else {
				r := strings.NewReader(source)
				compute(opt, ctx, r, outputEnabled)
			}
		}
		sb.Reset()
	}
}

func compute(opt *Options, ctx kern.ExprContext, r io.Reader, outputEnabled bool) {
	scanner := scan.NewScanner(r, scan.DefaultTranslations())
	parser := expr.NewParser()

	if ast, err := parser.Parse(scanner); err == nil {
		if opt.printPrefix {
			fmt.Println(ast)
		}
		if opt.printTree {
			printGraph()
		}

		if result, err := ast.Eval(ctx); err == nil {
			if outputEnabled && opt.output {
				printResult(opt, result)
			}
		} else {
			fmt.Fprintln(os.Stderr, "Eval Error:", err)
		}
	} else {
		fmt.Fprintln(os.Stderr, "Parse Error:", err)
	}
}

func printResult(opt *Options, result any) {
	if f, ok := result.(kern.Formatter); ok {
		fmt.Println(f.ToString(opt.formOpt))
	} else if types.IsInteger(result) {
		fmt.Printf(opt.baseVerb, result)
		fmt.Println()
	} else if str.IsString(result) {
		fmt.Printf("\"%s\"\n", result)
	} else {
		fmt.Println(result)
	}
}

func isReaderTerminal(r io.Reader) bool {
	if fh, ok := r.(*os.File); ok {
		return utils.StreamIsTerminal(fh)
	}
	return false
}

func registerLocalFunctions(ctx kern.ExprContext) {
	const (
		devParamProp   = "prop"
		devParamDigits = "digits"
	)

	aboutFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) {
		result = about()
		return
	}

	ctrlListFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) {
		vars := ctx.EnumVars(func(name string) bool {
			return len(name) > 0 && name[0] == '_'
		})
		result = array.ArrayFromStrings(vars)
		return
	}

	ctrlFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) {
		varName, _ := args[devParamProp].(string)
		if len(args) == 1 {
			result = expr.GlobalCtrlGet(ctx, varName)
		} else {
			result = expr.GlobalCtrlSet(ctx, varName, args[kern.ParamValue])
		}
		return
	}

	envSetFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) {
		var varName, value string
		var ok bool
		if varName, ok = args[kern.ParamName].(string); !ok {
			err = kern.ErrExpectedGot(name, kern.TypeString, args[kern.ParamName])
			return
		}
		if value, ok = args[kern.ParamValue].(string); !ok {
			err = kern.ErrExpectedGot(name, kern.TypeString, args[kern.ParamValue])
			return
		}
		if err = os.Setenv(varName, value); err == nil {
			result = value
		}
		return
	}

	envGetFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) {
		var varName string
		var ok bool
		if varName, ok = args[kern.ParamName].(string); !ok {
			err = kern.ErrExpectedGot(name, kern.TypeString, args[kern.ParamName])
			return
		}

		if result, ok = os.LookupEnv(varName); !ok {
			err = fmt.Errorf("environment variable %q does not exist", varName)
		}
		return
	}

	envListFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) {
		env := os.Environ()
		vars := make([]string, 0, len(env))
		for _, e := range env {
			name, _, _ := strings.Cut(e, "=")
			vars = append(vars, name)
		}
		result = array.ArrayFromStrings(vars)
		return
	}

	binFunc := func(ctx kern.ExprContext, name string, args map[string]any) (result any, err error) {
		var value, digits int64
		var ok bool
		var sb strings.Builder

		if value, ok = args[kern.ParamValue].(int64); !ok {
			err = kern.ErrExpectedGot(name, kern.TypeInt, args[kern.ParamValue])
			return
		}
		if digits, ok = args[devParamDigits].(int64); !ok {
			err = kern.ErrExpectedGot(name, kern.TypeInt, args[devParamDigits])
			return
		}
		if digits != 64 && digits != 32 && digits != 16 && digits != 8 {
			err = fmt.Errorf("%s param allows 8, 16, 32, or 64 values only", devParamDigits)
			return
		}

		mask := uint64(0)
		for i := 0; i < int(digits); i++ {
			mask |= (1 << i)
		}
		maskedValue := uint64(value) & mask
		// if maskedValue != uint64(value) {
		// 	err = fmt.Errorf("%s param (%d) is not compatible with the value (%d) of %s param", expr.ParamValue, value, digits, devParamDigits)
		// 	return
		// }

		for i := int(digits) - 1; i >= 0; i-- {
			if maskedValue&(1<<i) == 0 {
				sb.WriteByte('0')
			} else {
				sb.WriteByte('1')
			}
		}

		result = sb.String()
		return
	}

	ctx.RegisterFunc("about", kern.NewGolangFunctor(aboutFunc), kern.TypeString, []kern.ExprFuncParam{})
	ctx.RegisterFunc("ctrlList", kern.NewGolangFunctor(ctrlListFunc), kern.TypeListOfStrings, []kern.ExprFuncParam{})
	ctx.RegisterFunc("ctrl", kern.NewGolangFunctor(ctrlFunc), kern.TypeAny, []kern.ExprFuncParam{
		kern.NewFuncParam(devParamProp),
		kern.NewFuncParamFlag(kern.ParamValue, kern.PfOptional),
	})
	ctx.RegisterFunc("envSet", kern.NewGolangFunctor(envSetFunc), kern.TypeString, []kern.ExprFuncParam{
		kern.NewFuncParam(kern.ParamName),
		kern.NewFuncParam(kern.ParamValue),
	})
	ctx.RegisterFunc("envGet", kern.NewGolangFunctor(envGetFunc), kern.TypeString, []kern.ExprFuncParam{
		kern.NewFuncParam(kern.ParamName),
	})
	ctx.RegisterFunc("envList", kern.NewGolangFunctor(envListFunc), kern.TypeListOfStrings, []kern.ExprFuncParam{})

	ctx.RegisterFunc("bin", kern.NewGolangFunctor(binFunc), kern.TypeInt, []kern.ExprFuncParam{
		kern.NewFuncParam(kern.ParamValue),
		kern.NewFuncParamFlagDef(devParamDigits, kern.PfOptional|kern.PfDefault, int64(8)),
	})
}

func main() {
	setupCommands()
	opt := NewOptions()
	opt.loadRc()
	if err := opt.parseArgs(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	ctx := expr.NewSimpleStore()

	if err := importBuiltins(ctx, opt); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}

	registerLocalFunctions(ctx)

	if len(opt.expressions) == opt.rcCount || opt.forceInteractive {
		opt.expressions = append(opt.expressions, os.Stdin)
	}

	for _, input := range opt.expressions {
		if isReaderTerminal(input) {
			goInteractiveReadline(opt, ctx, input)
		} else {
			_, enableOutput := input.(*strings.Reader)
			goBatch(opt, ctx, input, enableOutput)
			if f, ok := input.(*os.File); ok {
				f.Close()
			}
		}
	}

	// TODO: why did I added these lines?
	// if opt.output {
	// 	printResult(opt, ctx.GetLast())
	// }
}
