// Package rules describes a Scrabble variant: board geometry, premium-square layout,
// the letter alphabet, per-letter tile values and bag counts, blanks, rack size and
// the all-tiles bonus. English() returns standard English Scrabble; the Ruleset type
// is general enough for other variants such as Russian "Эрудит" (same board, different
// tile values/counts and alphabet).
package rules

import (
	"fmt"
	"strings"

	"github.com/iliadenisov/alphabet"
)

// Premium is the bonus kind of a board square.
type Premium uint8

const (
	None Premium = iota
	DL           // double letter
	TL           // triple letter
	DW           // double word
	TW           // triple word
)

// LetterMult is the multiplier a premium applies to the tile placed on it.
func (p Premium) LetterMult() int {
	switch p {
	case DL:
		return 2
	case TL:
		return 3
	default:
		return 1
	}
}

// WordMult is the multiplier a premium applies to a word passing through it.
func (p Premium) WordMult() int {
	switch p {
	case DW:
		return 2
	case TW:
		return 3
	default:
		return 1
	}
}

// Ruleset is a complete description of a Scrabble variant.
type Ruleset struct {
	Name       string
	Rows, Cols int
	Alphabet   alphabet.Indexer // letter alphabet (no separator)
	Values     []int            // tile value per letter index; len == Alphabet.Size()
	Counts     []int            // bag count per letter index; len == Alphabet.Size()
	Blanks     int              // number of blank tiles in the bag
	RackSize   int              // tiles drawn to a full rack
	Bingo      int              // bonus for using the whole rack in one play
	Center     int              // row-major index of the centre square (first-move anchor)
	premiums   []Premium        // row-major premium per square
}

// Premium returns the premium of square (r, c).
func (rs *Ruleset) Premium(r, c int) Premium { return rs.premiums[r*rs.Cols+c] }

// PremiumAt returns the premium of the row-major square index i.
func (rs *Ruleset) PremiumAt(i int) Premium { return rs.premiums[i] }

// Size returns the number of letters in the alphabet (excluding blanks).
func (rs *Ruleset) Size() int { return rs.Alphabet.Size() }

// Validate checks that the slices are consistent with the alphabet and board.
func (rs *Ruleset) Validate() error {
	n := rs.Alphabet.Size()
	if len(rs.Values) != n {
		return fmt.Errorf("rules %q: %d values for %d letters", rs.Name, len(rs.Values), n)
	}
	if len(rs.Counts) != n {
		return fmt.Errorf("rules %q: %d counts for %d letters", rs.Name, len(rs.Counts), n)
	}
	if len(rs.premiums) != rs.Rows*rs.Cols {
		return fmt.Errorf("rules %q: %d premiums for a %dx%d board", rs.Name, len(rs.premiums), rs.Rows, rs.Cols)
	}
	if rs.Center < 0 || rs.Center >= rs.Rows*rs.Cols {
		return fmt.Errorf("rules %q: centre %d out of range", rs.Name, rs.Center)
	}
	return nil
}

// standardBoard is the classic 15x15 premium layout: T=triple word, D=double word,
// t=triple letter, d=double letter, .=plain, *=centre (a double word).
const standardBoard = `T..d...T...d..T
.D...t...t...D.
..D...d.d...D..
d..D...d...D..d
....D.....D....
.t...t...t...t.
..d...d.d...d..
T..d...*...d..T
..d...d.d...d..
.t...t...t...t.
....D.....D....
d..D...d...D..d
..D...d.d...D..
.D...t...t...D.
T..d...T...d..T`

// parsePremiums turns a board template into a premium grid and the centre index.
func parsePremiums(s string) (rows, cols int, prem []Premium, center int) {
	lines := strings.Split(strings.TrimSpace(s), "\n")
	rows = len(lines)
	cols = len(strings.TrimRight(lines[0], "\r"))
	prem = make([]Premium, rows*cols)
	center = -1
	for r, line := range lines {
		line = strings.TrimRight(line, "\r")
		for c := 0; c < cols && c < len(line); c++ {
			var p Premium
			switch line[c] {
			case 'd':
				p = DL
			case 't':
				p = TL
			case 'D':
				p = DW
			case 'T':
				p = TW
			case '*': // centre square, a double word
				p = DW
				center = r*cols + c
			case '+': // centre square with no premium
				center = r*cols + c
			}
			prem[r*cols+c] = p
		}
	}
	return rows, cols, prem, center
}

