import obspython as obs
import os
import subprocess
import json
import platform
from datetime import datetime

# Try to import mutagen at module level
try:
    from mutagen import File as MutagenFile
    HAS_MUTAGEN = True
except ImportError:
    HAS_MUTAGEN = False
    MutagenFile = None

# ==================== Version Info ====================
__version__ = "1.2.5
__author__ = "icy404"
__updated__ = "2025-11-11"

# ==================== Configuration ====================
music_folder = ""
text_source_name = ""
vlc_source_name = ""
output_file = ""
default_text = "your_brand_name"
update_interval = 2000  # milliseconds
show_format = "{title} - {artist}"  # Customizable format
enabled = True
use_vlc_source = True

# ==================== State ====================
metadata_cache = {}
last_file = ""
last_song = ""
stats = {"total_plays": 0, "session_start": None}

# ==================== Script Info ====================
def script_description():
    return f"""<center><h2>🎵 VLC Now Playing Monitor</h2></center>
    
    <p>Automatically detects and displays currently playing songs from VLC Video Source.</p>
    
    <p><b>Quick Setup:</b></p>
    <ol>
        <li>Create a <b>Text (GDI+)</b> or <b>Text (FreeType 2)</b> source</li>
        <li>Select it in 'Text Source' dropdown below</li>
        <li>Select your VLC source OR set music folder path</li>
        <li>Customize the display format</li>
        <li>Done! It updates automatically</li>
    </ol>
    
    <p><b>Format Variables:</b></p>
    <ul>
        <li><code>{{title}}</code> - Song title</li>
        <li><code>{{artist}}</code> - Artist name</li>
        <li><code>{{album}}</code> - Album name</li>
        <li><code>{{filename}}</code> - File name</li>
    </ul>
    
    <p><i>Example: "{{title}} - {{artist}}" displays "Bohemian Rhapsody - Queen"</i></p>
    
    <hr>
    <p><small>v{__version__} | Updated: {__updated__}</small></p>
    <p><small>Compatible with OBS 27+ | Cross-platform (Windows/Linux/macOS)</small></p>
    """

