package vm

import (
	"fmt"

	"gitlab.com/olanguage/olang/code"
	"gitlab.com/olanguage/olang/object"
)

// StackSize is an integer defining the size of our stack
const StackSize = 2048

// MaxFrames defines the maximum frames allowed in the VM
const MaxFrames = 1024

// GlobalsSize - The upper limit on the number of global bindings our VM supports
const GlobalsSize = 65536

// True, False, and Null - immutable & unique. No need to create new objects in memory each time we
// need one. Makes comparison easier as well because they always point to same place in memory

// True - Pointer to a O Language object.Boolean with value true
var True = &object.Boolean{Value: true}

// False - Pointer to a O Language object.Boolean of value false
var False = &object.Boolean{Value: false}

// Null - Pointer to a O Language object.Null
var Null = &object.Null{}

// VM defines our Virtual Machine
type VM struct {
	constants    []object.Object
	stack       []object.Object
	sp          int
	globals     []object.Object
	frames      []*Frame
	framesIndex int
	builtins    map[uint16]*object.Builtin
}

type Bytecode struct {
	Instructions code.Instructions
	Constants    []object.Object
}

// New creates a new VM instance
func New(bytecode *Bytecode) *VM {
	mainFrame := NewFrame(bytecode.Instructions, 0)
	frames := make([]*Frame, MaxFrames)
	frames[0] = mainFrame

	vm := &VM{
		constants:    bytecode.Constants,
		stack:       make([]object.Object, StackSize),
		sp:          0,
		globals:     make([]object.Object, GlobalsSize),
		frames:      frames,
		framesIndex: 1,
		builtins:    make(map[uint16]*object.Builtin),
	}

	// Builtin functions will be registered by the compiler
	return vm
}

func (vm *VM) RegisterBuiltin(index uint16, builtin *object.Builtin) {
	vm.builtins[index] = builtin
}

func (vm *VM) GetBuiltin(index uint16) (*object.Builtin, bool) {
	builtin, ok := vm.builtins[index]
	return builtin, ok
}

func (vm *VM) currentFrame() *Frame {
	return vm.frames[vm.framesIndex-1]
}

func (vm *VM) pushFrame(f *Frame) {
	vm.frames[vm.framesIndex] = f
	vm.framesIndex++
}

func (vm *VM) popFrame() *Frame {
	vm.framesIndex--
	return vm.frames[vm.framesIndex]
}

// LastPoppedStackElement returns last popped element on the top of the stack. We do not explicitly
// set them to nil or remove them when calling pop so it will point to last popped.
func (vm *VM) LastPoppedStackElement() object.Object {
	return vm.stack[vm.sp]
}

