package main

import (
	"image/color"
	"math/rand"
	"strconv"
	"time"

	"tinygo.org/x/tinyfont"
	"tinygo.org/x/tinyfont/freesans"
)

// Maze cell types
const (
	pmEmpty = 0
	pmWall  = 1
	pmDot   = 2
	pmPower = 3
)

// Directions
const (
	pmUp    = int16(0)
	pmDown  = int16(1)
	pmLeft  = int16(2)
	pmRight = int16(3)
)

// Maze dimensions and cell size.
// pmCell scales with displayHeight so the maze always fits the screen:
//   gopherbadge 320x240 → pmCell=10, maze fills 320x240 (no offset)
//   nicebadge   240x135 → pmCell=5,  maze fills 160x120, offset (40,7)
//   gobadge     160x128 → pmCell=5,  maze fills 160x120, offset (0,4)
const (
	pmW          = 32
	pmH          = 24
	pmCell       = displayHeight / pmH
	pmGhostCount = 4
	pmFrightLen  = 40 // frames of fright mode

	// Pixel offset to center the maze on the display
	pmOffsetX = (displayWidth - pmW*pmCell) / 2
	pmOffsetY = (displayHeight - pmH*pmCell) / 2

	// Scoring
	pmPointsDot   = 10
	pmPointsPower = 50
	pmPointsGhost = 200

	// Difficulty
	pmDiffEasy   = 0
	pmDiffNormal = 1
	pmDiffHard   = 2
)

// Drawing dimensions — all derived from pmCell so they scale automatically.
// Reference values shown for pmCell=10 / pmCell=5.
const (
	pmBodyOff = 1                      // border inset for pac/ghosts
	pmBodySz  = pmCell - 2*pmBodyOff   // body size: 8 / 3

	pmDotOff = pmCell/2 - 1            // dot center offset: 4 / 1
	pmDotSz  = 1 + pmCell/10           // dot size: 2 / 1

	pmPowOff = pmCell / 5              // power pellet inset: 2 / 1
	pmPowSz  = pmCell - 2*(pmCell/5)   // power pellet size: 6 / 3

	// Pacman mouth cutout (open/close animation)
	pmMouthOffX = pmCell * 7 / 10      // far-side mouth start: 7 / 3
	pmMouthOffY = pmCell * 2 / 5       // mouth vertical offset: 4 / 2
	pmMouthW    = pmCell * 3 / 10      // mouth width:  3 / 1
	pmMouthH    = pmCell / 5           // mouth height: 2 / 1

	// Ghost eyes
	pmEye1X = pmCell / 5              // left eye x:  2 / 1
	pmEye2X = pmCell * 6 / 10         // right eye x: 6 / 3
	pmEyeY  = pmCell * 3 / 10         // eye y:       3 / 1
	pmEyeSz = pmCell / 5              // eye size:    2 / 1
)

// Package-level state persists across games within a power cycle.
var pmHighScore int
var pmDifficulty int

var pmPowerPositions = [4][2]int16{
	{1, 3}, {30, 3}, {1, 20}, {30, 20},
}

var (
	pmColorWall   = color.RGBA{33, 33, 222, 255}
	pmColorDot    = color.RGBA{255, 255, 255, 255}
	pmColorPower  = color.RGBA{255, 255, 200, 255}
	pmColorPac    = color.RGBA{255, 255, 0, 255}
	pmColorFright = color.RGBA{33, 33, 200, 255}
	pmColorBg     = color.RGBA{0, 0, 0, 255}
	pmGhostColors = [pmGhostCount]color.RGBA{
		{255, 0, 0, 255},     // Blinky (red)
		{255, 184, 255, 255}, // Pinky
		{0, 255, 255, 255},   // Inky (cyan)
		{255, 184, 82, 255},  // Clyde (orange)
	}
)

type PacGhost struct {
	x, y           int16
	dir            int16
	fright         int
	startX, startY int16
}

