package main

import (
	"errors"
	"fmt"
	"log"
	"sort"
	"strings"

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

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

func buildFileSystem(inp []string) *dir {
	root := NewDir(nil, []string{}, "/")
	cwd := root
	for i := range inp {
		if i == 0 {
			// We've already done this
			continue
		}
		pts := strings.Fields(inp[i])
		switch pts[0] {
		case "$":
			if pts[1] == "cd" {
				if pts[2] == ".." {
					cwd = cwd.Parent()
				} else {
					ch, err := cwd.ChangeDir(pts[2])
					if err != nil {
						log.Fatalf("Failed to change directories: %s/%s, %s", strings.Join(cwd.Path(), "/"), pts[2], err.Error())
					}
					cwd = ch
				}
			}
		case "dir":
			cwd.AddDir(pts[1])
		default:
			cwd.AddFile(pts[1], h.Atoi(pts[0]))
		}
	}
	return root
}

func part1(inp []string) {
	fmt.Println("# Part 1")
	root := buildFileSystem(inp)
	smallDirs := root.FindDirectoriesMaxSize(100000)
	var sum int
	for i := range smallDirs {
		sum += smallDirs[i].Size()
	}
	fmt.Printf("Naive Size of Small Dirs: %d\n", sum)
}

func part2(inp []string) {
	fmt.Println("# Part 2")
	root := buildFileSystem(inp)
	total := 70000000
	required := 30000000
	used := root.Size()
	needed := required - (total - used)
	dir := root.FindSmallestDirectoryAtLeast(needed)
	if dir == nil {
		log.Fatal("No suitable directories found")
	}
	fmt.Printf("Disk Space %d / %d. Need %d more for update.\n", (total - used), total, needed)
	fmt.Println("Auto-deleting smallest directory that will permit update... Don't panic.")
	fmt.Printf("Freed space by deleting %s: %d\n", dir.FullPath(), dir.Size())
}

const (
	TpFile = iota
	TpDir
)

type node interface {
	Parent() *dir
	Path() []string
	Name() string
	Type() int
	Size() int
	FullPath() []string
	Print(depth int)
}

type file struct {
	parent *dir
	path   []string
	name   string
	size   int
}

func NewFile(parent *dir, path []string, nm string, size int) *file {
	return &file{
		parent: parent,
		path:   path,
		name:   nm,
		size:   size,
	}
}
func (f file) Parent() *dir       { return f.parent }
func (f file) Path() []string     { return f.path }
func (f file) Name() string       { return f.name }
func (f file) Type() int          { return TpFile }
func (f file) Size() int          { return f.size }
func (f file) FullPath() []string { return append(f.path, f.name) }
func (f file) Print(depth int) {
	fmt.Printf("%s- %s (file, size=%d)\n", strings.Repeat("  ", depth), f.Name(), f.Size())
}

type dir struct {
	parent       *dir
	path         []string
	name         string
	contents     map[string]node
	contentsList []string
}

func NewDir(parent *dir, path []string, name string) *dir {
	return &dir{
		parent:   parent,
		path:     path,
		name:     name,
		contents: make(map[string]node),
	}
}
func (d dir) Parent() *dir   { return d.parent }
func (d dir) Path() []string { return d.path }
func (d dir) Name() string   { return d.name }
func (d dir) Type() int      { return TpDir }
func (d dir) Size() int {
	var size int
	for i := range d.contents {
		size += d.contents[i].Size()
	}
	return size
}
func (d dir) FullPath() []string { return append(d.path, d.name) }
func (d *dir) ChangeDir(to string) (*dir, error) {
	if v, ok := d.contents[to]; !ok {
		return nil, errors.New("Directory not found")
	} else {
		switch d := v.(type) {
		case *dir:
			return d, nil
		default:
			return nil, errors.New("Not a directory")
		}
	}
}
func (d *dir) AddFile(nm string, size int) {
	d.contents[nm] = NewFile(d, d.FullPath(), nm, size)
	d.contentsList = append(d.contentsList, nm)
	sort.Strings(d.contentsList)
}
func (d *dir) AddDir(nm string) {
	d.contents[nm] = NewDir(d, d.FullPath(), nm)
	d.contentsList = append(d.contentsList, nm)
	sort.Strings(d.contentsList)
}
func (d dir) Print(depth int) {
	fmt.Printf("%s- %s (dir)\n", strings.Repeat("  ", depth), d.Name())
	for _, nm := range d.contentsList {
		n, ok := d.contents[nm]
		if !ok {
			log.Fatalf("Error printing directory %s: %s not found.", d.FullPath(), nm)
		}
		n.Print(depth + 1)
	}
}
func (d *dir) FreeSpace(driveSize int) int { return driveSize - d.Size() }

func (d *dir) FindDirectoriesMaxSize(size int) []*dir {
	var ret []*dir
	for _, node := range d.contents {
		switch child := node.(type) {
		case *dir:
			if child.Size() <= size {
				ret = append(ret, child)
			}
			ret = append(ret, child.FindDirectoriesMaxSize(size)...)
		}
	}
	return ret
}
func (d *dir) FindSmallestDirectoryAtLeast(needed int) *dir {
	var ret *dir
	retSize := d.Size()
	for _, node := range d.contents {
		switch child := node.(type) {
		case *dir:
			sz := child.Size()
			if sz >= needed && sz < retSize {
				ret = child
				retSize = sz
				rec := child.FindSmallestDirectoryAtLeast(needed)
				if rec != nil {
					ret = rec
					retSize = rec.Size()
				}
			}
		}
	}
	return ret
}
