package main

import (
	"errors"
	"math/rand"
	"time"
)

// ПРИМЕЧАНИЕ О ПРАВИЛАХ: классический Сапёр. Мины расставляются
// только ПОСЛЕ первого клика — так, чтобы кликнутая клетка и все её
// соседи гарантированно не содержали мину (это даёт более приятное,
// обычно достаточно большое первое открытие, а не голую цифру) —
// распространённое улучшение поверх самой базовой гарантии "хотя бы
// кликнутая клетка не мина". "Аккорд" (быстрое открытие соседей у
// уже открытой цифры, если рядом расставлено ровно нужное число
// флагов) реализован, как и в привычных версиях игры.

var (
	ErrMinesweeperGameOver    = errors.New("партия уже завершена")
	ErrMinesweeperOutOfBounds = errors.New("клетка вне пределов поля")
	ErrMinesweeperFlagged     = errors.New("сначала снимите флаг с этой клетки")
	ErrMinesweeperAlreadyOpen = errors.New("клетка уже открыта")
	ErrMinesweeperNotRevealed = errors.New("эта клетка ещё не открыта")
)

// MinesweeperPos — координаты клетки (0-based, row/col).
type MinesweeperPos struct {
	Row, Col int
}

// minesweeperCell — состояние одной клетки поля.
type minesweeperCell struct {
	Revealed      bool
	Flagged       bool
	IsMine        bool
	AdjacentMines int
}

// MinesweeperResult — итог завершившейся партии.
type MinesweeperResult struct {
	Won bool
}

// MinesweeperGameState — полное состояние партии в Сапёра.
type MinesweeperGameState struct {
	Width, Height int
	MineCount     int
	Board         [][]minesweeperCell

	minesPlaced   bool // мины расставляются только после первого клика
	RevealedCount int
	FlagCount     int

	Result *MinesweeperResult

	rng *rand.Rand
}

// NewMinesweeperGame создаёт новое поле width x height с mineCount
// мин (не больше width*height-1 — хотя бы одна клетка гарантированно
// останется небезопасной для мины, чтобы было куда сходить первым
// ходом). Сами мины появятся только после первого хода — см. Reveal.
func NewMinesweeperGame(width, height, mineCount int) *MinesweeperGameState {
	maxMines := width*height - 1
	if mineCount > maxMines {
		mineCount = maxMines
	}
	g := &MinesweeperGameState{
		Width: width, Height: height, MineCount: mineCount,
		rng: rand.New(rand.NewSource(time.Now().UnixNano())),
	}
	g.Board = make([][]minesweeperCell, height)
	for r := range g.Board {
		g.Board[r] = make([]minesweeperCell, width)
	}
	return g
}

func (g *MinesweeperGameState) inBounds(p MinesweeperPos) bool {
	return p.Row >= 0 && p.Row < g.Height && p.Col >= 0 && p.Col < g.Width
}

func msNeighbors(p MinesweeperPos) []MinesweeperPos {
	out := make([]MinesweeperPos, 0, 8)
	for dr := -1; dr <= 1; dr++ {
		for dc := -1; dc <= 1; dc++ {
			if dr == 0 && dc == 0 {
				continue
			}
			out = append(out, MinesweeperPos{Row: p.Row + dr, Col: p.Col + dc})
		}
	}
	return out
}

// placeMines расставляет мины случайно (используя переданный
// генератор rnd — так тесты могут вызывать её напрямую
// детерминированно), избегая точки first и всех её соседей
// (безопасная зона первого клика), затем считает число мин-соседей
// для каждой клетки.
func (g *MinesweeperGameState) placeMines(first MinesweeperPos, rnd *rand.Rand) {
	safe := map[MinesweeperPos]bool{first: true}
	for _, n := range msNeighbors(first) {
		if g.inBounds(n) {
			safe[n] = true
		}
	}

	var candidates []MinesweeperPos
	for r := 0; r < g.Height; r++ {
		for c := 0; c < g.Width; c++ {
			p := MinesweeperPos{Row: r, Col: c}
			if !safe[p] {
				candidates = append(candidates, p)
			}
		}
	}

	count := g.MineCount
	if count > len(candidates) {
		count = len(candidates) // защитный случай — слишком мало свободных клеток
	}
	rnd.Shuffle(len(candidates), func(i, j int) { candidates[i], candidates[j] = candidates[j], candidates[i] })
	for i := 0; i < count; i++ {
		p := candidates[i]
		g.Board[p.Row][p.Col].IsMine = true
	}

	for r := 0; r < g.Height; r++ {
		for c := 0; c < g.Width; c++ {
			p := MinesweeperPos{Row: r, Col: c}
			n := 0
			for _, adj := range msNeighbors(p) {
				if g.inBounds(adj) && g.Board[adj.Row][adj.Col].IsMine {
					n++
				}
			}
			g.Board[r][c].AdjacentMines = n
		}
	}

	g.minesPlaced = true
}