type PacmanGame struct {
	maze              [pmH][pmW]uint8
	pacX, pacY        int16
	dir, nextDir      int16
	ghosts            [pmGhostCount]PacGhost
	score             int
	dots              int
	totalDots         int
	powerRespawnCount int
	status            uint8
	frame             int
	won               bool
}

func NewPacmanGame() *PacmanGame {
	return &PacmanGame{status: GameSplash}
}

func (g *PacmanGame) Loop() {
	g.status = GameSplash
	g.showSplash()
	if g.status == GameQuit {
		return
	}
	g.startGame()
	drawn := false
	for {
		switch g.status {
		case GamePlay:
			g.update()
		case GameOver:
			if !drawn {
				g.showGameOver()
				drawn = true
			}
			getInput()
			if !buttonsOldState[buttonA] && buttonsState[buttonA] {
				g.status = GameSplash
				g.showSplash()
				if g.status == GameQuit {
					return
				}
				g.startGame()
				drawn = false
			}
			if goBack() {
				return
			}
		case GameQuit:
			return
		}
		time.Sleep(80 * time.Millisecond)
	}
}

func (g *PacmanGame) showSplash() {
	display.FillScreen(pmColorBg)
	if displayHeight >= 160 {
		tinyfont.WriteLine(&display, &freesans.Bold24pt7b, 55, 50, "PAC-MAN", pmColorPac)
		if pmHighScore > 0 {
			hs := "Best: " + strconv.Itoa(pmHighScore)
			w, _ := tinyfont.LineWidth(&freesans.Regular12pt7b, hs)
			tinyfont.WriteLine(&display, &freesans.Regular12pt7b, (displayWidth-int16(w))/2, 85, hs, colorYellow)
		}
	} else {
		tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 30, 25, "PAC-MAN", pmColorPac)
		if pmHighScore > 0 {
			hs := "Best: " + strconv.Itoa(pmHighScore)
			w, _ := tinyfont.LineWidth(&freesans.Regular9pt7b, hs)
			tinyfont.WriteLine(&display, &freesans.Regular9pt7b, (displayWidth-int16(w))/2, 42, hs, colorYellow)
		}
	}
	selected := pmDifficulty
	g.drawDifficultyMenu(selected)

	for {
		getInput()
		if !buttonsOldState[buttonUp] && buttonsState[buttonUp] {
			if selected > pmDiffEasy {
				selected--
				g.drawDifficultyMenu(selected)
			}
		}
		if !buttonsOldState[buttonDown] && buttonsState[buttonDown] {
			if selected < pmDiffHard {
				selected++
				g.drawDifficultyMenu(selected)
			}
		}
		if !buttonsOldState[buttonA] && buttonsState[buttonA] {
			pmDifficulty = selected
			return
		}
		if goBack() {
			g.status = GameQuit
			return
		}
		time.Sleep(50 * time.Millisecond)
	}
}

func (g *PacmanGame) drawDifficultyMenu(selected int) {
	labels := [3]string{"Easy", "Normal", "Hard"}
	if displayHeight >= 160 {
		for i := 0; i < 3; i++ {
			c := colorWhite
			if i == selected {
				c = colorYellow
			}
			tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 80, int16(125+i*28), labels[i], c)
		}
		tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 30, 215, "Press A to start", colorMellonGreen)
		tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 30, 238, "[B] EXIT", colorRed)
	} else {
		for i := 0; i < 3; i++ {
			c := colorWhite
			if i == selected {
				c = colorYellow
			}
			tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 60, int16(60+i*18), labels[i], c)
		}
		tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 110, "Press A to start", colorMellonGreen)
		tinyfont.WriteLine(&display, &freesans.Bold9pt7b, 10, 126, "[B] EXIT", colorRed)
	}
}

