"""
ChitraPad Universal Editor.
~~~~~~~~~~~~~~~~~~~~~~~~~~~

A universal text editor built using Tkinter. It supports file open/save,
syntax highlighting (for Python and JavaScript), theme toggling, code execution,
undo/redo, and find operations. A dynamic status bar displays the current
cursor position and word count.

Classes
-------
ChitraPadUniversal
    The main editor class.
"""

import re
import tkinter as tk
from tkinter import filedialog
from tkinter import messagebox
from tkinter import simpledialog
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from typing import Union


class ChitraPadUniversal(tk.Tk):
    """
    A universal text editor with syntax highlighting, theme toggling, file operations,
    code execution, and additional features like undo/redo and find.

    Attributes
    ----------
    is_dark_theme : bool
        Indicates whether the dark theme is active.
    language : str
        The selected programming language for syntax highlighting.
    """

    def __init__(
        self,
    ) -> None:
        """Initialize the ChitraPad editor, set up the text area, status bar, menu, and key bindings."""
        super().__init__()
        self.title("ChitraPad")
        self.geometry("800x600")

        # Initialize theme and language
        self.is_dark_theme: bool = False
        self.language: str = "Python"

        # Create a frame to hold the text area
        self.frame: tk.Frame = tk.Frame(self)
        self.frame.pack(
            expand=True,
            fill="both",
        )

        # Text area setup with undo support
        self.text_area: tk.Text = tk.Text(
            self.frame,
            wrap="word",
            font="Consolas 12",
            undo=True,
        )
        self.text_area.pack(
            side=tk.LEFT,
            expand=True,
            fill="both",
        )

        # Scrollbar for the text area
        scrollbar: tk.Scrollbar = tk.Scrollbar(
            self.frame,
            command=self.text_area.yview,
        )
        scrollbar.pack(
            side=tk.RIGHT,
            fill="y",
        )
        self.text_area.config(yscrollcommand=scrollbar.set)

        # Create status bar at the bottom BEFORE applying theme
        self.status_bar: tk.Label = tk.Label(
            self,
            text="",
            anchor="w",
        )
        self.status_bar.pack(fill="x")

        # Apply initial theme (updates text area, frame, and status bar)
        self.apply_theme()

        # Menu setup
        self.create_menu()

        # Bind events for syntax highlighting and status updates
        self.text_area.bind(
            "<KeyRelease>",
            self._on_key_release,
        )  # type: ignore
        self.text_area.bind(
            "<ButtonRelease>",
            self._update_status_bar,
        )  # type: ignore

        # Initial status bar update
        self._update_status_bar()

    def create_menu(
        self,
    ) -> None:
        """Create the menu bar for the application."""
        menu: tk.Menu = tk.Menu(self)
        self.config(menu=menu)

        # File menu
        file_menu: tk.Menu = tk.Menu(
            menu,
            tearoff=0,
        )
        menu.add_cascade(
            label="File",
            menu=file_menu,
        )
        file_menu.add_command(
            label="Open",
            command=self.open_file,
        )
        file_menu.add_command(
            label="Save",
            command=self.save_file,
        )
        file_menu.add_separator()
        file_menu.add_command(
            label="Exit",
            command=self.quit,
        )

        # Edit menu
        edit_menu: tk.Menu = tk.Menu(
            menu,
            tearoff=0,
        )
        menu.add_cascade(
            label="Edit",
            menu=edit_menu,
        )
        edit_menu.add_command(
            label="Undo",
            command=self.text_area.edit_undo,
            accelerator="Ctrl+Z",
        )
        edit_menu.add_command(
            label="Redo",
            command=self.text_area.edit_redo,
            accelerator="Ctrl+Y",
        )
        edit_menu.add_separator()
        edit_menu.add_command(
            label="Find",
            command=self.find_text,
            accelerator="Ctrl+F",
        )

        # Language menu
        language_menu: tk.Menu = tk.Menu(
            menu,
            tearoff=0,
        )
        menu.add_cascade(
            label="Language",
            menu=language_menu,
        )
        language_menu.add_command(
            label="Python",
            command=lambda: self.set_language("Python"),
        )
        language_menu.add_command(
            label="JavaScript",
            command=lambda: self.set_language("JavaScript"),
        )

        # Theme menu
        theme_menu: tk.Menu = tk.Menu(
            menu,
            tearoff=0,
        )
        menu.add_cascade(
            label="Theme",
            menu=theme_menu,
        )
        theme_menu.add_command(
            label="Toggle Dark/Light Theme",
            command=self.toggle_theme,
        )

        # Run menu
        run_menu: tk.Menu = tk.Menu(
            menu,
            tearoff=0,
        )
        menu.add_cascade(
            label="Run",
            menu=run_menu,
        )
        run_menu.add_command(
            label="Run Python Code",
            command=self.run_python_code,
        )

        # Key bindings for Undo, Redo, and Find
        self.bind(
            "<Control-z>",
            lambda event: self.text_area.edit_undo(),
        )
        self.bind(
            "<Control-y>",
            lambda event: self.text_area.edit_redo(),
        )
        self.bind(
            "<Control-f>",
            lambda event: self.find_text(),
        )

    def open_file(
        self,
    ) -> None:
        """Open a file and load its content into the text area."""
        file_path: Optional[str] = filedialog.askopenfilename(
            defaultextension=".txt",
            filetypes=[
                (
                    "Text Files",
                    "*.txt",
                ),
                (
                    "All Files",
                    "*.*",
                ),
            ],
        )
        if file_path:
            try:
                with open(
                    file_path,
                    "r",
                    encoding="utf-8",
                ) as file:
                    content: str = file.read()
                    self.text_area.delete(
                        "1.0",
                        tk.END,
                    )
                    self.text_area.insert(
                        tk.END,
                        content,
                    )
            except Exception as e:
                messagebox.showerror(
                    "Error",
                    f"Failed to open file: {e}",
                )  # type: ignore

    def save_file(
        self,
    ) -> None:
        """Save the content of the text area to a file."""
        file_path: Optional[str] = filedialog.asksaveasfilename(
            defaultextension=".txt",
            filetypes=[
                (
                    "Text Files",
                    "*.txt",
                ),
                (
                    "All Files",
                    "*.*",
                ),
            ],
        )
        if file_path:
            try:
                with open(
                    file_path,
                    "w",
                    encoding="utf-8",
                ) as file:
                    file.write(
                        self.text_area.get(
                            "1.0",
                            tk.END,
                        )
                    )
            except Exception as e:
                messagebox.showerror(
                    "Error",
                    f"Failed to save file: {e}",
                )  # type: ignore

    def set_language(
        self,
        language: str,
    ) -> None:
        """
        Set the programming language for syntax highlighting.

        Parameters
        ----------
        language : str
            The programming language (e.g., "Python", "JavaScript").
        """
        self.language = language
        self.highlight_syntax()

    def highlight_syntax(
        self,
        event: Optional[tk.Event] = None,
    ) -> None:
        """
        Highlight syntax based on the selected programming language.

        Parameters
        ----------
        event : Optional[tk.Event]
            The triggering event (default is None).
        """
        # Clear previous highlights
        self.text_area.tag_remove(
            "keyword",
            "1.0",
            tk.END,
        )
        content: str = self.text_area.get(
            "1.0",
            tk.END,
        )
        patterns: List[
            Tuple[
                str,
                str,
            ]
        ] = []

        if self.language == "Python":
            pattern: str = r"\b(def|class|import|from|as|if|else|elif|while|for|return|try|except|with|lambda)\b"
            patterns.append(
                (
                    pattern,
                    "green",
                )
            )
        elif self.language == "JavaScript":
            pattern: str = r"\b(function|var|let|const|if|else|for|while|return|class|import|from)\b"
            patterns.append(
                (
                    pattern,
                    "blue",
                )
            )
        else:
            return

        for (
            pat,
            color,
        ) in patterns:
            for match in re.finditer(
                pat,
                content,
            ):
                start_index: str = f"1.0+{match.start()}c"
                end_index: str = f"1.0+{match.end()}c"
                self.text_area.tag_add(
                    "keyword",
                    start_index,
                    end_index,
                )
            self.text_area.tag_config(
                "keyword",
                foreground=color,
            )

    def toggle_theme(
        self,
    ) -> None:
        """Toggle between dark and light themes."""
        self.is_dark_theme = not self.is_dark_theme
        self.apply_theme()

    def apply_theme(
        self,
    ) -> None:
        """Apply the selected theme to the application."""
        if self.is_dark_theme:
            bg_color: str = "#000000"  # Black background
            fg_color: str = "#FFFFFF"  # White text
            text_bg: str = "#111111"  # Dark text area background
            keyword_color: str = "green"  # Green for keywords
        else:
            bg_color = "#FFFFFF"  # White background
            fg_color = "#000000"  # Black text
            text_bg = "#FFDDDD"  # Light red for text area background
            keyword_color = "#FF4D4D"  # Light red for keywords

        # Apply colors to main window, frame, and text area
        self.config(bg=bg_color)
        self.frame.config(bg=bg_color)
        self.text_area.config(
            bg=text_bg,
            fg=fg_color,
            insertbackground=fg_color,
        )
        self.text_area.tag_config(
            "keyword",
            foreground=keyword_color,
        )

        # Update status bar theme (if it exists)
        if hasattr(
            self,
            "status_bar",
        ):
            self.status_bar.config(
                bg=bg_color,
                fg=fg_color,
            )

    def _update_status_bar(
        self,
        event: Optional[tk.Event] = None,
    ) -> None:
        """Update the status bar with the current cursor position, word count, and language."""
        index: str = self.text_area.index("insert")
        (
            line,
            col,
        ) = index.split(".")
        word_count: int = len(
            self.text_area.get(
                "1.0",
                tk.END,
            ).split()
        )
        status_text: str = f"Line: {line}, Column: {col} | Words: {word_count} | Language: {self.language}"
        self.status_bar.config(text=status_text)

    def _on_key_release(
        self,
        event: Optional[tk.Event] = None,
    ) -> None:
        """Handle key release events to update syntax highlighting and the status bar."""
        self.highlight_syntax()
        self._update_status_bar()

    def find_text(
        self,
    ) -> None:
        """Open a dialog to find text in the document and highlight all occurrences."""
        search_term: Optional[str] = simpledialog.askstring(
            "Find",
            "Enter text to find:",
        )
        if not search_term:
            return

        # Remove previous 'found' highlights
        self.text_area.tag_remove(
            "found",
            "1.0",
            tk.END,
        )
        content: str = self.text_area.get(
            "1.0",
            tk.END,
        )
        matches = list(
            re.finditer(
                re.escape(search_term),
                content,
                re.IGNORECASE,
            )
        )
        if not matches:
            messagebox.showinfo(
                "Find",
                "No matches found.",
            )
            return

        for match in matches:
            start_index: str = f"1.0+{match.start()}c"
            end_index: str = f"1.0+{match.end()}c"
            self.text_area.tag_add(
                "found",
                start_index,
                end_index,
            )
        self.text_area.tag_config(
            "found",
            background="yellow",
        )

    def run_python_code(
        self,
    ) -> None:
        """Execute Python code from the text area and display the output."""
        code: str = self.text_area.get(
            "1.0",
            tk.END,
        )
        try:
            exec_globals: Dict[
                str,
                Union[
                    str,
                    int,
                    float,
                    List,
                    Dict,
                ],
            ] = {}  # type: ignore
            exec(
                code,
                exec_globals,
            )  # type: ignore
            output: Optional[str] = exec_globals.get(
                "output",
                "",
            )  # type: ignore
            messagebox.showinfo(
                "Output",
                output or "No output.",
            )  # type: ignore
        except Exception as e:
            messagebox.showerror(
                "Error",
                str(e),
            )  # type: ignore

    def run(
        self,
    ) -> None:
        """Run the application."""
        self.mainloop()
