"""
AO-WOA Hybrid Algorithm
=======================

Hybrid algorithm combining Aquila Optimizer (AO) with Whale Optimization
Algorithm (WOA) for enhanced performance.

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 AOWOAHybrid(BaseOptimizer):
    """
    Aquila Optimizer + Whale Optimization Algorithm Hybrid
    
    Combines:
    - AO: Four hunting strategies of Aquila
    - WOA: Bubble-net attacking and spiral updating
    
    Parameters
    ----------
    population_size : int, default=30
        Size of the population
    max_iterations : int, default=100
        Maximum iterations
    b : float, default=1
        WOA spiral shape parameter
    """
    
    def __init__(self, population_size=30, max_iterations=100, b=1, **kwargs):
        super().__init__(population_size, max_iterations, **kwargs)
        self.algorithm_name = "AO-WOA Hybrid"
        self.aliases = ["ao_woa", "aquila_whale_hybrid"]
        self.b = b
    
    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
        agents = np.random.uniform(lb, ub, (self.population_size_, dimension))
        fitness = np.array([objective_function(a) for a in agents])
        
        best_idx = np.argmin(fitness)
        leader = agents[best_idx].copy()
        best_fitness = fitness[best_idx]
        global_fitness = []
        local_fitness = []
        local_positions = []
        
        for iteration in range(self.max_iterations_):
            # WOA parameters
            a_woa = 2 * (1 - iteration / self.max_iterations_)
            a2 = -1 + iteration * (-1 / self.max_iterations_)
            
            # AO time parameter
            t_ao = iteration / self.max_iterations_
            
            for i in range(self.population_size_):
                # Hybrid strategy selection
                if np.random.rand() < 0.5:
                    # AO component
                    avg_pos = np.mean(agents, axis=0)
                    
                    if t_ao <= 2/3:
                        if t_ao <= 1/3:  # Expanded exploration (X1)
                            r1 = np.random.rand()
                            agents[i] = leader * (1 - iteration / self.max_iterations_) + \
                                       (avg_pos - leader * r1)
                        else:  # Narrowed exploration (X2)
                            levy_step = self._levy_flight(dimension)
                            agents[i] = leader * levy_step + \
                                       np.random.rand(dimension) * (avg_pos - leader)
                    else:
                        if np.random.rand() < 0.5:  # Expanded exploitation (X3)
                            alpha = 0.1
                            delta = 0.1
                            agents[i] = (leader - avg_pos) * alpha - \
                                       np.random.rand() + \
                                       ((ub - lb) * np.random.rand() + lb) * delta
                        else:  # Narrowed exploitation (X4)
                            QF = iteration ** ((2 * np.random.rand() - 1) / 
                                             (1 - self.max_iterations_) ** 2)
                            levy_step = self._levy_flight(dimension)
                            agents[i] = QF * leader - \
                                       (np.random.rand(dimension) * agents[i] - leader) * levy_step
                else:
                    # WOA component
                    r = np.random.rand()
                    A = 2 * a_woa * r - a_woa
                    C = 2 * r
                    l = np.random.uniform(-1, 1)
                    p = np.random.rand()
                    
                    if p < 0.5:
                        if np.abs(A) < 1:  # Encircling prey
                            D = np.abs(C * leader - agents[i])
                            agents[i] = leader - A * D
                        else:  # Search for prey (exploration)
                            rand_idx = np.random.randint(0, self.population_size_)
                            D = np.abs(C * agents[rand_idx] - agents[i])
                            agents[i] = agents[rand_idx] - A * D
                    else:  # Spiral updating position
                        D_prime = np.abs(leader - agents[i])
                        agents[i] = D_prime * np.exp(self.b * l) * np.cos(2 * np.pi * l) + leader
                
                # Boundary control
                agents[i] = np.clip(agents[i], lb, ub)
                
                # Evaluate
                new_fitness = objective_function(agents[i])
                
                if new_fitness < fitness[i]:
                    fitness[i] = new_fitness
                    if new_fitness < best_fitness:
                        leader = agents[i].copy()
                        best_fitness = new_fitness
            
            global_fitness.append(best_fitness)
            # Note: Add local_fitness and local_positions tracking as needed
            
            if self.verbose_ and iteration % 10 == 0:
                print(f"AO-WOA Iteration {iteration}: Best Fitness = {best_fitness:.6e}")
        
        return leader, best_fitness, global_fitness, local_fitness, local_positions
    
    def _levy_flight(self, dimension):
        """Generate Levy flight step"""
        beta = 1.5
        sigma = (math.gamma(1 + beta) * np.sin(np.pi * beta / 2) / 
                (math.gamma((1 + beta) / 2) * beta * 2 ** ((beta - 1) / 2))) ** (1 / beta)
        u = np.random.randn(dimension) * sigma
        v = np.random.randn(dimension)
        step = u / (np.abs(v) ** (1 / beta))
        return 0.01 * step
