package main

import (
	"fmt"
	"math"
	"os"
	"sort"
	"time"

	"github.com/fatih/color"

	"../../"
)

var tWidth, tHeight int

func main() {
	fileNm := aoc.GetArgNumber(1)
	if len(os.Args) < 2 {
		fmt.Println("Usage: ./day24 <mazefile>")
		os.Exit(1)
	}
	var f *Floor
	var poiString string
	f = CreateFloorFromFile(fileNm)
	for i := 0; i < len(f.pois); i++ {
		poiString += f.pois[i].Name()
		if f.debug {
			for j := i + 1; j < len(f.pois); j++ {
				idx := f.pois[i].Name() + ";" + f.pois[j].Name()
				fmt.Println(idx, f.shortestPaths[idx])
			}
		}
	}
	poiPerms := aoc.StringPermutations(poiString)
	shortest := -1
	var shortestPerm string
	for _, perm := range poiPerms {
		if perm[0] != '0' {
			continue
		}
		if aoc.ArgIsSet("-2") {
			// For part 2 we return to 0
			perm = perm + "0"
		}
		var permTtl int
		for i := range perm {
			if i > 0 {
				beg, end := string(perm[i-1]), string(perm[i])
				if beg[0] > end[0] {
					beg, end = end, beg
				}
				permTtl += f.shortestPaths[beg+";"+end]
			}
		}
		if permTtl < shortest || shortest == -1 {
			shortestPerm = perm
			shortest = permTtl
		}
	}
	fmt.Println(shortestPerm, ": ", shortest)
}

type Floor struct {
	cells []string
	start *Coord
	end   *Coord
	pois  []Coord

	shortestPaths map[string]int

	testedPath Path
	solvePath  Path

	cellCount int

	debug bool
}

func CreateFloorFromFile(fileNm string) *Floor {
	f := new(Floor)
	f.debug = aoc.ArgIsSet("-d")
	f.cells = aoc.FileToStringSlice(fileNm)
	for y := range f.cells {
		for x := range f.cells[y] {
			if f.cells[y][x] != '#' {
				f.cellCount++
			}
			if f.cells[y][x] != '#' && f.cells[y][x] != '.' {
				// A point of interest
				wrkCoord := NewCoord(x, y, f.cells[y][x])
				f.pois = append(f.pois, *wrkCoord)
				if f.cells[y][x] == '0' {
					f.start = wrkCoord
				}
			}
		}
	}
	// Sort the pois
	sort.Sort(CoordByLabel(f.pois))

	// Find the shortest paths between all points of interest
	f.shortestPaths = make(map[string]int)
	if aoc.ArgIsSet("-2") {

		/* Output from running part 1
		   1;2 232
		   1;3 30
		   1;4 188
		   1;5 230
		   1;6 80
		   1;7 54
		   2;3 218
		   2;4 56
		   2;5 74
		   2;6 212
		   2;7 278
		   3;4 174
		   3;5 216
		   3;6 66
		   3;7 80
		   4;5 54
		   4;6 168
		   4;7 234
		   5;6 210
		   5;7 276
		   6;7 126
		*/

		f.shortestPaths["0;1"] = 30
		f.shortestPaths["0;2"] = 258
		f.shortestPaths["0;3"] = 56
		f.shortestPaths["0;4"] = 214
		f.shortestPaths["0;5"] = 256
		f.shortestPaths["0;6"] = 106
		f.shortestPaths["0;7"] = 44
		f.shortestPaths["1;2"] = 232
		f.shortestPaths["1;3"] = 30
		f.shortestPaths["1;4"] = 188
		f.shortestPaths["1;5"] = 230
		f.shortestPaths["1;6"] = 80
		f.shortestPaths["1;7"] = 54
		f.shortestPaths["2;3"] = 218
		f.shortestPaths["2;4"] = 56
		f.shortestPaths["2;5"] = 74
		f.shortestPaths["2;6"] = 212
		f.shortestPaths["2;7"] = 278
		f.shortestPaths["3;4"] = 174
		f.shortestPaths["3;5"] = 216
		f.shortestPaths["3;6"] = 66
		f.shortestPaths["3;7"] = 80
		f.shortestPaths["4;5"] = 54
		f.shortestPaths["4;6"] = 168
		//f.shortestPaths["4;7"] = 266
		//f.shortestPaths["5;6"] = 262
		//f.shortestPaths["5;7"] = 304
		f.shortestPaths["6;7"] = 126
	}
	for i := range f.pois {
		for j := range f.pois {
			if i != j {
				one, two := f.pois[i], f.pois[j]
				if one.label > two.label {
					one, two = two, one
				}
				idx := string(one.label) + ";" + string(two.label)
				if _, ok := f.shortestPaths[idx]; !ok {
					p := f.GetShortestPath(&one, &two)
					if f.debug {
						fmt.Println(p.Name())
						f.PrintPath(p)
						fmt.Println("Path length:", len(p.coords), "\n")
						for i := range p.coords {
							fmt.Print(p.coords[i].Name(), ",")
						}
						fmt.Println()
					}
					f.shortestPaths[idx] = len(p.coords) - 1
				}
			}
		}
	}
	return f
}