def script_properties():
    props = obs.obs_properties_create()
    
    # Version header
    obs.obs_properties_add_text(
        props, "version_header", 
        f"<center><b>VLC Now Playing Monitor v{__version__}</b></center>",
        obs.OBS_TEXT_INFO
    )
    
    # ===== Main Settings =====
    obs.obs_properties_add_text(
        props, "section_main", "<b>━━━ Main Settings ━━━</b>",
        obs.OBS_TEXT_INFO
    )
    
    # VLC Source selection
    p_vlc = obs.obs_properties_add_list(
        props, "vlc_source", "🎬 VLC Source (Recommended):",
        obs.OBS_COMBO_TYPE_EDITABLE,
        obs.OBS_COMBO_FORMAT_STRING
    )
    
    # Populate with VLC sources
    sources = obs.obs_enum_sources()
    if sources:
        for source in sources:
            source_id = obs.obs_source_get_unversioned_id(source)
            if source_id == "vlc_source":
                name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p_vlc, name, name)
    obs.source_list_release(sources)
    
    # Music folder (fallback method)
    obs.obs_properties_add_text(
        props, "folder_label", "<i>OR use folder monitoring (slower):</i>",
        obs.OBS_TEXT_INFO
    )
    
    obs.obs_properties_add_path(
        props, "music_folder", "📁 Music Folder:",
        obs.OBS_PATH_DIRECTORY, "", None
    )
    
    # Text source selection
    p = obs.obs_properties_add_list(
        props, "text_source", "📺 Text Source:",
        obs.OBS_COMBO_TYPE_EDITABLE,
        obs.OBS_COMBO_FORMAT_STRING
    )
    
    # Populate with text sources
    sources = obs.obs_enum_sources()
    if sources:
        for source in sources:
            source_id = obs.obs_source_get_unversioned_id(source)
            if source_id in ["text_gdiplus", "text_ft2_source", "text_gdiplus_v2", "text_ft2_source_v2"]:
                name = obs.obs_source_get_name(source)
                obs.obs_property_list_add_string(p, name, name)
    obs.source_list_release(sources)
    
    # ===== Display Settings =====
    obs.obs_properties_add_text(
        props, "section_display", "\n<b>━━━ Display Settings ━━━</b>",
        obs.OBS_TEXT_INFO
    )
    
    # Format string
    obs.obs_properties_add_text(
        props, "show_format", "🎨 Display Format:",
        obs.OBS_TEXT_DEFAULT
    )
    
    # Default text
    obs.obs_properties_add_text(
        props, "default_text", "⏸️ Default Text (when not playing):",
        obs.OBS_TEXT_DEFAULT
    )
    
    # Update interval
    obs.obs_properties_add_int_slider(
        props, "update_interval", "⏱️ Update Interval (seconds):",
        1, 10, 1
    )
    
    # ===== Advanced Settings =====
    obs.obs_properties_add_text(
        props, "section_advanced", "\n<b>━━━ Advanced Settings ━━━</b>",
        obs.OBS_TEXT_INFO
    )
    
    # Output file (optional)
    obs.obs_properties_add_path(
        props, "output_file", "💾 Also Save To File (optional):",
        obs.OBS_PATH_FILE_SAVE, "Text files (*.txt)", None
    )
    
    # Enable/disable
    obs.obs_properties_add_bool(
        props, "enabled", "✅ Enable Monitoring"
    )
    
    # Clear cache button
    obs.obs_properties_add_button(
        props, "clear_cache", "🗑️ Clear Metadata Cache",
        clear_cache_callback
    )
    
    # ===== Status =====
    obs.obs_properties_add_text(
        props, "section_status", "\n<b>━━━ Status ━━━</b>",
        obs.OBS_TEXT_INFO
    )
    
    # Status info
    status_text = get_status_text()
    obs.obs_properties_add_text(
        props, "status", status_text,
        obs.OBS_TEXT_INFO
    )
    
    return props

def script_defaults(settings):
    obs.obs_data_set_default_string(settings, "default_text", "BeatBoy")
    obs.obs_data_set_default_int(settings, "update_interval", 2)
    obs.obs_data_set_default_string(settings, "show_format", "{title} - {artist}")
    obs.obs_data_set_default_string(settings, "music_folder", "")
    obs.obs_data_set_default_string(settings, "vlc_source", "")
    obs.obs_data_set_default_string(settings, "output_file", "/home/turka/radio/now_playing.txt")
    obs.obs_data_set_default_bool(settings, "enabled", True)

def script_update(settings):
    global music_folder, output_file, default_text, text_source_name
    global update_interval, show_format, enabled, vlc_source_name
    
    music_folder = obs.obs_data_get_string(settings, "music_folder")
    vlc_source_name = obs.obs_data_get_string(settings, "vlc_source")
    output_file = obs.obs_data_get_string(settings, "output_file")
    
    # Expand home directory paths
    if output_file and '~' in output_file:
        output_file = os.path.expanduser(output_file)
    if music_folder and '~' in music_folder:
        music_folder = os.path.expanduser(music_folder)
    
    default_text = obs.obs_data_get_string(settings, "default_text")
    text_source_name = obs.obs_data_get_string(settings, "text_source")
    show_format = obs.obs_data_get_string(settings, "show_format")
    enabled = obs.obs_data_get_bool(settings, "enabled")
    update_interval = obs.obs_data_get_int(settings, "update_interval") * 1000
    
    # Validate paths
    if music_folder and not os.path.exists(music_folder):
        script_log(f"⚠️ Warning: Music folder not found: {music_folder}", "WARN")
    
    if output_file and output_file.strip():
        output_dir = os.path.dirname(output_file)
        if output_dir and not os.path.exists(output_dir):
            script_log(f"⚠️ Warning: Output directory not found: {output_dir}", "WARN")
    
    # Restart timer
    obs.timer_remove(update_song)
    if enabled and (vlc_source_name or music_folder):
        obs.timer_add(update_song, update_interval)
        update_song()
        method = "VLC source" if vlc_source_name else "folder monitoring"
        script_log(f" Monitoring started using {method} (text source: {'set' if text_source_name else 'not set'})")
    else:
        missing = []
        if not enabled:
            missing.append("disabled")
        if not vlc_source_name and not music_folder:
            missing.append("no VLC source or music folder")
        script_log(f"Monitoring stopped ({', '.join(missing)})")

