package main

import (
	"fmt"
	"log"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/fatih/color"
	termbox "github.com/nsf/termbox-go"
)

var tWidth, tHeight int

// Puzzle 1 Input: 1364 31 39
// Puzzle 1 Test Input: 10 7 4
func main() {
	mode := "solve"
	if len(os.Args) < 4 {
		fmt.Println("Usage: day13 <seed> <dest-x> <dest-y>")
		os.Exit(1)
	}
	seed := atoi(os.Args[1])
	destX, destY := atoi(os.Args[2]), atoi(os.Args[3])
	if len(os.Args) >= 5 {
		mode = os.Args[4]
	}
	f := CreateFloor(1, 1, destX, destY, seed)

	if err := termbox.Init(); err != nil {
		panic(err)
	}
	tWidth, tHeight = termbox.Size()
	termbox.Close()
	switch mode {
	case "solve":
		if f.Solve(f.start.x, f.start.y, 0, true) {
			f.dispCoord = f.end
		}
		ClearScreen()
		f.Print()
		fmt.Println("Shortest Path:", len(f.solvePath.coords))
	case "walk":
		dist := 50
		f.Walk(f.start.x, f.start.y, 0, dist, true)
		fmt.Println("Within", dist, "steps: ", len(f.testedPath.coords))
	}
}

type Coord struct {
	x, y, dist int
}

func (c *Coord) Is(x, y int) bool {
	return c.x == x && c.y == y
}

func (c *Coord) Equals(t *Coord) bool {
	return c.x == t.x && c.y == t.y
}

func NewCoord(x, y int) *Coord {
	return &Coord{x, y, -1}
}

type Path struct {
	coords []Coord
}

func (p *Path) Append(c Coord) {
	p.coords = append(p.coords, c)
}

func (p *Path) ContainsCoord(x, y int) bool {
	for i := range p.coords {
		if p.coords[i].Is(x, y) {
			return true
		}
	}
	return false
}

func (p *Path) GetCoordAt(x, y int) *Coord {
	for i := range p.coords {
		if p.coords[i].Is(x, y) {
			return &p.coords[i]
		}
	}
	return nil
}

type Floor struct {
	start      *Coord
	end        *Coord
	seed       int
	testedPath Path
	solvePath  Path
	dispCoord  *Coord
}

func CreateFloor(stX, stY, endX, endY, seed int) *Floor {
	f := Floor{
		start: NewCoord(stX, stY),
		end:   NewCoord(endX, endY),
		seed:  seed,
	}
	return &f
}

func (f *Floor) Walk(x, y, dist, maxDist int, print bool) {
	wrkCoord := Coord{x, y, dist}
	if f.IsWall(x, y) || f.testedPath.ContainsCoord(x, y) {
		return
	}
	if dist == maxDist {
		f.testedPath.Append(wrkCoord)
		return
	}
	if print {
		f.dispCoord = &wrkCoord
		ClearScreen()
		f.Print()
		fmt.Println("Tested Spots:", len(f.testedPath.coords))
		time.Sleep(time.Millisecond * 70)
	}
	if !f.IsWall(x-1, y) {
		if t := f.testedPath.GetCoordAt(x-1, y); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return
			}
		}
	}
	if !f.IsWall(x+1, y) {
		if t := f.testedPath.GetCoordAt(x+1, y); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return
			}
		}
	}
	if !f.IsWall(x, y-1) {
		if t := f.testedPath.GetCoordAt(x, y-1); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return
			}
		}
	}
	if !f.IsWall(x, y+1) {
		if t := f.testedPath.GetCoordAt(x, y+1); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return
			}
		}
	}
	f.testedPath.Append(wrkCoord)
	// Try intelligently first
	// (Attempt to move towards the exit)
	if x > 0 {
		f.Walk(x-1, y, wrkCoord.dist+1, maxDist, print)
	}
	if y > 0 {
		f.Walk(x, y-1, wrkCoord.dist+1, maxDist, print)
	}
	f.Walk(x+1, y, wrkCoord.dist+1, maxDist, print)
	f.Walk(x, y+1, wrkCoord.dist+1, maxDist, print)
}

