package main

import (
	"bufio"
	"bytes"
	"log"
	"math"
	"os"
)

func (s state) String() string {
	blank := `
#############
#...........#
###.#.#.#.###
  #.#.#.#.#
  #.#.#.#.#
  #.#.#.#.#
  #########
`
	out := bytes.Split([]byte(blank), []byte("\n"))
	for _, amp := range s {
		out[amp.pos[0]+2][amp.pos[1]+1] = amp.tp
	}
	return string(bytes.Join(out, []byte("\n")))
}

type amphipod struct {
	tp byte
	// pos: line-col
	// line 0 is the hallway, 1,2,3... are the depth of side rooms
	// col 2-4-6-8 are the side room columns
	pos [2]int
}

type state [16]amphipod

type energy map[state]int

const (
	depth    = 4  // depth of the side-rooms
	width    = 4  // number of side-rooms
	offset   = 3  // first item in the first side-room
	hwLength = 10 // length of the hallway line
)

// forbiddenHallway stores the forbidden coordinates of the hallway
var forbiddenHallway = [width]int{2, 4, 6, 8}

func main() {
	if err := myMain(); err != nil {
		log.Println(err)
	}
}

func myMain() error {
	amps, err := parseInput("input.txt")
	if err != nil {
		return err
	}

	log.Println(solveFirst(amps))

	return nil
}

func solveFirst(s state) int {
	// s[3].pos = [2]int{0, 10} // 3000
	// s[7].pos = [2]int{0, 0}  // 10
	// s[2].pos = [2]int{0, 9}  // 40
	// s[6].pos = [2]int{0, 7}  // 30

	// log.Println("starting state:", s)

	// breadcrumbs := map[state]state{}

	visited := energy{}

	queue := energy{}
	queue[s] = 0

	// queue[s] += 3000
	// queue[s] += 10
	// queue[s] += 40
	// queue[s] += 30

	for len(queue) > 0 {
		// for steps := 0; steps < 1; steps++ {
		curr, currCost := queue.getLowest()
		delete(queue, curr)

		if _, ok := visited[curr]; ok {
			continue
		}
		visited[curr] = currCost

		if curr.isInPlace() {
			// printBread(breadcrumbs, curr)
			return visited[curr]
		}

		for st, cost := range curr.getValidStates() {
			if _, ok := visited[st]; ok {
				continue
			}
			// breadcrumbs[st] = curr

			if prevCost, ok := queue[st]; ok {
				if prevCost < currCost+cost {
					continue
				}
			}
			queue[st] = currCost + cost
		}
		// log.Println(queue)
	}

	return len(queue)
}

func printBread(br map[state]state, end state) {
	curr := end
	for {
		next, ok := br[curr]
		if !ok {
			return
		}
		log.Println(next)
		curr = next
	}
}

func (e energy) getLowest() (state, int) {
	var ret state
	min := int(math.Inf(1))
	for st, cost := range e {
		if cost < min {
			ret = st
			min = cost
		}
	}
	return ret, min
}

// getValidStates returns all possible valid states based on the current state.
func (s state) getValidStates() energy {
	// 1. Check if an amphipod can go to its side-room
	if ret := s.checkSideRoom(); ret != nil {
		// log.Println("side-room:", ret)
		return ret
	}
	// log.Println("no way to side-room")

	// 2. Check if an amphipod can go to the hallway
	// 2.1 only consider amps _not_ on the hallway
	// 2.2 do not forget to consider all possible hallway spots where path is free!
	// 2.3 do not move when side-room is done
	ret := energy{}
	for col := offset - 1; col <= width*2; col += 2 {
		if s.sideRoomDone(col) {
			continue
		}
		for row := 1; row <= depth; row++ {
			first := [2]int{0, 0}
			idx := 999
			for i, amp := range s {
				if amp.pos == [2]int{row, col} {
					first = [2]int{row, col}
					idx = i
					break
				}
			}
			if first != [2]int{0, 0} {
				for hw := 0; hw <= hwLength; hw++ {
					if isForbidden(hw) {
						continue
					}
					steps, ok := s.checkPath(first, [2]int{0, hw})
					// log.Println(steps, ok, hw)
					if !ok {
						continue
					}

					// add item to possibilities
					next := s
					next[idx].pos = [2]int{0, hw}
					ret[next] = next[idx].calEnergy(steps)
				}
			}
		}
	}

	return ret
}