func (f *Floor) GetPoi(lbl byte) *Coord {
	for i := range f.pois {
		if f.pois[i].label == lbl {
			return &f.pois[i]
		}
	}
	return nil
}

var shortestFound int

func (f *Floor) GetShortestPath(st, end *Coord) Path {
	f.solvePath = *new(Path)
	f.testedPath = *new(Path)
	hldStart := f.start
	f.start = end
	f.end = st
	shortestFound = -1
	path, sol := f.Solve(f.start.x, f.start.y, 0)
	f.start = hldStart
	if sol {
		return path
	}
	return *new(Path)
}

func (f *Floor) Print() {
	for y := range f.cells {
		for x := range f.cells[y] {
			fmt.Print(string(f.cells[y][x]))
		}
		fmt.Println()
	}
}

func (f *Floor) PrintPath(p Path) {
	pathCol := color.New(color.BgGreen)
	for y := range f.cells {
		for x := range f.cells[y] {
			if p.ContainsCoord(x, y) {
				if f.cells[y][x] == '.' {
					pathCol.Print("O")
				} else {
					pathCol.Print(string(f.cells[y][x]))
				}
			} else {
				fmt.Print(string(f.cells[y][x]))
			}
		}
		fmt.Println()
	}
}

func (f *Floor) IsWall(x, y int) bool {
	return f.cells[y][x] == '#'
}

func (f *Floor) GetCoord(x, y int) *Coord {
	return NewCoord(x, y, f.cells[y][x])
}

