package main

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

func main() {
	doPart := 2
	inp := "vbqugkhl" // My puzzle input
	if len(os.Args) > 1 {
		if os.Args[1] == "-1" {
			doPart = 1
		} else {
			inp = strings.ToLower(os.Args[1])
		}
	}
	if doPart == 1 {
		part1(inp)
	} else {
		part2(inp)
	}
}

func part1(inp string) {
	var diskRows []string
	for i := 0; i < 128; i++ {
		diskRows = append(diskRows, KnotHash(fmt.Sprintf("%s-%d", inp, i)))
	}
	var usedSquares int
	for i := range diskRows {
		bin := GetBinaryString(diskRows[i])
		usedSquares += strings.Count(bin, "1")
	}
	fmt.Println(usedSquares, "used squares")
}

var diskGrid map[string]bool
var groups map[string]int

func part2(inp string) {
	diskGrid = make(map[string]bool)
	groups = make(map[string]int)

	fmt.Println("Building DiskGrid...")
	var grpCnt int
	for i := 0; i < 128; i++ {
		row := GetBinaryString(KnotHash(fmt.Sprintf("%s-%d", inp, i)))
		for j := range row {
			diskGrid[cs(i, j)] = (row[j] == '1')
			if row[j] == '1' {
				groups[cs(i, j)] = grpCnt
				grpCnt++
			}
		}
	}

	var iters int
	red := true
	for red {
		red = ReduceGroups()
		iters++
	}
	fmt.Println("Reduced", iters, "times")
	fmt.Println(GetGroupCount(), "regions")
}

func ReduceGroups() bool {
	var ret bool
	for x := 0; x < 128; x++ {
		for y := 0; y < 128; y++ {
			if oV, oOk := diskGrid[cs(x, y)]; oOk && oV {
				if dV, dOk := diskGrid[cs(x, y-1)]; dOk && dV && groups[cs(x, y-1)] != groups[cs(x, y)] {
					CombineBlockGroups(cs(x, y), cs(x, y-1))
					ret = true
				}
				if dV, dOk := diskGrid[cs(x-1, y)]; dOk && dV && groups[cs(x-1, y)] != groups[cs(x, y)] {
					CombineBlockGroups(cs(x, y), cs(x-1, y))
					ret = true
				}
				if dV, dOk := diskGrid[cs(x+1, y)]; dOk && dV && groups[cs(x+1, y)] != groups[cs(x, y)] {
					CombineBlockGroups(cs(x+1, y), cs(x, y))
					ret = true
				}
				if dV, dOk := diskGrid[cs(x, y+1)]; dOk && dV && groups[cs(x, y+1)] != groups[cs(x, y)] {
					CombineBlockGroups(cs(x, y+1), cs(x, y))
					ret = true
				}
			}
		}
	}
	return ret
}

func CombineBlockGroups(b1, b2 string) {
	if groups[b1] < groups[b2] {
		groups[b1] = groups[b2]
	} else {
		groups[b2] = groups[b1]
	}
}

func GetGroupCount() int {
	var gps []int
	for i := range groups {
		var have bool
		for j := range gps {
			if groups[i] == gps[j] {
				have = true
				break
			}
		}
		if !have {
			gps = append(gps, groups[i])
		}
	}
	return len(gps)
}

func FindGroup(x, y int) (int, error) {
	key := cs(x, y)
	if v, ok := groups[key]; ok {
		return v, nil
	}
	return -1, errors.New("Not in a group")
}

func PrintUsageChunk(stX, stY, endX, endY int, showGroups bool) {
	for x := stX; x < endX; x++ {
		for y := stY; y < endY; y++ {
			spot := "."
			if v, ok := groups[cs(x, y)]; ok {
				if showGroups {
					v = v % 16
					spot = fmt.Sprintf("%x", v)
				} else {
					spot = "#"
				}
			}
			fmt.Printf(spot)
		}
		fmt.Println("")
	}

}

// Get a map coordinate string for x, y
func cs(x, y int) string {
	return fmt.Sprint(x, "-", y)
}

// Get the x, y coordinate from a string
func sc(c string) (int, int) {
	pts := strings.Split(c, "-")
	return Atoi(pts[0]), Atoi(pts[1])
}

func KnotHash(inp string) string {
	var idx, skip int
	var list []int
	for i := 0; i < 256; i++ {
		list = append(list, i)
	}
	inpBts := []byte(inp)
	inpBts = append(inpBts, []byte{17, 31, 73, 47, 23}...)
	for j := 0; j < 64; j++ {
		for i := range inpBts {
			idx, skip, list = khRound(int(inpBts[i]), idx, skip, list)
		}
	}
	// Now calculate the dense hash
	var dense []byte
	for i := 0; i < len(list); i += 16 {
		dense = append(dense, xorList(list[i:i+16]))
	}
	return fmt.Sprintf("%x", dense)
}

func khRound(i, idx, skip int, list []int) (int, int, []int) {
	// if idx+i overflows, pull from the front
	var revList []int
	for j := idx; j < idx+i; j++ {
		revList = append([]int{list[j%256]}, revList...)
	}
	for j := 0; j < len(revList); j++ {
		list[(idx+j)%256] = revList[j]
	}

	idx += i + skip
	skip++
	return idx, skip, list
}

func xorList(inp []int) byte {
	var ret byte
	for i := range inp {
		ret ^= byte(inp[i])
	}
	return ret
}

func GetBinaryString(inp string) string {
	var bin string
	for i := range inp {
		var v int
		if inp[i] >= '0' && inp[i] <= '9' {
			v = int(inp[i] - '0')
		} else if inp[i] >= 'a' && inp[i] <= 'f' {
			v = int(inp[i] - 'a' + 10)
		}
		nibble := fmt.Sprintf("%04s", strconv.FormatInt(int64(v), 2))
		bin += nibble
	}
	return bin
}

func StdinToString() string {
	var input string
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		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")
	}
	return ret
}
