#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# Copyright (c) 2024 Stormux
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3 of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program; if not, write to the
# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
# Boston MA  02110-1301 USA.


# ======= Version Configuration =======
# Update version number here for new releases of Toby Doom Accessibility Mod
# Example: 8.0 for version 8.0, 8.5 for version 8.5, etc

TOBY_VERSION_NUMBER = 8.0

# DO NOT EDIT ANYTHING BELOW THIS LINE!
# ===================================


import configparser
import json
import sys
import os
import re
import subprocess
import time
import platform
import shutil
import glob
import threading
from pathlib import Path
from typing import Final, List, Dict, Optional, Tuple
from setproctitle import setproctitle
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QHBoxLayout, QVBoxLayout, 
    QComboBox, QPushButton, QLabel, QSpinBox, QMessageBox, QLineEdit, QDialog,
    QDialogButtonBox, QRadioButton)
from PySide6.QtCore import Qt, QTimer
import webbrowser

# Initialize speech provider based on platform
if platform.system() == "Windows":
    # Set up DLL paths for Windows
    if getattr(sys, 'frozen', False):
        # If running as compiled executable
        dllPath = os.path.join(sys._MEIPASS, 'lib')
        if os.path.exists(dllPath):
            os.add_dll_directory(dllPath)
        # Also add the executable's directory
        os.add_dll_directory(os.path.dirname(sys.executable))

    # Sound playback for audio manual on Windows
    # Initialize Windows speech provider
    try:
        import accessible_output2.outputs.auto
        s = accessible_output2.outputs.auto.Auto()
        speechProvider = "accessible_output2"
    except ImportError as e:
        print(f"Failed to initialize accessible_output2: {e}")
        sys.exit()
else:
    # Linux/Mac path
    try:
        output = subprocess.check_output(["pgrep", "cthulhu"])
        speechProvider = "cthulhu"
    except (subprocess.CalledProcessError, FileNotFoundError):
        try:
            import accessible_output2.outputs.auto
            s = accessible_output2.outputs.auto.Auto()
            speechProvider = "accessible_output2"
        except ImportError as e:
            try:
                import speechd
                spd = speechd.Client()
                speechProvider = "speechd"
            except ImportError:
                print("No speech providers found.")
                sys.exit()


class AccessibleComboBox(QComboBox):
    """ComboBox with enhanced keyboard navigation"""
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setEditable(True)
        self.lineEdit().setReadOnly(True)
        self.pageStep = 5  # Number of items to jump for page up/down

    def keyPressEvent(self, event):
        currentIndex = self.currentIndex()
        itemCount = self.count()
        
        if event.key() == Qt.Key_PageUp:
            newIndex = max(0, currentIndex - self.pageStep)
            self.setCurrentIndex(newIndex)
        elif event.key() == Qt.Key_PageDown:
            newIndex = min(itemCount - 1, currentIndex + self.pageStep)
            self.setCurrentIndex(newIndex)
        elif event.key() == Qt.Key_Home:
            self.setCurrentIndex(0)
            # Force update and focus events
            self.setFocus()
            self.currentIndexChanged.emit(0)
            self.activated.emit(0)
        elif event.key() == Qt.Key_End:
            lastIndex = itemCount - 1
            self.setCurrentIndex(lastIndex)
            # Force update and focus events
            self.setFocus()
            self.currentIndexChanged.emit(lastIndex)
            self.activated.emit(lastIndex)
        else:
            super().keyPressEvent(event)


class SpeechHandler:
    """Handles text-to-speech processing for game output"""
    
    # Class-level constants for patterns
    FILTER_PATTERNS = [
        r'^----+$',
        r'^$',
        r'^[0-9]',
        r'^P_StartScript:',
        r'^(ALSA|Cannot|Facing |fluidsynth|INTRO|MAP[0-9]+|Music "|Unknown)',
        r'^(\[Toby Accessibility Mod\] )?READ.*',
        r'^ *TITLEMAP',
        r'^\[Toby Accessibility Mod\] (INTRO|READMe)([0-9]+).*',
        r'key card',
        r'^New PDA Entry:',
        r"^(As |Computer Voice:|Holy|I |I've|Monorail|Sector |Ugh|What|Where)",
        r'Script warning, "',
        r'Tried to define'
    ]
    
    TEXT_REPLACEMENTS = [
        (r'^\[Toby Accessibility Mod\] M_', r'[Toby Accessibility Mod] '),
        (r'^\[Toby Accessibility Mod\] ', r''),
        (r'^MessageBoxMenu$', r'Confirmation menu: Press Y for yes or N for no'),
        (r'^Mainmenu$', r'Main menu'),
        (r'^Skillmenu$', r'Difficulty menu'),
        (r'^Episodemenu$', r'Episode menu'),
        (r'^Playerclassmenu$', r'Player class menu'),
        (r'^Loadmenu$', r'Load menu'),
        (r'^Savemenu$', r'Save menu'),
        (r'^Optionsmenu$', r'Options menu'),
        (r'([A-Z][a-z0-9]+)menu$', r'\1 menu'),
        (r'^NGAME$', r'New game'),
        (r'^(LOAD|SAVE|QUIT)G$', r'\1 game'),
        (r'"cl_run" = "true"', r'run'),
        (r'"cl_run" = "false"', r'walk'),
        (r'UAC', r'U A C'),
        (r'^JKILL', r"I'm too young to die"),
        (r'^ROUGH', r'Hey, not too rough'),
        (r'^HURT', r'Hurt me plenty'),
        (r'^ULTRA', r'Ultra-Violence'),
        (r'^\+', r''),
        (r' ?\*+ ?', r'')
    ]
    
    def __init__(self):
        """Initialize the speech handler"""
        self.platform = platform.system()
        
        # Compile all regex patterns once at initialization
        self.filterPatterns = [re.compile(pattern) for pattern in self.FILTER_PATTERNS]
        self.textReplacements = [(re.compile(pattern), repl) 
                               for pattern, repl in self.TEXT_REPLACEMENTS]

    def speak(self, text: str) -> None:
        """Speak text using available speech method"""
        if not text:
            return
            
        if speechProvider == "speechd":
            spd.cancel()
            spd.speak(text)
        elif speechProvider == "accessible_output2":
            s.speak(text, interrupt=True)
        else:  # Cthulhu
            try:
                process = subprocess.Popen(
                    ["socat", "-", "UNIX-CLIENT:/tmp/cthulhu.sock"],
                    stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE,
                    stderr=subprocess.PIPE,
                    text=True
                )
                process.communicate(input=text)
            except Exception as e:
                print(f"Cthulhu error: {e}", file=sys.stderr)

    def process_line(self, line: str) -> Optional[str]:
        """Process a line of game output for speech"""
        # Skip empty lines
        if not line.strip():
            return None
            
        # Check if line should be filtered out
        for pattern in self.filterPatterns:
            if pattern.search(line):
                return None
                
        # Apply replacements
        processedLine = line
        for pattern, repl in self.textReplacements:
            processedLine = pattern.sub(repl, processedLine)
            
        return processedLine.strip() if processedLine.strip() else None

    def speak_thread(self, process: subprocess.Popen):
        """Thread to handle speech processing"""
        startSpeech = False  # Don't start speaking until after initial output
        
        try:
            # Read directly from stdout, no need for buffer attribute
            for line in process.stdout:
                try:
                    # Handle encoding by trying utf-8 first, then fallback to latin1
                    try:
                        if isinstance(line, bytes):
                            lineStr = line.decode('utf-8').strip()
                        else:
                            lineStr = line.strip()
                    except UnicodeDecodeError:
                        lineStr = line.decode('latin1').strip()
                    
                    # Keep gzdoom's existing functionality of lines being printed to the console
                    print(lineStr)
                    
                    # Wait for the initial separator before starting speech
                    if not startSpeech:
                        if lineStr and all(c == '-' for c in lineStr):
                            startSpeech = True
                        continue
                    
                    processedLine = self.process_line(lineStr)
                    if processedLine:
                        self.speak(processedLine)
                        
                except Exception as e:
                    print(f"Error processing line: {e}", file=sys.stderr)
                    continue  # Skip this line and continue with the next
                    
        except Exception as e:
            print(f"Error in speech thread: {e}", file=sys.stderr)


