package main

import (
	"fmt"
	"log"
	"strconv"
	"strings"

	"github.com/alexchao26/advent-of-code-go/algos"
	"github.com/alexchao26/advent-of-code-go/util"
)

func main() {
	// read the input file, modify it to a slice of numbers
	inputFile := util.ReadFile("../input.txt")

	splitStrings := strings.Split(inputFile, ",")

	inputNumbers := make([]int, len(splitStrings))
	for i, v := range splitStrings {
		inputNumbers[i], _ = strconv.Atoi(v)
	}

	// Make perms via a util function
	perms := algos.PermuteIntSlice([]int{0, 1, 2, 3, 4})

	// iterate over all perms and run through a single pass of the Amps
	// if the final output (from Amp E) is higher, update the highestOutput variable
	highestOutput := 0
	for _, perm := range perms {
		// initialize 5 computers
		ampA := MakeComputer(inputNumbers, perm[0])
		ampB := MakeComputer(inputNumbers, perm[1])
		ampC := MakeComputer(inputNumbers, perm[2])
		ampD := MakeComputer(inputNumbers, perm[3])
		ampE := MakeComputer(inputNumbers, perm[4])

		// first input (besides phase setting) to Amp A is zero
		ampA.Step(0)
		ampB.Step(ampA.LastOutput)
		ampC.Step(ampB.LastOutput)
		ampD.Step(ampC.LastOutput)
		ampE.Step(ampD.LastOutput)

		if ampE.LastOutput > highestOutput {
			highestOutput = ampE.LastOutput
		}
	}

	// print highest output found
	fmt.Printf("Highest output is %v\n", highestOutput)
}

/*
Intcode is an OOP approach *************************************************
MakeComputer is equivalent to the constructor
Step takes in an input int and updates properties in the computer:
	- InstructionIndex: where to read the next instruction from
	- LastOutput, what the last opcode 4 outputted
	- PuzzleIndex based if the last instruction modified the puzzle at all
****************************************************************************/
type Intcode struct {
	PhaseSetting     int   // initial input: ID or number used to "prime"/setup the comp
	PuzzleInput      []int // file/puzzle input parsed into slice of ints
	InstructionIndex int   // stores the index where the next instruction is
	LastOutput       int   // last output from an opcode 4
}

// MakeComputer initializes a new comp
func MakeComputer(PuzzleInput []int, PhaseSetting int) Intcode {
	puzzleInputCopy := make([]int, len(PuzzleInput))
	copy(puzzleInputCopy, PuzzleInput)

	comp := Intcode{
		PhaseSetting,
		puzzleInputCopy,
		0,
		0,
	}

	// Prime the computer by running its initial phase setting through it
	// This will update the comp's InstructionIndex so it's pointing to the next command
	// will also update the PuzzleInput itself via opcode 3's insert
	// AND will run the computer until it asks for the next input, _comp is now primed_
	comp.Step(PhaseSetting)
	return comp
}

// Step will read the next 4 values in the input `sli` and make updates
// according to the opcodes
func (comp *Intcode) Step(input int) int {
	// read the instruction, opcode and the indexes where the params point to
	opcode, paramIndexes := comp.GetOpCodeAndIndexes()

	switch opcode {
	case 99: // 99: Terminates program
		// fmt.Println("Terminating...")
		return input
	case 1: // 1: Add next two paramIndexes, store in third
		comp.PuzzleInput[paramIndexes[2]] = comp.PuzzleInput[paramIndexes[0]] + comp.PuzzleInput[paramIndexes[1]]
		comp.InstructionIndex += 4
		return comp.Step(input)
	case 2: // 2: Multiply next two and store in third
		comp.PuzzleInput[paramIndexes[2]] = comp.PuzzleInput[paramIndexes[0]] * comp.PuzzleInput[paramIndexes[1]]
		comp.InstructionIndex += 4
		return comp.Step(input)
	case 3: // 3: Takes one input and saves it to position of one parameter
		// check if input has already been used (i.e. input == -1)
		// if it's been used, return the LastOutput
		// NOTE: making a big assumption that -1 will never be an input...
		if input == -1 {
			return comp.LastOutput
		}

		// otherwise use the input, then recurse with a -1 to signal the initial input has been used
		comp.PuzzleInput[paramIndexes[0]] = input
		comp.InstructionIndex += 2
		return comp.Step(-1)
	case 4: // 4: outputs its input value
		// set LastOutput of the computer & log it
		comp.LastOutput = comp.PuzzleInput[paramIndexes[0]]
		// fmt.Printf("Opcode 4 output: %v\n", comp.LastOutput)
		comp.InstructionIndex += 2

		// continue running until terminates or asks for another input
		return comp.Step(input)
	// 5: jump-if-true: if first param != 0, move pointer to second param, else nothing
	case 5:
		if comp.PuzzleInput[paramIndexes[0]] != 0 {
			comp.InstructionIndex = comp.PuzzleInput[paramIndexes[1]]
		} else {
			comp.InstructionIndex += 3
		}
		return comp.Step(input)
	// 6: jump-if-false, if first param == 0 then set instruction pointer to 2nd param, else nothing
	case 6:
		if comp.PuzzleInput[paramIndexes[0]] == 0 {
			comp.InstructionIndex = comp.PuzzleInput[paramIndexes[1]]
		} else {
			comp.InstructionIndex += 3
		}
		return comp.Step(input)
	// 7: less-than, if param1 < param2 then store 1 in postion of 3rd param, else store 0
	case 7:
		if comp.PuzzleInput[paramIndexes[0]] < comp.PuzzleInput[paramIndexes[1]] {
			comp.PuzzleInput[paramIndexes[2]] = 1
		} else {
			comp.PuzzleInput[paramIndexes[2]] = 0
		}
		comp.InstructionIndex += 4
		return comp.Step(input)
	// 8: equals, if param1 == param2 then set position of 3rd param to 1, else store 0
	case 8:
		if comp.PuzzleInput[paramIndexes[0]] == comp.PuzzleInput[paramIndexes[1]] {
			comp.PuzzleInput[paramIndexes[2]] = 1
		} else {
			comp.PuzzleInput[paramIndexes[2]] = 0
		}
		comp.InstructionIndex += 4
		return comp.Step(input)
	default:
		log.Fatal("Error: unknown opcode: ", opcode)
	}
	// this should never be called b/c switch statement will always return
	return -1
}

/*
GetOpCodeAndIndexes will parse the instruction at comp.PuzzleInput[comp.InstructionIndex]
- opcode will be the left two digits, mod by 100 will get that
- rest of instructions will be grabbed via mod 10
	- these also have to be parsed for the
*/
func (comp *Intcode) GetOpCodeAndIndexes() (int, [3]int) {
	instruction := comp.PuzzleInput[comp.InstructionIndex]

	// opcode is the lowest two digits, so mod by 100
	opcode := instruction % 100
	instruction /= 100

	// assign the indexes that need to be read by reading the parameter modes
	var paramIndexes [3]int
	for i := 1; i <= 3 && comp.InstructionIndex+i < len(comp.PuzzleInput); i++ {
		// grab the mode with a mod, last digit
		mode := instruction % 10
		instruction /= 10

		switch mode {
		case 1: // immediate mode, the index itself
			paramIndexes[i-1] = comp.InstructionIndex + i
		case 0: // position mode, index will be the value at the index
			paramIndexes[i-1] = comp.PuzzleInput[comp.InstructionIndex+i]
		}
	}

	return opcode, paramIndexes
}