func (g *PacmanGame) showGameOver() {
	if g.score > pmHighScore {
		pmHighScore = g.score
	}
	display.FillScreen(pmColorBg)
	if displayHeight >= 160 {
		if g.won {
			tinyfont.WriteLine(&display, &freesans.Bold24pt7b, 40, 50, "YOU WIN!", colorMellonGreen)
		} else {
			tinyfont.WriteLine(&display, &freesans.Bold24pt7b, 20, 50, "GAME OVER", colorRed)
		}
		scoreText := "Score: " + strconv.Itoa(g.score)
		w, _ := tinyfont.LineWidth(&freesans.Bold18pt7b, scoreText)
		tinyfont.WriteLine(&display, &freesans.Bold18pt7b, (displayWidth-int16(w))/2, 100, scoreText, colorYellow)
		hsText := "Best: " + strconv.Itoa(pmHighScore)
		w, _ = tinyfont.LineWidth(&freesans.Regular12pt7b, hsText)
		tinyfont.WriteLine(&display, &freesans.Regular12pt7b, (displayWidth-int16(w))/2, 135, hsText, colorWhite)
		tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 50, 190, "Press A to retry", colorWhite)
		tinyfont.WriteLine(&display, &freesans.Regular12pt7b, 50, 220, "Press B to exit", colorText)
	} else {
		if g.won {
			tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 30, 28, "YOU WIN!", colorMellonGreen)
		} else {
			tinyfont.WriteLine(&display, &freesans.Bold12pt7b, 10, 28, "GAME OVER", colorRed)
		}
		scoreText := "Score: " + strconv.Itoa(g.score)
		w, _ := tinyfont.LineWidth(&freesans.Bold12pt7b, scoreText)
		tinyfont.WriteLine(&display, &freesans.Bold12pt7b, (displayWidth-int16(w))/2, 55, scoreText, colorYellow)
		hsText := "Best: " + strconv.Itoa(pmHighScore)
		w, _ = tinyfont.LineWidth(&freesans.Regular9pt7b, hsText)
		tinyfont.WriteLine(&display, &freesans.Regular9pt7b, (displayWidth-int16(w))/2, 75, hsText, colorWhite)
		tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 20, 100, "Press A to retry", colorWhite)
		tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 20, 115, "Press B to exit", colorText)
	}
}

func (g *PacmanGame) startGame() {
	g.score = 0
	g.dots = 0
	g.won = false
	g.frame = 0
	g.dir = pmLeft
	g.nextDir = pmLeft
	g.powerRespawnCount = 0
	g.initMaze()
	g.totalDots = g.dots

	g.pacX = 15
	g.pacY = 17
	if g.maze[g.pacY][g.pacX] == pmDot {
		g.dots--
	}
	g.maze[g.pacY][g.pacX] = pmEmpty

	gx := [4]int16{14, 17, 14, 17}
	gy := [4]int16{9, 9, 13, 13}
	gd := [4]int16{pmLeft, pmRight, pmLeft, pmRight}
	for i := 0; i < pmGhostCount; i++ {
		g.ghosts[i] = PacGhost{
			x: gx[i], y: gy[i],
			dir:    gd[i],
			startX: gx[i], startY: gy[i],
		}
		if g.maze[gy[i]][gx[i]] == pmDot {
			g.dots--
			g.maze[gy[i]][gx[i]] = pmEmpty
		}
	}

	g.drawFullMaze()
	g.drawPacman()
	for i := 0; i < pmGhostCount; i++ {
		g.drawGhost(i)
	}
	g.status = GamePlay
}

