package scrabble

import (
	dawg "github.com/iliadenisov/dafsa"

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

// DAWGGenerator generates moves with the Appel-Jacobson two-phase algorithm
// (LeftPart then ExtendRight) over a plain left-to-right DAWG.
type DAWGGenerator struct {
	rules  *rules.Ruleset
	finder dawg.Finder
}

// NewDAWGGenerator builds a DAWG generator for the ruleset over the dictionary finder.
func NewDAWGGenerator(rs *rules.Ruleset, finder dawg.Finder) *DAWGGenerator {
	return &DAWGGenerator{rules: rs, finder: finder}
}

// Name identifies the generator.
func (g *DAWGGenerator) Name() string { return "dawg" }

// GenerateMoves returns every legal play for rk on b in the modes' orientations under
// standard rules. It satisfies the Generator interface; GenerateMovesOpts adds optional
// rule variations.
func (g *DAWGGenerator) GenerateMoves(b *board.Board, rk rack.Rack, mode Mode) []Move {
	return g.GenerateMovesOpts(b, rk, mode, PlayOptions{})
}

// GenerateMovesOpts is GenerateMoves with optional rule variations (PlayOptions). With
// opts.IgnoreCrossWords the cross-sets are unconstrained (every letter that extends the
// main word is allowed) and each move is scored by its main word only.
func (g *DAWGGenerator) GenerateMovesOpts(b *board.Board, rk rack.Rack, mode Mode, opts PlayOptions) []Move {
	return generateBoth(b, g.rules, rk, mode, opts, g.runAcross)
}

// tileInfo is a tentatively placed left-part tile (its column is fixed only once the
// left part's length is known, at record time).
type tileInfo struct {
	letter byte
	blank  bool
}

// acrossGen carries the state of one across-generation pass over a board.
type acrossGen struct {
	bd    *board.Board
	cur   *dawg.Cursor
	rs    *rules.Ruleset
	rk    rack.Rack
	size  int
	cross func(r, c int) letterSet
	emit  func(placements []Placement) // placements in bd's coordinates

	row   int
	left  []tileInfo  // left-part tiles, in word (left-to-right) order
	right []Placement // right-part tiles, with their columns
}

// runAcross generates all across plays on bd (cross-sets are computed as vertical words
// on bd) and reports each via emit in bd's coordinates. With opts.IgnoreCrossWords the
// cross-sets are unconstrained, so any letter that extends the main word may be placed.
func (g *DAWGGenerator) runAcross(bd *board.Board, rk rack.Rack, opts PlayOptions, emit func([]Placement)) {
	cur, err := dawg.NewCursor(g.finder)
	if err != nil {
		return
	}
	size := g.rules.Size()

	var crossFn func(r, c int) letterSet
	if opts.IgnoreCrossWords {
		full := fullSet(size)
		crossFn = func(int, int) letterSet { return full }
	} else {
		cross := make([]letterSet, bd.Rows()*bd.Cols())
		known := make([]bool, bd.Rows()*bd.Cols())
		crossFn = func(r, c int) letterSet {
			i := r*bd.Cols() + c
			if !known[i] {
				above, below := columnContext(bd, r, c)
				cross[i] = dawgCrossSet(cur, above, below, size)
				known[i] = true
			}
			return cross[i]
		}
	}

	ag := &acrossGen{bd: bd, cur: cur, rs: g.rules, rk: rk, size: size, cross: crossFn, emit: emit}

	firstMove := bd.IsEmpty()
	centerRow, centerCol := centerFor(bd, g.rules)
	for row := range bd.Rows() {
		ag.generateRow(row, firstMove, centerRow, centerCol)
	}
}

func (g *acrossGen) generateRow(row int, firstMove bool, centerRow, centerCol int) {
	g.row = row
	limit := 0
	for col := range g.bd.Cols() {
		if !g.bd.Empty(row, col) {
			limit = 0
			continue
		}
		anchor := false
		if firstMove {
			anchor = row == centerRow && col == centerCol
		} else {
			anchor = g.hasFilledNeighbor(row, col)
		}
		if !anchor {
			limit++
			continue
		}
		g.left = g.left[:0]
		g.right = g.right[:0]
		if col > 0 && g.bd.Filled(row, col-1) {
			if node, ok := g.walkPrefix(row, col); ok {
				g.extendRight(node, col, col)
			}
		} else {
			g.leftPart(g.cur.Root(), col, limit)
		}
		limit = 0
	}
}

func (g *acrossGen) hasFilledNeighbor(r, c int) bool {
	return g.bd.Filled(r-1, c) || g.bd.Filled(r+1, c) || g.bd.Filled(r, c-1) || g.bd.Filled(r, c+1)
}

// walkPrefix walks the DAWG through the contiguous filled run ending at col-1, returning
// the node reached and whether that prefix exists in the dictionary.
func (g *acrossGen) walkPrefix(row, col int) (dawg.Node, bool) {
	start := col - 1
	for start-1 >= 0 && g.bd.Filled(row, start-1) {
		start--
	}
	node := g.cur.Root()
	for c := start; c < col; c++ {
		var ok bool
		node, _, ok = g.cur.Next(node, encoding.Letter(g.bd.At(row, c)))
		if !ok {
			return node, false
		}
	}
	return node, true
}

// leftPart places left-part tiles from the rack (up to limit, on the empty squares left
// of the anchor), calling extendRight after each prefix.
func (g *acrossGen) leftPart(node dawg.Node, anchorCol, limit int) {
	g.extendRight(node, anchorCol, anchorCol)
	if limit == 0 {
		return
	}
	g.cur.Arcs(node, func(a dawg.Arc) bool {
		l := a.Label
		if g.rk.Has(l) {
			g.rk.Remove(l)
			g.left = append(g.left, tileInfo{letter: l})
			g.leftPart(a.Dest, anchorCol, limit-1)
			g.left = g.left[:len(g.left)-1]
			g.rk.Add(l)
		}
		if g.rk.Blanks() > 0 {
			g.rk.RemoveBlank()
			g.left = append(g.left, tileInfo{letter: l, blank: true})
			g.leftPart(a.Dest, anchorCol, limit-1)
			g.left = g.left[:len(g.left)-1]
			g.rk.AddBlank()
		}
		return true
	})
}

// extendRight extends the word rightward from col, placing rack tiles on empty squares
// (constrained by cross-sets) and following tiles already on the board. A word is
// recorded only past the anchor, so the play covers the anchor square.
func (g *acrossGen) extendRight(node dawg.Node, col, anchorCol int) {
	if col >= g.bd.Cols() {
		if col > anchorCol && g.cur.Final(node) {
			g.record(anchorCol)
		}
		return
	}
	if !g.bd.Empty(g.row, col) {
		if dest, _, ok := g.cur.Next(node, encoding.Letter(g.bd.At(g.row, col))); ok {
			g.extendRight(dest, col+1, anchorCol)
		}
		return
	}

	if col > anchorCol && g.cur.Final(node) {
		g.record(anchorCol)
	}
	cross := g.cross(g.row, col)
	g.cur.Arcs(node, func(a dawg.Arc) bool {
		l := a.Label
		if !cross.has(l) {
			return true
		}
		if g.rk.Has(l) {
			g.rk.Remove(l)
			g.right = append(g.right, Placement{Row: g.row, Col: col, Letter: l})
			g.extendRight(a.Dest, col+1, anchorCol)
			g.right = g.right[:len(g.right)-1]
			g.rk.Add(l)
		}
		if g.rk.Blanks() > 0 {
			g.rk.RemoveBlank()
			g.right = append(g.right, Placement{Row: g.row, Col: col, Letter: l, Blank: true})
			g.extendRight(a.Dest, col+1, anchorCol)
			g.right = g.right[:len(g.right)-1]
			g.rk.AddBlank()
		}
		return true
	})
}

// record assembles the play's placements (left part at fixed columns, then the right
// part) and reports it. It skips plays that lay no new tile.
func (g *acrossGen) record(anchorCol int) {
	if len(g.left)+len(g.right) == 0 {
		return
	}
	placements := make([]Placement, 0, len(g.left)+len(g.right))
	leftStart := anchorCol - len(g.left)
	for i, t := range g.left {
		placements = append(placements, Placement{Row: g.row, Col: leftStart + i, Letter: t.letter, Blank: t.blank})
	}
	placements = append(placements, g.right...)
	g.emit(placements)
}
