package main

import (
	"errors"
	"flag"
	"fmt"
	"log"
	"math/rand/v2"
	"strings"
	"time"

	"github.com/awesome-gocui/gocui"
)

type gameState struct {
	isGameOver    bool
	rows          int
	cols          int
	numMines      int
	field         [][]point
	gameStartedAt time.Time
	firstMove     bool
	cursorX       int
	cursorY       int
}

type point struct {
	isMine               bool
	isOpen               bool
	isMarked             bool
	numNeighbouringMines int
}

func (gs *gameState) applyToNeighbours(x, y int, do func(x, y int)) {
	dx := []int{-1, -1, -1, 0, 0, 1, 1, 1}
	dy := []int{-1, 0, 1, -1, 1, -1, 0, 1}
	for i := range dx {
		neighbourX := x + dx[i]
		neighbourY := y + dy[i]
		if neighbourX >= 0 && neighbourY >= 0 && neighbourX < gs.rows && neighbourY < gs.cols {
			do(neighbourX, neighbourY)
		}
	}
}

// Colors using gocui's attributes
var numberColors = []gocui.Attribute{
	gocui.ColorBlue,    // 1
	gocui.ColorGreen,   // 2
	gocui.ColorRed,     // 3
	gocui.ColorMagenta, // 4
	gocui.ColorYellow,  // 5
	gocui.ColorCyan,    // 6
	gocui.ColorWhite,   // 7
	gocui.ColorDefault, // 8
}

// Restore emoji cell representations
var (
	emojiUnopened = "🟦 "  // Note the trailing space for consistent width
	emojiMarked   = "🚩 "  // Note the trailing space
	emojiMine     = "💣 "  // Note the trailing space
	emojiEmpty    = "   " // Three spaces
)

func (gs *gameState) printField(v *gocui.View) {
	v.Clear()

	// Build the entire board string first to avoid partial rendering issues
	var board strings.Builder

	for i := range gs.field {
		for j := range gs.field[i] {
			cell := gs.field[i][j]

			if !cell.isOpen {
				if cell.isMarked {
					board.WriteString(emojiMarked) // Fixed-width emoji + space
				} else {
					board.WriteString(emojiUnopened) // Fixed-width emoji + space
				}
			} else {
				if cell.isMine {
					board.WriteString(emojiMine) // Fixed-width emoji + space
				} else if cell.numNeighbouringMines > 0 {
					num := cell.numNeighbouringMines
					// Format numbers to take exactly the same width as emojis (3 chars)
					board.WriteString(fmt.Sprintf(" %d ", num))
				} else {
					board.WriteString(emojiEmpty) // Three spaces
				}
			}
		}
		board.WriteString("\n") // End of row
	}

	// Write the entire board at once
	fmt.Fprint(v, board.String())
}

func (gs *gameState) initField(safeX, safeY int) {
	gs.field = make([][]point, gs.rows)
	for i := range gs.field {
		gs.field[i] = make([]point, gs.cols)
	}

	// Create a list of all possible mine positions except the first clicked cell and its neighbors
	possiblePositions := make([]struct{ x, y int }, 0, gs.rows*gs.cols)
	for i := 0; i < gs.rows; i++ {
		for j := 0; j < gs.cols; j++ {
			// Skip the safe cell and its neighbors
			if (i == safeX && j == safeY) || isCellNeighbor(i, j, safeX, safeY) {
				continue
			}
			possiblePositions = append(possiblePositions, struct{ x, y int }{i, j})
		}
	}

	// Shuffle the positions
	for i := range possiblePositions {
		j := rand.IntN(len(possiblePositions))
		possiblePositions[i], possiblePositions[j] = possiblePositions[j], possiblePositions[i]
	}

	// Place mines using the first numMines positions
	minesPlaced := 0
	for i := 0; i < len(possiblePositions) && minesPlaced < gs.numMines; i++ {
		pos := possiblePositions[i]
		gs.field[pos.x][pos.y].isMine = true
		gs.applyToNeighbours(pos.x, pos.y, func(x, y int) {
			gs.field[x][y].numNeighbouringMines++
		})
		minesPlaced++
	}
}

