package main

// Code based on the Recursive backtracker algorithm.
// https://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_backtracker
// See https://youtu.be/HyK_Q5rrcr4 as an example
// YouTube example ported to Go for the Pixel library.

// Created by Stephen Chavez

import (
	"crypto/rand"
	"errors"
	"flag"
	"fmt"
	"math/big"
	"time"

	"github.com/faiface/pixel"
	"github.com/faiface/pixel-examples/community/maze/stack"
	"github.com/faiface/pixel/imdraw"
	"github.com/faiface/pixel/pixelgl"

	"github.com/pkg/profile"
	"golang.org/x/image/colornames"
)

var visitedColor = pixel.RGB(0.5, 0, 1).Mul(pixel.Alpha(0.35))
var hightlightColor = pixel.RGB(0.3, 0, 0).Mul(pixel.Alpha(0.45))
var debug = false

type cell struct {
	walls [4]bool // Wall order: top, right, bottom, left

	row     int
	col     int
	visited bool
}

func (c *cell) Draw(imd *imdraw.IMDraw, wallSize int) {
	drawCol := c.col * wallSize // x
	drawRow := c.row * wallSize // y

	imd.Color = colornames.White
	if c.walls[0] {
		// top line
		imd.Push(pixel.V(float64(drawCol), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow)))
		imd.Line(3)
	}
	if c.walls[1] {
		// right Line
		imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow)), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
		imd.Line(3)
	}
	if c.walls[2] {
		// bottom line
		imd.Push(pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow+wallSize)))
		imd.Line(3)
	}
	if c.walls[3] {
		// left line
		imd.Push(pixel.V(float64(drawCol), float64(drawRow+wallSize)), pixel.V(float64(drawCol), float64(drawRow)))
		imd.Line(3)
	}
	imd.EndShape = imdraw.SharpEndShape

	if c.visited {
		imd.Color = visitedColor
		imd.Push(pixel.V(float64(drawCol), (float64(drawRow))), pixel.V(float64(drawCol+wallSize), float64(drawRow+wallSize)))
		imd.Rectangle(0)
	}
}

func (c *cell) GetNeighbors(grid []*cell, cols int, rows int) ([]*cell, error) {
	neighbors := []*cell{}
	j := c.row
	i := c.col

	top, _ := getCellAt(i, j-1, cols, rows, grid)
	right, _ := getCellAt(i+1, j, cols, rows, grid)
	bottom, _ := getCellAt(i, j+1, cols, rows, grid)
	left, _ := getCellAt(i-1, j, cols, rows, grid)

	if top != nil && !top.visited {
		neighbors = append(neighbors, top)
	}
	if right != nil && !right.visited {
		neighbors = append(neighbors, right)
	}
	if bottom != nil && !bottom.visited {
		neighbors = append(neighbors, bottom)
	}
	if left != nil && !left.visited {
		neighbors = append(neighbors, left)
	}

	if len(neighbors) == 0 {
		return nil, errors.New("We checked all cells...")
	}
	return neighbors, nil
}

func (c *cell) GetRandomNeighbor(grid []*cell, cols int, rows int) (*cell, error) {
	neighbors, err := c.GetNeighbors(grid, cols, rows)
	if neighbors == nil {
		return nil, err
	}
	nBig, err := rand.Int(rand.Reader, big.NewInt(int64(len(neighbors))))
	if err != nil {
		panic(err)
	}
	randomIndex := nBig.Int64()
	return neighbors[randomIndex], nil
}

func (c *cell) hightlight(imd *imdraw.IMDraw, wallSize int) {
	x := c.col * wallSize
	y := c.row * wallSize

	imd.Color = hightlightColor
	imd.Push(pixel.V(float64(x), float64(y)), pixel.V(float64(x+wallSize), float64(y+wallSize)))
	imd.Rectangle(0)
}

func newCell(col int, row int) *cell {
	newCell := new(cell)
	newCell.row = row
	newCell.col = col

	for i := range newCell.walls {
		newCell.walls[i] = true
	}
	return newCell
}

// Creates the inital maze slice for use.
func initGrid(cols, rows int) []*cell {
	grid := []*cell{}
	for j := 0; j < rows; j++ {
		for i := 0; i < cols; i++ {
			newCell := newCell(i, j)
			grid = append(grid, newCell)
		}
	}
	return grid
}

func setupMaze(cols, rows int) ([]*cell, *stack.Stack, *cell) {
	// Make an empty grid
	grid := initGrid(cols, rows)
	backTrackStack := stack.NewStack(len(grid))
	currentCell := grid[0]

	return grid, backTrackStack, currentCell
}

func cellIndex(i, j, cols, rows int) int {
	if i < 0 || j < 0 || i > cols-1 || j > rows-1 {
		return -1
	}
	return i + j*cols
}

