package opcodes

//go:generate go run gen/main.go

import (
	"database/sql"
	"fmt"
	"sort"
	"strings"
	"sync"
)

// OpcodeManager manages opcode translations for a specific client version
type OpcodeManager struct {
	version   int16
	emuToEQ   map[EmuOpcode]uint16
	eqToEmu   map[uint16]EmuOpcode
	emuToName map[EmuOpcode]string
	eqToName  map[uint16]string
	nameToEmu map[string]EmuOpcode
	mu        sync.RWMutex
}

// NewOpcodeManager creates a new opcode manager for the specified version
func NewOpcodeManager(version int16) *OpcodeManager {
	om := &OpcodeManager{
		version:   version,
		emuToEQ:   make(map[EmuOpcode]uint16),
		eqToEmu:   make(map[uint16]EmuOpcode),
		emuToName: make(map[EmuOpcode]string),
		eqToName:  make(map[uint16]string),
		nameToEmu: make(map[string]EmuOpcode),
	}
	om.initializeNameMappings() // Generated by go:generate
	return om
}

// LoadFromDatabase loads opcode mappings from database
func (om *OpcodeManager) LoadFromDatabase(db *sql.DB, tableName string) error {
	query := `
		SELECT name, opcode 
		FROM ` + tableName + `
		WHERE ? >= version_range1 
		AND (version_range2 = 0 OR ? <= version_range2)
		ORDER BY version_range1 DESC, version_range2 DESC
	`

	rows, err := db.Query(query, om.version, om.version)
	if err != nil {
		return err
	}
	defer rows.Close()

	om.mu.Lock()
	defer om.mu.Unlock()

	loaded := make(map[string]bool)

	for rows.Next() {
		var name string
		var opcode int16

		if err := rows.Scan(&name, &opcode); err != nil {
			continue
		}

		if loaded[name] {
			continue
		}
		loaded[name] = true

		eqOpcode := uint16(opcode)

		// Only map if we have a constant for this opcode name
		if emuOp, ok := om.nameToEmu[name]; ok {
			om.emuToEQ[emuOp] = eqOpcode
			om.eqToEmu[eqOpcode] = emuOp
			om.emuToName[emuOp] = name
			om.eqToName[eqOpcode] = name
		}
	}

	// Map missing opcodes to 0xFFFF
	for name, emuOp := range om.nameToEmu {
		if _, ok := om.emuToEQ[emuOp]; !ok {
			om.emuToEQ[emuOp] = 0xFFFF
			om.emuToName[emuOp] = name
		}
	}

	return rows.Err()
}

// EmuToEQ converts emulator opcode to EQ opcode
func (om *OpcodeManager) EmuToEQ(emu EmuOpcode) uint16 {
	om.mu.RLock()
	defer om.mu.RUnlock()

	if eq, ok := om.emuToEQ[emu]; ok {
		return eq
	}
	return 0xFFFF
}

// EQToEmu converts EQ opcode to emulator opcode
func (om *OpcodeManager) EQToEmu(eq uint16) EmuOpcode {
	om.mu.RLock()
	defer om.mu.RUnlock()

	if emu, ok := om.eqToEmu[eq]; ok {
		return emu
	}
	return OP_Unknown
}

// EmuToName returns the name of an emulator opcode
func (om *OpcodeManager) EmuToName(emu EmuOpcode) string {
	om.mu.RLock()
	defer om.mu.RUnlock()

	if name, ok := om.emuToName[emu]; ok {
		return name
	}
	return "OP_Unknown"
}

// EQToName returns the name of an EQ opcode
func (om *OpcodeManager) EQToName(eq uint16) string {
	om.mu.RLock()
	defer om.mu.RUnlock()

	if name, ok := om.eqToName[eq]; ok {
		return name
	}
	return "OP_Unknown"
}

// GetMissingOpcodes returns list of opcodes that map to 0xFFFF
func (om *OpcodeManager) GetMissingOpcodes() []string {
	om.mu.RLock()
	defer om.mu.RUnlock()

	var missing []string
	for emu, eq := range om.emuToEQ {
		if eq == 0xFFFF {
			if name, ok := om.emuToName[emu]; ok {
				missing = append(missing, name)
			}
		}
	}
	return missing
}

// IsValidOpcode checks if an EQ opcode is valid/known
func (om *OpcodeManager) IsValidOpcode(eq uint16) bool {
	om.mu.RLock()
	defer om.mu.RUnlock()

	_, exists := om.eqToEmu[eq]
	return exists
}

// DumpOpcodes returns a formatted string of all loaded opcodes for debugging
func (om *OpcodeManager) DumpOpcodes() string {
	om.mu.RLock()
	defer om.mu.RUnlock()

	var sb strings.Builder
	sb.WriteString(fmt.Sprintf("Opcode Manager for version %d:\n", om.version))
	sb.WriteString("=====================================\n")

	var eqOpcodes []uint16
	for eq := range om.eqToEmu {
		eqOpcodes = append(eqOpcodes, eq)
	}
	sort.Slice(eqOpcodes, func(i, j int) bool {
		return eqOpcodes[i] < eqOpcodes[j]
	})

	for _, eq := range eqOpcodes {
		emu := om.eqToEmu[eq]
		name := om.eqToName[eq]
		sb.WriteString(fmt.Sprintf("0x%04x -> 0x%04x : %s\n", eq, emu, name))
	}

	return sb.String()
}

// GetStats returns statistics about loaded opcodes
func (om *OpcodeManager) GetStats() map[string]int {
	om.mu.RLock()
	defer om.mu.RUnlock()

	return map[string]int{
		"version":        int(om.version),
		"total_opcodes":  len(om.eqToEmu),
		"named_opcodes":  len(om.emuToName),
		"mapped_opcodes": len(om.emuToEQ),
	}
}

// Global managers cache
var (
	managers   = make(map[int16]*OpcodeManager)
	managersMu sync.RWMutex
)

// GetManager returns opcode manager for a version
func GetManager(version int16, db *sql.DB, tableName string) *OpcodeManager {
	managersMu.RLock()
	if mgr, ok := managers[version]; ok {
		managersMu.RUnlock()
		return mgr
	}
	managersMu.RUnlock()

	managersMu.Lock()
	defer managersMu.Unlock()

	if mgr, ok := managers[version]; ok {
		return mgr
	}

	mgr := NewOpcodeManager(version)
	if db != nil {
		if err := mgr.LoadFromDatabase(db, tableName); err != nil {
			fmt.Printf("Warning: Failed to load opcodes for version %d: %v\n", version, err)
		}
	}

	managers[version] = mgr
	return mgr
}

// ClearCache clears the cached managers (useful for testing)
func ClearCache() {
	managersMu.Lock()
	defer managersMu.Unlock()
	managers = make(map[int16]*OpcodeManager)
}

// GetLoadedVersions returns all versions that have managers loaded
func GetLoadedVersions() []int16 {
	managersMu.RLock()
	defer managersMu.RUnlock()

	var versions []int16
	for v := range managers {
		versions = append(versions, v)
	}

	sort.Slice(versions, func(i, j int) bool {
		return versions[i] < versions[j]
	})

	return versions
}
