package main

import "errors"

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

// KlondikeCard — карта в колонне: помимо самой карты нужно знать,
// открыта она лицом вверх или ещё лежит рубашкой вверх.
type KlondikeCard struct {
	Card   Card
	FaceUp bool
}

var (
	ErrKlondikeEmptySource       = errors.New("в исходной стопке нет карт")
	ErrKlondikeNotMovable        = errors.New("эта карта (или последовательность под ней) не может быть перемещена")
	ErrKlondikeInvalidTableau    = errors.New("эту карту нельзя положить в эту колонну")
	ErrKlondikeInvalidFoundation = errors.New("эту карту нельзя положить на фундамент")
	ErrKlondikeMultiToFoundation = errors.New("на фундамент можно положить только одну карту")
	ErrKlondikeNoUndo            = errors.New("нечего отменять")
)

// KlondikeGameState — полное состояние партии пасьянса.
type KlondikeGameState struct {
	Tableau     [7][]KlondikeCard
	Foundations [4]int // индекс — Suit; значение — старший ранг на фундаменте (0 = пусто)
	Stock       []Card // рубашкой вверх; Stock[len-1] — верхняя (берётся первой)
	Waste       []Card // отбой лицом вверх; Waste[len-1] — верхняя (видимая)
	Moves       int
	Won         bool

	history []klondikeSnapshot
}

// NewKlondikeGame раздаёт новую партию: колонны по 1..7 карт (верхняя
// открыта), остаток — колода добора.
func NewKlondikeGame() *KlondikeGameState {
	deck := NewDeck()
	deck.Shuffle()

	g := &KlondikeGameState{}
	for col := 0; col < 7; col++ {
		for row := 0; row <= col; row++ {
			card, _ := deck.Draw()
			g.Tableau[col] = append(g.Tableau[col], KlondikeCard{Card: card, FaceUp: row == col})
		}
	}
	g.Stock = deck.cards
	return g
}

func isRedSuit(s Suit) bool {
	return s == Hearts || s == Diamonds
}

// movableRun возвращает длину наибольшей корректно собранной (по
// убыванию, с чередованием цвета) последовательности лицевых карт в
// конце (внизу) колонны col — именно столько карт можно перемещать
// вместе одним ходом. Если конец колонны не образует такой
// последовательности, движется только самая нижняя (видимая) карта.
func (g *KlondikeGameState) movableRun(col int) int {
	cards := g.Tableau[col]
	n := 0
	for i := len(cards) - 1; i >= 0; i-- {
		if !cards[i].FaceUp {
			break
		}
		if n == 0 {
			n = 1
			continue
		}
		below := cards[i]
		above := cards[i+1]
		if int(below.Card.Rank) == int(above.Card.Rank)+1 && isRedSuit(below.Card.Suit) != isRedSuit(above.Card.Suit) {
			n++
		} else {
			break
		}
	}
	return n
}

// klondikeSnapshot — полная копия состояния для отмены хода.
type klondikeSnapshot struct {
	tableau     [7][]KlondikeCard
	foundations [4]int
	stock       []Card
	waste       []Card
	moves       int
}

func (g *KlondikeGameState) snapshot() klondikeSnapshot {
	var s klondikeSnapshot
	for i := range g.Tableau {
		s.tableau[i] = append([]KlondikeCard{}, g.Tableau[i]...)
	}
	s.foundations = g.Foundations
	s.stock = append([]Card{}, g.Stock...)
	s.waste = append([]Card{}, g.Waste...)
	s.moves = g.Moves
	return s
}

func (g *KlondikeGameState) pushHistory() {
	g.history = append(g.history, g.snapshot())
	if len(g.history) > 200 {
		g.history = g.history[1:]
	}
}

// Undo отменяет последний совершённый ход.
func (g *KlondikeGameState) Undo() error {
	if len(g.history) == 0 {
		return ErrKlondikeNoUndo
	}
	last := g.history[len(g.history)-1]
	g.history = g.history[:len(g.history)-1]
	g.Tableau = last.tableau
	g.Foundations = last.foundations
	g.Stock = last.stock
	g.Waste = last.waste
	g.Moves = last.moves
	g.Won = false
	return nil
}

// DrawStock переносит верхнюю карту колоды добора в отбой лицом
// вверх; если колода добора пуста — перекладывает весь отбой
// обратно в колоду (в обратном порядке) для новой пересдачи.
func (g *KlondikeGameState) DrawStock() error {
	if len(g.Stock) == 0 && len(g.Waste) == 0 {
		return ErrKlondikeEmptySource
	}
	g.pushHistory()
	if len(g.Stock) == 0 {
		for i, j := 0, len(g.Waste)-1; i < j; i, j = i+1, j-1 {
			g.Waste[i], g.Waste[j] = g.Waste[j], g.Waste[i]
		}
		g.Stock = g.Waste
		g.Waste = nil
		return nil
	}
	card := g.Stock[len(g.Stock)-1]
	g.Stock = g.Stock[:len(g.Stock)-1]
	g.Waste = append(g.Waste, card)
	g.Moves++
	return nil
}