func getCellAt(i int, j int, cols int, rows int, grid []*cell) (*cell, error) {
	possibleIndex := cellIndex(i, j, cols, rows)

	if possibleIndex == -1 {
		return nil, fmt.Errorf("cellIndex: CellIndex is a negative number %d", possibleIndex)
	}
	return grid[possibleIndex], nil
}

func removeWalls(a *cell, b *cell) {
	x := a.col - b.col

	if x == 1 {
		a.walls[3] = false
		b.walls[1] = false
	} else if x == -1 {
		a.walls[1] = false
		b.walls[3] = false
	}

	y := a.row - b.row

	if y == 1 {
		a.walls[0] = false
		b.walls[2] = false
	} else if y == -1 {
		a.walls[2] = false
		b.walls[0] = false
	}
}

func run() {
	// unsiged integers, because easier parsing error checks.
	// We must convert these to intergers, as done below...
	uScreenWidth, uScreenHeight, uWallSize := parseArgs()

	var (
		// In pixels
		// Defualt is 800x800x40 = 20x20 wallgrid
		screenWidth  = int(uScreenWidth)
		screenHeight = int(uScreenHeight)
		wallSize     = int(uWallSize)

		frames = 0
		second = time.Tick(time.Second)

		grid           = []*cell{}
		cols           = screenWidth / wallSize
		rows           = screenHeight / wallSize
		currentCell    = new(cell)
		backTrackStack = stack.NewStack(1)
	)

	// Set game FPS manually
	fps := time.Tick(time.Second / 60)

	cfg := pixelgl.WindowConfig{
		Title:  "Pixel Rocks! - Maze example",
		Bounds: pixel.R(0, 0, float64(screenHeight), float64(screenWidth)),
	}

	win, err := pixelgl.NewWindow(cfg)
	if err != nil {
		panic(err)
	}

	grid, backTrackStack, currentCell = setupMaze(cols, rows)

	gridIMDraw := imdraw.New(nil)

	for !win.Closed() {
		if win.JustReleased(pixelgl.KeyR) {
			fmt.Println("R pressed")
			grid, backTrackStack, currentCell = setupMaze(cols, rows)
		}

		win.Clear(colornames.Gray)
		gridIMDraw.Clear()

		for i := range grid {
			grid[i].Draw(gridIMDraw, wallSize)
		}

		// step 1
		// Make the initial cell the current cell and mark it as visited
		currentCell.visited = true
		currentCell.hightlight(gridIMDraw, wallSize)

		// step 2.1
		// If the current cell has any neighbours which have not been visited
		// Choose a random unvisited cell
		nextCell, _ := currentCell.GetRandomNeighbor(grid, cols, rows)
		if nextCell != nil && !nextCell.visited {
			// step 2.2
			// Push the current cell to the stack
			backTrackStack.Push(currentCell)

			// step 2.3
			// Remove the wall between the current cell and the chosen cell

			removeWalls(currentCell, nextCell)

			// step 2.4
			// Make the chosen cell the current cell and mark it as visited
			nextCell.visited = true
			currentCell = nextCell
		} else if backTrackStack.Len() > 0 {
			currentCell = backTrackStack.Pop().(*cell)
		}

		gridIMDraw.Draw(win)
		win.Update()
		<-fps
		updateFPSDisplay(win, &cfg, &frames, grid, second)
	}
}

// Parses the maze arguments, all of them are optional.
// Uses uint as implicit error checking :)
func parseArgs() (uint, uint, uint) {
	var mazeWidthPtr = flag.Uint("w", 800, "w sets the maze's width in pixels.")
	var mazeHeightPtr = flag.Uint("h", 800, "h sets the maze's height in pixels.")
	var wallSizePtr = flag.Uint("c", 40, "c sets the maze cell's size in pixels.")

	flag.Parse()

	// If these aren't default values AND if they're not the same values.
	// We should warn the user that the maze will look funny.
	if *mazeWidthPtr != 800 || *mazeHeightPtr != 800 {
		if *mazeWidthPtr != *mazeHeightPtr {
			fmt.Printf("WARNING: maze width: %d and maze height: %d don't match. \n", *mazeWidthPtr, *mazeHeightPtr)
			fmt.Println("Maze will look funny because the maze size is bond to the window size!")
		}
	}

	return *mazeWidthPtr, *mazeHeightPtr, *wallSizePtr
}

func updateFPSDisplay(win *pixelgl.Window, cfg *pixelgl.WindowConfig, frames *int, grid []*cell, second <-chan time.Time) {
	*frames++
	select {
	case <-second:
		win.SetTitle(fmt.Sprintf("%s | FPS: %d with %d Cells", cfg.Title, *frames, len(grid)))
		*frames = 0
	default:
	}

}

func main() {
	if debug {
		defer profile.Start().Stop()
	}
	pixelgl.Run(run)
}
