#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Sound handling for Storm Games.

Provides functionality for:
- Playing background music and sound effects
- 2D positional audio (x,y)
- Volume controls
"""

import os
import pygame
import random
import re
import time
import math
from .services import VolumeService

# Global instance for backward compatibility
volumeService = VolumeService.get_instance()

class Sound:
    """Handles sound loading and playback."""

    def __init__(self, soundDir="sounds/", volumeService=None):
        """Initialize sound system."""
        self.soundDir = soundDir
        self.sounds = {}
        self.volumeService = volumeService or VolumeService.get_instance()

        if not pygame.mixer.get_init():
            pygame.mixer.pre_init(44100, -16, 2, 1024)
            pygame.mixer.init()
            pygame.mixer.set_num_channels(32)
            pygame.mixer.set_reserved(0)

        self.load_sounds()

    def load_sounds(self):
        """Load all sound files from the sound directory and its subdirectories."""
        try:
            for dirPath, _, fileNames in os.walk(self.soundDir):
                relPath = os.path.relpath(dirPath, self.soundDir)

                for fileName in fileNames:
                    if fileName.lower().endswith(('.ogg', '.wav')):
                        fullPath = os.path.join(dirPath, fileName)
                        baseName = os.path.splitext(fileName)[0]

                        soundKey = baseName if relPath == '.' else os.path.join(relPath, baseName).replace('\\', '/')
                        self.sounds[soundKey] = pygame.mixer.Sound(fullPath)
        except Exception as e:
            print(f"Error loading sounds: {e}")

    def _find_matching_sound(self, pattern):
        """Find a random sound matching the pattern."""
        keys = [k for k in self.sounds.keys() if re.match("^" + pattern + ".*", k)]
        return random.choice(keys) if keys else None

    def _handle_cutscene(self, soundName):
        """Play a sound as a cut scene."""
        pygame.event.clear()
        pygame.mixer.stop()

        channel = pygame.mixer.Channel(0)
        sfxVolume = self.volumeService.get_sfx_volume()
        channel.set_volume(sfxVolume, sfxVolume)

        channel.play(self.sounds[soundName])

        while pygame.mixer.get_busy():
            for event in pygame.event.get():
                if event.type == pygame.KEYDOWN and event.key in [pygame.K_ESCAPE, pygame.K_RETURN, pygame.K_SPACE]:
                    pygame.mixer.stop()
                    return None
            pygame.time.delay(10)

        return None

    def _get_stereo_panning(self, playerPos, objPos, centerDistance=None):
        """Calculate stereo panning based on positions."""
        # Extract x-positions
        playerX = playerPos[0] if isinstance(playerPos, (tuple, list)) else playerPos
        objX = objPos[0] if isinstance(objPos, (tuple, list)) else objPos

        # For directional sound with fixed distance
        if centerDistance is not None:
            if abs(playerX - objX) <= centerDistance:
                return (1, 1)  # Center
            elif playerX > objX:
                return (1, 0.505)  # Left
            else:
                return (0.505, 1)  # Right

        # Calculate regular panning
        volume, left, right = self.calculate_volume_and_pan(playerPos, objPos)
        return (volume * left, volume * right) if volume > 0 else (0, 0)

    def play_sound(self, soundName, volume=1.0, loop=False, playerPos=None, objPos=None, 
                  centerDistance=None, pattern=False, interrupt=False, pause=False, cutScene=False):
        """Unified method to play sounds with various options."""
        # Resolve sound name if pattern matching is requested
        if pattern:
            soundName = self._find_matching_sound(soundName)
            if not soundName:
                return None

        # Check if sound exists
        if soundName not in self.sounds:
            return None

        # Handle cut scene mode
        if cutScene:
            return self._handle_cutscene(soundName)

        # Handle interrupt (stop other sounds)
        if interrupt:
            pygame.event.clear()
            pygame.mixer.stop()

        # Play the sound
        channel = self.sounds[soundName].play(-1 if loop else 0)
        if not channel:
            return None

        # Apply appropriate volume settings
        sfx_volume = self.volumeService.get_sfx_volume()

        # Handle positional audio if positions are provided
        if playerPos is not None and objPos is not None:
            # Calculate stereo panning
            left_vol, right_vol = self._get_stereo_panning(playerPos, objPos, centerDistance)

            # Don't play if out of range
            if left_vol == 0 and right_vol == 0:
                channel.stop()
                return None

            # Apply positional volume adjustments
            channel.set_volume(volume * left_vol * sfx_volume, volume * right_vol * sfx_volume)
        else:
            # Non-positional sound
            channel.set_volume(volume * sfx_volume)

        # Pause execution if requested
        if pause:
            time.sleep(self.sounds[soundName].get_length())

        return channel

    def calculate_volume_and_pan(self, playerPos, objPos, maxDistance=12):
        """Calculate volume and stereo panning based on relative positions."""
        # Determine if we're using 2D or 1D positioning
        if isinstance(playerPos, (tuple, list)) and isinstance(objPos, (tuple, list)):
            # 2D distance calculation
            distance = math.sqrt((playerPos[0] - objPos[0])**2 + (playerPos[1] - objPos[1])**2)
            playerX, objX = playerPos[0], objPos[0]
        else:
            # 1D calculation (backward compatible)
            distance = abs(playerPos - objPos)
            playerX, objX = playerPos, objPos

        if distance > maxDistance:
            return 0, 0, 0  # No sound if out of range

        # Calculate volume (non-linear scaling for more noticeable changes)
        volume = (((maxDistance - distance) / maxDistance) ** 1.5) * self.volumeService.masterVolume

        # Determine left/right based on relative position
        if playerX < objX:  # Object is to the right
            left = max(0, 1 - (objX - playerX) / maxDistance)
            right = 1
        elif playerX > objX:  # Object is to the left
            left = 1
            right = max(0, 1 - (playerX - objX) / maxDistance)
        else:  # Player is on the object
            left = right = 1

        return volume, left, right

    def update_sound_position(self, channel, playerPos, objPos):
        """Update positional audio for a playing sound."""
        if not channel:
            return None

        # Calculate new stereo panning
        left_vol, right_vol = self._get_stereo_panning(playerPos, objPos)

        # Stop if out of range
        if left_vol == 0 and right_vol == 0:
            channel.stop()
            return None

        # Apply the volume and pan
        channel.set_volume(left_vol * self.volumeService.sfxVolume, right_vol * self.volumeService.sfxVolume)
        return channel

    def stop_sound(self, channel):
        """Stop a playing sound channel."""
        if channel:
            try:
                channel.stop()
            except:
                pass
        return None

    def play_falling_sound(self, soundPrefix, playerPos, objPos, startY, currentY=0, maxY=20, existingChannel=None):
        """Play or update a sound with positional audio that changes with height."""
        # Extract positions
        playerX = playerPos[0] if isinstance(playerPos, (tuple, list)) else playerPos
        objX = objPos[0] if isinstance(objPos, (tuple, list)) else objPos

        # Calculate volumes
        volume, left, right = self.calculate_volume_and_pan(playerX, objX)

        # Apply vertical fall multiplier (0 at maxY, 1 at y=0)
        fallMultiplier = 1 - (currentY / maxY)
        finalVolume = volume * fallMultiplier
        finalLeft = left * finalVolume
        finalRight = right * finalVolume

        # Update existing channel or create new one
        if existingChannel:
            if volume == 0:
                existingChannel.stop()
                return None
            existingChannel.set_volume(
                finalLeft * self.volumeService.sfxVolume,
                finalRight * self.volumeService.sfxVolume
            )
            return existingChannel
        else:
            if volume == 0:
                return None

            # Find a matching sound
            soundName = self._find_matching_sound(soundPrefix)
            if not soundName:
                return None

            # Play the sound
            channel = self.sounds[soundName].play()
            if channel:
                channel.set_volume(
                    finalLeft * self.volumeService.sfxVolume,
                    finalRight * self.volumeService.sfxVolume
                )
            return channel

    def play_bgm(self, musicFile):
        """Play background music with proper volume settings."""
        try:
            pygame.mixer.music.stop()
            pygame.mixer.music.load(musicFile)
            pygame.mixer.music.set_volume(self.volumeService.get_bgm_volume())
            pygame.mixer.music.play(-1)
        except Exception as e:
            print(f"Error playing background music: {e}")

    def adjust_master_volume(self, change):
        """Adjust the master volume for all sounds."""
        self.volumeService.adjust_master_volume(change, pygame.mixer)

    def adjust_bgm_volume(self, change):
        """Adjust only the background music volume."""
        self.volumeService.adjust_bgm_volume(change, pygame.mixer)

    def adjust_sfx_volume(self, change):
        """Adjust volume for sound effects only."""
        self.volumeService.adjust_sfx_volume(change, pygame.mixer)


# Optimized helper functions for global use
def _get_stereo_panning(playerPos, objPos, centerDistance=None, maxDistance=12):
    """Simplified panning calculation."""
    # Extract x-positions
    playerX = playerPos[0] if isinstance(playerPos, (tuple, list)) else playerPos
    objX = objPos[0] if isinstance(objPos, (tuple, list)) else objPos

    # For directional sound with fixed distance
    if centerDistance is not None:
        if abs(playerX - objX) <= centerDistance:
            return (1, 1)  # Center
        elif playerX > objX:
            return (1, 0.505)  # Left
        else:
            return (0.505, 1)  # Right

    # Calculate distance
    if isinstance(playerPos, (tuple, list)) and isinstance(objPos, (tuple, list)):
        distance = math.sqrt((playerPos[0] - objPos[0])**2 + (playerPos[1] - objPos[1])**2)
    else:
        distance = abs(playerPos - objPos)

    if distance > maxDistance:
        return (0, 0)  # No sound if out of range

    # Calculate volume (non-linear scaling for more noticeable changes)
    volume = (((maxDistance - distance) / maxDistance) ** 1.5) * volumeService.masterVolume

    # Determine left/right based on relative position
    if playerX < objX:  # Object is to the right
        left = max(0, 1 - (objX - playerX) / maxDistance)
        right = 1
    elif playerX > objX:  # Object is to the left
        left = 1
        right = max(0, 1 - (playerX - objX) / maxDistance)
    else:  # Player is on the object
        left = right = 1

    return (volume * left, volume * right)

def _play_cutscene(sound, sounds=None):
    """Play a sound as a cut scene."""
    pygame.event.clear()
    pygame.mixer.stop()

    channel = pygame.mixer.Channel(0)
    sfxVolume = volumeService.get_sfx_volume()
    channel.set_volume(sfxVolume, sfxVolume)

    # Determine which sound to play
    if isinstance(sound, pygame.mixer.Sound):
        channel.play(sound)
    elif isinstance(sounds, dict) and sound in sounds:
        channel.play(sounds[sound])
    elif isinstance(sounds, Sound) and sound in sounds.sounds:
        channel.play(sounds.sounds[sound])
    else:
        return None

    # Wait for completion or key press
    while pygame.mixer.get_busy():
        for event in pygame.event.get():
            if event.type == pygame.KEYDOWN and event.key in [pygame.K_ESCAPE, pygame.K_RETURN, pygame.K_SPACE]:
                pygame.mixer.stop()
                return None
        pygame.time.delay(10)

    return None

def _find_matching_sound(soundPattern, sounds):
    """Find sounds matching a pattern in a dictionary."""
    if isinstance(sounds, Sound):
        keys = [k for k in sounds.sounds.keys() if re.match("^" + soundPattern + ".*", k)]
    else:
        keys = [k for k in sounds.keys() if re.match("^" + soundPattern + ".*", k)]
    return random.choice(keys) if keys else None

# Global functions for backward compatibility
def play_bgm(musicFile):
    """Play background music with proper volume settings."""
    try:
        pygame.mixer.music.stop()
        pygame.mixer.music.load(musicFile)
        pygame.mixer.music.set_volume(volumeService.get_bgm_volume())
        pygame.mixer.music.play(-1)
    except: pass

def adjust_master_volume(change):
    """Adjust the master volume."""
    volumeService.adjust_master_volume(change, pygame.mixer)

def adjust_bgm_volume(change):
    """Adjust background music volume."""
    volumeService.adjust_bgm_volume(change, pygame.mixer)

def adjust_sfx_volume(change):
    """Adjust sound effects volume."""
    volumeService.adjust_sfx_volume(change, pygame.mixer)

def calculate_volume_and_pan(playerPos, objPos, maxDistance=12):
    """Calculate volume and stereo panning."""
    left_vol, right_vol = _get_stereo_panning(playerPos, objPos, None, maxDistance)
    # Convert to old format (volume, left, right)
    if left_vol == 0 and right_vol == 0:
        return 0, 0, 0
    elif left_vol >= right_vol:
        volume = left_vol
        return volume, 1, right_vol/left_vol
    else:
        volume = right_vol
        return volume, left_vol/right_vol, 1

def play_sound(sound_or_name, volume=1.0, loop=False, playerPos=None, objPos=None, 
              centerDistance=None, pattern=False, interrupt=False, pause=False, 
              cutScene=False, sounds=None):
    """Unified sound playing function with backward compatibility."""
    # Handle cut scene mode
    if cutScene:
        return _play_cutscene(sound_or_name, sounds)

    # Handle pattern matching
    if pattern and isinstance(sound_or_name, str) and sounds:
        matched_sound = _find_matching_sound(sound_or_name, sounds)
        if not matched_sound:
            return None
        sound_or_name = matched_sound

    # Handle interrupt
    if interrupt:
        pygame.event.clear()
        pygame.mixer.stop()

    # Case 1: Sound instance provided
    if isinstance(sound_or_name, Sound):
        return sound_or_name.play_sound(sound_or_name, volume, loop, playerPos, objPos, 
                                       centerDistance, False, False, pause, False)

    # Case 2: Sound name with Sound instance
    elif isinstance(sounds, Sound) and isinstance(sound_or_name, str):
        return sounds.play_sound(sound_or_name, volume, loop, playerPos, objPos,
                                centerDistance, False, False, pause, False)

    # Case 3: Direct pygame.Sound
    elif isinstance(sound_or_name, pygame.mixer.Sound):
        channel = sound_or_name.play(-1 if loop else 0)
        if channel:
            channel.set_volume(volume * volumeService.get_sfx_volume())
        return channel

    # Case 4: Sound name with dictionary
    elif isinstance(sounds, dict) and isinstance(sound_or_name, str) and sound_or_name in sounds:
        # Play the sound
        channel = sounds[sound_or_name].play(-1 if loop else 0)
        if not channel:
            return None

        # Apply volume settings
        sfx_vol = volumeService.get_sfx_volume()

        # Handle positional audio
        if playerPos is not None and objPos is not None:
            left_vol, right_vol = _get_stereo_panning(playerPos, objPos, centerDistance)
            if left_vol == 0 and right_vol == 0:
                channel.stop()
                return None
            channel.set_volume(volume * left_vol * sfx_vol, volume * right_vol * sfx_vol)
        else:
            channel.set_volume(volume * sfx_vol)

        # Pause if requested
        if pause:
            time.sleep(sounds[sound_or_name].get_length())

        return channel

    return None

def obj_update(channel, playerPos, objPos):
    """Update positional audio for a playing sound."""
    if not channel:
        return None

    left_vol, right_vol = _get_stereo_panning(playerPos, objPos)
    if left_vol == 0 and right_vol == 0:
        channel.stop()
        return None

    channel.set_volume(left_vol * volumeService.sfxVolume, right_vol * volumeService.sfxVolume)
    return channel

def obj_stop(channel):
    """Stop a sound channel."""
    if channel:
        try: channel.stop()
        except: pass
    return None

# Extremely concise lambda definitions for legacy functions
obj_play = lambda sounds, soundName, playerPos, objPos, loop=True: play_sound(
    soundName, 1.0, loop, playerPos, objPos, None, False, False, False, False, sounds)

play_ambiance = lambda sounds, soundNames, probability, randomLocation=False: play_sound(
    random.choice(soundNames) if random.randint(1, 100) <= probability and not any(
    pygame.mixer.find_channel(True) and pygame.mixer.find_channel(True).get_busy() 
    for _ in ([soundNames] if isinstance(soundNames, str) else soundNames)) else None,
    1.0, False, None, None, None, False, False, False, False,
    sounds if not isinstance(sounds, Sound) else None)

play_random = lambda sounds, soundName, pause=False, interrupt=False: play_sound(
    soundName, 1.0, False, None, None, None, True, interrupt, pause, False, sounds)

play_random_positional = lambda sounds, soundName, playerX, objectX: play_sound(
    soundName, 1.0, False, playerX, objectX, None, True, False, False, False, sounds)

play_directional_sound = lambda sounds, soundName, playerPos, objPos, centerDistance=3, volume=1.0: play_sound(
    soundName, volume, False, playerPos, objPos, centerDistance, False, False, False, False, sounds)

cut_scene = lambda sounds, soundName: _play_cutscene(soundName, sounds)

def play_random_falling(sounds, soundName, playerX, objectX, startY, currentY=0, maxY=20, existingChannel=None):
    """Handle falling sound."""
    if isinstance(sounds, Sound):
        return sounds.play_falling_sound(soundName, playerX, objectX, startY, currentY, maxY, existingChannel)

    # Legacy implementation
    left_vol, right_vol = _get_stereo_panning(playerX, objectX)
    if left_vol == 0 and right_vol == 0:
        if existingChannel:
            existingChannel.stop()
        return None

    # Calculate fall multiplier
    fallMultiplier = 1 - (currentY / maxY)
    finalLeft = left_vol * fallMultiplier
    finalRight = right_vol * fallMultiplier

    if existingChannel:
        existingChannel.set_volume(
            finalLeft * volumeService.sfxVolume,
            finalRight * volumeService.sfxVolume
        )
        return existingChannel

    # Find matching sound
    matched_sound = _find_matching_sound(soundName, sounds)
    if not matched_sound:
        return None

    # Play the sound
    channel = sounds[matched_sound].play()
    if channel:
        channel.set_volume(
            finalLeft * volumeService.sfxVolume,
            finalRight * volumeService.sfxVolume
        )
    return channel