// Reveal открывает клетку pos. Если это первый ход партии, сначала
// расставляет мины (см. placeMines).
func (g *MinesweeperGameState) Reveal(pos MinesweeperPos) error {
	if g.Result != nil {
		return ErrMinesweeperGameOver
	}
	if !g.inBounds(pos) {
		return ErrMinesweeperOutOfBounds
	}

	if !g.minesPlaced {
		g.placeMines(pos, g.rng)
	}

	cell := &g.Board[pos.Row][pos.Col]
	if cell.Flagged {
		return ErrMinesweeperFlagged
	}
	if cell.Revealed {
		return ErrMinesweeperAlreadyOpen
	}

	if cell.IsMine {
		g.revealAllMines()
		g.Result = &MinesweeperResult{Won: false}
		return nil
	}

	g.floodReveal(pos)
	g.checkWin()
	return nil
}

// floodReveal открывает pos и, если у неё 0 мин-соседей, рекурсивно
// (через очередь) открывает всех соседей — классическое каскадное
// открытие пустых областей.
func (g *MinesweeperGameState) floodReveal(start MinesweeperPos) {
	if g.Board[start.Row][start.Col].Revealed || g.Board[start.Row][start.Col].Flagged {
		return
	}
	queue := []MinesweeperPos{start}
	g.Board[start.Row][start.Col].Revealed = true
	g.RevealedCount++

	for len(queue) > 0 {
		cur := queue[0]
		queue = queue[1:]
		if g.Board[cur.Row][cur.Col].AdjacentMines != 0 {
			continue
		}
		for _, n := range msNeighbors(cur) {
			if !g.inBounds(n) {
				continue
			}
			nc := &g.Board[n.Row][n.Col]
			if nc.Revealed || nc.Flagged || nc.IsMine {
				continue
			}
			nc.Revealed = true
			g.RevealedCount++
			queue = append(queue, n)
		}
	}
}

// Chord — быстрое открытие соседей уже открытой цифры, если число
// флагов вокруг неё совпадает с самой цифрой (вызывается на уже
// открытой клетке).
func (g *MinesweeperGameState) Chord(pos MinesweeperPos) error {
	if g.Result != nil {
		return ErrMinesweeperGameOver
	}
	if !g.inBounds(pos) {
		return ErrMinesweeperOutOfBounds
	}
	cell := g.Board[pos.Row][pos.Col]
	if !cell.Revealed {
		return ErrMinesweeperNotRevealed
	}

	flagCount := 0
	for _, n := range msNeighbors(pos) {
		if g.inBounds(n) && g.Board[n.Row][n.Col].Flagged {
			flagCount++
		}
	}
	if flagCount != cell.AdjacentMines {
		return nil // недостаточно/слишком много флагов — аккорд не срабатывает, это не ошибка
	}

	for _, n := range msNeighbors(pos) {
		if !g.inBounds(n) {
			continue
		}
		nc := g.Board[n.Row][n.Col]
		if nc.Revealed || nc.Flagged {
			continue
		}
		if nc.IsMine {
			g.revealAllMines()
			g.Result = &MinesweeperResult{Won: false}
			return nil
		}
		g.floodReveal(n)
	}
	if g.Result == nil {
		g.checkWin()
	}
	return nil
}

func (g *MinesweeperGameState) revealAllMines() {
	for r := 0; r < g.Height; r++ {
		for c := 0; c < g.Width; c++ {
			if g.Board[r][c].IsMine {
				g.Board[r][c].Revealed = true
			}
		}
	}
}

func (g *MinesweeperGameState) checkWin() {
	total := g.Width * g.Height
	if g.RevealedCount == total-g.MineCount {
		g.Result = &MinesweeperResult{Won: true}
	}
}

// ToggleFlag ставит/снимает флаг на скрытой клетке.
func (g *MinesweeperGameState) ToggleFlag(pos MinesweeperPos) error {
	if g.Result != nil {
		return ErrMinesweeperGameOver
	}
	if !g.inBounds(pos) {
		return ErrMinesweeperOutOfBounds
	}
	cell := &g.Board[pos.Row][pos.Col]
	if cell.Revealed {
		return ErrMinesweeperAlreadyOpen
	}
	if cell.Flagged {
		cell.Flagged = false
		g.FlagCount--
	} else {
		cell.Flagged = true
		g.FlagCount++
	}
	return nil
}

// MinesRemaining — сколько мин, по мнению счётчика, ещё не
// помечено флагом (может уйти в минус при перефлаживании — это
// нормально и так же ведёт себя в привычных версиях игры).
func (g *MinesweeperGameState) MinesRemaining() int {
	return g.MineCount - g.FlagCount
}
