package engine

import (
	"fmt"
	"sync"

	"github.com/kevin/go-jdenticon/internal/util"
)

// Icon represents a generated jdenticon with its configuration and geometry
type Icon struct {
	Hash   string
	Size   float64
	Config ColorConfig
	Shapes []ShapeGroup
}

// ShapeGroup represents a group of shapes with the same color
type ShapeGroup struct {
	Color     Color
	Shapes    []Shape
	ShapeType string
}

// Shape represents a single geometric shape. It acts as a discriminated union
// where the `Type` field determines which other fields are valid.
// - For "polygon", `Points` is used.
// - For "circle", `CircleX`, `CircleY`, and `CircleSize` are used.
type Shape struct {
	Type       string
	Points     []Point
	Transform  Transform
	Invert     bool
	// Circle-specific fields
	CircleX    float64
	CircleY    float64
	CircleSize float64
}

// Generator encapsulates the icon generation logic and provides caching
type Generator struct {
	config ColorConfig
	cache  map[string]*Icon
	mu     sync.RWMutex
}

// NewGenerator creates a new Generator with the specified configuration
func NewGenerator(config ColorConfig) *Generator {
	config.Validate()
	return &Generator{
		config: config,
		cache:  make(map[string]*Icon),
	}
}

// NewDefaultGenerator creates a new Generator with default configuration
func NewDefaultGenerator() *Generator {
	return NewGenerator(DefaultColorConfig())
}

// Generate creates an icon from a hash string using the configured settings
func (g *Generator) Generate(hash string, size float64) (*Icon, error) {
	if hash == "" {
		return nil, fmt.Errorf("hash cannot be empty")
	}
	
	if size <= 0 {
		return nil, fmt.Errorf("size must be positive, got %f", size)
	}
	
	// Check cache first
	cacheKey := g.cacheKey(hash, size)
	g.mu.RLock()
	if cached, exists := g.cache[cacheKey]; exists {
		g.mu.RUnlock()
		return cached, nil
	}
	g.mu.RUnlock()
	
	// Validate hash format
	if !util.IsValidHash(hash) {
		return nil, fmt.Errorf("invalid hash format: %s", hash)
	}
	
	// Generate new icon
	icon, err := g.generateIcon(hash, size)
	if err != nil {
		return nil, err
	}
	
	// Cache the result
	g.mu.Lock()
	g.cache[cacheKey] = icon
	g.mu.Unlock()
	
	return icon, nil
}

// generateIcon performs the actual icon generation
func (g *Generator) generateIcon(hash string, size float64) (*Icon, error) {
	// Calculate padding and round to nearest integer (matching JavaScript)
	padding := int((0.5 + size*g.config.IconPadding))
	iconSize := size - float64(padding*2)
	
	// Calculate cell size and ensure it is an integer (matching JavaScript)
	cell := int(iconSize / 4)
	
	// Since the cell size is integer based, the actual icon will be slightly smaller than specified => center icon
	x := int(float64(padding) + iconSize/2 - float64(cell*2))
	y := int(float64(padding) + iconSize/2 - float64(cell*2))
	
	// Extract hue from hash (last 7 characters)
	hue, err := g.extractHue(hash)
	if err != nil {
		return nil, fmt.Errorf("generateIcon: %w", err)
	}
	
	// Generate color theme
	availableColors := GenerateColorTheme(hue, g.config)
	
	// Select colors for each shape layer
	selectedColorIndexes, err := g.selectColors(hash, availableColors)
	if err != nil {
		return nil, err
	}
	
	// Generate shape groups in exact JavaScript order
	shapeGroups := make([]ShapeGroup, 0, 3)
	
	// 1. Sides (outer edges) - renderShape(0, outerShape, 2, 3, [[1, 0], [2, 0], [2, 3], [1, 3], [0, 1], [3, 1], [3, 2], [0, 2]]);
	sideShapes, err := g.renderShape(hash, 0, 2, 3, 
		[][]int{{1, 0}, {2, 0}, {2, 3}, {1, 3}, {0, 1}, {3, 1}, {3, 2}, {0, 2}},
		x, y, cell, true)
	if err != nil {
		return nil, fmt.Errorf("generateIcon: failed to render side shapes: %w", err)
	}
	if len(sideShapes) > 0 {
		shapeGroups = append(shapeGroups, ShapeGroup{
			Color:     availableColors[selectedColorIndexes[0]],
			Shapes:    sideShapes,
			ShapeType: "sides",
		})
	}
	
	// 2. Corners - renderShape(1, outerShape, 4, 5, [[0, 0], [3, 0], [3, 3], [0, 3]]);
	cornerShapes, err := g.renderShape(hash, 1, 4, 5,
		[][]int{{0, 0}, {3, 0}, {3, 3}, {0, 3}},
		x, y, cell, true)
	if err != nil {
		return nil, fmt.Errorf("generateIcon: failed to render corner shapes: %w", err)
	}
	if len(cornerShapes) > 0 {
		shapeGroups = append(shapeGroups, ShapeGroup{
			Color:     availableColors[selectedColorIndexes[1]],
			Shapes:    cornerShapes,
			ShapeType: "corners",
		})
	}
	
	// 3. Center - renderShape(2, centerShape, 1, null, [[1, 1], [2, 1], [2, 2], [1, 2]]);
	centerShapes, err := g.renderShape(hash, 2, 1, -1,
		[][]int{{1, 1}, {2, 1}, {2, 2}, {1, 2}},
		x, y, cell, false)
	if err != nil {
		return nil, fmt.Errorf("generateIcon: failed to render center shapes: %w", err)
	}
	if len(centerShapes) > 0 {
		shapeGroups = append(shapeGroups, ShapeGroup{
			Color:     availableColors[selectedColorIndexes[2]],
			Shapes:    centerShapes,
			ShapeType: "center",
		})
	}
	
	return &Icon{
		Hash:   hash,
		Size:   size,
		Config: g.config,
		Shapes: shapeGroups,
	}, nil
}