func (g *PacmanGame) initMaze() {
	g.dots = 0
	for y := 0; y < pmH; y++ {
		for x := 0; x < pmW; x++ {
			g.maze[y][x] = pmDot
			g.dots++
		}
	}

	wall := func(x, y, w, h int) {
		for dy := 0; dy < h; dy++ {
			for dx := 0; dx < w; dx++ {
				if g.maze[y+dy][x+dx] == pmDot {
					g.dots--
				}
				g.maze[y+dy][x+dx] = pmWall
			}
		}
	}
	empty := func(x, y, w, h int) {
		for dy := 0; dy < h; dy++ {
			for dx := 0; dx < w; dx++ {
				if g.maze[y+dy][x+dx] == pmDot {
					g.dots--
				}
				g.maze[y+dy][x+dx] = pmEmpty
			}
		}
	}

	// Border
	wall(0, 0, pmW, 1)
	wall(0, pmH-1, pmW, 1)
	wall(0, 0, 1, pmH)
	wall(pmW-1, 0, 1, pmH)

	// Top section
	wall(2, 2, 4, 2)
	wall(7, 2, 6, 2)
	wall(14, 2, 4, 2)
	wall(19, 2, 6, 2)
	wall(26, 2, 4, 2)

	wall(2, 5, 4, 1)
	wall(7, 5, 2, 3)
	wall(10, 5, 5, 1)
	wall(17, 5, 5, 1)
	wall(23, 5, 2, 3)
	wall(26, 5, 4, 1)

	wall(2, 7, 4, 1)
	wall(10, 7, 5, 1)
	wall(17, 7, 5, 1)
	wall(26, 7, 4, 1)

	wall(2, 8, 4, 2)
	wall(7, 8, 2, 2)
	wall(23, 8, 2, 2)
	wall(26, 8, 4, 2)

	// Ghost house: rows 10-12, cols 12-19
	wall(12, 10, 8, 1)
	wall(12, 12, 8, 1)
	wall(12, 10, 1, 3)
	wall(19, 10, 1, 3)
	empty(13, 11, 6, 1)
	empty(15, 10, 2, 1) // entrance

	// Bottom section (mirrors top)
	wall(2, 14, 4, 2)
	wall(7, 14, 2, 2)
	wall(23, 14, 2, 2)
	wall(26, 14, 4, 2)

	wall(2, 16, 4, 1)
	wall(10, 16, 5, 1)
	wall(17, 16, 5, 1)
	wall(26, 16, 4, 1)

	wall(7, 17, 2, 1)
	wall(23, 17, 2, 1)

	wall(2, 18, 4, 1)
	wall(7, 18, 2, 3)
	wall(10, 18, 5, 1)
	wall(17, 18, 5, 1)
	wall(23, 18, 2, 3)
	wall(26, 18, 4, 1)

	wall(2, 20, 4, 2)
	wall(7, 20, 6, 2)
	wall(14, 20, 4, 2)
	wall(19, 20, 6, 2)
	wall(26, 20, 4, 2)

	// Power pellets
	for _, p := range pmPowerPositions {
		g.maze[p[1]][p[0]] = pmPower
	}
}

func (g *PacmanGame) spawnPowerPellet() {
	var eaten [4]int
	n := 0
	for i, p := range pmPowerPositions {
		if g.maze[p[1]][p[0]] == pmEmpty {
			eaten[n] = i
			n++
		}
	}
	if n == 0 {
		return
	}
	idx := eaten[rand.Int31n(int32(n))]
	p := pmPowerPositions[idx]
	g.maze[p[1]][p[0]] = pmPower
	g.dots++
	g.totalDots++
	g.drawCell(p[0], p[1])
}

func (g *PacmanGame) drawFullMaze() {
	display.FillScreen(pmColorBg)
	for y := int16(0); y < pmH; y++ {
		for x := int16(0); x < pmW; x++ {
			g.drawCell(x, y)
		}
	}
}

// drawCell redraws a single maze cell at grid coordinates (x, y).
// Pixel position includes pmOffsetX/Y so the maze is centered on any display.
func (g *PacmanGame) drawCell(x, y int16) {
	px := int16(pmOffsetX) + x*pmCell
	py := int16(pmOffsetY) + y*pmCell
	switch g.maze[y][x] {
	case pmWall:
		display.FillRectangle(px, py, pmCell, pmCell, pmColorWall)
	case pmDot:
		display.FillRectangle(px, py, pmCell, pmCell, pmColorBg)
		display.FillRectangle(px+pmDotOff, py+pmDotOff, pmDotSz, pmDotSz, pmColorDot)
	case pmPower:
		display.FillRectangle(px, py, pmCell, pmCell, pmColorBg)
		display.FillRectangle(px+pmPowOff, py+pmPowOff, pmPowSz, pmPowSz, pmColorPower)
	default:
		display.FillRectangle(px, py, pmCell, pmCell, pmColorBg)
	}
}

