#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tkinter as tk
from tkinter import filedialog, messagebox, ttk
import threading
import pygame
import os
import time
import re  # Regular expressions for smart text parsing

# Custom modules for Text-to-Speech engine, File Parsing, UI, and Internationalization
from core.tts_engine import TTSEngine, VOICE_DATA
from core.parser import FileParser
from ui.main_window import MainWindow
from core.messages import get_strings
from core.abbr_data import ABBREVIATIONS, get_all_abbr

class FussReaderApp:
    """
    Main Application Class for FUSS Reader.
    Manages the lifecycle of audio playback, text synchronization (karaoke),
    and user interface interactions.
    """
    def __init__(self, root):
        self.root = root
        
        # Load localized strings based on system locale (IT, DE, EN, etc.)
        self.msgs = get_strings()
        
        # Initialize Pygame mixer for high-performance audio playback
        pygame.mixer.init()
        
        # Initialize the TTS Engine (Neural Piper AI wrapper)
        self.tts = TTSEngine()
        
        # Initialize the Main GUI Window
        self.ui = MainWindow(self.root, self, self.msgs)
        
        # Application state flags
        self.is_reading = False
        self.is_paused = False
        self.stop_requested = False

    def load_file(self):
        """
        Opens a file selection dialog. Uses Zenity for a native Linux/GNOME 
        experience or falls back to Tkinter's standard dialog.
        """
        import subprocess
        
        # Zenity configuration for a modern file picker
        cmd = [
            "zenity", "--file-selection", 
            "--title=Select a document for FUSS Reader",
            "--filename=" + os.path.expanduser("~") + "/",
            "--file-filter=Supported Documents | *.pdf *.txt *.epub *.odt *.docx",
            "--file-filter=All files | *"
        ]
        
        try:
            # Attempt to use Zenity
            process = subprocess.run(cmd, capture_output=True, text=True)
            if process.returncode != 0: return # User cancelled
            file_path = process.stdout.strip()
            
            if file_path and os.path.exists(file_path):
                # Extract text based on file extension
                testo = FileParser.extract_text(file_path)
                self.ui.text_area.delete("1.0", "end")
                self.ui.text_area.insert("1.0", testo)
                self.ui.text_area.mark_set("insert", "1.0") # Reset cursor
                self.ui.text_area.see("1.0")
                self.ui.text_area.focus_set()
        except Exception:
            # Fallback to Tkinter if Zenity is missing or fails
            file_path = filedialog.askopenfilename(initialdir="~")
            if file_path:
                testo = FileParser.extract_text(file_path)
                self.ui.text_area.delete("1.0", "end")
                self.ui.text_area.insert("1.0", testo)

    def toggle_pause(self):
        """Switches between Paused and Playing states."""
        if not self.is_reading: return
        
        if not self.is_paused:
            pygame.mixer.music.pause()
            self.is_paused = True
            self.ui.pause_btn.config(text=self.msgs["btn_resume"], bg="#17a2b8")
        else:
            pygame.mixer.music.unpause()
            self.is_paused = False
            self.ui.pause_btn.config(text=self.msgs["btn_pause"], bg="#ff9800")

    def stop_reading(self):
        """Terminals audio playback and clears visual highlights."""
        self.stop_requested = True
        self.is_paused = False
        pygame.mixer.music.stop()
        self.ui.text_area.tag_remove("highlight", "1.0", "end")

    def clear_all(self):
        """Clears the text area after a user confirmation."""
        self.stop_reading()
        if messagebox.askyesno(self.msgs["confirm_clear_title"], self.msgs["confirm_clear_msg"]):
            self.ui.text_area.delete("1.0", "end")

    def start_reading(self):
        """
        Prepares the text based on cursor position and starts 
        the background reading thread.
        """
        if self.is_reading:
            if self.is_paused: self.toggle_pause()
            return
        
        # Determine starting point based on cursor (INSERT)
        cursor_pos = self.ui.text_area.index(tk.INSERT)
        remaining_text = self.ui.text_area.get(cursor_pos, "end-1c").strip()
        
        # If cursor is at the end or text area is empty
        if not remaining_text:
            total_text = self.ui.text_area.get("1.0", "end-1c").strip()
            if not total_text:
                messagebox.showinfo("FUSS Reader", self.msgs["msg_empty"])
                return
            else:
                # Text exists, but cursor at end: restart from top
                cursor_pos = "1.0"
                self.ui.text_area.mark_set("insert", "1.0")

        self.is_reading = True
        self.stop_requested = False
        self.ui.play_btn.config(state="disabled")
        
        # Launch reading loop in a separate thread to keep UI responsive
        threading.Thread(target=self.reading_loop, args=(cursor_pos,), daemon=True).start()

    def reading_loop(self, start_idx):
        """
        Main processing loop with improved ellipsis detection and sentence splitting.
        """
        full_text = self.ui.text_area.get(start_idx, "end-1c")
        
        # 1. IMPROVED SPLIT
        # Added '…' to the regex so the program splits sentences even at Unicode ellipses.
        raw_parts = re.split(r'([.!?…]+(?:\s+|$))', full_text)
        
        raw_chunks = []
        for i in range(0, len(raw_parts)-1, 2):
            raw_chunks.append(raw_parts[i] + raw_parts[i+1])
        if len(raw_parts) % 2 != 0 and raw_parts[-1]:
            raw_chunks.append(raw_parts[-1])

        # 2. SMART RE-JOINING (Abbreviations)
        chunks = []
        i = 0
        all_abbr = get_all_abbr()
        while i < len(raw_chunks):
            current_chunk = raw_chunks[i]
            while i + 1 < len(raw_chunks):
                found_abbr = False
                for abbr in all_abbr:
                    if re.search(rf'\b{re.escape(abbr)}\.\s*$', current_chunk, re.IGNORECASE):
                        found_abbr = True
                        break
                if not found_abbr and re.search(rf'\b[a-zA-Z]\.\s*$', current_chunk):
                    found_abbr = True
                
                if found_abbr:
                    i += 1
                    current_chunk += raw_chunks[i]
                else:
                    break
            chunks.append(current_chunk)
            i += 1

        # 3. EXECUTION LOOP
        current_offset = len(self.ui.text_area.get("1.0", start_idx))

        for chunk in chunks:
            if self.stop_requested: break
            if not chunk.strip():
                current_offset += len(chunk)
                continue

            v_id = self.ui.voice_var.get()
            v_info = VOICE_DATA[v_id]
            current_speed = self.ui.speed_scale.get()

            # Remove dots from abbreviations for Piper, but keep ellipsis intact
            tts_text = chunk
            for abbr in all_abbr:
                tts_text = re.sub(rf'\b{re.escape(abbr)}\.(?!\.)', abbr, tts_text, flags=re.IGNORECASE)

            audio_wav, durata_ms = self.tts.generate_speech(tts_text, v_info, current_speed)
            
            if audio_wav:
                with open("/tmp/reader.wav", "wb") as f: f.write(audio_wav)
                pygame.mixer.music.load("/tmp/reader.wav")
                pygame.mixer.music.play()

                self.karaoke_smart_sync(chunk, durata_ms, current_offset)
                
                while pygame.mixer.music.get_busy() or self.is_paused:
                    if self.stop_requested: break
                    time.sleep(0.02)

                # --- FORCED PAUSE FOR ELLIPSIS ---
                # If the chunk ends with 2+ dots or the Unicode ellipsis, wait 1 full second.
                if re.search(r'\.{2,}|…', chunk):
                    time.sleep(1.0) 

                self.root.after(0, lambda: self.ui.text_area.tag_remove("highlight", "1.0", "end"))

            current_offset += len(chunk)
        
        self.is_reading = False
        self.root.after(0, lambda: self.ui.play_btn.config(state="normal"))
        
    def estimate_spoken_weight(self, word):
        """
        Calculates phonetic weight using the global abbreviation database.
        """
        weight = 0
        clean_word = word.lower().rstrip('.,;:!?')
        
        # Check against all abbreviations in the database
        all_known_abbr = [a.lower() for a in get_all_abbr()]
        
        if clean_word in all_known_abbr:
            # Abbreviations are spoken as full words (e.g., 'Mr.' -> 'Mister')
            weight += 6.5
        else:
            for char in word:
                if char.isdigit(): weight += 3.8
                elif char in ".,;:!?": weight += 2.0
                else: weight += 1.0
        return weight
        
    def karaoke_smart_sync(self, text_chunk, total_duration_ms, global_offset):
        """
        Distributes the total duration of the audio chunk among words
        based on their phonetic weight rather than simple character count.
        """
        words = text_chunk.split()
        if not words: return

        # Pre-calculate phonetic weights for the whole sentence
        word_weights = [self.estimate_spoken_weight(w) for w in words]
        total_weight = sum(word_weights)
        
        # Time allocated to each unit of weight
        ms_per_weight = total_duration_ms / total_weight if total_weight > 0 else 0
        
        current_pos_in_chunk = 0
        for i, word in enumerate(words):
            if self.stop_requested: break
            while self.is_paused:
                if self.stop_requested: break
                time.sleep(0.1)

            # Find exact character position within the current chunk
            inizio_parola = text_chunk.find(word, current_pos_in_chunk)
            
            # Translate to Tkinter text coordinates
            s = f"1.0 + {global_offset + inizio_parola} chars"
            e = f"1.0 + {global_offset + inizio_parola + len(word)} chars"
            
            # Execute visual update on the main thread
            self.root.after(0, lambda start=s, end=e: self.apply_highlight(start, end))
            
            # Wait for the calculated phonetic duration
            sleep_time = (word_weights[i] * ms_per_weight) / 1000
            time.sleep(sleep_time)
            
            current_pos_in_chunk = inizio_parola + len(word)

    def apply_highlight(self, s, e):
        """Visual helper to move the yellow background and scroll the window."""
        self.ui.text_area.tag_remove("highlight", "1.0", "end")
        self.ui.text_area.tag_add("highlight", s, e)
        self.ui.text_area.see(s) # Auto-scroll to keep current word in view

    def paste_text(self):
        """Handles clipboard operations and clears welcome message if present."""
        try:
            clipboard_text = self.root.clipboard_get()
            if clipboard_text:
                current_content = self.ui.text_area.get("1.0", "end-1c").strip()
                # Clear welcome screen if user is pasting fresh content
                if current_content == self.msgs["msg_welcome"].strip():
                    self.ui.text_area.delete("1.0", "end")
                try:
                    # Remove selection if it exists before pasting
                    self.ui.text_area.delete("sel.first", "sel.last")
                except tk.TclError: pass
                
                self.ui.text_area.insert(tk.INSERT, clipboard_text)
                self.ui.text_area.focus_set()
                self.ui.text_area.see(tk.INSERT)
            return "break" # Prevent default Tkinter paste event
        except tk.TclError: return "break"
            
    def quit_app(self):
        """Ensures audio is stopped before closing the window."""
        self.stop_reading()
        self.root.destroy()

if __name__ == "__main__":
    root = tk.Tk()
    app = FussReaderApp(root)
    root.mainloop()
