package main

import (
	"fmt"
	"math/rand"
	"strings"
	"time"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

// Physics tuned so per-frame displacement stays around one row, which keeps
// motion smooth and the 7-row gap threadable at 30fps.
const (
	fps         = 30
	gravity     = 0.09  // rows/frame^2
	jumpVel     = -0.95 // rows/frame; peak rise ≈ v²/2g ≈ 5 rows over ~0.35s
	maxFall     = 1.15  // terminal velocity, rows/frame — never skip >1 row
	pipeSpeed   = 0.5   // cells/frame → 15 cells/s
	pipeGap     = 7
	pipeSpacing = 28 // cells between pipes → one pipe ≈ every 1.9s
	pipeWidth   = 3
	groundH     = 2
)

// tickMsg advances the simulation. gen guards against stale tick chains:
// without it, restarting can leave an old chain in flight and the game
// silently runs at double speed.
type tickMsg struct{ gen int }

func tick(gen int) tea.Cmd {
	return tea.Tick(time.Second/fps, func(time.Time) tea.Msg {
		return tickMsg{gen: gen}
	})
}

type pipe struct {
	x      float64
	gapY   int
	scored bool
}

type star struct{ x, y int }

type model struct {
	termW, termH int
	width        int // field width in cells
	height       int // field height in cells (terminal minus header+footer)
	birdX        int
	birdY        float64
	birdVel      float64
	pipes        []pipe
	stars        []star
	score        int
	best         int
	gen          int // tick generation
	gameOver     bool
	started      bool
	resized      bool
}

func newGame(best, gen int, termW, termH int) model {
	m := model{best: best, gen: gen}
	if termW > 0 && termH > 0 {
		m.applySize(termW, termH)
	}
	return m
}

// applySize records the terminal size, sizes the field, and rebuilds the
// static starfield. The starfield must be stable between frames — rolling
// new random speckles every render is what caused full-screen flicker.
func (m *model) applySize(w, h int) {
	m.termW, m.termH = w, h
	m.width = w
	fieldH := h - 2 // one row header, one row footer
	if fieldH < 8 {
		fieldH = 8
	}
	m.height = fieldH
	m.birdX = w / 4
	if m.birdX < 4 {
		m.birdX = 4
	}
	if !m.resized {
		m.birdY = float64(m.height-groundH) / 2
		m.resized = true
	}
	// keep the bird inside the new bounds after a resize
	floor := float64(m.height - groundH - 1)
	if m.birdY > floor {
		m.birdY = floor
	}
	// static starfield, ~1 speckle per 40 sky cells
	skyH := m.height - groundH
	m.stars = m.stars[:0]
	n := m.width * skyH / 40
	for i := 0; i < n; i++ {
		m.stars = append(m.stars, star{x: rand.Intn(m.width), y: rand.Intn(skyH)})
	}
}

func (m model) Init() tea.Cmd { return nil }

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.applySize(msg.Width, msg.Height)
		return m, nil

	case tea.KeyMsg:
		switch msg.String() {
		case " ", "space", "enter", "up", "k":
			if m.gameOver {
				// restart: bump generation so any in-flight tick is ignored
				nm := newGame(m.best, m.gen+1, m.termW, m.termH)
				nm.started = true
				nm.birdVel = jumpVel // start with a flap, like the original
				return nm, tick(nm.gen)
			}
			if !m.started {
				m.started = true
				m.birdVel = jumpVel
				return m, tick(m.gen)
			}
			m.birdVel = jumpVel
		case "q", "esc", "ctrl+c":
			return m, tea.Quit
		}

	case tickMsg:
		if msg.gen != m.gen {
			return m, nil // stale chain — drop it
		}
		if !m.started || m.gameOver {
			return m, nil // stop the loop; nothing animates on end screens
		}
		if m.resized {
			m.step()
		}
		return m, tick(m.gen)
	}
	return m, nil
}

func (m *model) step() {
	m.birdVel += gravity
	if m.birdVel > maxFall {
		m.birdVel = maxFall
	}
	m.birdY += m.birdVel

	// ceiling clamp
	if m.birdY < 0 {
		m.birdY = 0
		m.birdVel = 0
	}
	// ground collision
	floor := float64(m.height - groundH - 1)
	if m.birdY >= floor {
		m.birdY = floor
		m.gameOver = true
	}

	// advance pipes
	for i := range m.pipes {
		m.pipes[i].x -= pipeSpeed
	}
	for len(m.pipes) > 0 && m.pipes[0].x+pipeWidth < 0 {
		m.pipes = m.pipes[1:]
	}
	// spawn pipes
	usableH := m.height - groundH - pipeGap - 4
	if usableH < 1 {
		usableH = 1
	}
	if len(m.pipes) == 0 || m.pipes[len(m.pipes)-1].x < float64(m.width-pipeSpacing) {
		gapY := rand.Intn(usableH) + 2
		m.pipes = append(m.pipes, pipe{x: float64(m.width), gapY: gapY})
	}

	by := int(m.birdY)
	for i := range m.pipes {
		p := &m.pipes[i]
		px := int(p.x)
		// score once the pipe's trailing edge passes the bird
		if !p.scored && px+pipeWidth <= m.birdX {
			p.scored = true
			m.score++
			if m.score > m.best {
				m.best = m.score
			}
		}
		// collision with pipe body
		if m.birdX >= px && m.birdX < px+pipeWidth {
			if by < p.gapY || by > p.gapY+pipeGap {
				m.gameOver = true
			}
		}
	}
}