func (f *Floor) Solve(x, y, dist int) (Path, bool) {
	var tPathContains bool
	shortest := new(Path)
	wrkCoord := *f.GetCoord(x, y)
	wrkCoord.dist = dist

	if f.end.Is(x, y) {
		shortest.Append(wrkCoord)
		if shortestFound == -1 || dist < shortestFound {
			shortestFound = dist
		}
		return *shortest, true
	}
	if f.IsWall(x, y) || (shortestFound != -1 && dist > shortestFound) {
		return *shortest, false
	}
	// Test if we already have this coord via a shorter path
	if t := f.testedPath.GetCoordAt(x, y); t != nil {
		if t.dist <= dist {
			return *shortest, false
		}
		tPathContains = true
		t.dist = dist
	}
	// 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 *shortest, false
			}
		}
	}
	if !f.IsWall(x+1, y) {
		if t := f.testedPath.GetCoordAt(x+1, y); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return *shortest, false
			}
		}
	}
	if !f.IsWall(x, y-1) {
		if t := f.testedPath.GetCoordAt(x, y-1); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return *shortest, false
			}
		}
	}
	if !f.IsWall(x, y+1) {
		if t := f.testedPath.GetCoordAt(x, y+1); t != nil {
			if t.dist+1 < wrkCoord.dist {
				return *shortest, false
			}
		}
	}
	// We haven't found a shorter path to this coordinate, so carry on
	if !tPathContains {
		f.testedPath.Append(wrkCoord)
		if f.debug {
			fmt.Println(aoc.ClearScreen, f.start.Name(), "=>", f.end.Name(), "\n", len(f.testedPath.coords), "/", f.cellCount, "\n", "Shortest:", shortestFound)
			f.PrintPath(f.testedPath)
			time.Sleep(time.Millisecond * 50)
		}
	}
	var solvePaths []Path
	var didN, didE, didS, didW bool

	var preferNS bool
	if math.Abs(float64(f.end.y-y)) > math.Abs(float64(f.end.x-x)) {
		// Favor N/S movements
		preferNS = true
	}

	for tdir := 0; tdir < 2; tdir++ {
		if preferNS {
			switch {
			case f.end.y < y && !didN:
				f.TestAndUpdate(x, y-1, dist+1)
				if p, sol := f.Solve(x, y-1, wrkCoord.dist+1); sol {
					p.Append(wrkCoord)
					solvePaths = append(solvePaths, p)
				}
				didN = true
			case f.end.y > y && !didS:
				f.TestAndUpdate(x, y+1, dist+1)
				if p, sol := f.Solve(x, y+1, wrkCoord.dist+1); sol {
					p.Append(wrkCoord)
					solvePaths = append(solvePaths, p)
				}
				didS = true
			case f.end.x > x && !didE:
				f.TestAndUpdate(x+1, y, dist+1)
				if p, sol := f.Solve(x+1, y, wrkCoord.dist+1); sol {
					p.Append(wrkCoord)
					solvePaths = append(solvePaths, p)
				}
				didE = true
			case f.end.x < x && !didW:
				f.TestAndUpdate(x-1, y, dist+1)
				if p, sol := f.Solve(x-1, y, wrkCoord.dist+1); sol {
					p.Append(wrkCoord)
					solvePaths = append(solvePaths, p)
				}
				didW = true
			}
		} else {
			switch {
			case f.end.x > x && !didE:
				f.TestAndUpdate(x+1, y, dist+1)
				if p, sol := f.Solve(x+1, y, wrkCoord.dist+1); sol {
					p.Append(wrkCoord)
					solvePaths = append(solvePaths, p)
				}
				didE = true
			case f.end.x < x && !didW:
				f.TestAndUpdate(x-1, y, dist+1)
				if p, sol := f.Solve(x-1, y, wrkCoord.dist+1); sol {
					p.Append(wrkCoord)
					solvePaths = append(solvePaths, p)
				}
				didW = true
			case f.end.y < y && !didN:
				f.TestAndUpdate(x, y-1, dist+1)
				if p, sol := f.Solve(x, y-1, wrkCoord.dist+1); sol {
					p.Append(wrkCoord)
					solvePaths = append(solvePaths, p)
				}
				didN = true
			case f.end.y > y && !didS:
				f.TestAndUpdate(x, y+1, dist+1)
				if p, sol := f.Solve(x, y+1, wrkCoord.dist+1); sol {
					p.Append(wrkCoord)
					solvePaths = append(solvePaths, p)
				}
				didS = true
			}
		}
	}
	if !didN {
		f.TestAndUpdate(x, y-1, dist+1)
		if p, sol := f.Solve(x, y-1, wrkCoord.dist+1); sol {
			p.Append(wrkCoord)
			solvePaths = append(solvePaths, p)
		}
	}
	if !didE {
		f.TestAndUpdate(x+1, y, dist+1)
		if p, sol := f.Solve(x+1, y, wrkCoord.dist+1); sol {
			p.Append(wrkCoord)
			solvePaths = append(solvePaths, p)
		}
	}
	if !didS {
		f.TestAndUpdate(x, y+1, dist+1)
		if p, sol := f.Solve(x, y+1, wrkCoord.dist+1); sol {
			p.Append(wrkCoord)
			solvePaths = append(solvePaths, p)
		}
	}
	if !didW {
		f.TestAndUpdate(x-1, y, dist+1)
		if p, sol := f.Solve(x-1, y, wrkCoord.dist+1); sol {
			p.Append(wrkCoord)
			solvePaths = append(solvePaths, p)
		}
	}

	var sol bool
	if sol = len(solvePaths) > 0; sol {
		shortest = &solvePaths[0]
		for i := range solvePaths {
			if solvePaths[i].Length()+1 < shortest.Length() {
				shortest = &solvePaths[i]
			}
		}
	}
	return *shortest, sol
}

func (f *Floor) TestAndUpdate(x, y, dist int) {
	if t := f.testedPath.GetCoordAt(x, y); t != nil {
		if t.dist > dist+1 {
			f.testedPath.SetCoordDist(x, y, dist+1)
		}
	}
}

type Coord struct {
	x, y, dist int
	label      byte
}

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

func (c *Coord) Name() string {
	if c.label != '#' && c.label != '.' && c.label != 0 {
		return string(c.label)
	}
	return fmt.Sprint("(", c.x, ",", c.y, ")")
}

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

type CoordByLabel []Coord

func (a CoordByLabel) Len() int           { return len(a) }
func (a CoordByLabel) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
func (a CoordByLabel) Less(i, j int) bool { return a[i].label < a[j].label }

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
}

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

func (p *Path) Length() int {
	return len(p.coords)
}

func (p *Path) Name() string {
	return p.coords[0].Name() + " -> " + p.coords[len(p.coords)-1].Name()
}
