package main

import (
	"git.jfdev.de/JonasFranzDEV/hal/hal"
	"git.jfdev.de/JonasFranzDEV/hal/parser"
	"github.com/stretchr/testify/assert"
	"math"
	"testing"
)

func TestAddition(t *testing.T) {
	input := []string{
		"1 START",
		"2 IN 0",
		"3 STORE 10",
		"4 IN 0",
		"5 ADD 10",
		"6 OUT 1",
		"7 STOP",
	}

	program, err := parser.ParseProgram(input)
	assert.NoError(t, err)

	module, err := hal.NewHALModule(program, 256, 2, false)
	assert.NoError(t, err)

	module.IO[0] = hal.NewMockIO(10, 10)
	outputMock := hal.NewMockIO()
	module.IO[1] = outputMock

	err = module.Run()
	assert.NoError(t, err)

	assert.Equal(t, float64(20), outputMock.Outputs[0])
}

func TestMax(t *testing.T) {
	input := []string{
		"00 IN 0",
		"01 STORE 1",
		"02 IN 0",
		"03 STORE 2",
		"04 SUB 1",
		"05 JUMPPOS 9",
		"06 LOAD 1",
		"07 OUT 1",
		"08 STOP",
		"09 LOAD 2",
		"10 OUT 1",
		"11 STOP",
	}

	program, err := parser.ParseProgram(input)
	assert.NoError(t, err)

	module, err := hal.NewHALModule(program, 256, 2, false)
	assert.NoError(t, err)

	module.IO[0] = hal.NewMockIO(10, 15)
	outputMock := hal.NewMockIO()
	module.IO[1] = outputMock

	err = module.Run()
	assert.NoError(t, err)

	assert.Len(t, outputMock.Outputs, 1)
	assert.Equal(t, float64(15), outputMock.Outputs[0])
}

func TestNewton1(t *testing.T) {
	input := []string{
		"01 START",
		"02 LOADNUM 1",
		"03 STORE 10", // Every x is saved to reg 10

		// f(x)
		"10 MUL 10",   // x²
		"11 MUL 10",   // x³
		"12 STORE 12", // Saving x³ for later
		"13 MUL 10",   // x⁴
		"14 MUL 10",   // x⁵
		"15 STORE 11", // Save x⁵ in reg 11
		"21 LOAD 12",  // Load x³
		"22 MULNUM 5", // x³ * 5
		"30 ADD 11",   // (x³ * 5) + x⁵
		"31 SUBNUM 5", // ((x³ * 5) + x⁵) - 5
		"32 STORE 20", // Store the result of f(x) in 20

		// Exit condition
		"33 SUB 50",      // reg 50 contains the last result which we compare with the last one
		"34 JUMPNULL 66", // If both are equal, we go to line 66
		"35 LOAD 20",     // Otherwise save the last result of f(x) as last result
		"36 STORE 50",    // and continue

		// f'(x)
		"40 LOAD 10",   // Load x
		"41 MUL 10",    // x²
		"42 STORE 21",  // Save x² for later
		"43 MUL 10",    // x³
		"44 MUL 10",    // x⁴
		"45 MULNUM 5",  // x⁴ * 5
		"46 STORE 22",  // Save 5x⁴ to reg 22
		"47 LOAD 21",   // load x² from earlier
		"48 MULNUM 15", // x² * 15
		"49 ADD 22",    // 15x² + 5x⁴ from earlier
		"50 STORE 30",  // Save the result of f'(x) to reg 30

		// x - (f(x) / f'(x))
		"60 LOAD 20", // f(x) / f'(x)
		"61 DIV 30",
		"62 STORE 40", // Store the result of it in reg 40
		"63 LOAD 10",  // Load the x again
		"64 SUB 40",   // x - (f(x) / f'(x))
		"65 JUMP 03",  // Start at the beginning

		// Load the result and print it
		"66 LOAD 10",
		"67 OUT 0",
		"68 STOP",
	}

	program, err := parser.ParseProgram(input)
	assert.NoError(t, err)

	module, err := hal.NewHALModule(program, 256, 1, false)
	assert.NoError(t, err)

	outputMock := hal.NewMockIO()
	module.IO[0] = outputMock

	err = module.Run()
	assert.NoError(t, err)

	calculateNewton := func(start float64) float64 {
		f1 := func(x float64) float64 {
			return math.Pow(x, 5) + 5*math.Pow(x, 3) - 5
		}

		f1derived := func(x float64) float64 {
			return 5*math.Pow(x, 4) + 15*math.Pow(x, 2)
		}

		var last float64
		x := start
		for x != last {
			last = x
			x = x - f1(x)/f1derived(x)
		}
		return x
	}

	assert.Equal(t, calculateNewton(1), outputMock.Outputs[0])
}