func (gs *gameState) layout(g *gocui.Gui) error {
	maxX, maxY := g.Size()

	// Calculate game view dimensions based on the grid size
	// Each cell is exactly 3 characters wide (emoji + space)
	gameWidth := gs.cols*3 + 2 // +2 for the frame
	gameHeight := gs.rows + 2  // +2 for the frame (reduced padding)

	// Center the game view precisely
	gameX := (maxX - gameWidth) / 2
	gameY := (maxY - gameHeight - 3) / 2 // Adjust position to center vertically, leaving room for restart button

	// Game view - centered with proper spacing
	if v, err := g.SetView("game", gameX, gameY, gameX+gameWidth, gameY+gameHeight, 0); err != nil {
		if !errors.Is(err, gocui.ErrUnknownView) {
			return err
		}
		v.Title = "Minesweeper"
		v.Clear()
		// Remove the blank line padding that was causing extra space
		gs.printField(v)
		if _, err := g.SetCurrentView("game"); err != nil {
			return err
		}
	}

	// Restart button - positioned below the game view
	buttonWidth := 14
	buttonY := gameY + gameHeight + 1 // Position right below the game view
	if v, err := g.SetView("restart", maxX/2-buttonWidth/2, buttonY, maxX/2+buttonWidth/2, buttonY+2, 0); err != nil {
		if !errors.Is(err, gocui.ErrUnknownView) {
			return err
		}
		v.Title = "Restart"
		fmt.Fprintln(v, "      [R]      ")
	}

	return nil
}

func (gs *gameState) keybindings(g *gocui.Gui) error {
	if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, gs.quit); err != nil {
		return err
	}

	// Left click to open cells
	if err := g.SetKeybinding("game", gocui.MouseLeft, gocui.ModNone, gs.handleMouseClick); err != nil {
		return err
	}

	// Right click to mark cells
	if err := g.SetKeybinding("game", gocui.MouseRight, gocui.ModNone, gs.handleRightClick); err != nil {
		return err
	}

	// 'R' key to restart the game - making it lowercase and uppercase
	if err := g.SetKeybinding("", 'r', gocui.ModNone, gs.restart); err != nil {
		return err
	}
	if err := g.SetKeybinding("", 'R', gocui.ModNone, gs.restart); err != nil {
		return err
	}

	// Mouse binding for restart button
	if err := g.SetKeybinding("restart", gocui.MouseLeft, gocui.ModNone, gs.restart); err != nil {
		return err
	}
	return nil
}

func (gs *gameState) quit(g *gocui.Gui, v *gocui.View) error {
	return gocui.ErrQuit
}

// Use the MousePosition method from gocui instead of view cursor
func (gs *gameState) handleMouseClick(g *gocui.Gui, v *gocui.View) error {
	// Get global mouse position
	mx, my := g.MousePosition()

	// Get view's position
	vx0, vy0, _, _, err := g.ViewPosition("game")
	if err != nil {
		return err
	}

	// Calculate local position within the view (accounting for frame)
	localX := mx - vx0 - 1 // -1 for left frame
	localY := my - vy0 - 1 // -1 for top frame

	// Calculate cell coordinates
	cellX := localY
	cellY := localX / 3

	// Proceed with the game logic
	if cellX >= 0 && cellX < gs.rows && cellY >= 0 && cellY < gs.cols {
		// If it's the first move, initialize the field after the click
		if gs.firstMove {
			gs.initField(cellX, cellY) // Pass click coordinates to initField
			gs.firstMove = false
			gs.gameStartedAt = time.Now() // Reset the start time to when the game actually begins
		}

		if !gs.field[cellX][cellY].isOpen && !gs.field[cellX][cellY].isMarked {
			if gs.field[cellX][cellY].isMine {
				gs.revealAllMines()
				gs.isGameOver = true
			} else {
				gs.openCell(cellX, cellY)
			}
			gs.printField(v)

			gs.updateGameState(g)
		}
	}
	return nil
}

func (gs *gameState) openCell(x, y int) {
	if x < 0 || y < 0 || x >= gs.rows || y >= gs.cols || gs.field[x][y].isOpen {
		return // Bounds check and already opened check
	}

	gs.field[x][y].isOpen = true

	if gs.field[x][y].numNeighbouringMines == 0 {
		gs.applyToNeighbours(x, y, gs.openCell) // Recursively open neighbours if no adjacent mines
	}
}

func (gs *gameState) revealAllMines() {
	for i := 0; i < gs.rows; i++ {
		for j := 0; j < gs.cols; j++ {
			if gs.field[i][j].isMine {
				gs.field[i][j].isOpen = true // Reveal all mines
			}
		}
	}
}

func (gs *gameState) isGameWon() bool {
	openedCells := 0
	for i := 0; i < gs.rows; i++ {
		for j := 0; j < gs.cols; j++ {
			if gs.field[i][j].isOpen && !gs.field[i][j].isMine {
				openedCells++
			}
		}
	}
	return openedCells == (gs.rows*gs.cols - gs.numMines) // Check if all non-mine cells are opened
}