class MenuDialog(QDialog):
    """Dialog for game configuration options"""
    
    def __init__(self, title: str, options: Dict[str, dict], parent=None):
        super().__init__(parent)
        self.setWindowTitle(title)
        self.dialogOptions = options
        self.init_dialog_ui()

    def init_dialog_ui(self):
        """Initialize the dialog UI components"""
        dialogLayout = QVBoxLayout(self)
        
        for key, opt in self.dialogOptions.items():
            if opt['type'] == 'radio':
                dialogWidget = QRadioButton(opt['label'])
            elif opt['type'] == 'spinbox':
                # Create label first
                label = QLabel(opt['label'])
                dialogWidget = QSpinBox()
                dialogWidget.setRange(opt['min'], opt['max'])
                dialogWidget.setValue(opt.get('default', opt['min']))
                # Set accessibility label
                dialogWidget.setAccessibleName(opt['label'])
                # Add label to layout first
                dialogLayout.addWidget(label)
            elif opt['type'] == 'text':
                dialogWidget = QLineEdit()
                dialogWidget.setPlaceholderText(opt['placeholder'])
            elif opt['type'] == 'combobox':
                dialogWidget = AccessibleComboBox()
                dialogWidget.addItems(opt['items'])
                dialogLayout.addWidget(QLabel(opt['label']))
            else:
                continue
                
            setattr(self, f"{key}_widget", dialogWidget)
            dialogLayout.addWidget(dialogWidget)

        dialogButtons = QDialogButtonBox(
            QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        dialogButtons.accepted.connect(self.accept)
        dialogButtons.rejected.connect(self.reject)
        dialogLayout.addWidget(dialogButtons)

    def get_dialog_values(self) -> dict:
        """Get the current values from all dialog widgets"""
        values = {}
        for key in self.dialogOptions.keys():
            widget = getattr(self, f"{key}_widget")
            if isinstance(widget, QSpinBox):
                values[key] = widget.value()
            elif isinstance(widget, QComboBox):
                values[key] = widget.currentText()
            elif isinstance(widget, QRadioButton):
                values[key] = widget.isChecked()
            else:
                values[key] = widget.text()
        return values


class AudioPlayer:
    """Handles cross-platform audio playback using VLC if available"""

    def __init__(self):
        self.currentTrack = None
        self.currentIndex = -1
        self.isPlaying = False
        self.playAllMode = False
        self.tracks = []
        self.vlcAvailable = False
        
        # State monitoring timer
        self.stateTimer = QTimer()
        self.stateTimer.setInterval(500)  # Check every 500ms
        self.stateTimer.timeout.connect(self.checkPlayerState)
        
        try:
            import vlc
            self.instance = vlc.Instance()
            self.player = self.instance.media_player_new()
            # Store VLC states we care about
            self.State_Ended = vlc.State.Ended
            self.State_Error = vlc.State.Error
            self.State_Playing = vlc.State.Playing
            self.vlcAvailable = True
        except Exception as e:
            print(f"VLC not available: {e}", file=sys.stderr)
            self.instance = None
            self.player = None
    
    def checkPlayerState(self):
        """Monitor VLC player state"""
        if not self.vlcAvailable or not self.player or not self.isPlaying:
            return
            
        try:
            state = self.player.get_state()
            
            # Check for end states
            if state in (self.State_Ended, self.State_Error):
                print(f"Track ended (state: {state})")
                self.isPlaying = False
                self.stateTimer.stop()
                
                if self.playAllMode and self.currentIndex < len(self.tracks) - 1:
                    self.currentIndex += 1
                    self.play()
                    
        except Exception as e:
            print(f"Error checking player state: {e}", file=sys.stderr)
        
    def loadTracks(self, files):
        """Load list of tracks to play"""
        if not self.vlcAvailable:
            return
        self.tracks = [str(f) for f in files]
        self.currentIndex = 0 if self.tracks else -1
        print(f"Loaded tracks: {self.tracks}")
        
    def play(self):
        """Play current track"""
        if not self.vlcAvailable:
            return False
            
        if self.currentIndex >= 0 and self.currentIndex < len(self.tracks):
            self.stop() # Stop any current playback first
            self.currentTrack = self.tracks[self.currentIndex]
            try:
                print(f"Attempting to play: {self.currentTrack}")
                media = self.instance.media_new(self.currentTrack)
                self.player.set_media(media)
                result = self.player.play()
                if result == 0: # VLC returns 0 on success
                    self.isPlaying = True
                    self.stateTimer.start()  # Start state monitoring
                    return True
                else:
                    print(f"VLC play() returned error: {result}", file=sys.stderr)
                    return False
            except Exception as e:
                print(f"Playback error: {e}", file=sys.stderr)
                self.isPlaying = False
                return False
        return False
        
    def stop(self):
        """Stop playback"""
        if not self.vlcAvailable:
            return
            
        if self.player and self.isPlaying:
            try:
                self.stateTimer.stop() # Stop state monitoring
                self.player.stop()
                self.isPlaying = False
                print("Playback stopped")
            except Exception as e:
                print(f"Stop error: {e}", file=sys.stderr)
            
    def nextTrack(self):
        """Move to next track"""
        if not self.vlcAvailable:
            return False
            
        if self.currentIndex < len(self.tracks) - 1:
            self.stop()
            self.currentIndex += 1
            return True
        return False
            
    def previousTrack(self):
        """Move to previous track"""
        if not self.vlcAvailable:
            return False
            
        if self.currentIndex > 0:
            self.stop()
            self.currentIndex -= 1
            return True
        return False
            
    def getCurrentTrackName(self):
        """Get current track name"""
        if not self.vlcAvailable:
            return ""
            
        if self.currentTrack:
            return Path(self.currentTrack).stem
        return ""


class AudioManualDialog(QDialog):
    """Dialog for audio manual playback"""
    def __init__(self, manualPath, parent=None):
        super().__init__(parent)
        self.manualPath = manualPath
        self.audioPlayer = AudioPlayer()
        
        # Show warning if VLC not available
        if not self.audioPlayer.vlcAvailable:
            QMessageBox.warning(self, "VLC Not Found", 
                "VLC is required for Audio Manual playback. Please install VLC media player. If running from source, also install python-vlc.")
            self.close()
            return
        
        # Create update timer for checking playback state
        self.stateTimer = QTimer(self)
        self.stateTimer.timeout.connect(self.checkPlaybackState)
        self.stateTimer.start(500)  # Check every 500ms
        
        self.initUI()


class AudioManualDialog(QDialog):
    """Dialog for audio manual playback"""
    def __init__(self, manualPath, parent=None):
        super().__init__(parent)
        self.manualPath = manualPath
        self.audioPlayer = AudioPlayer()
        
        # Create update timer for checking playback state
        self.stateTimer = QTimer(self)
        self.stateTimer.timeout.connect(self.checkPlaybackState)
        self.stateTimer.start(500)  # Check every 500ms
        
        self.initUI()
        
    def checkPlaybackState(self):
        """Periodically check playback state and update UI"""
        if not self.audioPlayer.isPlaying and self.stopButton.isEnabled():
            self.updateButtonStates()

    def initUI(self):
        """Initialize the dialog UI"""
        self.setWindowTitle("Audio Manual")
        layout = QVBoxLayout(self)

        # Manual selection
        manualLabel = QLabel("Select Manual:")
        self.manualCombo = AccessibleComboBox()
        self.manualCombo.setAccessibleName("Manual Selection")
        self.populateManuals()
        layout.addWidget(manualLabel)
        layout.addWidget(self.manualCombo)

        # Track selection
        trackLabel = QLabel("Select Track:")
        self.trackCombo = AccessibleComboBox()
        self.trackCombo.setAccessibleName("Track Selection")
        layout.addWidget(trackLabel)
        layout.addWidget(self.trackCombo)

        # Create buttons
        buttonLayout = QHBoxLayout()
        self.prevButton = QPushButton("Previous")
        self.playButton = QPushButton("Play")
        self.stopButton = QPushButton("Stop")
        self.nextButton = QPushButton("Next")
        
        # Setup focus and keyboard interaction
        self.manualCombo.setFocusPolicy(Qt.StrongFocus)
        self.trackCombo.setFocusPolicy(Qt.StrongFocus)
        # Allow Enter key to play selected track
        self.trackCombo.lineEdit().returnPressed.connect(self.playAudio)

        # Set keyboard shortcuts and accessibility for buttons
        self.prevButton.setShortcut("Left")
        self.playButton.setShortcut("Space")
        self.stopButton.setShortcut("S")
        self.nextButton.setShortcut("Right")
        
        # Set accessible names for buttons
        self.prevButton.setAccessibleName("Previous Track")
        self.playButton.setAccessibleName("Play Track")
        self.stopButton.setAccessibleName("Stop Playback")
        self.nextButton.setAccessibleName("Next Track")
        
        # Connect button signals
        self.prevButton.clicked.connect(self.previousTrack)
        self.playButton.clicked.connect(self.playAudio)
        self.stopButton.clicked.connect(self.stopAudio)
        self.nextButton.clicked.connect(self.nextTrack)
        
        # Add buttons to layout
        buttonLayout.addWidget(self.prevButton)
        buttonLayout.addWidget(self.playButton)
        buttonLayout.addWidget(self.stopButton)
        buttonLayout.addWidget(self.nextButton)
        layout.addLayout(buttonLayout)

        # Status label
        self.statusLabel = QLabel("")
        self.statusLabel.setAccessibleName("Playback Status")
        layout.addWidget(self.statusLabel)

        # Update tracks when manual changes
        self.manualCombo.currentTextChanged.connect(self.populateTracks)

        # Close button
        closeButton = QPushButton("Close")
        closeButton.setAccessibleName("Close Dialog")
        closeButton.setShortcut("Escape")
        closeButton.clicked.connect(self.close)
        layout.addWidget(closeButton)
        
        # Initial setup
        self.populateTracks()
        self.updateButtonStates()

    def closeEvent(self, event):
        """Handle dialog close event"""
        self.stateTimer.stop()
        self.audioPlayer.stop()
        super().closeEvent(event)

    def populateManuals(self):
        """Populate manual selection combo box"""
        manualDirs = sorted([d for d in self.manualPath.iterdir() if d.is_dir()])
        self.manualCombo.addItems([m.name for m in manualDirs])

    def populateTracks(self):
        """Populate track selection combo box"""
        self.trackCombo.clear()
        selectedManual = self.manualPath / self.manualCombo.currentText()
        if selectedManual.exists():
            tracks = sorted(selectedManual.glob('*.mp3'))
            self.trackCombo.addItem("Play All")
            self.trackCombo.addItems([t.stem for t in tracks])
            # Update button states after populating tracks
            self.updateButtonStates()

    def playAudio(self):
        """Start audio playback"""
        if not self.manualCombo.currentText() or not self.trackCombo.currentText():
            return
            
        selectedManual = self.manualPath / self.manualCombo.currentText()
        selectedTrack = self.trackCombo.currentText()

        # Stop any current playback
        self.audioPlayer.stop()
        self.audioPlayer.playAllMode = selectedTrack == "Play All"

        if selectedTrack == "Play All":
            # Get sorted list of MP3 files
            tracks = sorted(selectedManual.glob('*.mp3'))
            print(f"Loading {len(tracks)} tracks for Play All")
            self.audioPlayer.loadTracks([str(t) for t in tracks])
        else:
            print("Loading single track")
            tracks = [selectedManual / f"{selectedTrack}.mp3"]
            self.audioPlayer.loadTracks([str(t) for t in tracks])

        # Start playback
        if self.audioPlayer.play():
            self.statusLabel.setText(f"Playing: {self.audioPlayer.getCurrentTrackName()}")
        else:
            self.statusLabel.setText("Playback error")
        self.updateButtonStates()

    def stopAudio(self):
        """Stop audio playback"""
        self.audioPlayer.playAllMode = False  # Reset play all flag
        self.audioPlayer.stop()
        self.statusLabel.setText("Playback stopped")
        self.updateButtonStates()

    def nextTrack(self):
        """Play next track"""
        if self.audioPlayer.nextTrack():
            if self.audioPlayer.play():
                self.statusLabel.setText(f"Playing: {self.audioPlayer.getCurrentTrackName()}")
            else:
                self.statusLabel.setText("Playback error")
        self.updateButtonStates()

    def previousTrack(self):
        """Play previous track"""
        if self.audioPlayer.previousTrack():
            if self.audioPlayer.play():
                self.statusLabel.setText(f"Playing: {self.audioPlayer.getCurrentTrackName()}")
            else:
                self.statusLabel.setText("Playback error")
        self.updateButtonStates()

    def updateButtonStates(self):
        """Update button enabled states"""
        trackCount = len(self.audioPlayer.tracks)
        hasMultipleTracks = trackCount > 1
        isFirst = self.audioPlayer.currentIndex <= 0
        isLast = self.audioPlayer.currentIndex >= trackCount - 1

        # Enable play button if we have any tracks selected or available
        hasTrackSelected = (self.trackCombo.count() > 0 and self.trackCombo.currentText()) or trackCount > 0

        # Allow navigation during playback for Play All mode
        self.prevButton.setEnabled(hasMultipleTracks and not isFirst)
        self.nextButton.setEnabled(hasMultipleTracks and not isLast)
        self.playButton.setEnabled(hasTrackSelected and not self.audioPlayer.isPlaying)
        self.stopButton.setEnabled(self.audioPlayer.isPlaying)


class IWADSelector:
    """Handles IWAD file detection and selection"""
    
    def __init__(self):
        if platform.system() == "Windows":
            self.configFile = Path.cwd() / 'TobyConfig.ini'
        else:
            self.configFile = Path(os.getenv('XDG_CONFIG_HOME', Path.home() / '.config')) / 'gzdoom/gzdoom.ini'
        self.wadPaths = self._get_wad_paths()
        
    def _get_wad_paths(self) -> List[str]:
        """Extract IWAD search paths from GZDoom config"""
        if not self.configFile.exists():
            print("Config file not found")
            return []
            
        try:
            # Read the file directly to handle duplicate keys
            paths = []
            currentSection = None
            with open(self.configFile, 'r') as f:
                for line in f:
                    line = line.strip()
                    if line.startswith('['):
                        currentSection = line[1:-1]
                    elif currentSection == 'IWADSearch.Directories' and '=' in line:
                        key, value = line.split('=', 1)
                        if key.strip().lower().startswith('path'):
                            value = value.strip()
                            # Handle special paths
                            if value == '$DOOMWADDIR' or value == '$PROGDIR':
                                continue  # Skip these as they're GZDoom internal
                            elif value == '$HOME':
                                value = str(Path.home())
                            paths.append(value)
                
        except Exception as e:
            print(f"Error reading config: {e}", file=sys.stderr)
        
        # Additional paths to check
        if platform.system() == "Windows":
            paths.append(str(Path.cwd()))
        else:
                paths.append(str(Path("/usr/share/doom")))
                paths.append(str(Path("/usr/share/games/doom")))
                paths.append(str(Path.home() / ".local/games/doom"))
                paths.append(str(Path.home() / ".local/share/doom"))
        return paths

    def is_iwad(self, file_path: str) -> bool:
        """Check if a file is an IWAD or IPK3"""
        path = Path(file_path)
        if path.suffix.lower() == '.ipk3':
            return True
            
        try:
            with open(file_path, 'rb') as f:
                header = f.read(4)
                return header == b'IWAD'
        except Exception:
            return False
            
    def find_iwads(self) -> Dict[str, str]:
        """Find all available IWADs in configured paths"""
        uniqueWads = {}
        
        for path in self.wadPaths:
            wadDir = Path(path)
            if not wadDir.is_dir():
                continue
                
            # Only look at files directly in the directory, not subdirectories
            for wadFile in wadDir.iterdir():
                if wadFile.is_file():
                    # Check if it's a WAD or IPK3 file
                    if wadFile.suffix.lower() in ['.wad', '.iwad']:
                        if self.is_iwad(str(wadFile)):
                            wadName = wadFile.stem.lower()
                            uniqueWads[wadName] = str(wadFile)
                    elif wadFile.suffix.lower() == '.ipk3':
                        if self.is_iwad(str(wadFile)):
                            wadName = wadFile.stem.lower()
                            uniqueWads[wadName] = str(wadFile)
                    
        return uniqueWads


class CustomGameDialog(QDialog):
    """Dialog for selecting and configuring custom games"""
    def __init__(self, customGames: Dict[str, dict], parent=None):
        super().__init__(parent)
        self.setWindowTitle("Custom Game Selection")
        self.customGames = customGames

        # Create layout
        layout = QVBoxLayout(self)
        
        # Game selection combobox
        label = QLabel("Select Custom Game:")
        self.gameCombo = AccessibleComboBox()
        self.gameCombo.setAccessibleName("Custom Game Selection")
        self.gameCombo.addItems(sorted(customGames.keys()))
        # Connect enter key to accept
        self.gameCombo.lineEdit().returnPressed.connect(self.accept)
        
        layout.addWidget(label)
        layout.addWidget(self.gameCombo)

        # Dialog buttons
        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)

    def keyPressEvent(self, event):
        """Handle key press events"""
        if event.key() in (Qt.Key_Return, Qt.Key_Enter):
            self.accept()
        else:
            super().keyPressEvent(event)

    def get_selected_game(self) -> Optional[str]:
        """Get the selected game name"""
        if self.result() == QDialog.Accepted:
            return self.gameCombo.currentText()
        return None