// Run runs our VM and starts the fetch-decode-execute cycle
func (vm *VM) Run() error {
	var ip int
	var ins code.Instructions
	var op code.Opcode

	for vm.currentFrame().ip < len(vm.currentFrame().Instructions())-1 {
		vm.currentFrame().ip++

		ip = vm.currentFrame().ip
		ins = vm.currentFrame().Instructions()
		op = code.Opcode(ins[ip])

		switch op {
		case code.OpConstant:
			constIndex := code.ReadUint16(ins[ip+1:])
			vm.currentFrame().ip += 2
			err := vm.push(vm.constants[constIndex])
			if err != nil {
				return err
			}

		case code.OpPop:
			vm.pop()

		case code.OpAdd, code.OpSub, code.OpMul, code.OpDiv, code.OpMod:
			err := vm.executeBinaryOperation(op)
			if err != nil {
				return err
			}

		case code.OpTrue:
			err := vm.push(True)
			if err != nil {
				return err
			}

		case code.OpFalse:
			err := vm.push(False)
			if err != nil {
				return err
			}

		case code.OpEqualEqual, code.OpNotEqual, code.OpGreater, code.OpGreaterEqual:
			err := vm.executeComparison(op)
			if err != nil {
				return err
			}

		case code.OpAnd, code.OpOr:
			err := vm.executeLogicalOperator(op)
			if err != nil {
				return err
			}

		case code.OpBang:
			err := vm.executeBangOperator()
			if err != nil {
				return err
			}

		case code.OpMinus:
			err := vm.executeMinusOperator()
			if err != nil {
				return err
			}

		case code.OpPlusPlus, code.OpMinusMinus:
			err := vm.executePostfixOperator(op, ins, ip)
			if err != nil {
				return err
			}

		case code.OpJump:
			pos := int(code.ReadUint16(ins[ip+1:]))
			vm.currentFrame().ip = pos - 1

		case code.OpJumpNotTruthy:
			pos := int(code.ReadUint16(ins[ip+1:]))
			vm.currentFrame().ip += 2
			condition := vm.pop()
			if !isTruthy(condition) {
				vm.currentFrame().ip = pos - 1
			}

		case code.OpNull:
			err := vm.push(Null)
			if err != nil {
				return err
			}

		case code.OpSetGlobal:
			globalIndex := code.ReadUint16(ins[ip+1:])
			vm.currentFrame().ip += 2
			vm.globals[globalIndex] = vm.pop()

		case code.OpGetGlobal:
			globalIndex := code.ReadUint16(ins[ip+1:])
			vm.currentFrame().ip += 2

			err := vm.push(vm.globals[globalIndex])
			if err != nil {
				return err
			}

		case code.OpArray:
			numElements := int(code.ReadUint16(ins[ip+1:]))
			vm.currentFrame().ip += 2

			array := vm.buildArray(vm.sp-numElements, vm.sp)
			vm.sp = vm.sp - numElements

			err := vm.push(array)
			if err != nil {
				return err
			}

		case code.OpHash:
			numElements := int(code.ReadUint16(ins[ip+1:]))
			vm.currentFrame().ip += 2

			hash, err := vm.buildHash(vm.sp-numElements, vm.sp)
			if err != nil {
				return err
			}
			vm.sp = vm.sp - numElements

			err = vm.push(hash)
			if err != nil {
				return err
			}

		case code.OpIndex:
			index := vm.pop()
			left := vm.pop()

			err := vm.executeIndexExpr(left, index)
			if err != nil {
				return err
			}

		case code.OpCall:
			numArgs := code.ReadUint8(ins[ip+1:])
			vm.currentFrame().ip++

			err := vm.executeCall(int(numArgs))
			if err != nil {
				return err
			}

		case code.OpReturnValue:
			returnValue := vm.pop()

			frame := vm.popFrame()
			vm.sp = frame.basePointer - 1

			err := vm.push(returnValue)
			if err != nil {
				return err
			}

		case code.OpReturn:
			frame := vm.popFrame()
			vm.sp = frame.basePointer - 1

			err := vm.push(Null)
			if err != nil {
				return err
			}

		case code.OpSetLocal:
			localIndex := code.ReadUint8(ins[ip+1:])
			vm.currentFrame().ip++

			frame := vm.currentFrame()

			vm.stack[frame.basePointer+int(localIndex)] = vm.pop()

		case code.OpGetLocal:
			localIndex := code.ReadUint8(ins[ip+1:])
			vm.currentFrame().ip++

			frame := vm.currentFrame()

			err := vm.push(vm.stack[frame.basePointer+int(localIndex)])
			if err != nil {
				return err
			}

		case code.OpGetBuiltin:
			builtinIndex := code.ReadUint8(ins[ip+1:])
			vm.currentFrame().ip += 1
			
			builtin, ok := vm.builtins[uint16(builtinIndex)]
			if !ok {
				return fmt.Errorf("not a builtin function at index %d", builtinIndex)
			}
			
			err := vm.push(builtin)
			if err != nil {
				return err
			}

		case code.OpClosure:
			constIndex := code.ReadUint16(ins[ip+1:])
			numFree := code.ReadUint8(ins[ip+3:])
			vm.currentFrame().ip += 3

			err := vm.pushClosure(int(constIndex), int(numFree))
			if err != nil {
				return err
			}

		case code.OpGetFree:
			freeIndex := code.ReadUint8(ins[ip+1:])
			vm.currentFrame().ip++

			currentClosure := vm.stack[vm.sp-1].(*object.Closure)
			err := vm.push(currentClosure.Free[freeIndex])
			if err != nil {
				return err
			}

		case code.OpCurrentClosure:
			currentClosure := vm.stack[vm.sp-1].(*object.Closure)
			err := vm.push(currentClosure)
			if err != nil {
				return err
			}

		case code.OpCallBuiltin:
			numArgs := code.ReadUint8(ins[ip+1:])
			vm.currentFrame().ip++
			
			// Stack'in tepesindeki builtin fonksiyonu al
			builtin, ok := vm.stack[vm.sp-1-int(numArgs)].(*object.Builtin)
			if !ok {
				return fmt.Errorf("not a builtin function")
			}
			
			err := vm.executeBuiltin(builtin, int(numArgs))
			if err != nil {
				return err
			}
		}
	}

	return nil
}