// Helper function to check if a cell is a neighbor of another cell
func isCellNeighbor(x1, y1, x2, y2 int) bool {
	return abs(x1-x2) <= 1 && abs(y1-y2) <= 1
}

// Helper function for absolute value
func abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}

func (gs *gameState) restart(g *gocui.Gui, v *gocui.View) error {
	// Reset game state
	gs.isGameOver = false
	gs.firstMove = true
	gs.gameStartedAt = time.Now()
	gs.field = make([][]point, gs.rows) // Clear field
	for i := range gs.field {
		gs.field[i] = make([]point, gs.cols)
	}

	// Delete any message view
	g.DeleteView("message")

	// Find the game view and refresh it
	if gameView, err := g.View("game"); err == nil {
		gs.printField(gameView)
	}

	return nil
}

// Add a new function to handle right-clicks for marking cells
func (gs *gameState) handleRightClick(g *gocui.Gui, v *gocui.View) error {
	// Get global mouse position
	mx, my := g.MousePosition()

	// Get view's position
	vx0, vy0, _, _, err := g.ViewPosition("game")
	if err != nil {
		return err
	}

	// Calculate local position within the view (accounting for frame)
	localX := mx - vx0 - 1 // -1 for left frame
	localY := my - vy0 - 1 // -1 for top frame

	// Calculate cell coordinates
	cellX := localY
	cellY := localX / 3

	// Proceed with the game logic
	if cellX >= 0 && cellX < gs.rows && cellY >= 0 && cellY < gs.cols {
		// Can only mark cells that aren't already open
		if !gs.field[cellX][cellY].isOpen {
			// Toggle the marked state
			gs.field[cellX][cellY].isMarked = !gs.field[cellX][cellY].isMarked
			gs.printField(v)
		}
	}
	return nil
}

// Add a dedicated function for game state changes
func (gs *gameState) updateGameState(g *gocui.Gui) {
	if gs.isGameWon() {
		gs.revealAllMines()
		gs.isGameOver = true
		gs.showMessage(g, "YEET DAB!", "Congratulation! You're winRAR!\nR to restart or Ctrl+C to ragequit")
	} else if gs.isGameOver {
		gs.showMessage(g, "OH SHIET!", "You dun goofed!\nR to restart or Ctrl+C to ragequit")
	}
}

// Helper for message display
func (gs *gameState) showMessage(g *gocui.Gui, title, message string) error {
	maxX, maxY := g.Size()
	width, height := 40, 5 // Slightly larger for better appearance

	msgView, err := g.SetView("message", maxX/2-width/2, maxY/2-height/2,
		maxX/2+width/2, maxY/2+height/2, 0)
	if err != nil && !errors.Is(err, gocui.ErrUnknownView) {
		return err
	}

	msgView.Title = title
	msgView.Clear()

	// Add padding for better appearance
	fmt.Fprintln(msgView, "")
	fmt.Fprintln(msgView, "  "+message)

	return nil
}

func main() {
	// Define command line flags
	rowsFlag := flag.Int("rows", 10, "Number of rows in the grid")
	colsFlag := flag.Int("cols", 10, "Number of columns in the grid")
	minesFlag := flag.Int("mines", 10, "Number of mines in the grid")

	// Parse command line flags
	flag.Parse()

	// Validate input
	if *rowsFlag < 5 {
		log.Println("Warning: Minimum 5 rows required, setting to 5")
		*rowsFlag = 5
	}
	if *colsFlag < 5 {
		log.Println("Warning: Minimum 5 columns required, setting to 5")
		*colsFlag = 5
	}

	// Make sure mines don't exceed 1/3 of the grid
	maxMines := (*rowsFlag * *colsFlag) / 3
	if *minesFlag > maxMines {
		log.Printf("Warning: Too many mines for this grid size, setting to %d\n", maxMines)
		*minesFlag = maxMines
	}

	g, err := gocui.NewGui(gocui.OutputNormal, true)
	if err != nil {
		log.Panicln(err)
	}
	defer g.Close()

	g.Cursor = false
	g.Mouse = true

	gs := &gameState{
		rows:          *rowsFlag,
		cols:          *colsFlag,
		numMines:      *minesFlag,
		gameStartedAt: time.Now(),
		firstMove:     true,
	}

	gs.initField(0, 0)

	g.SetManagerFunc(gs.layout)

	if err := gs.keybindings(g); err != nil {
		log.Panicln(err)
	}

	if err := g.MainLoop(); err != nil && !errors.Is(err, gocui.ErrQuit) {
		log.Panicln(err)
	}
}
