package scrabble

import (
	"errors"
	"fmt"
	"sort"

	"gitea.iliadenisov.ru/developer/scrabble-solver/board"
	"gitea.iliadenisov.ru/developer/scrabble-solver/internal/encoding"
	"gitea.iliadenisov.ru/developer/scrabble-solver/rules"
)

// coord maps a line coordinate (fixed, axis) to a board (row, col) for direction dir.
// For Horizontal the fixed coordinate is the row and the axis runs along columns; for
// Vertical it is the reverse.
func coord(dir Direction, fixed, axis int) (row, col int) {
	if dir == Horizontal {
		return fixed, axis
	}
	return axis, fixed
}

// fixedAxis is the inverse of coord: it splits a (row, col) into the fixed and axis
// coordinates for direction dir.
func fixedAxis(dir Direction, row, col int) (fixed, axis int) {
	if dir == Horizontal {
		return row, col
	}
	return col, row
}

func perpendicular(d Direction) Direction {
	if d == Horizontal {
		return Vertical
	}
	return Horizontal
}

// Evaluate computes the words formed and the score for placing tiles on b in direction
// dir under ruleset rs. It validates geometry — the tiles lie on one line, on empty
// squares, and form a single contiguous run together with existing tiles — but does not
// check the dictionary or board connectivity; ValidatePlay layers those on top. tiles
// need not be sorted. It evaluates under standard rules; use EvaluateOpts to apply optional
// rule variations.
func Evaluate(b *board.Board, rs *rules.Ruleset, dir Direction, tiles []Placement) (Move, error) {
	return EvaluateOpts(b, rs, dir, tiles, PlayOptions{})
}

// EvaluateOpts is Evaluate with optional rule variations (PlayOptions). With
// opts.IgnoreCrossWords set, the play's perpendicular cross-words are neither formed nor
// scored: the returned move carries no Cross words and its Score counts only the main word
// (plus any all-tiles bonus).
func EvaluateOpts(b *board.Board, rs *rules.Ruleset, dir Direction, tiles []Placement, opts PlayOptions) (Move, error) {
	if len(tiles) == 0 {
		return Move{}, errors.New("scrabble: empty play")
	}

	ts := append([]Placement(nil), tiles...)
	sort.Slice(ts, func(i, j int) bool {
		_, ai := fixedAxis(dir, ts[i].Row, ts[i].Col)
		_, aj := fixedAxis(dir, ts[j].Row, ts[j].Col)
		return ai < aj
	})

	fixed, _ := fixedAxis(dir, ts[0].Row, ts[0].Col)
	prevAxis := 0
	for i, t := range ts {
		f, a := fixedAxis(dir, t.Row, t.Col)
		if f != fixed {
			return Move{}, errors.New("scrabble: tiles are not on one line")
		}
		if !b.InBounds(t.Row, t.Col) {
			return Move{}, fmt.Errorf("scrabble: tile (%d,%d) off board", t.Row, t.Col)
		}
		if !b.Empty(t.Row, t.Col) {
			return Move{}, fmt.Errorf("scrabble: square (%d,%d) is occupied", t.Row, t.Col)
		}
		if i > 0 && a == prevAxis {
			return Move{}, errors.New("scrabble: two tiles on the same square")
		}
		prevAxis = a
	}

	main, err := buildMainWord(b, rs, dir, fixed, ts)
	if err != nil {
		return Move{}, err
	}

	move := Move{Dir: dir, Tiles: ts, Main: main, Score: main.Score}
	if !opts.IgnoreCrossWords {
		for _, t := range ts {
			if cw, ok := crossWord(b, rs, dir, t); ok {
				move.Cross = append(move.Cross, cw)
				move.Score += cw.Score
			}
		}
	}
	if len(ts) == rs.RackSize {
		move.Bonus = rs.Bingo
		move.Score += rs.Bingo
	}
	return move, nil
}

// buildMainWord assembles the word along dir through the (sorted) placements together
// with the existing tiles that extend and bridge them, and scores it. New tiles apply
// their squares' premiums; existing tiles score at face value.
func buildMainWord(b *board.Board, rs *rules.Ruleset, dir Direction, fixed int, ts []Placement) (Word, error) {
	_, minA := fixedAxis(dir, ts[0].Row, ts[0].Col)
	_, maxA := fixedAxis(dir, ts[len(ts)-1].Row, ts[len(ts)-1].Col)

	start := minA
	for {
		r, c := coord(dir, fixed, start-1)
		if !b.Filled(r, c) {
			break
		}
		start--
	}
	end := maxA
	for {
		r, c := coord(dir, fixed, end+1)
		if !b.Filled(r, c) {
			break
		}
		end++
	}

	letters := make([]byte, 0, end-start+1)
	blanks := make([]bool, 0, end-start+1)
	letterSum, wordMult := 0, 1
	ti := 0
	for a := start; a <= end; a++ {
		r, c := coord(dir, fixed, a)
		if ti < len(ts) {
			if _, ta := fixedAxis(dir, ts[ti].Row, ts[ti].Col); ta == a {
				t := ts[ti]
				ti++
				prem := rs.Premium(r, c)
				if !t.Blank {
					letterSum += rs.Values[t.Letter] * prem.LetterMult()
				}
				wordMult *= prem.WordMult()
				letters = append(letters, t.Letter)
				blanks = append(blanks, t.Blank)
				continue
			}
		}
		if b.Filled(r, c) {
			cell := b.At(r, c)
			l, bl := encoding.Letter(cell), encoding.IsBlank(cell)
			if !bl {
				letterSum += rs.Values[l]
			}
			letters = append(letters, l)
			blanks = append(blanks, bl)
			continue
		}
		return Word{}, fmt.Errorf("scrabble: gap in the play at line position %d", a)
	}

	wr, wc := coord(dir, fixed, start)
	return Word{Row: wr, Col: wc, Dir: dir, Letters: letters, Blanks: blanks, Score: letterSum * wordMult}, nil
}

// crossWord builds the perpendicular word formed by a single new tile, if any. It
// returns ok=false when the tile has no perpendicular neighbour.
func crossWord(b *board.Board, rs *rules.Ruleset, dir Direction, t Placement) (Word, bool) {
	cdir := perpendicular(dir)
	fixed, axis := fixedAxis(cdir, t.Row, t.Col)

	start := axis
	for {
		r, c := coord(cdir, fixed, start-1)
		if !b.Filled(r, c) {
			break
		}
		start--
	}
	end := axis
	for {
		r, c := coord(cdir, fixed, end+1)
		if !b.Filled(r, c) {
			break
		}
		end++
	}
	if start == end {
		return Word{}, false
	}

	letters := make([]byte, 0, end-start+1)
	blanks := make([]bool, 0, end-start+1)
	letterSum, wordMult := 0, 1
	for a := start; a <= end; a++ {
		r, c := coord(cdir, fixed, a)
		if a == axis {
			prem := rs.Premium(r, c)
			if !t.Blank {
				letterSum += rs.Values[t.Letter] * prem.LetterMult()
			}
			wordMult *= prem.WordMult()
			letters = append(letters, t.Letter)
			blanks = append(blanks, t.Blank)
		} else {
			cell := b.At(r, c)
			l, bl := encoding.Letter(cell), encoding.IsBlank(cell)
			if !bl {
				letterSum += rs.Values[l]
			}
			letters = append(letters, l)
			blanks = append(blanks, bl)
		}
	}
	wr, wc := coord(cdir, fixed, start)
	return Word{Row: wr, Col: wc, Dir: cdir, Letters: letters, Blanks: blanks, Score: letterSum * wordMult}, true
}
