/*
Copyright © 2023 Thomas von Dein

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package main

import (
	"errors"
	"fmt"
	"math"
	"os"
	"regexp"
	"strconv"
	"strings"

	"github.com/chzyer/readline"
)

type Calc struct {
	debug       bool
	batch       bool
	stdin       bool
	stack       *Stack
	history     []string
	completer   readline.AutoCompleter
	interpreter *Interpreter
}

// help for lua functions will be added dynamically
const Help string = `Available commands:
batch                toggle batch mode
debug                toggle debug output
dump                 display the stack contents
clear                clear the whole stack
shift                remove the last element of the stack
history              display calculation history
help|?               show this message
quit|exit|c-d|c-c    exit program

Available operators:
basic operators: + - x /

Available math functions:
sqrt                 square root
mod                  remainder of division
max                  batch mode only: max of all values
min                  batch mode only: min of all values
mean                 batch mode only: mean of all values (alias: avg)
median               batch mode only: median of all values
%                    percent
%-                   substract percent
%+                   add percent

Math operators:
^                    power`

// That way we can add custom functions to completion
func GetCompleteCustomFunctions() func(string) []string {
	return func(line string) []string {
		funcs := []string{}
		for luafunc := range LuaFuncs {
			funcs = append(funcs, luafunc)
		}
		return funcs
	}
}

func NewCalc() *Calc {
	c := Calc{stack: NewStack(), debug: false}

	c.completer = readline.NewPrefixCompleter(
		// custom lua functions
		readline.PcItemDynamic(GetCompleteCustomFunctions()),

		// commands
		readline.PcItem("dump"),
		readline.PcItem("reverse"),
		readline.PcItem("debug"),
		readline.PcItem("undebug"),
		readline.PcItem("clear"),
		readline.PcItem("batch"),
		readline.PcItem("shift"),
		readline.PcItem("undo"),
		readline.PcItem("help"),
		readline.PcItem("history"),
		readline.PcItem("exit"),
		readline.PcItem("quit"),

		// ops
		readline.PcItem("+"),
		readline.PcItem("-"),
		readline.PcItem("*"),
		readline.PcItem("/"),
		readline.PcItem("^"),
		readline.PcItem("%"),
		readline.PcItem("%-"),
		readline.PcItem("%+"),

		// constants
		readline.PcItem("Pi"),
		readline.PcItem("Phi"),
		readline.PcItem("Sqrt2"),
		readline.PcItem("SqrtE"),
		readline.PcItem("SqrtPi"),
		readline.PcItem("SqrtPhi"),
		readline.PcItem("Ln2"),
		readline.PcItem("Log2E"),
		readline.PcItem("Ln10"),
		readline.PcItem("Log10E"),

		// math functions
		readline.PcItem("sqrt"),
		readline.PcItem("remainder"),
		readline.PcItem("avg"),
		readline.PcItem("mean"), // alias for avg
		readline.PcItem("min"),
		readline.PcItem("max"),
		readline.PcItem("median"),
	)

	return &c
}

// setup the interpreter, called from main()
func (c *Calc) SetInt(I *Interpreter) {
	c.interpreter = I
}

func (c *Calc) ToggleDebug() {
	c.debug = !c.debug
	c.stack.ToggleDebug()
	fmt.Printf("debugging set to %t\n", c.debug)
}

func (c *Calc) ToggleBatch() {
	c.batch = !c.batch
	fmt.Printf("batchmode set to %t\n", c.batch)
}

func (c *Calc) ToggleStdin() {
	c.stdin = !c.stdin
}

func (c *Calc) Prompt() string {
	p := "\033[31m»\033[0m "
	b := ""
	if c.batch {
		b = "->batch"
	}
	d := ""
	v := ""
	if c.debug {
		d = "->debug"
		v = fmt.Sprintf("/rev%d", c.stack.rev)
	}

	return fmt.Sprintf("rpn%s%s [%d%s]%s", b, d, c.stack.Len(), v, p)
}

// the actual work horse, evaluate a line of calc command[s]
func (c *Calc) Eval(line string) {
	line = strings.TrimSpace(line)

	if line == "" {
		return
	}

	space := regexp.MustCompile(`\s+`)
	simple := regexp.MustCompile(`^[\+\-\*\/x\^]$`)
	constants := []string{"E", "Pi", "Phi", "Sqrt2", "SqrtE", "SqrtPi",
		"SqrtPhi", "Ln2", "Log2E", "Ln10", "Log10E"}
	functions := []string{"sqrt", "remainder", "mod", "%", "%-", "%+"}
	batch := []string{"median", "avg", "mean", "max", "min"}

	luafuncs := []string{}
	for name := range LuaFuncs {
		luafuncs = append(luafuncs, name)
	}

	for _, item := range space.Split(line, -1) {
		num, err := strconv.ParseFloat(item, 64)

		if err == nil {
			c.stack.Backup()
			c.stack.Push(num)
		} else {
			if simple.MatchString(item) {
				// simple ops like + or x
				c.simple(item[0])
				continue
			}

			if contains(constants, item) {
				// put the constant onto the stack
				c.stack.Backup()
				c.stack.Push(const2num(item))
				continue
			}

			if contains(functions, item) {
				// go builtin math function, if implemented
				c.mathfunc(item)
				continue
			}

			if contains(batch, item) {
				// math functions only supported in batch mode like max or mean
				c.batchfunc(item)
				continue
			}

			if contains(luafuncs, item) {
				// user provided custom lua functions
				c.luafunc(item)
				continue
			}

			// management commands
			switch item {
			case "?":
				fallthrough
			case "help":
				fmt.Println(Help)
				fmt.Println("Lua functions:")
				for name, function := range LuaFuncs {
					fmt.Printf("%-20s %s\n", name, function.help)
				}
			case "dump":
				c.stack.Dump()
			case "debug":
				c.ToggleDebug()
			case "undebug":
				c.debug = false
			case "batch":
				c.ToggleBatch()
			case "clear":
				c.stack.Backup()
				c.stack.Clear()
			case "shift":
				c.stack.Backup()
				c.stack.Shift()
			case "reverse":
				c.stack.Backup()
				c.stack.Reverse()
			case "undo":
				c.stack.Restore()
			case "history":
				for _, entry := range c.history {
					fmt.Println(entry)
				}
			case "exit":
				fallthrough
			case "quit":
				os.Exit(0)
			default:
				fmt.Println("unknown command or operator!")
			}
		}
	}
}

// just a textual representation of math operations, viewable with the
// history command
func (c *Calc) History(format string, args ...any) {
	c.history = append(c.history, fmt.Sprintf(format, args...))
}

// print the result
func (c *Calc) Result() float64 {
	if !c.stdin {
		fmt.Print("= ")
	}

	fmt.Println(c.stack.Last())

	return c.stack.Last()
}

func (c *Calc) Debug(msg string) {
	if c.debug {
		fmt.Printf("DEBUG(calc): %s\n", msg)
	}
}

// do simple calculations
func (c *Calc) simple(op byte) {
	c.stack.Backup()

	for c.stack.Len() > 1 {
		b := c.stack.Pop()
		a := c.stack.Pop()
		var x float64

		c.Debug(fmt.Sprintf("evaluating: %.2f %c %.2f", a, op, b))

		switch op {
		case '+':
			x = a + b
		case '-':
			x = a - b
		case 'x':
			fallthrough // alias for *
		case '*':
			x = a * b
		case '/':
			if b == 0 {
				fmt.Println("error: division by null!")
				return
			}
			x = a / b
		case '^':
			x = math.Pow(a, b)
		default:
			panic("invalid operator!")
		}

		c.stack.Push(x)

		c.History("%f %c %f = %f", a, op, b, x)

		if !c.batch {
			break
		}
	}

	c.Result()
}

// calc using go math lib functions
func (c *Calc) mathfunc(funcname string) {
	c.stack.Backup()

	for c.stack.Len() > 0 {
		var x float64

		switch funcname {
		case "sqrt":
			a := c.stack.Pop()
			x = math.Sqrt(a)
			c.History("sqrt(%f) = %f", a, x)

		case "mod":
			fallthrough // alias
		case "remainder":
			b := c.stack.Pop()
			a := c.stack.Pop()
			x = math.Remainder(a, b)
			c.History("remainderf(%f / %f) = %f", a, b, x)

		case "%":
			b := c.stack.Pop()
			a := c.stack.Pop()

			x = (a / 100) * b
			c.History("%f percent of %f = %f", b, a, x)

		case "%-":
			b := c.stack.Pop()
			a := c.stack.Pop()

			x = a - ((a / 100) * b)
			c.History("%f minus %f percent of %f = %f", a, b, a, x)

		case "%+":
			b := c.stack.Pop()
			a := c.stack.Pop()

			x = a + ((a / 100) * b)
			c.History("%f plus %f percent of %f = %f", a, b, a, x)

		}

		c.stack.Push(x)

		if !c.batch {
			break
		}
	}

	c.Result()
}

// execute pure batch functions, operating on the whole stack
func (c *Calc) batchfunc(funcname string) {
	if !c.batch {
		fmt.Println("error: only available in batch mode")
	}

	c.stack.Backup()
	var x float64
	count := c.stack.Len()

	switch funcname {
	case "median":
		all := []float64{}

		for c.stack.Len() > 0 {
			all = append(all, c.stack.Pop())
		}

		middle := count / 2

		x = all[middle]
		c.History("median(all)")

	case "mean":
		fallthrough // alias
	case "avg":
		var sum float64

		for c.stack.Len() > 0 {
			sum += c.stack.Pop()
		}

		x = sum / float64(count)
		c.History("avg(all)")
	case "min":
		x = c.stack.Pop() // initialize with the last one

		for c.stack.Len() > 0 {
			val := c.stack.Pop()
			if val < x {
				x = val
			}
		}

		c.History("min(all)")
	case "max":
		x = c.stack.Pop() // initialize with the last one

		for c.stack.Len() > 0 {
			val := c.stack.Pop()
			if val > x {
				x = val
			}
		}

		c.History("max(all)")
	}

	c.stack.Push(x)
	_ = c.Result()
}

func (c *Calc) luafunc(funcname string) {
	// called from calc loop
	var x float64
	var err error

	switch c.interpreter.FuncNumArgs(funcname) {
	case 1:
		x, err = c.interpreter.CallLuaFunc(funcname, []float64{c.stack.Last()})
	case 2:
		x, err = c.interpreter.CallLuaFunc(funcname, c.stack.LastTwo())
	case -1:
		x, err = c.interpreter.CallLuaFunc(funcname, c.stack.All())
	default:
		x, err = 0, errors.New("invalid number of argument requested")
	}

	if err != nil {
		fmt.Println(err)
		return
	}

	c.stack.Backup()

	switch c.interpreter.FuncNumArgs(funcname) {
	case 1:
		a := c.stack.Pop()
		c.History("%s(%f) = %f", funcname, a, x)
	case 2:
		a := c.stack.Pop()
		b := c.stack.Pop()
		c.History("%s(%f,%f) = %f", funcname, a, b, x)
	case -1:
		c.stack.Clear()
		c.History("%s(*) = %f", funcname, x)
	}

	c.stack.Push(x)

	c.Result()
}
