package elm
import (
	"fmt"
	"errors"
)

const STACK_SIZE = 1024

type Operation int64

const (
	HALT Operation = 0
	PUSH Operation = 1
	POP Operation = 2
	DUP Operation = 3
	ADD Operation = 4
	SUB Operation = 5
	MUL Operation = 6
	DIV Operation = 7
	EQ Operation = 8
	JMP Operation = 9
	JMPIF Operation = 10
	CALL Operation = 11
	RET Operation = 12
	STORE Operation = 13
	GET Operation = 14
)

type Inst struct {
	Operation Operation
	Operand int64
}

type Frame struct {
	Locals map[int]ElmValue
	ReturnAddress int
}

func CreateFrame(returnAddress int64) *Frame {
	f := Frame{}
	f.Locals = make(map[int]ElmValue)
	f.ReturnAddress = int(returnAddress)

	return &f
}

func (f *Frame) StoreVariable(address int, value ElmValue) {
	f.Locals[address] = value
}

func (f *Frame) GetVariable(address int) ElmValue {
	return f.Locals[address]
}
 
type Machine struct {
	program []Inst
	stack []ElmValue
	stackFrame []*Frame
	ip int64
	sp int64
	isHalted bool;
}

func Constructor() *Machine {
	m := Machine{}
	m.program = make([]Inst, 0)
	m.stack = make([]ElmValue, STACK_SIZE)
	m.stackFrame = make([]*Frame, 0)
	m.stackFrame = append(m.stackFrame, CreateFrame(0))
	m.ip = 0
	m.sp = 0
	m.isHalted = false

	return &m
}

func (m* Machine) Push(i Inst) error {
	m.program = append(m.program, i)
	return nil
}

func add[T INumber] (a T, b T) T {return a + b}
func sub[T INumber] (a T, b T) T {return b - a}
func div[T INumber] (a T, b T) T {return b / a}
func mul[T INumber] (a T, b T) T {return a * b}