// canPlaceOnTableau проверяет, можно ли положить card на верхнюю
// (видимую) карту колонны col (или в пустую колонну — только
// король).
func (g *KlondikeGameState) canPlaceOnTableau(col int, card Card) bool {
	cards := g.Tableau[col]
	if len(cards) == 0 {
		return card.Rank == King
	}
	top := cards[len(cards)-1]
	if !top.FaceUp {
		return false
	}
	return int(card.Rank)+1 == int(top.Card.Rank) && isRedSuit(card.Suit) != isRedSuit(top.Card.Suit)
}

// canPlaceOnFoundation проверяет, можно ли положить card на
// фундамент её масти прямо сейчас.
func (g *KlondikeGameState) canPlaceOnFoundation(card Card) bool {
	return int(card.Rank) == g.Foundations[card.Suit]+1
}

func (g *KlondikeGameState) flipTopIfNeeded(col int) {
	cards := g.Tableau[col]
	if len(cards) > 0 && !cards[len(cards)-1].FaceUp {
		g.Tableau[col][len(cards)-1].FaceUp = true
	}
}

func (g *KlondikeGameState) checkWin() {
	for _, r := range g.Foundations {
		if r != int(King) {
			return
		}
	}
	g.Won = true
}

// MoveWasteToTableau перекладывает верхнюю карту отбоя в колонну col.
func (g *KlondikeGameState) MoveWasteToTableau(col int) error {
	if len(g.Waste) == 0 {
		return ErrKlondikeEmptySource
	}
	card := g.Waste[len(g.Waste)-1]
	if !g.canPlaceOnTableau(col, card) {
		return ErrKlondikeInvalidTableau
	}
	g.pushHistory()
	g.Waste = g.Waste[:len(g.Waste)-1]
	g.Tableau[col] = append(g.Tableau[col], KlondikeCard{Card: card, FaceUp: true})
	g.Moves++
	return nil
}

// MoveWasteToFoundation перекладывает верхнюю карту отбоя на
// фундамент её масти.
func (g *KlondikeGameState) MoveWasteToFoundation() error {
	if len(g.Waste) == 0 {
		return ErrKlondikeEmptySource
	}
	card := g.Waste[len(g.Waste)-1]
	if !g.canPlaceOnFoundation(card) {
		return ErrKlondikeInvalidFoundation
	}
	g.pushHistory()
	g.Waste = g.Waste[:len(g.Waste)-1]
	g.Foundations[card.Suit] = int(card.Rank)
	g.Moves++
	g.checkWin()
	return nil
}

// MoveTableauToFoundation перекладывает верхнюю (видимую) карту
// колонны from на фундамент её масти.
func (g *KlondikeGameState) MoveTableauToFoundation(from int) error {
	cards := g.Tableau[from]
	if len(cards) == 0 {
		return ErrKlondikeEmptySource
	}
	top := cards[len(cards)-1]
	if !top.FaceUp {
		return ErrKlondikeNotMovable
	}
	if !g.canPlaceOnFoundation(top.Card) {
		return ErrKlondikeInvalidFoundation
	}
	g.pushHistory()
	g.Tableau[from] = cards[:len(cards)-1]
	g.Foundations[top.Card.Suit] = int(top.Card.Rank)
	g.flipTopIfNeeded(from)
	g.Moves++
	g.checkWin()
	return nil
}

// MoveTableauToTableau перемещает наибольшую корректную
// последовательность в конце колонны from в конец колонны to.
// MoveTableauToTableau переносит ВСЮ корректно собранную
// последовательность в конце колонны from в колонну to (см.
// movableRun). Чтобы перенести только ЧАСТЬ последовательности,
// используйте MoveTableauToTableauN с явным количеством карт.
func (g *KlondikeGameState) MoveTableauToTableau(from, to int) error {
	if g.movableRun(from) == 0 {
		return ErrKlondikeEmptySource
	}
	return g.MoveTableauToTableauN(from, to, g.movableRun(from))
}

// MoveTableauToTableauN переносит ровно count карт с конца
// (снизу, лицевой стороной вверх) колонны from в колонну to. count
// может быть меньше полной длины корректной последовательности
// (movableRun) — любой "хвост" корректной последовательности сам по
// себе тоже корректная последовательность, так что достаточно
// проверить, что count в допустимых пределах и что верхняя (первая)
// из переносимых карт подходит по правилам на колонну to.
func (g *KlondikeGameState) MoveTableauToTableauN(from, to, count int) error {
	if from == to {
		return ErrKlondikeInvalidTableau
	}
	maxRun := g.movableRun(from)
	if count < 1 || count > maxRun {
		return ErrKlondikeInvalidTableau
	}
	cards := g.Tableau[from]
	moving := cards[len(cards)-count:]
	if !g.canPlaceOnTableau(to, moving[0].Card) {
		return ErrKlondikeInvalidTableau
	}

	g.pushHistory()
	movingCopy := append([]KlondikeCard{}, moving...)
	g.Tableau[from] = cards[:len(cards)-count]
	g.Tableau[to] = append(g.Tableau[to], movingCopy...)
	g.flipTopIfNeeded(from)
	g.Moves++
	return nil
}