// extractHue extracts the hue value from the hash string
func (g *Generator) extractHue(hash string) (float64, error) {
	// Use the last 7 characters of the hash to determine hue
	hueValue, err := util.ParseHex(hash, -7, 7)
	if err != nil {
		return 0, fmt.Errorf("extractHue: %w", err)
	}
	return float64(hueValue) / 0xfffffff, nil
}

// selectColors selects 3 colors from the available color palette
func (g *Generator) selectColors(hash string, availableColors []Color) ([]int, error) {
	if len(availableColors) == 0 {
		return nil, fmt.Errorf("no available colors")
	}
	
	selectedIndexes := make([]int, 3)
	
	for i := 0; i < 3; i++ {
		indexValue, err := util.ParseHex(hash, 8+i, 1)
		if err != nil {
			return nil, fmt.Errorf("selectColors: failed to parse color index at position %d: %w", 8+i, err)
		}
		index := indexValue % len(availableColors)
		
		// Apply color conflict resolution rules from JavaScript implementation
		if g.isDuplicateColor(index, selectedIndexes[:i], []int{0, 4}) || // Disallow dark gray and dark color combo
			g.isDuplicateColor(index, selectedIndexes[:i], []int{2, 3}) { // Disallow light gray and light color combo
			index = 1 // Use mid color as fallback
		}
		
		selectedIndexes[i] = index
	}
	
	return selectedIndexes, nil
}

// contains checks if a slice contains a specific value
func contains(slice []int, value int) bool {
	for _, item := range slice {
		if item == value {
			return true
		}
	}
	return false
}

// isDuplicateColor checks for problematic color combinations
func (g *Generator) isDuplicateColor(index int, selected []int, forbidden []int) bool {
	if !contains(forbidden, index) {
		return false
	}
	for _, s := range selected {
		if contains(forbidden, s) {
			return true
		}
	}
	return false
}


// renderShape implements the JavaScript renderShape function exactly
func (g *Generator) renderShape(hash string, colorIndex, shapeHashIndex, rotationHashIndex int, positions [][]int, x, y, cell int, isOuter bool) ([]Shape, error) {
	shapeIndexValue, err := util.ParseHex(hash, shapeHashIndex, 1)
	if err != nil {
		return nil, fmt.Errorf("renderShape: failed to parse shape index at position %d: %w", shapeHashIndex, err)
	}
	shapeIndex := shapeIndexValue
	
	var rotation int
	if rotationHashIndex >= 0 {
		rotationValue, err := util.ParseHex(hash, rotationHashIndex, 1)
		if err != nil {
			return nil, fmt.Errorf("renderShape: failed to parse rotation at position %d: %w", rotationHashIndex, err)
		}
		rotation = rotationValue
	}
	
	shapes := make([]Shape, 0, len(positions))
	
	for i, pos := range positions {
		// Calculate transform exactly like JavaScript: new Transform(x + positions[i][0] * cell, y + positions[i][1] * cell, cell, r++ % 4)
		transformX := float64(x + pos[0]*cell)
		transformY := float64(y + pos[1]*cell)
		var transformRotation int
		if rotationHashIndex >= 0 {
			transformRotation = (rotation + i) % 4
		} else {
			// For center shapes (rotationIndex is null), r starts at 0 and increments
			transformRotation = i % 4
		}
		
		transform := NewTransform(transformX, transformY, float64(cell), transformRotation)
		
		// Create shape using graphics with transform
		graphics := NewGraphicsWithTransform(&shapeCollector{}, transform)
		
		if isOuter {
			RenderOuterShape(graphics, shapeIndex, float64(cell))
		} else {
			RenderCenterShape(graphics, shapeIndex, float64(cell), float64(i))
		}
		
		collector := graphics.renderer.(*shapeCollector)
		for _, shape := range collector.shapes {
			shapes = append(shapes, shape)
		}
	}
	
	return shapes, nil
}

// shapeCollector implements Renderer interface to collect shapes during generation
type shapeCollector struct {
	shapes []Shape
}

func (sc *shapeCollector) AddPolygon(points []Point) {
	sc.shapes = append(sc.shapes, Shape{
		Type:   "polygon",
		Points: points,
	})
}

func (sc *shapeCollector) AddCircle(topLeft Point, size float64, invert bool) {
	// Store circle with dedicated circle geometry fields
	sc.shapes = append(sc.shapes, Shape{
		Type:       "circle",
		CircleX:    topLeft.X,
		CircleY:    topLeft.Y,
		CircleSize: size,
		Invert:     invert,
	})
}



// cacheKey generates a cache key for the given parameters
func (g *Generator) cacheKey(hash string, size float64) string {
	return fmt.Sprintf("%s:%.2f", hash, size)
}

// ClearCache clears the internal cache
func (g *Generator) ClearCache() {
	g.mu.Lock()
	defer g.mu.Unlock()
	g.cache = make(map[string]*Icon)
}

// GetCacheSize returns the number of cached icons
func (g *Generator) GetCacheSize() int {
	g.mu.RLock()
	defer g.mu.RUnlock()
	return len(g.cache)
}

// SetConfig updates the generator configuration and clears cache
func (g *Generator) SetConfig(config ColorConfig) {
	config.Validate()
	g.mu.Lock()
	g.config = config
	g.cache = make(map[string]*Icon)
	g.mu.Unlock()
}

// GetConfig returns a copy of the current configuration
func (g *Generator) GetConfig() ColorConfig {
	g.mu.RLock()
	defer g.mu.RUnlock()
	return g.config
}