func (g *PacmanGame) drawPacman() {
	px := int16(pmOffsetX) + g.pacX*pmCell
	py := int16(pmOffsetY) + g.pacY*pmCell
	display.FillRectangle(px+pmBodyOff, py+pmBodyOff, pmBodySz, pmBodySz, pmColorPac)
	if g.frame%4 < 2 {
		switch g.dir {
		case pmRight:
			display.FillRectangle(px+pmMouthOffX, py+pmMouthOffY, pmMouthW, pmMouthH, pmColorBg)
		case pmLeft:
			display.FillRectangle(px, py+pmMouthOffY, pmMouthW, pmMouthH, pmColorBg)
		case pmUp:
			display.FillRectangle(px+pmMouthOffY, py, pmMouthH, pmMouthW, pmColorBg)
		case pmDown:
			display.FillRectangle(px+pmMouthOffY, py+pmMouthOffX, pmMouthH, pmMouthW, pmColorBg)
		}
	}
}

func (g *PacmanGame) drawGhost(idx int) {
	ghost := &g.ghosts[idx]
	px := int16(pmOffsetX) + ghost.x*pmCell
	py := int16(pmOffsetY) + ghost.y*pmCell
	c := pmGhostColors[idx]
	if ghost.fright > 0 {
		c = pmColorFright
	}
	display.FillRectangle(px+pmBodyOff, py+pmBodyOff, pmBodySz, pmBodySz, c)
	display.FillRectangle(px+pmEye1X, py+pmEyeY, pmEyeSz, pmEyeSz, pmColorDot)
	display.FillRectangle(px+pmEye2X, py+pmEyeY, pmEyeSz, pmEyeSz, pmColorDot)
}

func (g *PacmanGame) update() {
	getInput()
	if goBack() {
		g.status = GameQuit
		return
	}
	if buttonsState[buttonRight] {
		g.nextDir = pmRight
	} else if buttonsState[buttonLeft] {
		g.nextDir = pmLeft
	} else if buttonsState[buttonUp] {
		g.nextDir = pmUp
	} else if buttonsState[buttonDown] {
		g.nextDir = pmDown
	}

	g.frame++

	if g.canMove(g.pacX, g.pacY, g.nextDir) {
		g.dir = g.nextDir
	}
	if g.canMove(g.pacX, g.pacY, g.dir) {
		oldX, oldY := g.pacX, g.pacY
		g.pacX += pmDX(g.dir)
		g.pacY += pmDY(g.dir)

		cell := g.maze[g.pacY][g.pacX]
		if cell == pmDot {
			g.maze[g.pacY][g.pacX] = pmEmpty
			points := pmPointsDot
			if pmDifficulty == pmDiffHard {
				points *= 2
			}
			g.score += points
			g.dots--
		} else if cell == pmPower {
			g.maze[g.pacY][g.pacX] = pmEmpty
			g.score += pmPointsPower
			g.dots--
			for i := range g.ghosts {
				g.ghosts[i].fright = pmFrightLen
			}
		}

		if g.powerRespawnCount < 2 && pmDifficulty != pmDiffHard {
			eaten := g.totalDots - g.dots
			threshold := g.totalDots / 2
			if g.powerRespawnCount == 1 {
				threshold = g.totalDots * 3 / 4
			}
			if eaten >= threshold {
				g.spawnPowerPellet()
				g.powerRespawnCount++
			}
		}

		g.drawCell(oldX, oldY)

		if g.dots <= 0 {
			g.won = true
			g.status = GameOver
			return
		}

		for i := 0; i < pmGhostCount; i++ {
			if g.ghosts[i].x == g.pacX && g.ghosts[i].y == g.pacY {
				if g.ghosts[i].fright > 0 {
					g.eatGhost(i)
				} else {
					g.status = GameOver
					return
				}
			}
		}
	}

	g.drawPacman()

	moveGhosts := false
	switch pmDifficulty {
	case pmDiffEasy:
		moveGhosts = g.frame%3 == 0
	case pmDiffNormal:
		moveGhosts = g.frame%2 == 0
	case pmDiffHard:
		moveGhosts = g.frame%4 != 0
	}
	if moveGhosts {
		for i := 0; i < pmGhostCount; i++ {
			oldX, oldY := g.ghosts[i].x, g.ghosts[i].y
			g.moveGhost(i)
			g.drawCell(oldX, oldY)
			g.drawGhost(i)
			if g.ghosts[i].fright > 0 {
				g.ghosts[i].fright--
			}
			if g.ghosts[i].x == g.pacX && g.ghosts[i].y == g.pacY {
				if g.ghosts[i].fright > 0 {
					g.eatGhost(i)
				} else {
					g.status = GameOver
					return
				}
			}
		}
	}
}

