package scrabble

import (
	"errors"
	"fmt"
	"sort"

	dawg "github.com/iliadenisov/dafsa"

	"scrabble-solver/board"
	"scrabble-solver/rack"
	"scrabble-solver/rules"
)

// Solver is the high-level entry point: it generates ranked plays and scores or
// validates arbitrary plays for a ruleset over a dictionary.
type Solver struct {
	rules  *rules.Ruleset
	finder dawg.Finder
	gen    *DAWGGenerator
}

// NewSolver returns a Solver for the ruleset over the dictionary finder.
func NewSolver(rs *rules.Ruleset, finder dawg.Finder) *Solver {
	return &Solver{rules: rs, finder: finder, gen: NewDAWGGenerator(rs, finder)}
}

// Rules returns the solver's ruleset.
func (s *Solver) Rules() *rules.Ruleset { return s.rules }

// GenerateMoves returns every legal play for rack r on board b in the requested
// orientations, ranked by descending score (ties broken deterministically by the move's
// canonical key).
func (s *Solver) GenerateMoves(b *board.Board, r rack.Rack, mode Mode) []Move {
	moves := s.gen.GenerateMoves(b, r, mode)
	sort.Slice(moves, func(i, j int) bool {
		if moves[i].Score != moves[j].Score {
			return moves[i].Score > moves[j].Score
		}
		return moves[i].Key() < moves[j].Key()
	})
	return moves
}

// ScorePlay computes the words and score for placing tiles on b in direction dir. It
// checks geometry only (see Evaluate); use ValidatePlay to also check the dictionary and
// connectivity.
func (s *Solver) ScorePlay(b *board.Board, dir Direction, tiles []Placement) (Move, error) {
	return Evaluate(b, s.rules, dir, tiles)
}

// ValidatePlay scores a play and verifies that every word it forms is in the dictionary
// and that it connects to the board (or covers the centre on the first move). It returns
// the scored move; the error is nil exactly when the play is legal.
func (s *Solver) ValidatePlay(b *board.Board, dir Direction, tiles []Placement) (Move, error) {
	m, err := Evaluate(b, s.rules, dir, tiles)
	if err != nil {
		return Move{}, err
	}
	if len(m.Main.Letters) < 2 {
		return m, errors.New("scrabble: play forms no word of length 2 or more")
	}
	if s.finder.IndexOfB(m.Main.Letters) < 0 {
		return m, fmt.Errorf("scrabble: main word is not in the dictionary")
	}
	for _, cw := range m.Cross {
		if s.finder.IndexOfB(cw.Letters) < 0 {
			return m, fmt.Errorf("scrabble: a cross word is not in the dictionary")
		}
	}
	if !s.connected(b, m) {
		return m, errors.New("scrabble: play does not connect to the board")
	}
	return m, nil
}

// connected reports whether the play touches the existing position (or covers the centre
// on the first move).
func (s *Solver) connected(b *board.Board, m Move) bool {
	if b.IsEmpty() {
		cr, cc := s.rules.Center/s.rules.Cols, s.rules.Center%s.rules.Cols
		return wordCovers(m.Main, cr, cc)
	}
	// The main word incorporated an existing tile, or a new tile formed a cross word.
	return len(m.Main.Letters) > len(m.Tiles) || len(m.Cross) > 0
}

func wordCovers(w Word, r, c int) bool {
	for i := range w.Letters {
		rr, cc := w.Row, w.Col
		if w.Dir == Horizontal {
			cc += i
		} else {
			rr += i
		}
		if rr == r && cc == c {
			return true
		}
	}
	return false
}
