package main

import (
	"fmt"
	"log"
	"reflect"
)

type Interpreter struct {
	env    map[string]any
	errors []error
}

type RuntimeError struct {
	token Token
	msg   string
}

func (re *RuntimeError) Error() string {
	return fmt.Sprintf("RuntimeError [%d][%s] Error: %s", re.token.line, re.token.typ, re.msg)
}

func newInterpreter() *Interpreter {
	return &Interpreter{env: make(map[string]any)}
}

func (i *Interpreter) interpret(stmts []Stmt) []error {
	defer i.recover()

	i.errors = []error{}

	for _, stmt := range stmts {
		stmt.accept(i)
	}

	return i.errors
}

func (i *Interpreter) recover() {
	if err := recover(); err != nil {
		_, ok := err.(*RuntimeError)

		if !ok {
			panic(err)
		}
	}
}

func (i *Interpreter) evaluate(e Expr) any {
	return e.accept(i)
}

func (i *Interpreter) visitBinary(b *Binary) any {
	left := i.evaluate(b.left)
	right := i.evaluate(b.right)

	switch b.op.typ {
	case MINUS:
		i.checkIfFloats(b.op, left, right)
		return left.(float64) - right.(float64)
	case SLASH:
		i.checkIfFloats(b.op, left, right)
		return left.(float64) / right.(float64)
	case STAR:
		i.checkIfFloats(b.op, left, right)
		return left.(float64) * right.(float64)
	case GREATER:
		i.checkIfFloats(b.op, left, right)
		return left.(float64) > right.(float64)
	case LESS:
		i.checkIfFloats(b.op, left, right)
		return left.(float64) < right.(float64)
	case GREATER_EQUAL:
		i.checkIfFloats(b.op, left, right)
		return left.(float64) >= right.(float64)
	case LESS_EQUAL:
		i.checkIfFloats(b.op, left, right)
		return left.(float64) <= right.(float64)
	case BANG_EQUAL:
		return !reflect.DeepEqual(left, right)
	case EQUAL_EQUAL:
		return reflect.DeepEqual(left, right)
	case PLUS:
		if isFloats(left, right) {
			return left.(float64) + right.(float64)
		}

		if isStrings(left, right) {
			return left.(string) + right.(string)
		}
	}

	i.panic(&RuntimeError{b.op, fmt.Sprintf("Operands must be numbers or strings: %v %s %v", left, b.op.lexeme, right)})

	return nil
}

func (i *Interpreter) visitLiteral(l *Literal) any {
	return l.value
}

func (i *Interpreter) visitGrouping(g *Grouping) any {
	return i.evaluate(g.expression)
}

func (i *Interpreter) visitUnary(u *Unary) any {
	val := i.evaluate(u.right)

	switch u.op.typ {
	case MINUS:
		i.checkIfFloat(u.op, val)
		return -val.(float64)
	case BANG:
		return !isTruthy(val)
	}

	return nil
}

func (i *Interpreter) visitVariable(v *Variable) any {
	if found, ok := i.env[v.name.lexeme]; ok {
		return found
	}

	i.panic(&RuntimeError{v.name, fmt.Sprintf("Undefined variable '%s'.", v.name.lexeme)})

	return nil
}

func (i *Interpreter) visitAssignment(a *Assign) any {
	if _, ok := i.env[a.variable.lexeme]; ok {
		val := i.evaluate(a.value)
		i.env[a.variable.lexeme] = val
		return val
	}

	i.panic(&RuntimeError{a.variable, fmt.Sprintf("Undefined variable '%s'.", a.variable.lexeme)})

	return nil
}

func (i *Interpreter) visitPrintStmt(p *PrintStmt) {
	fmt.Printf("%v\n", i.evaluate(p.val))
}

func (i *Interpreter) visitExprStmt(se *ExprStmt) {
	i.evaluate(se.expr)
}

func (i *Interpreter) visitVarStmt(v *VarStmt) {

	var val any = nil

	if v.initializer != nil {
		val = i.evaluate(v.initializer)
	}

	i.env[v.name.lexeme] = val
}

func (i *Interpreter) panic(re *RuntimeError) {
	i.errors = append(i.errors, re)
	log.Println(re)
	panic(re)
}

func (i *Interpreter) checkIfFloat(op Token, val any) {
	if _, ok := val.(float64); ok {
		return
	}

	i.panic(&RuntimeError{op, "value must be a number."})
}

func (i *Interpreter) checkIfFloats(op Token, a any, b any) {
	if isFloats(a, b) {
		return
	}

	i.panic(&RuntimeError{op, fmt.Sprintf("Operands must be numbers: %v %s %v", a, op.lexeme, b)})
}

func isFloats(a any, b any) bool {
	ltype := reflect.TypeOf(a)
	rtype := reflect.TypeOf(b)

	return ltype.Kind() == rtype.Kind() && ltype.Kind() == reflect.Float64
}

func isStrings(a any, b any) bool {
	ltype := reflect.TypeOf(a)
	rtype := reflect.TypeOf(b)

	return ltype.Kind() == rtype.Kind() && ltype.Kind() == reflect.String
}

func isTruthy(val any) bool {
	if val == nil {
		return false
	}

	if boolean, ok := val.(bool); ok {
		return boolean
	}

	return true
}
