package game

import (
	"fmt"
	"strconv"
	"strings"
)

// writeGCG renders a game as GCG text in the standard (Poslfit) dialect, plus
// #note lines for resignations and timeouts, which the standard does not cover.
// It is derived entirely from the decoded journal, so it needs no dictionary
// (docs/ARCHITECTURE.md §9.1). names supplies each seat's display name; the
// GCG nicknames are p1, p2, … .
func writeGCG(g Game, names []string, moves []HistoryMove) string {
	var b strings.Builder
	fmt.Fprintln(&b, "#character-encoding UTF-8")
	for seat := 0; seat < g.Players; seat++ {
		fmt.Fprintf(&b, "#player%d %s %s\n", seat+1, nick(seat), playerName(names, seat))
	}
	fmt.Fprintf(&b, "#lexicon %s/%s\n", g.Variant, g.DictVersion)
	fmt.Fprintf(&b, "#title game %s\n", g.ID)

	for _, mv := range moves {
		rack := gcgTiles(mv.Rack)
		switch mv.Action {
		case "play":
			fmt.Fprintf(&b, ">%s: %s %s %s +%d %d\n",
				nick(mv.Seat), rack, gcgPos(mv), gcgWord(mv), mv.Score, mv.RunningTotal)
		case "pass":
			fmt.Fprintf(&b, ">%s: %s - +0 %d\n", nick(mv.Seat), rack, mv.RunningTotal)
		case "exchange":
			fmt.Fprintf(&b, ">%s: %s -%s +0 %d\n", nick(mv.Seat), rack, gcgTiles(mv.Exchanged), mv.RunningTotal)
		case "resign":
			fmt.Fprintf(&b, "#note %s resigned (rack %s)\n", nick(mv.Seat), rack)
		case "timeout":
			fmt.Fprintf(&b, "#note %s timed out (rack %s)\n", nick(mv.Seat), rack)
		}
	}
	// An aborted game ends in a draw because it could no longer be reconstructed; record it
	// as an impersonal organizer note (free-text #note; GCG readers ignore pragmas).
	if g.EndReason == "aborted" {
		fmt.Fprintln(&b, "#note [organizer] game could not be continued and was ended in a draw")
	}
	return b.String()
}

// nick is the GCG nickname for a seat: p1, p2, … (space-free, as GCG requires).
func nick(seat int) string { return "p" + strconv.Itoa(seat+1) }

// playerName returns the display name for a seat, or a generic fallback.
func playerName(names []string, seat int) string {
	if seat < len(names) && names[seat] != "" {
		return names[seat]
	}
	return "Player " + strconv.Itoa(seat+1)
}

// gcgTiles renders a rack or exchanged set in GCG form: upper-cased letters with
// "?" for a blank.
func gcgTiles(tiles []string) string {
	var b strings.Builder
	for _, t := range tiles {
		if t == "?" {
			b.WriteByte('?')
			continue
		}
		b.WriteString(strings.ToUpper(t))
	}
	return b.String()
}

// gcgPos renders a play's board coordinate: row-then-column (e.g. 8G) for an
// across play, column-then-row (e.g. H8) for a down play. Rows are 1-based and
// columns are lettered from A.
func gcgPos(mv HistoryMove) string {
	col := string(rune('A' + mv.MainCol))
	row := strconv.Itoa(mv.MainRow + 1)
	if mv.Dir == "V" {
		return col + row
	}
	return row + col
}

// gcgWord renders the main word: each cell along it is the newly-placed tile's
// letter (lower-cased for a blank, upper-cased otherwise) or "." for a tile
// already on the board.
func gcgWord(mv HistoryMove) string {
	placed := make(map[[2]int]tileLetter, len(mv.Tiles))
	for _, t := range mv.Tiles {
		placed[[2]int{t.Row, t.Col}] = tileLetter{letter: t.Letter, blank: t.Blank}
	}
	var word string
	if len(mv.Words) > 0 {
		word = mv.Words[0]
	}
	n := len([]rune(word))
	var b strings.Builder
	for i := range n {
		row, col := mv.MainRow, mv.MainCol
		if mv.Dir == "V" {
			row += i
		} else {
			col += i
		}
		t, ok := placed[[2]int{row, col}]
		if !ok {
			b.WriteByte('.')
			continue
		}
		if t.blank {
			b.WriteString(strings.ToLower(t.letter))
		} else {
			b.WriteString(strings.ToUpper(t.letter))
		}
	}
	return b.String()
}

// tileLetter is a placed tile's concrete letter and blank flag, keyed by cell in
// gcgWord.
type tileLetter struct {
	letter string
	blank  bool
}
