package fbdraw

import (
	"context"
	"fmt"
	"sync"
	"time"

	"git.eeqj.de/sneak/hdmistat/internal/font"
)

// Screen represents a single screen in the carousel
type Screen struct {
	Name      string
	Generator FrameGenerator
	ticker    *time.Ticker
	stop      chan struct{}
}

// Carousel manages rotating between multiple screens
type Carousel struct {
	display          FramebufferDisplay
	screens          []*Screen
	currentIndex     int
	rotationInterval time.Duration
	fontSize         float64
	gridWidth        int
	gridHeight       int

	mu     sync.Mutex
	ctx    context.Context
	cancel context.CancelFunc
	wg     sync.WaitGroup
}

const DefaultFontSize float64 = 24

// NewCarousel creates a new carousel
func NewCarousel(
	display FramebufferDisplay,
	rotationInterval time.Duration,
) *Carousel {
	ctx, cancel := context.WithCancel(context.Background())

	c := &Carousel{
		display:          display,
		screens:          make([]*Screen, 0),
		currentIndex:     0,
		rotationInterval: rotationInterval,
		fontSize:         DefaultFontSize, // Default font size
		ctx:              ctx,
		cancel:           cancel,
	}

	// Calculate grid dimensions based on font size
	if err := c.calculateGridDimensions(); err != nil {
		// Log error but continue with default dimensions
		log.Printf("Warning: failed to calculate grid dimensions: %v", err)
	}

	return c
}

// SetFontSize sets the font size and recalculates grid dimensions
func (c *Carousel) SetFontSize(size float64) error {
	c.mu.Lock()
	defer c.mu.Unlock()

	c.fontSize = size
	return c.calculateGridDimensions()
}

// calculateGridDimensions calculates grid size based on display and font
func (c *Carousel) calculateGridDimensions() error {
	// Get pixel dimensions
	pixelWidth, pixelHeight := c.display.PixelSize()

	// Calculate character dimensions first
	charWidth, charHeight, err := CalculateCharDimensions(
		font.FamilyIBMPlexMono,
		c.fontSize,
	)
	if err != nil {
		return fmt.Errorf("calculating char dimensions: %w", err)
	}

	// Log the calculated dimensions for debugging
	fmt.Printf(
		"Font size: %.0f, Char dimensions: %dx%d pixels\n",
		c.fontSize,
		charWidth,
		charHeight,
	)

	// Calculate grid dimensions
	gridWidth, gridHeight, err := CalculateGridSize(
		pixelWidth, pixelHeight,
		font.FamilyIBMPlexMono, c.fontSize,
	)
	if err != nil {
		return err
	}

	fmt.Printf(
		"Display: %dx%d pixels, Grid: %dx%d chars\n",
		pixelWidth,
		pixelHeight,
		gridWidth,
		gridHeight,
	)

	c.gridWidth = gridWidth
	c.gridHeight = gridHeight

	// Update display if it's an FBDisplay
	if fbDisplay, ok := c.display.(*FBDisplay); ok {
		fbDisplay.charWidth = charWidth
		fbDisplay.charHeight = charHeight
	}

	return nil
}

// AddScreen adds a new screen to the carousel
func (c *Carousel) AddScreen(name string, generator FrameGenerator) error {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Initialize the generator with the calculated grid dimensions
	if err := generator.Init(c.gridWidth, c.gridHeight); err != nil {
		return fmt.Errorf("failed to initialize %s: %w", name, err)
	}

	screen := &Screen{
		Name:      name,
		Generator: generator,
		stop:      make(chan struct{}),
	}

	c.screens = append(c.screens, screen)
	return nil
}

// Run starts the carousel
func (c *Carousel) Run() error {
	if len(c.screens) == 0 {
		return fmt.Errorf("no screens added to carousel")
	}

	// Start with first screen
	if err := c.activateScreen(0); err != nil {
		return err
	}

	// If no rotation, just wait for context cancellation
	if c.rotationInterval <= 0 {
		<-c.ctx.Done()
		return c.ctx.Err()
	}

	// Start rotation timer
	rotationTicker := time.NewTicker(c.rotationInterval)
	defer rotationTicker.Stop()

	// Main loop
	for {
		select {
		case <-c.ctx.Done():
			return c.ctx.Err()

		case <-rotationTicker.C:
			// Move to next screen
			c.mu.Lock()
			nextIndex := (c.currentIndex + 1) % len(c.screens)
			c.mu.Unlock()

			if err := c.activateScreen(nextIndex); err != nil {
				return err
			}
		}
	}
}

// Stop stops the carousel
func (c *Carousel) Stop() {
	// Cancel the context
	c.cancel()

	// Stop the current screen
	c.mu.Lock()
	if c.currentIndex >= 0 && c.currentIndex < len(c.screens) {
		screen := c.screens[c.currentIndex]
		if screen.ticker != nil {
			screen.ticker.Stop()
		}
		// Close stop channel if it's still open
		select {
		case <-screen.stop:
			// Already closed
		default:
			close(screen.stop)
		}
	}
	c.mu.Unlock()

	// Wait for all goroutines
	c.wg.Wait()

	// Close the display
	_ = c.display.Close()
}

// activateScreen switches to the specified screen
func (c *Carousel) activateScreen(index int) error {
	c.mu.Lock()
	defer c.mu.Unlock()

	// Stop current screen if any
	if c.currentIndex >= 0 && c.currentIndex < len(c.screens) {
		close(c.screens[c.currentIndex].stop)
		if c.screens[c.currentIndex].ticker != nil {
			c.screens[c.currentIndex].ticker.Stop()
		}
	}

	// Wait for current screen to stop
	c.wg.Wait()

	// Start new screen
	c.currentIndex = index
	screen := c.screens[index]

	// Calculate frame interval
	fps := screen.Generator.FramesPerSecond()
	if fps <= 0 {
		fps = 1 // Default to 1 FPS if invalid
	}
	frameInterval := time.Duration(float64(time.Second) / fps)

	// Create new stop channel and ticker
	screen.stop = make(chan struct{})
	screen.ticker = time.NewTicker(frameInterval)

	// Start frame generation goroutine
	c.wg.Add(1)
	go c.runScreen(screen)

	return nil
}

// runScreen runs a single screen's frame generation loop
func (c *Carousel) runScreen(screen *Screen) {
	defer c.wg.Done()

	// Create grid with calculated dimensions
	grid := NewCharGrid(c.gridWidth, c.gridHeight)
	grid.FontSize = c.fontSize

	// Set the character dimensions for proper rendering
	charWidth, charHeight, _ := CalculateCharDimensions(
		font.FamilyIBMPlexMono,
		c.fontSize,
	)
	grid.CharWidth = charWidth
	grid.CharHeight = charHeight

	// Generate first frame immediately
	if err := screen.Generator.GenerateFrame(grid); err == nil {
		_ = c.display.Write(grid)
	}

	// Frame generation loop
	for {
		select {
		case <-screen.stop:
			return

		case <-screen.ticker.C:
			if err := screen.Generator.GenerateFrame(grid); err == nil {
				_ = c.display.Write(grid)
			}
		}
	}
}