func (vm *VM) push(obj object.Object) error {
	if vm.sp >= StackSize {
		return fmt.Errorf("stack overflow")
	}

	vm.stack[vm.sp] = obj
	vm.sp++

	return nil
}

func (vm *VM) pop() object.Object {
	obj := vm.stack[vm.sp-1]
	vm.sp--
	return obj
}

func (vm *VM) executeBinaryOperation(op code.Opcode) error {
	right := vm.pop()
	left := vm.pop()

	leftType := left.Type()
	rightType := right.Type()

	switch {
	case leftType == object.INTEGER_OBJ && rightType == object.INTEGER_OBJ:
		return vm.executeBinaryIntegerOperation(op, left, right)
	case leftType == object.STRING_OBJ && rightType == object.STRING_OBJ:
		return vm.executeBinaryStringOperation(op, left, right)
	}

	return fmt.Errorf("Unsupported types for binary operation: %s %s", leftType, rightType)
}

func (vm *VM) executeBinaryIntegerOperation(op code.Opcode, left, right object.Object) error {
	leftValue := left.(*object.Integer).Value
	rightValue := right.(*object.Integer).Value

	var result int64

	switch op {
	case code.OpAdd:
		result = leftValue + rightValue
	case code.OpSub:
		result = leftValue - rightValue
	case code.OpMul:
		result = leftValue * rightValue
	case code.OpDiv:
		result = leftValue / rightValue
	case code.OpMod:
		result = leftValue % rightValue
	default:
		return fmt.Errorf("Unknown integer operator: %d", op)
	}

	return vm.push(&object.Integer{Value: result})
}

func (vm *VM) executeBinaryStringOperation(op code.Opcode, left, right object.Object) error {
	if op != code.OpAdd {
		return fmt.Errorf("Unknown String operator %d", op)
	}

	leftValue := left.(*object.String).Value
	rightValue := right.(*object.String).Value

	return vm.push(&object.String{Value: leftValue + rightValue})
}

func (vm *VM) executeComparison(op code.Opcode) error {
	right := vm.pop()
	left := vm.pop()

	if left.Type() == object.INTEGER_OBJ || right.Type() == object.INTEGER_OBJ {
		return vm.executeIntegerComparison(op, left, right)
	}

	switch op {
	case code.OpEqualEqual:
		if right.Type() == object.STRING_OBJ && left.Type() == object.STRING_OBJ {
			return vm.push(nativeBoolToBooleanObj(right.Inspect() == left.Inspect()))
		}
		return vm.push(nativeBoolToBooleanObj(right == left))
	case code.OpNotEqual:
		if right.Type() == object.STRING_OBJ && left.Type() == object.STRING_OBJ {
			return vm.push(nativeBoolToBooleanObj(right.Inspect() != left.Inspect()))
		}
		return vm.push(nativeBoolToBooleanObj(right != left))
	default:
		return fmt.Errorf("Unknown operator: %d (%s %s)", op, left.Type(), right.Type())
	}
}