func (s state) sideRoomDone(col int) bool {
	var goodType byte
	switch col {
	case 2:
		goodType = 'A'
	case 4:
		goodType = 'B'
	case 6:
		goodType = 'C'
	case 8:
		goodType = 'D'
	default:
		log.Panicln("unknown col")
	}
	for _, amp := range s {
		if amp.pos[1] == col && amp.tp != goodType {
			return false
		}
	}
	return true
}

func isForbidden(hw int) bool {
	for _, fb := range forbiddenHallway {
		if hw == fb {
			return true
		}
	}
	return false
}

func (s state) checkSideRoom() energy {
	// 1.1 there is place in the side-room
	// 1.2 there are only proper type amps in the room
	for i, amp := range s {
		sideRoom := int((offset - 1) * (amp.tp - 'A' + 1))
		if amp.pos[1] == sideRoom {
			// amphipod already on right place
			continue
		}

		counter := 0
		for j, other := range s {
			if i == j {
				continue
			}
			if other.pos[1] == sideRoom { // line must not be 0, do not have to check
				// this is the proper side-room
				if other.tp != amp.tp {
					// type mismatch!
					counter = 999
					break
				}
				counter++
			}
		}

		if counter == 999 || counter >= depth {
			// type mismatch or the side-room is full
			continue
		}

		// 1.3 check if path is free to side-room, count steps
		steps, ok := s.checkPath(amp.pos, [2]int{depth - counter, sideRoom})
		if !ok {
			continue
		}

		ret := energy{}
		next := s
		next[i].pos = [2]int{depth - counter, sideRoom}
		ret[next] = next[i].calEnergy(steps)
		return ret
	}
	return nil
}

func (amp amphipod) calEnergy(steps int) int {
	power := 0
	switch amp.tp {
	case 'A':
		power = 0
	case 'B':
		power = 1
	case 'C':
		power = 2
	case 'D':
		power = 3
	default:
		log.Panicln("no such amphipod type")
	}
	return steps * int(math.Pow10(power))
}

func (s state) checkPath(from, to [2]int) (int, bool) {
	// log.Println("checkPath", from, to)
	steps := 0
	for row, col := from[0], from[1]; ; steps++ {
		// check if occupied or not
		if steps != 0 {
			for _, amp := range s {
				if amp.pos == [2]int{row, col} {
					return 0, false
				}
			}
		}

		// reached destination, finish
		if row == to[0] && col == to[1] {
			break
		}

		// come out of the current side-room
		if row != 0 && col != to[1] {
			row--
			continue
		}

		// move along the hallway
		if row == 0 && col != to[1] {
			if col > to[1] {
				col--
			} else {
				col++
			}
			continue
		}

		// go into the poroper depth of the proper sideroom
		row++
	}
	return steps, true
}

func (amp amphipod) isOnHallway() bool {
	if amp.pos[0] == 0 {
		return true
	}
	return false
}

// isInPlace returns true when all amphipods are in right place.
func (amps state) isInPlace() bool {
	for _, amp := range amps {
		if amp.isOnHallway() { // amphipod is on the hallway
			return false
		}

		// check if amphipod is in right side-room
		switch amp.tp {
		case 'A':
			if amp.pos[1] != (offset-1)*1 {
				return false
			}
		case 'B':
			if amp.pos[1] != (offset-1)*2 {
				return false
			}
		case 'C':
			if amp.pos[1] != (offset-1)*3 {
				return false
			}
		case 'D':
			if amp.pos[1] != (offset-1)*4 {
				return false
			}
		}
	}
	return true
}

func parseInput(fileName string) (state, error) {
	ret := state{}
	fd, err := os.Open(fileName)
	if err != nil {
		return ret, err
	}
	defer fd.Close()

	buf := bufio.NewScanner(fd)
	for line := 0; buf.Scan(); line++ {
		if line == 2 || line == 3 || line == 4 || line == 5 {
			for i, counter := offset, (line-2)*4; i < width*2+offset; i, counter = i+2, counter+1 {
				amp := amphipod{}
				amp.tp = buf.Text()[i]
				amp.pos = [2]int{line - 1, i - 1}
				ret[counter] = amp
			}
		}
	}

	return ret, nil
}