package main

import (
	"fmt"
	"strings"

	h "git.bullercodeworks.com/brian/adventofcode/helpers"
)

func main() {
	inp := h.StdinToStringSlice()
	part1(BuildCaves(inp))
	fmt.Println()
	part2(BuildCaves(inp))
}

func BuildCaves(inp []string) map[string]Cave {
	ret := make(map[string]Cave)
	for i := range inp {
		pts := strings.Split(inp[i], "-")
		var c1, c2 Cave
		var ok bool
		if c1, ok = ret[pts[0]]; !ok {
			c1 = NewCave(pts[0])
		}
		if c2, ok = ret[pts[1]]; !ok {
			c2 = NewCave(pts[1])
		}
		if !c1.ExitsTo(c2) {
			c1.Exits = append(c1.Exits, c2.Name)
		}
		if !c2.ExitsTo(c1) {
			c2.Exits = append(c2.Exits, c1.Name)
		}
		ret[c1.Name] = c1
		ret[c2.Name] = c2
	}
	return ret
}

func part1(inp map[string]Cave) {
	curr, _ := inp["start"]
	res := part1_findPaths([]Cave{curr}, inp)
	fmt.Println("# Part 1")
	fmt.Println("There are", len(res), "paths")
}

func part1_shouldVisit(c string, path []Cave) bool {
	if c[0] < 'A' || c[0] > 'Z' {
		for i := range path {
			if path[i].Name == c {
				return false
			}
		}
	}
	return true
}

func part1_findPaths(path []Cave, system map[string]Cave) [][]Cave {
	var ret [][]Cave
	curr := path[len(path)-1]
	if curr.Name == "end" {
		return append(ret, path)
	}
	for _, c := range curr.Exits {
		if part1_shouldVisit(c, path) {
			newPaths := part1_findPaths(append(path, system[c]), system)
			ret = append(ret, newPaths...)
		}
	}
	return ret
}

func part2(inp map[string]Cave) {
	curr, _ := inp["start"]
	res := part2_findPaths([]Cave{curr}, inp)
	fmt.Println("# Part 2")
	fmt.Println("There are", len(res), "paths")
}

func part2_shouldVisit(c string, path []Cave) bool {
	if path[len(path)-1].Name == "end" {
		// If the previous spot was end, we shouldn't visit anything
		return false
	}
	if c[0] < 'A' || c[0] > 'Z' {
		if c == "start" { // We never return to start
			return false
		}
		// Check if we've already visited this cave
		visits := make(map[string]int)
		var visited, twice bool
		for i := range path {
			if path[i].Name == c {
				visited = true
			}
			visits[path[i].Name]++
			if !path[i].Big && visits[path[i].Name] > 1 {
				twice = true
			}
		}
		if visited && twice {
			return false
		}
	}
	return true
}

func part2_findPaths(path []Cave, system map[string]Cave) [][]Cave {
	var ret [][]Cave
	curr := path[len(path)-1]
	if curr.Name == "end" {
		return append(ret, path)
	}
	for _, c := range curr.Exits {
		if part2_shouldVisit(c, path) {
			newPaths := part2_findPaths(append(path, system[c]), system)
			ret = append(ret, newPaths...)
		}
	}
	return ret
}

type Cave struct {
	Name  string
	Big   bool
	Exits []string
}

func NewCave(n string) Cave {
	return Cave{
		Name: n,
		Big:  (n[0] >= 'A' && n[0] <= 'Z'),
	}
}

func (c Cave) ExitsTo(e Cave) bool {
	for i := range c.Exits {
		if c.Exits[i] == e.Name {
			return true
		}
	}
	return false
}

func (c Cave) String() string {
	return c.Name
}

func (c Cave) Detail() string {
	return fmt.Sprintf("{%s -> %s}", c.Name, c.Exits)
}

func PrintMap(inp map[string]Cave) {
	for k := range inp {
		fmt.Println(inp[k].Detail())
	}
}