func (vm *VM) executeLogicalOperator(op code.Opcode) error {
	right := vm.pop()
	left := vm.pop()

	var result bool

	if op == code.OpAnd {
		result = coerceObjToNativeBool(left) && coerceObjToNativeBool(right)
	} else if op == code.OpOr {
		result = coerceObjToNativeBool(left) || coerceObjToNativeBool(right)
	}

	return vm.push(nativeBoolToBooleanObj(result))
}

func (vm *VM) executeIntegerComparison(op code.Opcode, left, right object.Object) error {
	leftValue := left.(*object.Integer).Value
	rightValue := right.(*object.Integer).Value

	switch op {
	case code.OpEqualEqual:
		return vm.push(nativeBoolToBooleanObj(rightValue == leftValue))
	case code.OpNotEqual:
		return vm.push(nativeBoolToBooleanObj(rightValue != leftValue))
	case code.OpGreater:
		return vm.push(nativeBoolToBooleanObj(leftValue > rightValue))
	case code.OpGreaterEqual:
		return vm.push(nativeBoolToBooleanObj(leftValue >= rightValue))
	default:
		return fmt.Errorf("Unknown operator: %d", op)
	}
}

func nativeBoolToBooleanObj(input bool) *object.Boolean {
	if input {
		return True
	}
	return False
}

// Coerce our different object types to booleans for truthy/falsey values
func coerceObjToNativeBool(o object.Object) bool {
	if rv, ok := o.(*object.ReturnValue); ok {
		o = rv.Value
	}

	switch obj := o.(type) {
	case *object.Boolean:

		return obj.Value
	case *object.String:
		return obj.Value != ""
	case *object.Null:
		return false
	case *object.Integer:
		return obj.Value != 0
	case *object.Array:
		return len(obj.Elements) > 0
	case *object.Hash:
		return len(obj.Pairs) > 0
	default:
		return true
	}
}

func (vm *VM) executeBangOperator() error {
	operand := vm.pop()

	switch operand {
	case True:
		return vm.push(False)
	case False:
		return vm.push(True)
	case Null:
		return vm.push(True)
	default:
		return vm.push(False)
	}
}

func (vm *VM) executeMinusOperator() error {
	operand := vm.pop()

	if operand.Type() != object.INTEGER_OBJ {
		return fmt.Errorf("Unsupported type for negation: %s", operand.Type())
	}

	value := operand.(*object.Integer).Value

	return vm.push(&object.Integer{Value: -value})
}

func (vm *VM) executePostfixOperator(op code.Opcode, ins code.Instructions, ip int) error {
	// Get the operand, must be an integer identifier
	operand := vm.pop()
	if operand.Type() != object.INTEGER_OBJ {
		return fmt.Errorf("Invalid left-hand side expression in postfix operation: %s", operand.Type())
	}

	// Increment or decrement the operand based on opcode
	if op == code.OpPlusPlus {
		operand.(*object.Integer).Value++
	} else {
		operand.(*object.Integer).Value--
	}

	// Based on whether the operand was a global or local binding, set the new
	// updated operand appropriately
	if code.Opcode(ins[ip-3]) == code.OpGetGlobal {
		globalIndex := code.ReadUint16(ins[ip-5:])
		vm.currentFrame().ip++
		vm.globals[globalIndex] = operand
	} else if code.Opcode(ins[ip-2]) == code.OpGetLocal {
		localIndex := code.ReadUint8(ins[ip-3:])
		vm.currentFrame().ip++
		frame := vm.currentFrame()
		vm.stack[frame.basePointer+int(localIndex)] = operand
	}

	return nil
}

func (vm *VM) buildArray(startIndex, endIndex int) object.Object {
	elements := make([]object.Object, endIndex-startIndex)

	for i := startIndex; i < endIndex; i++ {
		elements[i-startIndex] = vm.stack[i]
	}

	return &object.Array{Elements: elements}
}

