"""
SMA-GA Hybrid Algorithm
=======================

Hybrid algorithm combining Slime Mould Algorithm (SMA) with Genetic
Algorithm (GA) for enhanced diversity and convergence.

Author: MHA Flow Development Team
License: MIT
"""

import numpy as np
import math
from ...base import BaseOptimizer
from ..levy_flight_universal import add_levy_flight_to_position


class SMAGAHybrid(BaseOptimizer):
    """
    Slime Mould Algorithm + Genetic Algorithm Hybrid
    
    Combines:
    - SMA: Oscillation behavior and foraging mechanism
    - GA: Selection, crossover, and mutation operations
    
    Parameters
    ----------
    population_size : int, default=30
        Size of the population
    max_iterations : int, default=100
        Maximum iterations
    mutation_rate : float, default=0.1
        GA mutation probability
    z : float, default=0.03
        SMA parameter
    """
    
    def __init__(self, population_size=30, max_iterations=100, 
                 mutation_rate=0.1, z=0.03, **kwargs):
        super().__init__(population_size, max_iterations, **kwargs)
        self.algorithm_name = "SMA-GA Hybrid"
        self.aliases = ["sma_ga", "slime_genetic_hybrid"]
        self.mutation_rate = mutation_rate
        self.z = z
    
    def _optimize(self, objective_function, X=None, y=None, **kwargs):
        # Levy flight available via: add_levy_flight_to_position()
        # Get dimensions and bounds
        if X is not None:
            dimension = X.shape[1]
            lb, ub = 0.0, 1.0
        else:
            dimension = kwargs.get('dimensions', getattr(self, 'dimensions_', 10))
            lb = kwargs.get('lower_bound', getattr(self, 'lower_bound_', -100.0))
            ub = kwargs.get('upper_bound', getattr(self, 'upper_bound_', 100.0))
            if hasattr(lb, '__getitem__'):
                lb = lb[0]
            if hasattr(ub, '__getitem__'):
                ub = ub[0]
        
        
        # Initialize population
        population = np.random.uniform(lb, ub, (self.population_size_, dimension))
        fitness = np.array([objective_function(p) for p in population])
        
        best_idx = np.argmin(fitness)
        best_solution = population[best_idx].copy()
        best_fitness = fitness[best_idx]
        global_fitness = []
        local_fitness = []
        local_positions = []
        
        # Weight for each slime
        weights = np.zeros(self.population_size_)
        
        for iteration in range(self.max_iterations_):
            # Hybrid strategy selection
            if np.random.rand() < 0.5:
                # SMA component
                a = math.atanh(min(-(iteration / self.max_iterations_) + 1, 1 - 1e-10))
                sorted_idx = np.argsort(fitness)
                
                # Update weights
                for i in range(self.population_size_):
                    if i < self.population_size_ // 2:
                        weights[sorted_idx[i]] = 1 + np.random.rand() * math.log10(
                            (best_fitness - fitness[sorted_idx[i]]) / 
                            (best_fitness - fitness[sorted_idx[-1]] + 1e-10) + 1
                        )
                    else:
                        weights[sorted_idx[i]] = 1 - np.random.rand() * math.log10(
                            (best_fitness - fitness[sorted_idx[i]]) / 
                            (best_fitness - fitness[sorted_idx[-1]] + 1e-10) + 1
                        )
                
                # Update positions
                for i in range(self.population_size_):
                    p = math.tanh(abs(fitness[i] - best_fitness))
                    vb = np.random.uniform(-a, a, dimension)
                    vc = np.random.uniform(-1, 1, dimension)
                    
                    for j in range(dimension):
                        r = np.random.rand()
                        
                        if r < self.z:
                            population[i][j] = (ub - lb) * np.random.rand() + lb
                        else:
                            if r < p:
                                population[i][j] = best_solution[j] + vb[j] * (
                                    weights[i] * population[sorted_idx[0]][j] - population[i][j]
                                )
                            else:
                                population[i][j] = vc[j] * population[i][j]
            else:
                # GA component
                # Tournament selection (select 2 parents)
                parents = []
                for _ in range(2):
                    if self.population_size_ >= 2:
                        idx1, idx2 = np.random.choice(self.population_size_, 2, replace=False)
                    else:
                        idx1 = idx2 = 0
                    if fitness[idx1] < fitness[idx2]:
                        parents.append(population[idx1].copy())
                    else:
                        parents.append(population[idx2].copy())
                
                # Crossover (blend crossover)
                alpha = np.random.rand()
                child = alpha * parents[0] + (1 - alpha) * parents[1]
                
                # Mutation
                if np.random.rand() < self.mutation_rate:
                    mutation_idx = np.random.randint(0, dimension)
                    child[mutation_idx] = np.random.uniform(lb, ub)
                
                # Replace worst individual
                worst_idx = np.argmax(fitness)
                population[worst_idx] = child
                fitness[worst_idx] = objective_function(child)
            
            # Boundary control and evaluation
            for i in range(self.population_size_):
                population[i] = np.clip(population[i], lb, ub)
                fitness[i] = objective_function(population[i])
                if fitness[i] < best_fitness:
                    best_solution = population[i].copy()
                    best_fitness = fitness[i]
            
            global_fitness.append(best_fitness)
            # Note: Add local_fitness and local_positions tracking as needed
            
            if self.verbose_ and iteration % 10 == 0:
                print(f"SMA-GA Iteration {iteration}: Best Fitness = {best_fitness:.6e}")
        
        return best_solution, best_fitness, global_fitness, local_fitness, local_positions