def script_load(settings):
    global stats
    stats["session_start"] = datetime.now()
    
    if not HAS_MUTAGEN:
        script_log("⚠️ Mutagen not installed - metadata will be limited to filenames", "WARN")
        script_log("   Install with: pip install mutagen", "INFO")
    
    script_log(f"✓ Script loaded successfully on {platform.system()}")

def script_unload():
    obs.timer_remove(update_song)
    script_log(f"Script unloaded. Total plays this session: {stats['total_plays']}")

def script_save(settings):
    pass

# ==================== Helper Functions ====================
def script_log(message, level="INFO"):
    """Log message to OBS script log"""
    print(f"[VLC Monitor][{level}] {message}")

def get_status_text():
    """Generate status text for UI"""
    status_lines = []
    
    if stats["session_start"]:
        uptime = datetime.now() - stats["session_start"]
        status_lines.append(f"⏱️ Uptime: {str(uptime).split('.')[0]}")
    
    status_lines.append(f"📊 Songs played: {stats['total_plays']}")
    status_lines.append(f"💾 Cache size: {len(metadata_cache)} items")
    
    if last_song:
        status_lines.append(f"🎵 Current: {last_song}")
    
    status_lines.append(f"🏷️ Mutagen: {'✓ Available' if HAS_MUTAGEN else '✗ Not installed'}")
    status_lines.append(f"💻 Platform: {platform.system()}")
    
    return "\n".join(status_lines)

def clear_cache_callback(props, prop):
    """Clear metadata cache"""
    global metadata_cache
    old_size = len(metadata_cache)
    metadata_cache.clear()
    script_log(f"Cleared {old_size} cached items")
    return True

# ==================== VLC Source Reading ====================
def get_vlc_current_file():
    """Get currently playing file from VLC source (cross-platform)"""
    if not vlc_source_name:
        return None
    
    try:
        source = obs.obs_get_source_by_name(vlc_source_name)
        if not source:
            return None
        
        # Get source settings
        settings = obs.obs_source_get_settings(source)
        
        # Get playlist array
        playlist_array = obs.obs_data_get_array(settings, "playlist")
        if not playlist_array:
            obs.obs_data_release(settings)
            obs.obs_source_release(source)
            return None
        
        # Get current playing index
        count = obs.obs_data_array_count(playlist_array)
        
        current_file = None
        for i in range(count):
            item = obs.obs_data_array_item(playlist_array, i)
            if item:
                filepath = obs.obs_data_get_string(item, "value")
                if filepath and os.path.exists(filepath):
                    # Check if this file is a music file
                    ext = os.path.splitext(filepath)[1].lower()
                    if ext in ['.mp3', '.flac', '.ogg', '.m4a', '.wav', '.opus', '.aac', '.wma']:
                        current_file = filepath
                        obs.obs_data_release(item)
                        break
                obs.obs_data_release(item)
        
        obs.obs_data_array_release(playlist_array)
        obs.obs_data_release(settings)
        obs.obs_source_release(source)
        
        return current_file
        
    except Exception as e:
        script_log(f"Error reading VLC source: {e}", "ERROR")
        return None

