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

# 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.3.2"
__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}

# ==================== Cached OBS Process (Windows) ====================
_obs_cache = {"pid": None, "time": 0}
OBS_CACHE_DURATION = 2  # seconds

def get_obs_process_cached():
    """Return the OBS process (Windows) with caching to reduce CPU load"""
    try:
        import psutil
    except ImportError:
        script_log("psutil not installed - cannot use folder monitoring on Windows", "ERROR")
        return None
    
    now = time.time()
    if _obs_cache["pid"] and now - _obs_cache["time"] < OBS_CACHE_DURATION:
        try:
            return psutil.Process(_obs_cache["pid"])
        except psutil.NoSuchProcess:
            _obs_cache["pid"] = None  # process died
    
    # Search for OBS process
    for proc in psutil.process_iter(["name", "pid"]):
        if "obs" in proc.info["name"].lower():
            _obs_cache["pid"] = proc.info["pid"]
            _obs_cache["time"] = now
            return proc
    
    _obs_cache["pid"] = None
    return None

# ==================== Script Info / Properties ====================
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

# ==================== Script Defaults ====================
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)

# ==================== Script Update ====================
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")
    
    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
    
    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


# ==================== Logging / Status ====================
def script_log(message, level="INFO"):
    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 / File Detection ====================
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:
            script_log(f"VLC source '{vlc_source_name}' not found", "WARN")
            return None
        
        settings = obs.obs_source_get_settings(source)
        playlist_array = obs.obs_data_get_array(settings, "playlist")
        if not playlist_array:
            script_log("VLC playlist array not found in settings", "WARN")
            obs.obs_data_release(settings)
            obs.obs_source_release(source)
            return None
        
        count = obs.obs_data_array_count(playlist_array)
        if count == 0:
            script_log("VLC playlist is empty", "WARN")
            obs.obs_data_array_release(playlist_array)
            obs.obs_data_release(settings)
            obs.obs_source_release(source)
            return None
        
        # Try to get the current playing index from settings (may not be exposed)
        current_index = obs.obs_data_get_int(settings, "playlist_pos")
        script_log(f"VLC: Found {count} items in playlist, current_index={current_index}", "INFO")
        
        # Fallback: If playlist_pos doesn't work, try playlist_index or just use first valid file
        if current_index < 0 or current_index >= count:
            script_log(f"Invalid playlist_pos ({current_index}), trying fallback methods", "WARN")
            
            # Try alternative property names
            current_index = obs.obs_data_get_int(settings, "current_item")
            if current_index < 0 or current_index >= count:
                current_index = obs.obs_data_get_int(settings, "playlist_index")
            if current_index < 0 or current_index >= count:
                # Last resort: return None to fall back to file monitoring
                script_log("Could not determine current playlist position, falling back to file monitoring", "WARN")
                obs.obs_data_array_release(playlist_array)
                obs.obs_data_release(settings)
                obs.obs_source_release(source)
                return None
        
        # Get the file at the current index
        current_file = None
        item = obs.obs_data_array_item(playlist_array, current_index)
        if item:
            filepath = obs.obs_data_get_string(item, "value")
            script_log(f"VLC: Item {current_index}: {filepath}", "DEBUG")
            if filepath and os.path.exists(filepath):
                ext = os.path.splitext(filepath)[1].lower()
                if ext in ['.mp3', '.flac', '.ogg', '.m4a', '.wav', '.opus', '.aac', '.wma']:
                    current_file = filepath
                    script_log(f"VLC: Using file at index {current_index}: {filepath}", "INFO")
                else:
                    script_log(f"VLC: File at index {current_index} is not a music file (ext: {ext})", "WARN")
            else:
                script_log(f"VLC: File at index {current_index} does not exist or is empty path", "WARN")
            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")
        import traceback
        script_log(f"Traceback: {traceback.format_exc()}", "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
    
    obs_process = get_obs_process_cached()
    if not obs_process:
        return None
    
    extensions = ('.mp3', '.flac', '.ogg', '.m4a', '.wav', '.opus', '.aac', '.wma')
    
    try:
        import psutil
        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 Exception as e:
        if "AccessDenied" in str(type(e).__name__) or "NoSuchProcess" in str(type(e).__name__):
            script_log("Cannot access OBS process files (try running OBS as administrator)", "WARN")
        else:
            script_log(f"Error reading OBS process files: {e}", "ERROR")
        return None
    
    return None

def get_playing_file_lsof():
    if not music_folder or not os.path.exists(music_folder):
        return None
    try:
        result = subprocess.run(
            ['lsof', '-c', 'obs'],
            capture_output=True,
            text=True,
            timeout=2
        )
        extensions = ('.mp3', '.flac', '.ogg', '.m4a', '.wav', '.opus', '.aac', '.wma')
        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 Exception as e:
        script_log(f"Error getting file via lsof: {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 platform-specific file monitoring
    system = platform.system()
    
    if system == "Windows":
        if music_folder:
            return get_playing_file_windows()
        return None
    elif system in ["Linux", "Darwin"]:  # Darwin = macOS
        if music_folder:
            return get_playing_file_lsof()
        return None
    
    script_log(f"Unsupported platform: {system}", "ERROR")
    return None

# ==================== Metadata / Display ====================
def get_song_metadata(filepath):
    global metadata_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:
        return None
    
    filename_no_ext = os.path.splitext(os.path.basename(filepath))[0]
    clean_title = filename_no_ext.replace('_', ' ')
    
    metadata = {'title': clean_title, 'artist': '', 'album': '', 'filename': os.path.basename(filepath)}
    
    if music_folder and music_folder in filepath:
        metadata_cache[cache_key] = metadata
        return metadata
    
    if HAS_MUTAGEN and MutagenFile is not None:
        try:
            audio = MutagenFile(filepath)
            if audio:
                for key in ['title', 'artist', 'album']:
                    if key in audio:
                        val = audio[key]
                        metadata[key] = str(val[0]) if isinstance(val, list) else str(val)
                metadata = {k: v.strip() if v else '' for k,v in metadata.items()}
        except Exception as e:
            script_log(f"Error reading metadata: {e}", "WARN")
    
    metadata_cache[cache_key] = metadata
    if len(metadata_cache) > 500:
        keys = list(metadata_cache.keys())
        for key in keys[:250]:
            del metadata_cache[key]
    return metadata

def format_song_display(metadata):
    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:
        name = os.path.splitext(filename)[0].replace('_', ' ').strip()
        return name if name else default_text

def update_text_source(text):
    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 Exception as e:
        script_log(f"Error writing to file: {e}", "ERROR")

# ==================== Main Update Loop ====================
def update_song():
    global last_file, last_song, stats
    if not enabled:
        return
    current_file = get_playing_file()
    if current_file and current_file != last_file:
        metadata = get_song_metadata(current_file)
        if not metadata:
            return
        current_song = format_song_display(metadata)
        if current_song != last_song:
            update_text_source(current_song)
            write_to_file(current_song)
            script_log(f"♫ Now Playing: {current_song}")
            stats["total_plays"] += 1
            last_song = current_song
        last_file = current_file
    elif not current_file and last_file:
        update_text_source(default_text)
        write_to_file(default_text)
        last_file = ""
        last_song = ""