func (vm *VM) buildHash(startIndex, endIndex int) (object.Object, error) {
	hashedPairs := make(map[object.HashKey]object.HashPair)

	for i := startIndex; i < endIndex; i += 2 {
		key := vm.stack[i]
		value := vm.stack[i+1]

		pair := object.HashPair{
			Key:   key,
			Value: value,
		}

		hashKey, ok := key.(object.Hashable)
		if !ok {
			return nil, fmt.Errorf("Unusable as a hash key: %s", key.Type())
		}

		hashedPairs[hashKey.HashKey()] = pair
	}

	return &object.Hash{Pairs: hashedPairs}, nil
}

func (vm *VM) executeIndexExpr(left, index object.Object) error {
	switch {
	case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ:
		return vm.executeArrayIndex(left, index)
	case left.Type() == object.HASH_OBJ:
		return vm.executeHashIndex(left, index)
	default:
		return fmt.Errorf("Index operator not supported: %s", left.Type())
	}
}

func (vm *VM) executeArrayIndex(array, index object.Object) error {
	arrayObject := array.(*object.Array)
	i := index.(*object.Integer).Value
	max := int64(len(arrayObject.Elements) - 1)

	if i < 0 || i > max {
		return vm.push(Null)
	}

	return vm.push(arrayObject.Elements[i])
}

func (vm *VM) executeHashIndex(hash, index object.Object) error {
	hashObject := hash.(*object.Hash)

	key, ok := index.(object.Hashable)
	if !ok {
		return fmt.Errorf("Unusable as a hash key: %s", index.Type())
	}

	pair, ok := hashObject.Pairs[key.HashKey()]
	if !ok {
		return vm.push(Null)
	}

	return vm.push(pair.Value)
}

func (vm *VM) executeCall(numArgs int) error {
	callee := vm.stack[vm.sp-1-numArgs]
	switch callee := callee.(type) {
	case *object.Closure:
		return vm.callClosure(callee, numArgs)
	case *object.CompiledFunction:
		return vm.callFunction(callee, numArgs)
	default:
		return fmt.Errorf("calling non-function: %s", callee.Type())
	}
}

func (vm *VM) callFunction(fn *object.CompiledFunction, numArgs int) error {
	frame := NewFrame(fn.Instructions, vm.sp-numArgs)
	vm.pushFrame(frame)
	vm.sp = frame.basePointer + fn.NumLocals
	return nil
}

func (vm *VM) callClosure(cl *object.Closure, numArgs int) error {
	frame := NewFrame(cl.Fn.Instructions, vm.sp-numArgs)
	vm.pushFrame(frame)
	vm.sp = frame.basePointer + cl.Fn.NumLocals
	return nil
}

func (vm *VM) pushClosure(constIndex int, numFree int) error {
	constant := vm.constants[constIndex]
	function, ok := constant.(*object.CompiledFunction)
	if !ok {
		return fmt.Errorf("not a function: %+v", constant)
	}

	free := make([]object.Object, numFree)
	for freeIndex := 0; freeIndex < numFree; freeIndex++ {
		free[freeIndex] = vm.stack[vm.sp-numFree+freeIndex]
	}
	vm.sp = vm.sp - numFree

	closure := &object.Closure{Fn: function, Free: free}
	return vm.push(closure)
}

func (vm *VM) executeBuiltin(builtin *object.Builtin, numArgs int) error {
	args := make([]object.Object, numArgs)
	start := vm.sp - numArgs
	
	copy(args, vm.stack[start:vm.sp])
	
	result := builtin.Fn(args...)
	vm.sp = start
	
	if result != nil {
		return vm.push(result)
	}
	return vm.push(Null)
}

func isTruthy(obj object.Object) bool {
	switch obj := obj.(type) {
	case *object.Boolean:
		return obj.Value
	case *object.Null:
		return false
	default:
		return true
	}
}