func (m* Machine) Execute() error {
	instr := m.program[m.ip]

	binaryOpNumber := func(fn func (int64, int64) int64) {
		op1 := m.stack[m.sp - 1].Number
		op2 := m.stack[m.sp - 2].Number
		m.stack[m.sp - 2] = Number(fn(op1, op2))
		m.sp--
	}

	switch op := instr.Operation; op {
	case HALT:
		m.isHalted = true
	case PUSH:
		if (m.sp >= STACK_SIZE) {
            return errors.New("Stack Overflow");
        }

		m.stack[m.sp] = Number(instr.Operand)
		m.sp++
		m.ip++
	case POP:
		if (m.sp <= 0) {
			return errors.New("Empty Stack")
		}

		m.stack[m.sp - 1] = Number(0)
		m.sp--
		m.ip++
	case DUP:
		if (m.sp - instr.Operand <= 0) {
			return errors.New("Stack Underflow")
		}

		if (m.sp >= STACK_SIZE) {
            return errors.New("Stack Overflow");
        }

		if (instr.Operand < 0) {
			return errors.New("Illegal access")
		}

		m.stack[m.sp] = m.stack[m.sp - 1 - instr.Operand]
		m.sp++
		m.ip++
	case ADD:
		if (m.sp < 2) {
			return errors.New("Stack size is less than required to execute binary operation")
		}
		
		binaryOpNumber(add[int64])
		m.ip++
	case SUB:
		if (m.sp < 2) {
			return errors.New("Stack size is less than required to execute binary operation")
		}

		binaryOpNumber(sub[int64])
		m.ip++
	case MUL:
		if (m.sp < 2) {
			return errors.New("Stack size is less than required to execute binary operation")
		}

		binaryOpNumber(mul[int64])
		m.ip++
	case DIV:
		if (m.sp < 2) {
			return errors.New("Stack size is less than required to execute binary operation")
		}

		if (m.stack[m.sp - 2].Number == 0 || m.stack[m.sp - 1].Number == 0) {
			return errors.New("Divide by zero exception")
		}

		binaryOpNumber(div[int64])
		m.ip++
	case EQ:
		if (m.sp < 2) {
			return errors.New("Stack size is less than required to execute binary operation")
		}

		eq := m.stack[m.sp - 1] == m.stack[m.sp - 2]
		if (eq) {
			m.stack[m.sp - 2] = Number(1)
		} else {
			m.stack[m.sp - 2] = Number(0)
		}

		m.sp--
		m.ip++
	case JMP:
		if (int64(len(m.program) - 1) < instr.Operand || instr.Operand < 0) {
			return errors.New("Illegal access")
		}

		m.ip = instr.Operand
	case JMPIF:
		if (m.sp < 1) {
			return errors.New("Stack Underflow")
		}

		if (int64(len(m.program) - 1) < instr.Operand || instr.Operand < 0) {
			return errors.New("Illegal access")
		}
		
		if (m.stack[m.sp - 1].Number >= 1) {
			m.sp--
			m.ip = instr.Operand
		} else {
			m.ip++
		}
	case CALL:
		if (int64(len(m.program) - 1) < instr.Operand || instr.Operand < 0) {
			return errors.New("Illegal access")
		}

		m.stackFrame = append(m.stackFrame, CreateFrame(m.ip + 1))
		m.ip = instr.Operand
	case RET:
		if len(m.stackFrame) < 2 {
			return errors.New("Stackframe underflow")
		}

		currentFrame := m.stackFrame[len(m.stackFrame) - 1]
		m.stackFrame = m.stackFrame[:len(m.stackFrame) - 1]
		m.ip = int64(currentFrame.ReturnAddress)
	case STORE:
		if (m.sp < 1) {
			return errors.New("Nothing to store on the stack")
		}

		if (instr.Operand < 0) {
			return errors.New("Incorrect variable address")
		}

		currentFrame := m.stackFrame[len(m.stackFrame) - 1]
		currentFrame.StoreVariable(int(instr.Operand), m.stack[m.sp - 1])
		m.sp--
		m.ip++
	case GET:
		if (instr.Operand < 0) {
			return errors.New("Incorrect variable address")
		}

		currentFrame := m.stackFrame[len(m.stackFrame) - 1]
		m.stack[m.sp] = currentFrame.GetVariable(int(instr.Operand))
		m.sp++
		m.ip++
	}
	return nil
}

func (m* Machine) Run() error {
	for !m.isHalted {
		err := m.Execute()
		if err != nil {
			return err
		}
	}
	return nil
}

func (m* Machine) Print() {
	fmt.Println("Stack:");
	for i := 0; int64(i) < m.sp; i++ {
		fmt.Println(m.stack[i])
	}
}

/*
func (m* Machine) DumpProgramBinary(filename string) error {
	f, err := os.Create(filename)
    if err != nil {
        return errors.New("Couldn't open file")
    }
    defer f.Close()

	err = binary.Write(f, binary.LittleEndian, m.program)
    if err != nil {
		return err
        //return errors.New("Couldn't write to file")
    }

	return nil
}

func (m* Machine) LoadProgramFromBinary(filename string) error {
	f, err := os.Open(filename)
	if err != nil {
		return errors.New("Couldn't open file")
	}
	defer f.Close()

	program := make([]Inst, STACK_SIZE)
	err = binary.Read(f, binary.LittleEndian, &program)
	if err != nil {
		return err
	}

	m.program = program
	return nil
}
*/
/*
func main() {
	m := Constructor()
	
	// err := m.InterpretProgramFromFile("program.lil")
	// if err != nil {
	// 	fmt.Fprintf(os.Stderr, "[ERR] %s\n", err);
	// 	os.Exit(1)
	// }

	stackCounter := 0
	jumpCounter := 0
	for !m.isHalted && stackCounter < 100 {

		if (m.program[m.ip].Operation == JMPIF) {
			jumpCounter++
			fmt.Printf("Jump Counter: %d \n", jumpCounter)
		}
		
		err = m.Execute()
		//m.Print()
		if err != nil {
			fmt.Fprintf(os.Stderr, "[ERR] %s\n", err);
			os.Exit(1)
		}
		
		stackCounter++
	}
	m.Print()
}
*/