# ==================== File Detection ====================
def get_playing_file_lsof():
    """Find which music file is currently being read (Linux/macOS only)"""
    if not music_folder or not os.path.exists(music_folder):
        return None
    
    try:
        # Look for obs process specifically (faster)
        result = subprocess.run(
            ['lsof', '-c', 'obs'],
            capture_output=True,
            text=True,
            timeout=2
        )
        
        extensions = ('.mp3', '.flac', '.ogg', '.m4a', '.wav', '.opus', '.aac', '.wma')
        
        # Check if any line contains our music folder
        for line in result.stdout.split('\n'):
            if music_folder in line and any(ext in line.lower() for ext in extensions):
                parts = line.split()
                if len(parts) > 8:
                    filepath = ' '.join(parts[8:])
                    if os.path.exists(filepath) and os.path.isfile(filepath):
                        return filepath
        
        return None
        
    except FileNotFoundError:
        script_log("lsof command not found - cannot use folder monitoring on this system", "ERROR")
        return None
    except subprocess.TimeoutExpired:
        script_log("lsof command timed out", "WARN")
        return None
    except Exception as e:
        script_log(f"Error getting file via lsof: {e}", "ERROR")
        return None

def get_playing_file_windows():
    """Find which music file is currently being read (Windows)"""
    if not music_folder or not os.path.exists(music_folder):
        return None
    
    try:
        import psutil
        
        # Find OBS process
        obs_process = None
        for proc in psutil.process_iter(['name', 'pid']):
            if 'obs' in proc.info['name'].lower():
                obs_process = proc
                break
        
        if not obs_process:
            return None
        
        # Get open files
        extensions = ('.mp3', '.flac', '.ogg', '.m4a', '.wav', '.opus', '.aac', '.wma')
        
        try:
            for file in obs_process.open_files():
                if music_folder in file.path and file.path.lower().endswith(extensions):
                    if os.path.exists(file.path):
                        return file.path
        except (psutil.AccessDenied, psutil.NoSuchProcess):
            script_log("Cannot access OBS process files (try running OBS as administrator)", "WARN")
            return None
        
        return None
        
    except ImportError:
        script_log("psutil not installed - cannot use folder monitoring on Windows", "ERROR")
        script_log("Install with: pip install psutil", "INFO")
        return None
    except Exception as e:
        script_log(f"Error getting file on Windows: {e}", "ERROR")
        return None

def get_playing_file():
    """Cross-platform file detection"""
    # Method 1: Try VLC source first (most reliable and cross-platform)
    if vlc_source_name:
        file = get_vlc_current_file()
        if file:
            return file
    
    # Method 2: Fallback to file monitoring
    if not music_folder:
        return None
    
    system = platform.system()
    
    if system == 'Windows':
        return get_playing_file_windows()
    elif system in ['Linux', 'Darwin']:  # Darwin = macOS
        return get_playing_file_lsof()
    else:
        script_log(f"Unsupported platform: {system}", "ERROR")
        return None

# ==================== Metadata ====================
def get_song_metadata(filepath):
    """Get song metadata from file"""
    global metadata_cache
    
    # Check cache
    try:
        mtime = os.path.getmtime(filepath)
        cache_key = f"{filepath}:{mtime}"
        
        if cache_key in metadata_cache:
            return metadata_cache[cache_key]
    except OSError as e:
        script_log(f"Error accessing file {filepath}: {e}", "ERROR")
        return None
    
    # Clean filename for title
    filename_no_ext = os.path.splitext(os.path.basename(filepath))[0]
    clean_title = filename_no_ext.replace('_', ' ')
    
    # Default metadata
    metadata = {
        'title': clean_title,
        'artist': '',
        'album': '',
        'filename': os.path.basename(filepath)
    }
    
    # If using music_folder (folder monitoring), just use filename - don't parse metadata
    if music_folder and music_folder in filepath:
        script_log(f"Using filename for folder monitoring: {clean_title}")
        # Cache it and return
        try:
            cache_key = f"{filepath}:{mtime}"
            metadata_cache[cache_key] = metadata
        except Exception as e:
            script_log(f"Error updating cache: {e}", "ERROR")
        return metadata
    
    # For VLC source, try to read metadata
    if HAS_MUTAGEN and MutagenFile is not None:
        try:
            audio = MutagenFile(filepath)
            if audio:
                # Get title
                if 'title' in audio:
                    title = audio['title']
                    metadata['title'] = str(title[0]) if isinstance(title, list) else str(title)
                
                # Get artist
                if 'artist' in audio:
                    artist = audio['artist']
                    metadata['artist'] = str(artist[0]) if isinstance(artist, list) else str(artist)
                
                # Get album
                if 'album' in audio:
                    album = audio['album']
                    metadata['album'] = str(album[0]) if isinstance(album, list) else str(album)
                
                # Clean up
                metadata = {k: v.strip() if v else '' for k, v in metadata.items()}
                
        except Exception as e:
            script_log(f"Error reading metadata from {os.path.basename(filepath)}: {e}", "WARN")
    
    # Cache it
    try:
        cache_key = f"{filepath}:{mtime}"
        metadata_cache[cache_key] = metadata
        
        # Limit cache size and remove stale entries
        if len(metadata_cache) > 500:
            # Remove entries where file no longer exists
            metadata_cache = {
                k: v for k, v in metadata_cache.items()
                if os.path.exists(k.split(':', 1)[0])
            }
            
            # If still too large, remove oldest half
            if len(metadata_cache) > 500:
                keys = list(metadata_cache.keys())
                for key in keys[:250]:
                    del metadata_cache[key]
                script_log(f"Cache cleaned: {len(metadata_cache)} items remaining")
                
    except Exception as e:
        script_log(f"Error updating cache: {e}", "ERROR")
    
    return metadata