class DoomLauncher(QMainWindow):
    """Main launcher window for Toby Doom"""
    
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Toby Doom Launcher")
    
        if platform.system() == "Windows":
            self.configFile = Path.cwd() / 'TobyConfig.ini'
            self.gamePath = Path.cwd()
        else:
            self.gamePath = Path.home() / ".local/games/doom"
            self.configFile = Path(os.getenv('XDG_CONFIG_HOME', Path.home() / '.config')) / 'gzdoom/gzdoom.ini'
        
        self.tobyVersion = TOBY_VERSION
        self.speechHandler = SpeechHandler()
        self.iwadSelector = IWADSelector()  # Add IWAD selector
        self.init_launcher_ui()

    def keyPressEvent(self, event):
        """Handle key press events"""
        if event.key() == Qt.Key_Escape:
            self.close()

    def handle_button_keypress(self, event, button):
        """Handle key press events for buttons"""
        if event.key() in (Qt.Key_Return, Qt.Key_Enter):
            button.click()
        # Make sure to call the parent class's key press event
        QPushButton.keyPressEvent(button, event)

    def populate_iwad_list(self):
        """Populate the IWAD selection combo box"""
        iwads = self.iwadSelector.find_iwads()
        for name, path in iwads.items():
            self.iwadCombo.addItem(name, userData=path)

    def init_launcher_ui(self):
        """Initialize the main launcher UI"""
        centralWidget = QWidget()
        self.setCentralWidget(centralWidget)
        mainLayout = QVBoxLayout(centralWidget)
    
        # IWAD Selection
        iwadLabel = QLabel("Select IWAD:")
        self.iwadCombo = AccessibleComboBox(self)
        self.iwadCombo.setAccessibleName("IWAD Selection")
        self.populate_iwad_list()
        mainLayout.addWidget(iwadLabel)
        mainLayout.addWidget(self.iwadCombo)
    
        # Game Selection
        self.gameCombo = AccessibleComboBox(self)
        self.gameCombo.setAccessibleName("Game Selection")
        self.populate_game_list()
        self.gameCombo.lineEdit().returnPressed.connect(self.launch_single_player)
        mainLayout.addWidget(QLabel("Select Game:"))
        mainLayout.addWidget(self.gameCombo)
    
        # Narration style selection
        self.narrationCombo = AccessibleComboBox(self)
        self.narrationCombo.setAccessibleName("Narration Style")
        self.narrationCombo.addItems(["Self-voiced", "Text to Speech"])
        # Set current value based on config
        current = self.get_narration_type()
        self.narrationCombo.setCurrentText(
            "Self-voiced" if current == 0 else "Text to Speech"
        )
        self.narrationCombo.currentTextChanged.connect(self.narration_type_changed)
    
        mainLayout.addWidget(QLabel("Narration Style:"))
        mainLayout.addWidget(self.narrationCombo)

        # Create buttons
        self.singlePlayerBtn = QPushButton("&Single Player")
        self.deathMatchBtn = QPushButton("&Deathmatch")
        self.customDeathMatchBtn = QPushButton("C&ustom Deathmatch")  # Alt+U
        self.coopBtn = QPushButton("&Co-op")
        self.audioManualBtn = QPushButton("&Audio Manual")  # Alt+A

        self.singlePlayerBtn.clicked.connect(self.launch_single_player)
        self.deathMatchBtn.clicked.connect(self.show_deathmatch_dialog)
        self.customDeathMatchBtn.clicked.connect(self.show_custom_deathmatch_dialog)  # New line
        self.coopBtn.clicked.connect(self.show_coop_dialog)
        self.audioManualBtn.clicked.connect(self.show_audio_manual)

        self.singlePlayerBtn.keyPressEvent = lambda e: self.handle_button_keypress(e, self.singlePlayerBtn)
        self.deathMatchBtn.keyPressEvent = lambda e: self.handle_button_keypress(e, self.deathMatchBtn)
        self.customDeathMatchBtn.keyPressEvent = lambda e: self.handle_button_keypress(e, self.customDeathMatchBtn)  # New line
        self.coopBtn.keyPressEvent = lambda e: self.handle_button_keypress(e, self.coopBtn)
        self.audioManualBtn.keyPressEvent = lambda e: self.handle_button_keypress(e, self.audioManualBtn)

        mainLayout.addWidget(self.singlePlayerBtn)
        mainLayout.addWidget(self.deathMatchBtn)
        mainLayout.addWidget(self.customDeathMatchBtn)
        mainLayout.addWidget(self.coopBtn)
        mainLayout.addWidget(self.audioManualBtn)
        

    def get_narration_type(self) -> int:
        """Get the current narration type from config file"""
        try:
            if not self.configFile.exists():
                return 0  # Default if file doesn't exist
            
            with open(self.configFile, 'r') as f:
                for line in f:
                    line = line.strip()
                    if line.startswith('Toby_NarrationOutputType='):
                        value = line.split('=')[1].strip()
                        return int(value)
            return 0  # Default to self-voiced if not found
        except Exception as e:
            print(f"Error reading config: {e}", file=sys.stderr)
            return 0

    def set_narration_type(self, value: int) -> bool:
        """Set the narration type in config file
    
        Args:
            value (int): Narration type (0 for self-voiced, 2 for TTS)
        
        Returns:
            bool: True if successful, False otherwise
        """
        try:
            if not self.configFile.exists():
                # Create new config with default section
                with open(self.configFile, 'w') as f:
                    f.write('[GlobalSettings]\n')
                    f.write(f'Toby_NarrationOutputType={value}\n')
                return True

            # Read all lines
            with open(self.configFile, 'r') as f:
                lines = f.readlines()

            # Try to find and replace existing setting
            found = False
            for i, line in enumerate(lines):
                if line.strip().startswith('Toby_NarrationOutputType='):
                    lines[i] = f'Toby_NarrationOutputType={value}\n'
                    found = True
                    break

            # If not found, add to end or after [GlobalSettings]
            if not found:
                globalSettingsIndex = -1
                for i, line in enumerate(lines):
                    if line.strip() == '[GlobalSettings]':
                        globalSettingsIndex = i
                        break

                if globalSettingsIndex >= 0:
                    # Insert after [GlobalSettings]
                    lines.insert(globalSettingsIndex + 1, f'Toby_NarrationOutputType={value}\n')
                else:
                    # Add [GlobalSettings] section if it doesn't exist
                    lines.append('\n[GlobalSettings]\n')
                    lines.append(f'Toby_NarrationOutputType={value}\n')

            # Write back the modified content
            with open(self.configFile, 'w') as f:
                f.writelines(lines)
            return True

        except Exception as e:
            print(f"Error writing config: {e}", file=sys.stderr)
            return False

    def narration_type_changed(self, text: str):
        """Handle narration type combobox changes"""
        value = 0 if text == "Self-voiced" else 2
        if not self.set_narration_type(value):
            QMessageBox.warning(
                self,
                "Error",
                "Failed to update narration setting. Check file permissions."
            )
            # Reset combobox to current value
            current = self.get_narration_type()
            self.narrationCombo.setCurrentText(
                "Self-voiced" if current == 0 else "Text to Speech"
            )

    def populate_game_list(self):
        """Populate the game selection combo box"""
        gameList = [
            "Toby Demo Map",
            "Classic Doom",
            "Toby Doom",
            "OperationMDK",
            "Classic Heretic",
            "Toby Heretic",
            "Classic Hexen",
            "Toby Hexen",
            "Custom Game"
        ]

        for gameName in gameList:
            self.gameCombo.addItem(gameName)

    def find_freedm(self) -> Optional[str]:
        """Find freedm.wad in standard locations"""
        # Check common locations
        locations = [
            self.gamePath / "freedm.wad",
            Path("/usr/share/games/doom/freedm.wad"),
            Path("/usr/share/doom/freedm.wad")
        ]
    
        for loc in locations:
            if loc.exists():
                return str(loc)
            
        return None

    def find_gzdoom(self) -> Optional[str]:
        """Find the GZDoom executable"""
        if platform.system() == "Windows":
            gzdoomPath = Path.cwd() / "gzdoom.exe"
            return str(gzdoomPath) if gzdoomPath.exists() else None
        
        return shutil.which("gzdoom")

    def get_addon_files(self, game_type: str = "DOOM") -> List[str]:
        """Get all addon PK3 files for specified game type"""
        addonFiles = []
        # MENU addons are common to all games
        menuPath = self.gamePath / "Addons" / "MENU"
        if menuPath.exists():
            addonFiles.extend(str(p) for p in menuPath.glob("Toby*.pk3"))

        # Game specific addons
        gamePath = self.gamePath / "Addons" / game_type
        if gamePath.exists():
            if game_type == "HERETIC":
                pattern = "TobyHeretic*.pk3"
            elif game_type == "HEXEN":
                pattern = "TobyHexen*.pk3"
            else:  # DOOM
                pattern = "Toby*.pk3"
            addonFiles.extend(str(p) for p in gamePath.glob(pattern))

        return addonFiles

    def get_selected_game_files(self) -> List[str]:
        tobyMod = self.gamePath / f"TobyAccMod_V{self.tobyVersion}.pk3"
        if not tobyMod.exists():
            QMessageBox.critical(self, "Error", f"Could not find {tobyMod}")
            return []

        baseFiles = [str(tobyMod)]
        selectedGame = self.gameCombo.currentText()

        # Determine game type and get corresponding addons
        if "Heretic" in selectedGame:
            gameType = "HERETIC"
            if "Toby Heretic" in selectedGame:
                baseFiles.append(str(self.gamePath / "Addons/MAPS/TobyHereticLevels.wad"))
        elif "Hexen" in selectedGame:
            gameType = "HEXEN"
            if "Toby Hexen" in selectedGame:
                baseFiles.append(str(self.gamePath / "Addons/MAPS/TobyHexen.pk3"))
        else:  # Doom games
            gameType = "DOOM"
            if "Demo Map" in selectedGame:
                baseFiles.append(str(self.gamePath / "Addons/MAPS/Toby-Demo-Level.wad"))
            elif "Toby Doom" in selectedGame:
                baseFiles.append(str(self.gamePath / "Addons/MAPS/TobyDoomLevels.wad"))
                musicRenamer = self.gamePath / "Toby-Doom-Level-Music-Renamer.pk3"
                if musicRenamer.exists():
                    baseFiles.append(str(musicRenamer))
            elif "OperationMDK" in selectedGame:
                baseFiles.append(str(self.gamePath / "OpMDK.wad"))

            # Add metal music mod if available (Doom only)
            metalV7 = self.gamePath / "DoomMetalVol7.wad"
            metalV6 = self.gamePath / "DoomMetalVol6.wad"
            if metalV7.exists():
                baseFiles.append(str(metalV7))
            elif metalV6.exists():
                baseFiles.append(str(metalV6))

        # Add game-specific addons
        baseFiles.extend(self.get_addon_files(gameType))
        return baseFiles

    def show_custom_deathmatch_dialog(self):
        """Show custom deathmatch configuration dialog"""
        # First find available PK3s for customization
        pk3List = []
        for item in self.gamePath.glob('*.pk3'):
            if item.stat().st_size > 10 * 1024 * 1024:  # >10MB
                pk3List.append(str(item))
    
        # Add Army of Darkness if available
        aodWad = self.gamePath / "aoddoom1.wad"
        if aodWad.exists():
            pk3List.append(str(aodWad))
    
        if not pk3List:
            QMessageBox.warning(self, "Error", "No custom mods found")
            return
        
        # Create mod selection dialog
        modDialog = QDialog(self)
        modDialog.setWindowTitle("Select Customization")
        dialogLayout = QVBoxLayout(modDialog)
    
        modLabel = QLabel("Select Mod:")
        modCombo = AccessibleComboBox(modDialog)
        modCombo.setAccessibleName("Mod Selection")
        for pk3 in pk3List:
            modCombo.addItem(Path(pk3).stem, userData=pk3)
    
        dialogLayout.addWidget(modLabel)
        dialogLayout.addWidget(modCombo)
    
        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        buttons.accepted.connect(modDialog.accept)
        buttons.rejected.connect(modDialog.reject)
        dialogLayout.addWidget(buttons)
    
        if not modDialog.exec():
            return
        
        selectedMod = modCombo.currentData()
    
        # Show map selection dialog (same as regular deathmatch)
        mapOptions = {
            'map': {
                'type': 'combobox',
                'label': 'Select Map',
                'items': [
                    "Com Station (2-4 players)",
                    "Warehouse (2-4 players)",
                    "Sector 3 (2-4 players)",
                    "Dungeon of Doom (2-4 players)",
                    "Ocean Fortress (2-4 players)",
                    "Water Treatment Facility (2-4 players)",
                    "Phobos Base Site 4 (2-4 players)",
                    "Hangar Bay 18 (2-4 players)",
                    "Garden of Demon (2-4 players)",
                    "Outpost 69 (2-4 players)"
                ]
            }
        }

        mapDialog = MenuDialog("Select Map", mapOptions, self)
        if not mapDialog.exec():
            return
    
        selectedMap = mapDialog.get_dialog_values()['map']
        mapIndex = mapOptions['map']['items'].index(selectedMap) + 1  # 1-based index

        # Show game options dialog
        options = {
            'mode': {
                'type': 'combobox',
                'label': 'Game Mode',
                'items': [
                    "Host Game",
                    "Join Game",
                    "Bots Only"
                ]
            },
            'ip': {
                'type': 'text',
                'placeholder': 'Enter IP address to join (required for joining)'
            },
            'fraglimit': {
                'type': 'spinbox',
                'label': 'Frag Limit',
                'min': 1,
                'max': 500,
                'default': 20
            },
            'players': {
                'type': 'spinbox',
                'label': 'Number of Players',
                'min': 2,
                'max': 4,
                'default': 2
            },
            'skill': {
                'type': 'spinbox',
                'label': 'Skill Level',
                'min': 1,
                'max': 5,
                'default': 3
            }
        }
    
        dialog = MenuDialog("Deathmatch Options", options, self)
        if dialog.exec():
            values = dialog.get_dialog_values()
        
            # Set up game files
            gameFiles = [
                str(self.gamePath / f"TobyAccMod_V{self.tobyVersion}.pk3")
            ]
        
            # Add menu addons
            menuPath = self.gamePath / "Addons/MENU"
            if menuPath.exists():
                gameFiles.extend(str(p) for p in menuPath.glob("Toby*.pk3"))
        
            # Add selected mod
            gameFiles.append(selectedMod)
        
            # Add deathmatch map
            deathMatchMap = str(self.gamePath / "Addons/MAPS/TobyDeathArena_V1-5.wad")
            if Path(deathMatchMap).exists():
                gameFiles.append(deathMatchMap)
            
            # Get deathmatch flags and add map selection
            gameFlags = self.get_deathmatch_flags(values)
            gameFlags.extend(["-warp", str(mapIndex)])
        
            # Check/set freedm.wad as IWAD
            freedmPath = self.find_freedm()
            if not freedmPath:
                QMessageBox.critical(self, "Error", "Could not find freedm.wad")
                return
    
            # Force freedm.wad selection
            for i in range(self.iwadCombo.count()):
                if "freedm" in self.iwadCombo.itemText(i).lower():
                    self.iwadCombo.setCurrentIndex(i)
                    break

            # Launch the game
            self.launch_game(gameFiles, gameFlags)

    def load_custom_games(self) -> Dict[str, dict]:
        """Load all custom game configurations"""
        customGames = {}
        if platform.system() == "Windows":
            customDir = Path.cwd() / "TobyCustom"
        else:
            pathList = [
                Path(__file__).parent / "TobyCustom",
                self.gamePath / "TobyCustom",
                Path(os.path.expanduser("~/.local/share/doom/TobyCustom"))
            ]
    
            # Use first existing path or fall back to original
            customDir = next(
                (path for path in pathList if path.exists()),
                Path(__file__).parent / "TobyCustom"
            )

        if not customDir.exists():
            return customGames

        for json_file in customDir.glob("*.json"):
            try:
                with open(json_file, 'r') as f:
                    game_config = json.load(f)
                    customGames[game_config['name']] = game_config
            except Exception as e:
                print(f"Error loading custom game {json_file}: {e}")
                
        return customGames

    def check_dependencies(self, dependencies: List[dict]) -> bool:
        """Check if required files exist and show download info if not"""
        for dep in dependencies:
            file_path = self.gamePath / dep['file']
            if not file_path.exists():
                message = [
                    f"You are missing the \"{dep['file']}\" Package.\n",
                    f"You can get it from \"{dep['url']}\"\n",
                    "The URL will now open in your browser.\n"
                ]
                message.extend(f"{msg}\n" for msg in dep.get('messages', []))
                
                QMessageBox.critical(
                    self,
                    "Missing Dependency",
                    "".join(message)
                )
                
                # Open the URL in browser
                try:
                    webbrowser.open(dep['url'])
                except Exception:
                    pass
                    
                return False
        return True

    def show_custom_game_dialog(self):
        """Show dialog for custom game selection"""
        customGames = self.load_custom_games()
        if not customGames:
            QMessageBox.warning(
                self,
                "No Custom Games",
                "No custom game configurations found in TobyCustom directory."
            )
            return

        dialog = CustomGameDialog(customGames, self)
        if not dialog.exec():
            return
        
        selectedGame = dialog.get_selected_game()
        if selectedGame and selectedGame in customGames:
            config = customGames[selectedGame]
        
            # Check dependencies before launching
            if not self.check_dependencies(config.get('dependencies', [])):
                return
            
            gameFiles = []  # We'll build this up as we go
        
            # Always start with TobyAccMod
            tobyMod = self.gamePath / f"TobyAccMod_V{self.tobyVersion}.pk3"
            if not tobyMod.exists():
                QMessageBox.critical(self, "Error", f"Could not find {tobyMod}")
                return
            gameFiles.append(str(tobyMod))
        
            # Handle map selection right after TobyAccMod if specified
            if config.get('use_map_menu', False) and 'submenu' not in config:
                mapFiles = ["None"]  # Start with None option
                mapsDir = self.gamePath / "Addons/MAPS"
                if mapsDir.exists():
                    mapFiles.extend([p.name for p in mapsDir.glob("*.wad") 
                                if p.name != "TobyDeathArena_V1-5.wad"])
            
                # Add Operation MDK as special case
                opMDK = self.gamePath / "OpMDK.wad"
                if opMDK.exists():
                    mapFiles.append("OpMDK.wad")
            
                mapDialog = QDialog(self)
                mapDialog.setWindowTitle("Select Map")
                dialogLayout = QVBoxLayout(mapDialog)
            
                mapLabel = QLabel("Select Map:")
                mapCombo = AccessibleComboBox(mapDialog)
                mapCombo.setAccessibleName("Map Selection")
                mapCombo.addItems(mapFiles)
            
                dialogLayout.addWidget(mapLabel)
                dialogLayout.addWidget(mapCombo)
            
                buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
                buttons.accepted.connect(mapDialog.accept)
                buttons.rejected.connect(mapDialog.reject)
                dialogLayout.addWidget(buttons)
            
                if not mapDialog.exec():
                    return
                
                selectedMap = mapCombo.currentText()
                if selectedMap != "None":
                    if selectedMap == "OpMDK.wad":
                        mapPath = str(self.gamePath / selectedMap)
                    else:
                        mapPath = str(self.gamePath / "Addons/MAPS" / selectedMap)
                    if Path(mapPath).exists():
                        gameFiles.append(mapPath)
        
            # Handle submenu if present
            if 'submenu' in config:
                selectedFile = self.show_submenu_dialog(config['submenu'])
                if not selectedFile:
                    return
                gameFiles.append(selectedFile)
            
            # Add remaining files
            tobyBaseVersion = self.tobyVersion.split('-')[0]
            for filePath in config.get('files', []):
                filePath = filePath.format(toby_base_version=tobyBaseVersion)
                # Handle glob patterns
                if '*' in filePath:
                    pathObj = self.gamePath / filePath.split('*')[0]
                    pattern = filePath.split('/')[-1]
                    if pathObj.parent.exists():
                        matches = list(pathObj.parent.glob(pattern))
                        gameFiles.extend(str(p) for p in matches)
                else:
                    fullPath = self.gamePath / filePath
                    if fullPath.exists():
                        gameFiles.append(str(fullPath))
            
            # Add optional files last
            for optFile in config.get('optional_files', []):
                optPath = self.gamePath / optFile
                if optPath.exists():
                    gameFiles.append(str(optPath))
        
            # Get any custom flags
            gameFlags = config.get('flags', [])
        
            # Launch the game if we have files
            if gameFiles:
                iwadIndex = self.iwadCombo.currentIndex()
                if iwadIndex < 0:
                    QMessageBox.critical(self, "Error", "Please select an IWAD first")
                    return
            
                self.launch_game(gameFiles, gameFlags)

    def show_submenu_dialog(self, submenu_config) -> Optional[str]:
        """Show dialog for selecting submenu option"""
        dialog = QDialog(self)
        dialog.setWindowTitle(submenu_config['title'])
        dialogLayout = QVBoxLayout(dialog)
    
        # Game selection combobox
        label = QLabel("Select Version:")
        gameCombo = AccessibleComboBox(dialog)
        gameCombo.setAccessibleName("Game Version Selection")
    
        # Add options and store full file paths as user data
        for option in submenu_config['options']:
            gameCombo.addItem(option['name'], userData=str(self.gamePath / option['file']))
    
        dialogLayout.addWidget(label)
        dialogLayout.addWidget(gameCombo)
    
        # Dialog buttons
        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        buttons.accepted.connect(dialog.accept)
        buttons.rejected.connect(dialog.reject)
        dialogLayout.addWidget(buttons)
    
        if dialog.exec():
            return gameCombo.currentData()
        return None

    def launch_single_player(self):
        """Launch single player game"""
        selectedGame = self.gameCombo.currentText()
        if selectedGame == "Custom Game":
            self.show_custom_game_dialog()
        elif selectedGame == "Audio Manual":
            self.show_audio_manual()
        else:
            gameFiles = self.get_selected_game_files()
            if gameFiles:
                # Get selected IWAD
                iwadIndex = self.iwadCombo.currentIndex()
                if iwadIndex < 0:
                    QMessageBox.critical(self, "Error", "Please select an IWAD first")
                    return
            
                iwadPath = self.iwadCombo.itemData(iwadIndex)
                cmdLine = [self.find_gzdoom(), "-iwad", iwadPath] + gameFiles
                if cmdLine[0]:  # If gzdoom was found
                    self.launch_game(gameFiles)

    def show_audio_manual(self):
        """Show and play audio manual"""
        manualPath = self.gamePath / "Manual"
        if not manualPath.exists():
            QMessageBox.warning(self, "Error", "Manual directory not found")
            return

        dialog = AudioManualDialog(manualPath, self)
        dialog.exec()
        
    def show_deathmatch_dialog(self):
        """Show deathmatch configuration dialog"""
        # First show map selection
        mapOptions = {
            'map': {
                'type': 'combobox',
                'label': 'Select Map',
                'items': [
                    "Com Station (2-4 players)",
                    "Warehouse (2-4 players)",
                    "Sector 3 (2-4 players)",
                    "Dungeon of Doom (2-4 players)",
                    "Ocean Fortress (2-4 players)",
                    "Water Treatment Facility (2-4 players)",
                    "Phobos Base Site 4 (2-4 players)",
                    "Hangar Bay 18 (2-4 players)",
                    "Garden of Demon (2-4 players)",
                    "Outpost 69 (2-4 players)"
                ]
            }
        }
        
        mapDialog = MenuDialog("Select Map", mapOptions, self)
        if not mapDialog.exec():
            return
        
        selectedMap = mapDialog.get_dialog_values()['map']
        mapIndex = mapOptions['map']['items'].index(selectedMap) + 1  # 1-based index

        # Show game options dialog
        options = {
            'mode': {
                'type': 'combobox',
                'label': 'Game Mode',
                'items': [
                    "Host Game",
                    "Join Game",
                    "Bots Only"
                ]
            },
            'ip': {
                'type': 'text',
                'placeholder': 'Enter IP address to join (required for joining)'
            },
            'fraglimit': {
                'type': 'spinbox',
                'label': 'Frag Limit',
                'min': 1,
                'max': 500,
                'default': 20
            },
            'players': {
                'type': 'spinbox',
                'label': 'Number of Players',
                'min': 2,
                'max': 4,
                'default': 2
            },
            'skill': {
                'type': 'spinbox',
                'label': 'Skill Level',
                'min': 1,
                'max': 5,
                'default': 3
            }
}
        
        dialog = MenuDialog("Deathmatch Options", options, self)
        if dialog.exec():
            values = dialog.get_dialog_values()
            gameFiles = self.get_selected_game_files()
            # Add deathmatch map
            deathMatchMap = str(self.gamePath / "Addons/MAPS/TobyDeathArena_V1-5.wad")
            if Path(deathMatchMap).exists():
                gameFiles.append(deathMatchMap)
            gameFlags = self.get_deathmatch_flags(values)
            # Add map selection flag
            gameFlags.extend(["-warp", str(mapIndex)])

            # Check/set freedm.wad as IWAD
            freedmPath = self.find_freedm()
            if not freedmPath:
                QMessageBox.critical(self, "Error", "Could not find freedm.wad")
                return
    
            # Force freedm.wad selection
            for i in range(self.iwadCombo.count()):
                if "freedm" in self.iwadCombo.itemText(i).lower():
                    self.iwadCombo.setCurrentIndex(i)
                    break

            self.launch_game(gameFiles, gameFlags)

    def show_coop_dialog(self):
        """Show co-op configuration dialog"""
        options = {
            'host': {
                'type': 'radio',
                'label': 'Host Game'
            },
            'ip': {
                'type': 'text',
                'placeholder': 'Enter IP address to join'
            },
            'players': {
                'type': 'spinbox',
                'label': 'Number of Players',
                'min': 2,
                'max': 10,
                'default': 2
            },
            'skill': {
                'type': 'spinbox',
                'label': 'Skill Level',
                'min': 1,
                'max': 5,
                'default': 3
            }
        }
        
        dialog = MenuDialog("Co-op Options", options, self)
        if dialog.exec():
            values = dialog.get_dialog_values()
            gameFiles = self.get_selected_game_files()
            # Add keyshare for co-op
            keyshareFile = str(self.gamePath / "keyshare-universal.pk3")
            if Path(keyshareFile).exists():
                gameFiles.append(keyshareFile)
            gameFlags = self.get_coop_flags(values)
            self.launch_game(gameFiles, gameFlags)

    def get_deathmatch_flags(self, values: dict) -> List[str]:
        """Get command line flags for deathmatch mode"""
        mode = values['mode']
    
        if mode == "Join Game":
            if not values['ip'].strip():
                QMessageBox.warning(self, "Error", "IP address required for joining")
                return []
            return ["-join", values['ip']]
    
        # Handle both Host Game and Bots Only
        if mode == "Bots Only":
            values['players'] = 1
            QMessageBox.information(
                self,
                "Bot Instructions",
                "When the game starts, press ` to open the console.\n"
                "Type addbot and press enter.\n"
                "Repeat addbot for as many bots as you want.\n"
                "Press ` again to close the console."
            )
    
        return [
            "-host", str(values['players']),
            "-skill", str(values['skill']),
            "-deathmatch",
            "+Toby_SnapToTargetTargetingMode", "0",
            "+set", "sv_cheats", "1",
            "+fraglimit", str(values['fraglimit']),
            "+dmflags", "16384",
            "+dmflags", "4",
            "+dmflags", "128",
            "+dmflags", "4096",
            "+dmflags2", "512",
            "+dmflags2", "1024",
            "-extratic",
            "-dup", "3"
        ]

    def get_coop_flags(self, values: dict) -> List[str]:
        """Get command line flags for co-op mode"""
        if not values['host']:
            if not values['ip'].strip():
                QMessageBox.warning(self, "Error", "IP address required for joining")
                return []
            return ["-join", values['ip']]
            
        return [
            "-host", str(values['players']),
            "-skill", str(values['skill']),
            "+set", "sv_cheats", "1",
            "+set", "sv_weaponsstay", "1",
            "+set", "sv_respawnprotect", "1",
            "+set", "sv_respawnsuper", "1",
            "+set", "alwaysapplydmflags", "1",
            "-extratic",
            "-dup", "3"
        ]

    def monitor_game_process(self, process):
        """Monitor game process and exit when it's done"""
        process.wait()  # Wait for the game to finish
        QApplication.instance().quit()  # Quit the application

    def launch_game(self, gameFiles: List[str], gameFlags: List[str] = None):
        """Launch game with speech processing"""
        if not gameFiles:
            return
            
        gzdoomPath = self.find_gzdoom()
        if not gzdoomPath:
            QMessageBox.critical(self, "Error", "GZDoom executable not found")
            return
            
        # Get selected IWAD
        iwadIndex = self.iwadCombo.currentIndex()
        if iwadIndex < 0:
            QMessageBox.critical(self, "Error", "Please select an IWAD first")
            return
            
        iwadPath = self.iwadCombo.itemData(iwadIndex)

        try:
            if platform.system() == "Windows":
                configFile = Path.cwd() / 'TobyConfig.ini'
                # For Windows, use unbuffered stdout for accessible_output2
                cmdLine = [gzdoomPath, "-stdout", "-config", str(configFile), 
                        "-iwad", iwadPath, "-file"] + gameFiles
                if gameFlags:
                    cmdLine.extend(gameFlags)
                
                # Use CREATE_NO_WINDOW flag to prevent console window
                startupinfo = subprocess.STARTUPINFO()
                startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
                startupinfo.wShowWindow = subprocess.SW_HIDE
                
                process = subprocess.Popen(
                    cmdLine,
                    cwd=str(self.gamePath),
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    bufsize=1,  # Line buffered
                    universal_newlines=True,  # This handles text encoding
                    env=dict(os.environ, PYTHONUNBUFFERED="1"),
                    startupinfo=startupinfo
                )
                
            else:
                # For Linux/Mac, use stdbuf to unbuffer output
                cmdLine = ["stdbuf", "-oL", gzdoomPath, "-stdout", 
                        "-iwad", iwadPath, "-file"] + gameFiles
                if gameFlags:
                    cmdLine.extend(gameFlags)
                
                process = subprocess.Popen(
                    cmdLine,
                    cwd=str(self.gamePath),
                    stdout=subprocess.PIPE,
                    stderr=subprocess.STDOUT,
                    bufsize=1,  # Line buffered
                    universal_newlines=True,  # This handles text encoding
                    env=dict(os.environ, PYTHONUNBUFFERED="1")
                )
            
            # Start speech processing thread
            speechThread = threading.Thread(
                target=self.speechHandler.speak_thread,
                args=(process,),
                daemon=True
            )
            speechThread.start()
            
            # Start process monitor thread
            monitorThread = threading.Thread(
                target=self.monitor_game_process,
                args=(process,),
                daemon=True
            )
            monitorThread.start()
        
            # Hide the window
            self.hide()
            
        except Exception as e:
            QMessageBox.critical(self, "Error", f"Failed to launch game: {e}")


if __name__ == "__main__":
    # Converts version number to required format (e.g., 8.0 -> "8-0")
    TOBY_VERSION: Final[str] = f"{int(TOBY_VERSION_NUMBER)}-{int(TOBY_VERSION_NUMBER * 10 % 10)}"
    setproctitle("Toby Doom Launcher")
    app = QApplication(sys.argv)
    window = DoomLauncher()
    window.show()
    sys.exit(app.exec())