// FromTemplate builds a ruleset from a premium-layout template (see standardBoard for
// the character legend; '+' marks a centre square with no premium). It returns an error
// if the resulting ruleset is inconsistent.
func FromTemplate(name string, idx alphabet.Indexer, values, counts []int, blanks, rackSize, bingo int, template string) (*Ruleset, error) {
	rows, cols, prem, center := parsePremiums(template)
	rs := &Ruleset{
		Name: name, Rows: rows, Cols: cols, Alphabet: idx,
		Values: values, Counts: counts,
		Blanks: blanks, RackSize: rackSize, Bingo: bingo,
		Center: center, premiums: prem,
	}
	if err := rs.Validate(); err != nil {
		return nil, err
	}
	return rs, nil
}

// English returns the standard English Scrabble ruleset (15x15, the classic premium
// layout, English tile values and distribution, 2 blanks, a 7-tile rack and a 50-point
// bingo bonus).
func English() *Ruleset {
	rs, err := FromTemplate("English Scrabble", alphabet.Latin(),
		//    a  b  c  d  e   f  g  h  i  j  k  l  m  n  o  p   q  r  s  t  u  v  w  x  y   z
		[]int{1, 3, 3, 2, 1, 4, 2, 4, 1, 8, 5, 1, 3, 1, 1, 3, 10, 1, 1, 1, 1, 4, 4, 8, 4, 10},
		[]int{9, 2, 2, 4, 12, 2, 3, 2, 9, 1, 1, 4, 2, 6, 8, 2, 1, 6, 4, 6, 4, 2, 2, 1, 2, 1},
		2, 7, 50, standardBoard)
	if err != nil {
		panic(err) // a programming error in this package, not a runtime condition
	}
	return rs
}

// eruditBoard is the standard 15x15 layout but with a non-doubling centre ('+'), as in
// the Russian "Эрудит" variant.
const eruditBoard = `T..d...T...d..T
.D...t...t...D.
..D...d.d...D..
d..D...d...D..d
....D.....D....
.t...t...t...t.
..d...d.d...d..
T..d...+...d..T
..d...d.d...d..
.t...t...t...t.
....D.....D....
d..D...d...D..d
..D...d.d...D..
.D...t...t...D.
T..d...T...d..T`

// russian returns the embedded 33-letter Russian alphabet (а..я including ё at index 6).
func russian() alphabet.Indexer { return alphabet.Embedded(alphabet.Langs.LangRu) }

// RussianScrabble returns the Russian Scrabble ruleset: the 33-letter alphabet, the
// standard board, 2 blanks, a 7-tile rack and a 50-point bonus.
func RussianScrabble() *Ruleset {
	rs, err := FromTemplate("Russian Scrabble", russian(),
		//    а  б  в  г  д  е  ё  ж  з  и  й  к  л  м  н  о  п  р  с  т  у   ф  х  ц  ч  ш   щ   ъ  ы  ь  э  ю  я
		[]int{1, 3, 1, 3, 2, 1, 3, 5, 5, 1, 4, 2, 2, 2, 1, 1, 2, 1, 1, 1, 2, 10, 5, 5, 5, 8, 10, 10, 4, 3, 8, 8, 3},
		[]int{8, 2, 4, 2, 4, 8, 1, 1, 2, 5, 1, 4, 4, 3, 5, 10, 4, 5, 5, 5, 4, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 2},
		2, 7, 50, standardBoard)
	if err != nil {
		panic(err)
	}
	return rs
}

// Erudit returns the Russian "Эрудит" ruleset. Ё carries no tiles (count 0); fold Ё→Е
// when preparing the dictionary (see wordlist.FoldYo). The centre square does not double
// the word, there are 3 blanks (each scoring 0), and the all-tiles bonus is 15.
func Erudit() *Ruleset {
	rs, err := FromTemplate("Эрудит", russian(),
		//    а  б  в  г  д  е  ё  ж  з  и  й  к  л  м  н  о  п  р  с  т  у   ф  х   ц  ч   ш   щ   ъ   ы  ь   э   ю  я
		[]int{1, 3, 2, 3, 2, 1, 0, 5, 5, 1, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 3, 10, 5, 10, 5, 10, 10, 10, 5, 5, 10, 10, 3},
		[]int{10, 3, 5, 3, 5, 9, 0, 2, 2, 8, 4, 6, 4, 5, 8, 10, 6, 6, 6, 5, 3, 1, 2, 1, 2, 1, 1, 1, 2, 2, 1, 1, 3},
		3, 7, 15, eruditBoard)
	if err != nil {
		panic(err)
	}
	return rs
}