def format_song_display(metadata):
    """Format song info - {title} - {artist} (same logic as standalone)"""
    if not metadata:
        return default_text
    
    title = metadata.get('title')
    artist = metadata.get('artist')
    filename = metadata.get('filename', '')
    
    if title and artist:
        return f"{title} - {artist}"
    elif title:
        return title
    else:
        # Fallback to cleaned filename
        name = os.path.splitext(filename)[0]
        name = name.replace('_', ' ')
        name = name.replace('  ', ' ')
        return name.strip() if name.strip() else default_text

def update_text_source(text):
    """Update the OBS text source"""
    if not text_source_name:
        return False
    
    source = obs.obs_get_source_by_name(text_source_name)
    if not source:
        script_log(f"Text source '{text_source_name}' not found", "WARN")
        return False
    
    try:
        settings = obs.obs_data_create()
        obs.obs_data_set_string(settings, "text", text)
        obs.obs_source_update(source, settings)
        obs.obs_data_release(settings)
        obs.obs_source_release(source)
        return True
    except Exception as e:
        script_log(f"Error updating text source: {e}", "ERROR")
        obs.obs_source_release(source)
        return False

def write_to_file(text):
    if not output_file or not output_file.strip():
        return
    try:
        expanded_path = os.path.expanduser(output_file)
        output_dir = os.path.dirname(expanded_path)
        if output_dir and not os.path.exists(output_dir):
            os.makedirs(output_dir, exist_ok=True)
        with open(expanded_path, 'w', encoding='utf-8') as f:
            f.write(f" {text}")
        script_log(f"Wrote to file: {text}")
    except OSError as e:
        script_log(f"File write error to {output_file}: {e}", "ERROR")
    except Exception as e:
        script_log(f"Unexpected error writing to file: {e}", "ERROR")

def update_song():
    """Main update callback - runs every interval"""
    global last_file, last_song, stats
    
    if not enabled:
        return
    
    current_file = get_playing_file()
    
    if current_file and current_file != last_file:
        # New song detected
        metadata = get_song_metadata(current_file)
        if not metadata:
            return
        
        current_song = format_song_display(metadata)
        
        if current_song != last_song:
            # Update display
            update_text_source(current_song)
            write_to_file(current_song)
            
            # Log
            script_log(f"♫ Now Playing: {current_song}")
            
            # Update stats
            stats["total_plays"] += 1
            last_song = current_song
        
        last_file = current_file
        
    elif not current_file and last_file:
        # Nothing playing
        update_text_source(default_text)
        write_to_file(default_text)
        last_file = ""
        last_song = ""