func TestNewton2(t *testing.T) {
	input := []string{
		"01 START",
		"02 LOADNUM 1",
		"03 STORE 10", // Every x is saved to reg 10

		// f(x)
		"10 MUL 10",   // x²
		"11 STORE 11", // Save x² to reg 11 for later use since we need that in the second part of the function
		"12 MUL 10",   // x³
		"13 MUL 10",   // x⁴
		"14 MUL 10",   // x⁵
		"15 STORE 20", // This is x⁵ which we save here to save a few instructions
		"16 MUL 10",   // x⁶
		"17 MULNUM 8", // x⁶ * 8
		"18 STORE 12", // Reg 12 now contains the result of the first part of f(x)
		"19 LOAD 11",  // Put x² from earlier back in the accumulator
		"20 MULNUM 3", // x² * 3
		"21 ADD 12",   // Add the first and second part of the function together, basically 8x⁶ + 3x²
		"22 SUBNUM 3", // (8x⁶ + 3x²) - 3
		"23 STORE 15", // Result in reg 15

		// f'(x)
		"30 LOAD 20",   // x⁵ which we saved earlier while calculating x⁶
		"31 MULNUM 48", // x⁵ * 48
		"32 STORE 21",  // Store the result in 21
		"33 LOAD 10",   // Load our x
		"34 MULNUM 6",  // x * 6
		"35 ADD 21",    // (x * 6) + 48x⁵
		"36 STORE 25",  // Store the result, which is the total result of f'(x) in reg 25

		// x - f(x) / f'(x)
		"40 LOAD 15",   // load f(x)
		"41 DIV 25",    // f(x) / f'(x)
		"42 MULNUM -1", // Invert the result
		"43 ADD 10",    // Add x: Because we inverted the result before, we can do this instead of SUB - lets us reuse the content of the accumulator

		"44 STORE 35", // Reg 35 now contains the end result of x - f(x) / f'(x)

		// This is our exit condition:
		// We compare the current result with the last calculated result, if both are equal, we exit
		"50 SUB 50",      // Reg 50 contains the last result
		"51 JUMPNULL 60", // If both are equal, we exit
		"52 LOAD 35",     // Otherwise load reg 15 again, store it as the current last result and continue
		"53 STORE 50",

		// Go to the beginning
		"54 JUMP 03",

		"60 LOAD 10", // Load the result
		"61 OUT 0",   // Print it
		"62 STOP",
	}

	program, err := parser.ParseProgram(input)
	assert.NoError(t, err)

	module, err := hal.NewHALModule(program, 256, 1, false)
	assert.NoError(t, err)

	outputMock := hal.NewMockIO()
	module.IO[0] = outputMock

	err = module.Run()
	assert.NoError(t, err)

	calculateNewton := func(start float64) float64 {
		f1 := func(x float64) float64 {
			return 8*math.Pow(x, 6) + 3*math.Pow(x, 2) - 3
		}

		f1derived := func(x float64) float64 {
			return 48*math.Pow(x, 5) + 6*x
		}

		var last float64
		x := start
		for x != last {
			last = x
			x = x - f1(x)/f1derived(x)
		}
		return x
	}

	assert.Equal(t, calculateNewton(1), outputMock.Outputs[0])
}