// styles ----------------------------------------------------------------------

var (
	birdStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("220")).Bold(true)
	birdUpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("226")).Bold(true)
	birdDnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true)
	pipeStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("34"))
	pipeHi      = lipgloss.NewStyle().Foreground(lipgloss.Color("46"))
	capStyle    = lipgloss.NewStyle().Foreground(lipgloss.Color("36"))
	skyStyle    = lipgloss.NewStyle().Foreground(lipgloss.Color("33"))
	groundStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("142"))
	dirtStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("130"))
	scoreStyle  = lipgloss.NewStyle().Foreground(lipgloss.Color("15")).Bold(true)
	bestStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true)
	overStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Bold(true)
	hintStyle   = lipgloss.NewStyle().Foreground(lipgloss.Color("245"))
	titleStyle  = lipgloss.NewStyle().
			Foreground(lipgloss.Color("220")).
			Background(lipgloss.Color("23")).
			Bold(true)
)

func birdGlyph(vel float64) (string, lipgloss.Style) {
	switch {
	case vel < -0.3:
		return "➚", birdUpStyle
	case vel > 0.5:
		return "➘", birdDnStyle
	default:
		return "➙", birdStyle
	}
}

func (m model) View() string {
	if !m.resized {
		return hintStyle.Render(" sizing terminal… ")
	}
	width, height := m.width, m.height
	grid := make([][]string, height)
	for i := range grid {
		grid[i] = make([]string, width)
		for j := range grid[i] {
			grid[i][j] = " "
		}
	}

	// static sky speckles (precomputed — stable between frames)
	for _, s := range m.stars {
		if s.y < height-groundH && s.x < width {
			grid[s.y][s.x] = skyStyle.Render("·")
		}
	}

	// pipes
	for _, p := range m.pipes {
		px := int(p.x)
		topCap := p.gapY - 1
		botCap := p.gapY + pipeGap + 1
		for x := px; x < px+pipeWidth && x < width; x++ {
			if x < 0 {
				continue
			}
			for y := 0; y < height-groundH; y++ {
				if y < p.gapY || y > p.gapY+pipeGap {
					if y == topCap || y == botCap {
						grid[y][x] = capStyle.Render("▓")
					} else if x == px {
						grid[y][x] = pipeHi.Render("█")
					} else {
						grid[y][x] = pipeStyle.Render("█")
					}
				}
			}
		}
	}

	// ground
	for y := height - groundH; y < height; y++ {
		for x := 0; x < width; x++ {
			if y == height-groundH {
				grid[y][x] = groundStyle.Render("▔")
			} else {
				grid[y][x] = dirtStyle.Render("▓")
			}
		}
	}

	// bird
	by := int(m.birdY)
	if by >= 0 && by < height && m.birdX >= 0 && m.birdX < width {
		glyph, style := birdGlyph(m.birdVel)
		grid[by][m.birdX] = style.Render(glyph)
	}

	// overlays are written directly into the grid, cell by cell, so styled
	// field content underneath can never be corrupted by string splicing.
	if !m.started && !m.gameOver {
		m.overlayGrid(grid, []overline{
			{"  FLAPPY BIRD  ", titleStyle},
			{},
			{"press SPACE to flap", hintStyle},
		})
	} else if m.gameOver {
		m.overlayGrid(grid, []overline{
			{"✖ GAME OVER ✖", overStyle},
			{},
			{fmt.Sprintf("score %d   •   SPACE to retry", m.score), hintStyle},
		})
	}

	// compose field
	var b strings.Builder
	for _, row := range grid {
		for _, c := range row {
			b.WriteString(c)
		}
		b.WriteString("\n")
	}
	field := b.String()

	header := scoreStyle.Render(fmt.Sprintf(" SCORE %d ", m.score)) +
		"  " +
		bestStyle.Render(fmt.Sprintf("BEST %d ", m.best))
	footer := hintStyle.Render(" SPACE: flap   •   Q: quit ")

	return header + "\n" + field + footer
}

// overline is one centred line of overlay text with its style.
type overline struct {
	text  string
	style lipgloss.Style
}

// overlayGrid writes overlay lines into the middle of the grid, one styled
// cell per rune. Empty lines clear a centred band for readability.
func (m model) overlayGrid(grid [][]string, lines []overline) {
	startY := (len(grid) - len(lines)) / 2
	if startY < 0 {
		startY = 0
	}
	// widest line determines the cleared band
	maxW := 0
	for _, ol := range lines {
		if n := len([]rune(ol.text)); n > maxW {
			maxW = n
		}
	}
	for i, ol := range lines {
		y := startY + i
		if y < 0 || y >= len(grid) {
			continue
		}
		// clear a band as wide as the widest line so text sits on a clean row
		bandX := (m.width - maxW - 2) / 2
		for j := 0; j < maxW+2; j++ {
			x := bandX + j
			if x >= 0 && x < m.width {
				grid[y][x] = " "
			}
		}
		runes := []rune(ol.text)
		startX := (m.width - len(runes)) / 2
		for j, r := range runes {
			x := startX + j
			if x < 0 || x >= m.width {
				continue
			}
			grid[y][x] = ol.style.Render(string(r))
		}
	}
}

func main() {
	p := tea.NewProgram(newGame(0, 0, 0, 0), tea.WithAltScreen())
	if _, err := p.Run(); err != nil {
		fmt.Println("error running game:", err)
	}
}