func (g *PacmanGame) eatGhost(idx int) {
	g.score += pmPointsGhost
	g.ghosts[idx].x = g.ghosts[idx].startX
	g.ghosts[idx].y = g.ghosts[idx].startY
	g.ghosts[idx].fright = 0
	g.drawGhost(idx)
}

func (g *PacmanGame) moveGhost(idx int) {
	ghost := &g.ghosts[idx]
	reverse := pmReverse(ghost.dir)

	type opt struct {
		dir  int16
		x, y int16
		dist int16
	}
	var opts [4]opt
	n := 0

	for d := int16(0); d < 4; d++ {
		if d == reverse {
			continue
		}
		nx := ghost.x + pmDX(d)
		ny := ghost.y + pmDY(d)
		if nx < 0 || nx >= pmW || ny < 0 || ny >= pmH {
			continue
		}
		if g.maze[ny][nx] == pmWall {
			continue
		}
		dist := pmAbs(nx-g.pacX) + pmAbs(ny-g.pacY)
		opts[n] = opt{d, nx, ny, dist}
		n++
	}

	if n == 0 {
		d := reverse
		nx := ghost.x + pmDX(d)
		ny := ghost.y + pmDY(d)
		if nx >= 0 && nx < pmW && ny >= 0 && ny < pmH && g.maze[ny][nx] != pmWall {
			ghost.x = nx
			ghost.y = ny
			ghost.dir = d
		}
		return
	}

	if ghost.fright > 0 {
		c := rand.Int31n(int32(n))
		ghost.x = opts[c].x
		ghost.y = opts[c].y
		ghost.dir = opts[c].dir
	} else {
		best := 0
		for i := 1; i < n; i++ {
			if opts[i].dist < opts[best].dist {
				best = i
			}
		}
		if n > 1 && rand.Int31n(4) == 0 {
			best = int(rand.Int31n(int32(n)))
		}
		ghost.x = opts[best].x
		ghost.y = opts[best].y
		ghost.dir = opts[best].dir
	}
}

func (g *PacmanGame) canMove(x, y, dir int16) bool {
	nx := x + pmDX(dir)
	ny := y + pmDY(dir)
	if nx < 0 || nx >= pmW || ny < 0 || ny >= pmH {
		return false
	}
	return g.maze[ny][nx] != pmWall
}

func pmDX(dir int16) int16 {
	switch dir {
	case pmLeft:
		return -1
	case pmRight:
		return 1
	}
	return 0
}

func pmDY(dir int16) int16 {
	switch dir {
	case pmUp:
		return -1
	case pmDown:
		return 1
	}
	return 0
}

func pmReverse(dir int16) int16 {
	switch dir {
	case pmUp:
		return pmDown
	case pmDown:
		return pmUp
	case pmLeft:
		return pmRight
	case pmRight:
		return pmLeft
	}
	return -1
}

func pmAbs(x int16) int16 {
	if x < 0 {
		return -x
	}
	return x
}
