#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Utility functions and Game class for Storm Games.

Provides:
- Game class for centralized management
- Miscellaneous helper functions
- Version checking utilities
"""

import pygame
import random
import math
import numpy as np
import time
import re
import requests
import os
from .input import check_for_exit
from setproctitle import setproctitle

from .services import PathService, ConfigService, VolumeService
from .sound import Sound
from .speech import Speech
from .scoreboard import Scoreboard

class Game:
    """Central class to manage all game systems."""

    def __init__(self, title):
        """Initialize a new game.

        Args:
            title (str): Title of the game
        """
        self.title = title

        # Initialize services
        self.pathService = PathService.get_instance().initialize(title)
        self.configService = ConfigService.get_instance()
        self.configService.set_game_info(title, self.pathService)
        self.volumeService = VolumeService.get_instance()

        # Initialize game components (lazy loaded)
        self._speech = None
        self._sound = None
        self._scoreboard = None

        # Display text instructions flag
        self.displayTextUsageInstructions = False

    @property
    def speech(self):
        """Get the speech system (lazy loaded).

        Returns:
            Speech: Speech system instance
        """
        if not self._speech:
            self._speech = Speech.get_instance()
        return self._speech

    @property
    def sound(self):
        """Get the sound system (lazy loaded).

        Returns:
            Sound: Sound system instance
        """
        if not self._sound:
            self._sound = Sound("sounds/", self.volumeService)
        return self._sound

    @property
    def scoreboard(self):
        """Get the scoreboard (lazy loaded).

        Returns:
            Scoreboard: Scoreboard instance
        """
        if not self._scoreboard:
            self._scoreboard = Scoreboard(self.configService)
        return self._scoreboard

    def initialize(self):
        """Initialize the game GUI and sound system.

        Returns:
            Game: Self for method chaining
        """
        # Set process title
        setproctitle(str.lower(str.replace(self.title, " ", "")))

        # Seed the random generator
        random.seed()

        # Initialize pygame
        pygame.init()
        pygame.display.set_mode((800, 600))
        pygame.display.set_caption(self.title)

        # Set up audio system
        pygame.mixer.pre_init(44100, -16, 2, 1024)
        pygame.mixer.init()
        pygame.mixer.set_num_channels(32)
        pygame.mixer.set_reserved(0)  # Reserve channel for cut scenes

        # Enable key repeat for volume controls
        pygame.key.set_repeat(500, 100)

        # Load sound effects
        self.sound

        # Play intro sound if available
        if 'game-intro' in self.sound.sounds:
            self.sound.cut_scene('game-intro')

        return self

    def speak(self, text, interrupt=True):
        """Speak text using the speech system.

        Args:
            text (str): Text to speak
            interrupt (bool): Whether to interrupt current speech

        Returns:
            Game: Self for method chaining
        """
        self.speech.speak(text, interrupt)
        return self

    def play_bgm(self, musicFile):
        """Play background music.

        Args:
            musicFile (str): Path to music file

        Returns:
            Game: Self for method chaining
        """
        self.sound.play_bgm(musicFile)
        return self

    def display_text(self, textLines):
        """Display text with navigation controls.

        Args:
            textLines (list): List of text lines

        Returns:
            Game: Self for method chaining
        """
        # Store original text with blank lines for copying
        originalText = textLines.copy()

        # Create navigation text by filtering out blank lines
        navText = [line for line in textLines if line.strip()]

        # Add instructions at the start on the first display
        if not self.displayTextUsageInstructions:
            instructions = ("Press space to read the whole text. Use up and down arrows to navigate "
                        "the text line by line. Press c to copy the current line to the clipboard "
                        "or t to copy the entire text. Press enter or escape when you are done reading.")
            navText.insert(0, instructions)
            self.displayTextUsageInstructions = True

        # Add end marker
        navText.append("End of text.")

        currentIndex = 0
        self.speech.speak(navText[currentIndex])

        while True:
            event = pygame.event.wait()
            if event.type == pygame.KEYDOWN:
                # Check for Alt modifier
                mods = pygame.key.get_mods()
                altPressed = mods & pygame.KMOD_ALT

                # Volume controls (require Alt)
                if altPressed:
                    if event.key == pygame.K_PAGEUP:
                        self.volumeService.adjust_master_volume(0.1, pygame.mixer)
                    elif event.key == pygame.K_PAGEDOWN:
                        self.volumeService.adjust_master_volume(-0.1, pygame.mixer)
                    elif event.key == pygame.K_HOME:
                        self.volumeService.adjust_bgm_volume(0.1, pygame.mixer)
                    elif event.key == pygame.K_END:
                        self.volumeService.adjust_bgm_volume(-0.1, pygame.mixer)
                    elif event.key == pygame.K_INSERT:
                        self.volumeService.adjust_sfx_volume(0.1, pygame.mixer)
                    elif event.key == pygame.K_DELETE:
                        self.volumeService.adjust_sfx_volume(-0.1, pygame.mixer)
                else:
                    if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
                        return self

                    if event.key in [pygame.K_DOWN, pygame.K_s] and currentIndex < len(navText) - 1:
                        currentIndex += 1
                        self.speech.speak(navText[currentIndex])

                    if event.key in [pygame.K_UP, pygame.K_w] and currentIndex > 0:
                        currentIndex -= 1
                        self.speech.speak(navText[currentIndex])

                    if event.key == pygame.K_SPACE:
                        # Join with newlines to preserve spacing in speech
                        self.speech.speak('\n'.join(originalText[1:-1]))

                    if event.key == pygame.K_c:
                        try:
                            import pyperclip
                            pyperclip.copy(navText[currentIndex])
                            self.speech.speak("Copied " + navText[currentIndex] + " to the clipboard.")
                        except:
                            self.speech.speak("Failed to copy the text to the clipboard.")

                    if event.key == pygame.K_t:
                        try:
                            import pyperclip
                            # Join with newlines to preserve blank lines in full text
                            pyperclip.copy(''.join(originalText[2:-1]))
                            self.speech.speak("Copied entire message to the clipboard.")
                        except:
                            self.speech.speak("Failed to copy the text to the clipboard.")

            pygame.event.clear()
            time.sleep(0.001)

    def exit(self):
        """Clean up and exit the game."""
        if self._speech and self.speech.providerName == "speechd":
            self.speech.close()
        pygame.mixer.music.stop()
        pygame.quit()
        import sys
        sys.exit()

# Utility functions

def check_for_updates(currentVersion, gameName, url):
    """Check for game updates.

    Args:
        currentVersion (str): Current version string (e.g. "1.0.0")
        gameName (str): Name of the game
        url (str): URL to check for updates

    Returns:
        dict: Update information or None if no update available
    """
    try:
        response = requests.get(url, timeout=5)
        if response.status_code == 200:
            data = response.json()
            if 'version' in data and data['version'] > currentVersion:
                return {
                    'version': data['version'],
                    'url': data.get('url', ''),
                    'notes': data.get('notes', '')
                }
    except Exception as e:
        print(f"Error checking for updates: {e}")
    return None

def get_version_tuple(versionStr):
    """Convert version string to comparable tuple.

    Args:
        versionStr (str): Version string (e.g. "1.0.0")

    Returns:
        tuple: Version as tuple of integers
    """
    return tuple(map(int, versionStr.split('.')))

def check_compatibility(requiredVersion, currentVersion):
    """Check if current version meets minimum required version.

    Args:
        requiredVersion (str): Minimum required version string
        currentVersion (str): Current version string

    Returns:
        bool: True if compatible, False otherwise
    """
    req = get_version_tuple(requiredVersion)
    cur = get_version_tuple(currentVersion)
    return cur >= req

def sanitize_filename(filename):
    """Sanitize a filename to be safe for all operating systems.

    Args:
        filename (str): Original filename

    Returns:
        str: Sanitized filename
    """
    # Remove invalid characters
    filename = re.sub(r'[\\/*?:"<>|]', "", filename)
    # Replace spaces with underscores
    filename = filename.replace(" ", "_")
    # Limit length
    if len(filename) > 255:
        filename = filename[:255]
    return filename

def lerp(start, end, factor):
    """Linear interpolation between two values.

    Args:
        start (float): Start value
        end (float): End value
        factor (float): Interpolation factor (0.0-1.0)

    Returns:
        float: Interpolated value
    """
    return start + (end - start) * factor

def smooth_step(edge0, edge1, x):
    """Hermite interpolation between two values.

    Args:
        edge0 (float): Start edge
        edge1 (float): End edge
        x (float): Value to interpolate

    Returns:
        float: Interpolated value with smooth step
    """
    # Scale, bias and saturate x to 0..1 range
    x = max(0.0, min(1.0, (x - edge0) / (edge1 - edge0)))
    # Evaluate polynomial
    return x * x * (3 - 2 * x)

def distance_2d(x1, y1, x2, y2):
    """Calculate Euclidean distance between two 2D points.

    Args:
        x1 (float): X coordinate of first point
        y1 (float): Y coordinate of first point
        x2 (float): X coordinate of second point
        y2 (float): Y coordinate of second point

    Returns:
        float: Distance between points
    """
    return math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)

def generate_tone(frequency, duration=0.1, sampleRate=44100, volume=0.2):
    """Generate a tone at the specified frequency.

    Args:
        frequency (float): Frequency in Hz
        duration (float): Duration in seconds (default: 0.1)
        sampleRate (int): Sample rate in Hz (default: 44100)
        volume (float): Volume from 0.0 to 1.0 (default: 0.2)

    Returns:
        pygame.mixer.Sound: Sound object with the generated tone
    """

    t = np.linspace(0, duration, int(sampleRate * duration), False)
    tone = np.sin(2 * np.pi * frequency * t)
    stereoTone = np.vstack((tone, tone)).T  # Create a 2D array for stereo
    stereoTone = (stereoTone * 32767 * volume).astype(np.int16)  # Apply volume
    stereoTone = np.ascontiguousarray(stereoTone)  # Ensure C-contiguous array
    return pygame.sndarray.make_sound(stereoTone)

def x_powerbar():
    """Sound based horizontal power bar

    Returns:
        int: Selected position between -50 and 50
    """

    clock = pygame.time.Clock()
    screen = pygame.display.get_surface()
    position = -50  # Start from the leftmost position
    direction = 1   # Move right initially
    barHeight = 20

    while True:
        frequency = 440  # A4 note
        leftVolume = (50 - position) / 100
        rightVolume = (position + 50) / 100
        tone = generate_tone(frequency)
        channel = tone.play()
        channel.set_volume(leftVolume, rightVolume)

        # Visual representation
        screen.fill((0, 0, 0))
        barWidth = screen.get_width() - 40  # Leave 20px margin on each side
        pygame.draw.rect(screen, (100, 100, 100), (20, screen.get_height() // 2 - barHeight // 2, barWidth, barHeight))
        markerPos = int(20 + (position + 50) / 100 * barWidth)
        pygame.draw.rect(screen, (255, 0, 0), (markerPos - 5, screen.get_height() // 2 - barHeight, 10, barHeight * 2))
        pygame.display.flip()

        for event in pygame.event.get():
            check_for_exit()
            if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                channel.stop()
                return position  # This will return a value between -50 and 50

        position += direction
        if position > 50:
            position = 50
            direction = -1
        elif position < -50:
            position = -50
            direction = 1

        clock.tick(40) # Speed of bar

def y_powerbar():
    """Sound based vertical power bar

    Returns:
        int: Selected power level between 0 and 100
    """

    clock = pygame.time.Clock()
    screen = pygame.display.get_surface()
    power = 0
    direction = 1  # 1 for increasing, -1 for decreasing
    barWidth = 20

    while True:
        frequency = 220 + (power * 5)  # Adjust these values to change the pitch range
        tone = generate_tone(frequency)
        channel = tone.play()

        # Visual representation
        screen.fill((0, 0, 0))
        barHeight = screen.get_height() - 40  # Leave 20px margin on top and bottom
        pygame.draw.rect(screen, (100, 100, 100), (screen.get_width() // 2 - barWidth // 2, 20, barWidth, barHeight))
        markerPos = int(20 + (100 - power) / 100 * barHeight)
        pygame.draw.rect(screen, (255, 0, 0), (screen.get_width() // 2 - barWidth, markerPos - 5, barWidth * 2, 10))
        pygame.display.flip()

        for event in pygame.event.get():
            check_for_exit()
            if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                channel.stop()
                return power

        power += direction
        if power >= 100 or power <= 0:
            direction *= -1  # Reverse direction at limits

        clock.tick(40)