func (f *Floor) Solve(x, y, dist int, print bool) bool {
	wrkCoord := Coord{x, y, dist}
	if f.end.Is(x, y) {
		return true
	}
	if f.IsWall(x, y) || f.testedPath.ContainsCoord(x, y) {
		return false
	}
	// Test if there is a shorter path to this coordinate
	if !f.IsWall(x-1, y) {
		if t := f.testedPath.GetCoordAt(x-1, y); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return false
			}
		}
	}
	if !f.IsWall(x+1, y) {
		if t := f.testedPath.GetCoordAt(x+1, y); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return false
			}
		}
	}
	if !f.IsWall(x, y-1) {
		if t := f.testedPath.GetCoordAt(x, y-1); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return false
			}
		}
	}
	if !f.IsWall(x, y+1) {
		if t := f.testedPath.GetCoordAt(x, y+1); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return false
			}
		}
	}
	if print {
		f.dispCoord = &wrkCoord
		ClearScreen()
		f.Print()
		fmt.Println("Tested Spots:", len(f.testedPath.coords))
		time.Sleep(time.Millisecond * 70)
	}
	f.testedPath.Append(wrkCoord)
	// Try intelligently first
	// (Attempt to move towards the exit)
	if x > f.end.x && x > 0 {
		if f.Solve(x-1, y, wrkCoord.dist+1, print) {
			f.solvePath.Append(wrkCoord)
			return true
		}
	}
	if y > f.end.y && y > 0 {
		if f.Solve(x, y-1, wrkCoord.dist+1, print) {
			f.solvePath.Append(wrkCoord)
			return true
		}
	}
	if x < f.end.x {
		if f.Solve(x+1, y, wrkCoord.dist+1, print) {
			f.solvePath.Append(wrkCoord)
			return true
		}
	}
	if y < f.end.y {
		if f.Solve(x, y+1, wrkCoord.dist+1, print) {
			f.solvePath.Append(wrkCoord)
			return true
		}
	}
	// Intelligence failed us... Just find a move
	if x > 0 {
		if f.Solve(x-1, y, wrkCoord.dist+1, print) {
			f.solvePath.Append(wrkCoord)
			return true
		}
	}
	if y > 0 {
		if f.Solve(x, y-1, wrkCoord.dist+1, print) {
			f.solvePath.Append(wrkCoord)
			return true
		}
	}
	// This is where it gets shaky...
	// Since we have an infinite maze, this could run forever
	// So we have a hard cutoff at:
	var MaxInt = int(^uint(0) >> 1)
	if len(f.testedPath.coords) >= MaxInt {
		fmt.Println("ERROR: Couldn't find a path.")
		os.Exit(1)
	}
	if f.Solve(x+1, y, wrkCoord.dist+1, print) {
		f.solvePath.Append(wrkCoord)
		return true
	}
	if f.Solve(x, y+1, wrkCoord.dist+1, print) {
		f.solvePath.Append(wrkCoord)
		return true
	}
	return false
}

func (f *Floor) IsWall(x, y int) bool {
	sum := (x*x + 3*x + 2*x*y + y + y*y + f.seed)
	s := fmt.Sprintf("%b", sum)
	if strings.Count(s, "1")%2 == 0 {
		return false
	}
	return true
}

func (f *Floor) Print() {
	wall := color.New(color.BgWhite)
	space := color.New(color.FgWhite)
	g := color.New(color.BgGreen).Add(color.FgBlack)
	g.Add(color.Bold)
	r := color.New(color.BgRed)
	b := color.New(color.BgBlue).Add(color.BgBlack)
	b.Add(color.Bold)
	topY, topX := tHeight, tWidth
	botY, botX := 0, 0
	// We want to center approx 20x20 on our current location
	// f.testedPath[len(f.testedPath)-1]?
	if len(f.testedPath.coords) > 0 {
		cntrCoord := f.testedPath.coords[len(f.testedPath.coords)-1]
		if topY < cntrCoord.y+(tHeight/2) {
			topY = cntrCoord.y + (tHeight / 2)
		}
		if topY > tHeight {
			botY = topY - tHeight
		}
		if topX < cntrCoord.x+(tWidth/2) {
			topX = cntrCoord.x + (tWidth / 2)
		}
		if topX > tWidth {
			botX = topX - tWidth
		}
	}
	for y := botY; y < topY; y++ {
		for x := botX; x < topX; x++ {
			if f.dispCoord != nil && f.dispCoord.Is(x, y) {
				g.Print("O")
			} else if f.solvePath.ContainsCoord(x, y) {
				g.Print(".")
			} else if f.testedPath.ContainsCoord(x, y) {
				r.Print(" ")
			} else {
				if f.end.Is(x, y) {
					b.Print("X")
				} else if f.IsWall(x, y) {
					wall.Print(" ")
				} else {
					space.Print(".")
				}
			}
		}
		fmt.Println()
	}
}

func ClearScreen() {
	fmt.Print("\033[H\033[2J")
}

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
}
