package main

import (
	"bufio"
	"errors"
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
)

func main() {
	input := stdinToStringSlice()
	pt2Idx := part1(input)
	part2(input[pt2Idx:])
}

var probs map[string]int

func part1(input []string) int {
	probs = make(map[string]int)
	var messySamples int
	var ret int
	for i := 0; i < len(input); i += 4 {
		if !strings.HasPrefix(input[i], "Before: ") {
			// We're done with the part 1 input
			ret = i
			break
		}
		aft := NewState()
		aft.loadState(input[i+2])
		if cmd, err := aft.parseCommand(input[i+1]); err == nil {
			origCd := cmd[0]
			var numMatches int
			for j := 0; j < 16; j++ {
				bef := NewState()
				bef.loadState(input[i])
				cmd[0] = j
				bef.run(cmd)
				if bef.registerString() == aft.registerString() {
					numMatches++
					key := fmt.Sprintf("%d:%d", origCd, j)
					probs[key]++
				}
			}
			if numMatches > 2 {
				messySamples++
			}
		}
	}
	fmt.Println("= Part 1 =")
	fmt.Println(messySamples)
	return ret + 2
}

var opMap [16]int

func part2(input []string) {
	for i := 0; i < 16; i++ {
		opMap[i] = -1
	}
	fmt.Println("= Part 2 =")
	m1, m2 := -1, -1
	for {
		m1, m2, probs = findOnlyChance(probs)
		if m1 == -1 {
			break
		}
		opMap[m1] = m2
	}

	vpc := NewState()
	for i := range input {
		if cmd, err := vpc.parseCommand(input[i]); err == nil {
			cmd[0] = opMap[cmd[0]]
			vpc.run(cmd)
		}
	}
	fmt.Println(vpc.reg[0])
}

func findOnlyChance(of map[string]int) (int, int, map[string]int) {
	m1, m2 := -1, -1
	for i := 0; i < 16; i++ {
		var num int
		for k := range of {
			codes := strings.Split(k, ":")
			if Atoi(codes[0]) == i {
				num++
				if num > 1 {
					m2 = -1
					break
				}
				m2 = Atoi(codes[1])
			}
		}
		if num == 1 {
			m1 = i
			break
		}
		if num > 1 {
			continue
		}
	}
	if m1 == -1 {
		return -1, -1, of
	}
	// If we're here, we found something
	// Remove other possibilities for m1: and :m2 and return
	for k := range of {
		a1 := strconv.Itoa(m1)
		a2 := strconv.Itoa(m2)
		if strings.HasPrefix(k, a1+":") ||
			strings.HasSuffix(k, ":"+a2) {
			delete(of, k)
		}
	}
	return m1, m2, of
}

type State struct {
	reg     [4]int
	OpCodes map[int]func(a, b, c int)
}

func NewState() *State {
	s := &State{}
	s.OpCodes = make(map[int]func(a, b, c int))
	s.OpCodes[0] = s.addr
	s.OpCodes[1] = s.addi
	s.OpCodes[2] = s.mulr
	s.OpCodes[3] = s.muli
	s.OpCodes[4] = s.banr
	s.OpCodes[5] = s.bani
	s.OpCodes[6] = s.borr
	s.OpCodes[7] = s.bori
	s.OpCodes[8] = s.setr
	s.OpCodes[9] = s.seti
	s.OpCodes[10] = s.gtir
	s.OpCodes[11] = s.gtri
	s.OpCodes[12] = s.gtrr
	s.OpCodes[13] = s.eqir
	s.OpCodes[14] = s.eqri
	s.OpCodes[15] = s.eqrr
	return s
}

func (s *State) registerString() string {
	return fmt.Sprint(s.reg)
}

func (s *State) loadState(inp string) {
	s.reg = [4]int{0, 0, 0, 0}
	inp = strings.Trim(inp, "ABeftor: []")
	pts := strings.Split(inp, ",")
	for i := range pts {
		s.reg[i] = Atoi(strings.TrimSpace(pts[i]))
	}
}

func (s *State) parseCommand(inp string) ([]int, error) {
	var ret []int
	pts := strings.Split(inp, " ")
	for i := range pts {
		ret = append(ret, Atoi(pts[i]))
	}
	if len(ret) != 4 {
		return ret, errors.New("Invalid Command: " + inp)
	}
	return ret, nil
}

func (s *State) run(cmd []int) error {
	if fn, ok := s.OpCodes[cmd[0]]; !ok {
		return errors.New(fmt.Sprintf("Invalid OpCode: %d", cmd[0]))
	} else {
		fn(cmd[1], cmd[2], cmd[3])
	}
	return nil
}

func (s *State) addr(r1, r2, r3 int) {
	s.reg[r3] = s.reg[r1] + s.reg[r2]
}

func (s *State) addi(r1, v2, r3 int) {
	s.reg[r3] = s.reg[r1] + v2
}

func (s *State) mulr(r1, r2, r3 int) {
	s.reg[r3] = s.reg[r1] * s.reg[r2]
}

func (s *State) muli(r1, v2, r3 int) {
	s.reg[r3] = s.reg[r1] * v2
}

func (s *State) banr(r1, r2, r3 int) {
	s.reg[r3] = s.reg[r1] & s.reg[r2]
}

func (s *State) bani(r1, v2, r3 int) {
	s.reg[r3] = s.reg[r1] & v2
}

func (s *State) borr(r1, r2, r3 int) {
	s.reg[r3] = s.reg[r1] | s.reg[r2]
}

func (s *State) bori(r1, v2, r3 int) {
	s.reg[r3] = s.reg[r1] | v2
}

func (s *State) setr(r1, r2, r3 int) {
	s.reg[r3] = s.reg[r1]
}

func (s *State) seti(v1, v2, r3 int) {
	s.reg[r3] = v1
}

func (s *State) gtir(v1, r2, r3 int) {
	s.reg[r3] = boolToInt(v1 > s.reg[r2])
}

func (s *State) gtri(r1, v2, r3 int) {
	s.reg[r3] = boolToInt(s.reg[r1] > v2)
}

func (s *State) gtrr(r1, r2, r3 int) {
	s.reg[r3] = boolToInt(s.reg[r1] > s.reg[r2])
}

func (s *State) eqir(v1, r2, r3 int) {
	s.reg[r3] = boolToInt(v1 == s.reg[r2])
}

func (s *State) eqri(r1, v2, r3 int) {
	s.reg[r3] = boolToInt(s.reg[r1] == v2)
}

func (s *State) eqrr(r1, r2, r3 int) {
	s.reg[r3] = boolToInt(s.reg[r1] == s.reg[r2])
}

func boolToInt(v bool) int {
	if v {
		return 1
	}
	return 0
}

func stdinToStringSlice() []string {
	var input []string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		input = append(input, scanner.Text())
	}
	return input
}

func Atoi(i string) int {
	var ret int
	var err error
	if ret, err = strconv.Atoi(i); err != nil {
		log.Fatal("Invalid Atoi: " + i)
	}
	return ret
}
