#!/usr/bin/env python3
"""
Basic Standalone Face Recognition Application
Features: Real-time detection, recognition, and history
"""

import cv2
import numpy as np
import customtkinter as ctk
from tkinter import ttk, messagebox
import os
import json
import csv
from datetime import datetime
import threading
from PIL import Image, ImageTk

# Set appearance mode
ctk.set_appearance_mode("dark")
# Note: set_color_theme removed for compatibility with older customtkinter versions

class FaceRecognitionApp:
    def __init__(self):
        self.root = ctk.CTk()
        self.root.title("Face Recognition System")
        self.root.geometry("1200x800")
        
        # Initialize variables
        self.is_running = False
        self.cap = None
        self.face_cascade = None
        self.recognizer = None
        self.user_data = {}
        self.current_camera_index = 0  # Added for camera selection
        
        # Load models and data
        self.load_models()
        
        # Create UI
        self.create_ui()
        
        # Start camera
        self.start_camera()
        
    def load_models(self):
        """Load face detection and recognition models"""
        try:
            # Load face cascade
            cascade_path = "haarcascade_frontalface_default.xml"
            if not os.path.exists(cascade_path):
                messagebox.showerror("Error", f"Face cascade file not found: {cascade_path}")
                return
            self.face_cascade = cv2.CascadeClassifier(cascade_path)
            
            # Load face recognizer
            recognizer_path = "face_recognizer.yml"
            if not os.path.exists(recognizer_path):
                messagebox.showerror("Error", f"Face recognizer file not found: {recognizer_path}")
                return
            self.recognizer = cv2.face.LBPHFaceRecognizer_create()
            self.recognizer.read(recognizer_path)
            
            # Load user data
            if os.path.exists("user_data.json"):
                with open("user_data.json", "r") as f:
                    self.user_data = json.load(f)
                print(f"✅ Loaded {len(self.user_data)} users")
            else:
                print("⚠️ No user_data.json found")
                
        except Exception as e:
            messagebox.showerror("Error", f"Failed to load models: {e}")
            
    def create_ui(self):
        """Create the user interface"""
        # Main frame
        main_frame = ctk.CTkFrame(self.root)
        main_frame.pack(fill="both", expand=True, padx=10, pady=10)
        
        # Title
        title_label = ctk.CTkLabel(main_frame, text="Face Recognition System", 
                                 font=ctk.CTkFont(size=24, weight="bold"))
        title_label.pack(pady=(0, 20))
        
        # Content frame with two columns
        content_frame = ctk.CTkFrame(main_frame)
        content_frame.pack(fill="both", expand=True)
        
        # Left column - Camera and controls
        left_frame = ctk.CTkFrame(content_frame)
        left_frame.pack(side="left", fill="both", expand=True, padx=(0, 10))
        
        # Camera frame with fixed size
        camera_frame = ctk.CTkFrame(left_frame)
        camera_frame.pack(pady=(0, 20))
        
        # Camera label (fixed size)
        self.camera_label = ctk.CTkLabel(camera_frame, text="Camera Loading...", 
                                       width=480, height=360)
        self.camera_label.pack(padx=10, pady=10)
        
        # Control buttons
        controls_frame = ctk.CTkFrame(left_frame)
        controls_frame.pack(fill="x", pady=(0, 20))
        
        self.start_button = ctk.CTkButton(controls_frame, text="Start Recognition", 
                                        command=self.start_recognition)
        self.start_button.pack(side="left", padx=(10, 5), pady=10)
        
        self.stop_button = ctk.CTkButton(controls_frame, text="Stop Recognition", 
                                       command=self.stop_recognition, state="disabled")
        self.stop_button.pack(side="left", padx=5, pady=10)
        
        # Settings button
        settings_button = ctk.CTkButton(controls_frame, text="Settings", 
                                     command=self.open_settings)
        settings_button.pack(side="right", padx=(5, 10), pady=10)
        
        # Status label
        self.status_label = ctk.CTkLabel(left_frame, text="Status: Ready", 
                                       font=ctk.CTkFont(size=14))
        self.status_label.pack(pady=(0, 10))
        
        # Right column - Recognition history
        right_frame = ctk.CTkFrame(content_frame)
        right_frame.pack(side="right", fill="both", expand=True)
        
        # History title
        history_title = ctk.CTkLabel(right_frame, text="Recognition History", 
                                   font=ctk.CTkFont(size=18, weight="bold"))
        history_title.pack(pady=(0, 10))
        
        # History treeview
        columns = ("Time", "User", "Confidence", "Status")
        self.history_tree = ttk.Treeview(right_frame, columns=columns, show="headings", height=15)
        
        # Configure columns
        self.history_tree.heading("Time", text="Time")
        self.history_tree.heading("User", text="User")
        self.history_tree.heading("Confidence", text="Confidence")
        self.history_tree.heading("Status", text="Status")
        
        # Set column widths
        self.history_tree.column("Time", width=120)
        self.history_tree.column("User", width=120)
        self.history_tree.column("Confidence", width=100)
        self.history_tree.column("Status", width=80)
        
        # Add scrollbar
        history_scrollbar = ttk.Scrollbar(right_frame, orient="vertical", command=self.history_tree.yview)
        self.history_tree.configure(yscrollcommand=history_scrollbar.set)
        
        self.history_tree.pack(side="left", fill="both", expand=True, padx=(10, 0))
        history_scrollbar.pack(side="right", fill="y", padx=(0, 10))
        
        # Clear history button
        clear_button = ctk.CTkButton(right_frame, text="Clear History", 
                                   command=self.clear_history)
        clear_button.pack(pady=10)
        
    def start_camera(self):
        """Initialize and start the camera"""
        try:
            self.cap = cv2.VideoCapture(self.current_camera_index)
            if not self.cap.isOpened():
                messagebox.showerror("Error", f"Could not open camera {self.current_camera_index}")
                return
                
            # Set camera resolution
            self.cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
            self.cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
            
            print(f"✅ Camera {self.current_camera_index} started successfully")
            
        except Exception as e:
            messagebox.showerror("Error", f"Failed to start camera {self.current_camera_index}: {e}")
            
    def start_recognition(self):
        """Start face recognition loop"""
        if not self.is_running:
            self.is_running = True
            self.start_button.configure(state="disabled")
            self.stop_button.configure(state="normal")
            self.status_label.configure(text="Status: Recognition Active")
            
            # Start recognition thread
            self.recognition_thread = threading.Thread(target=self.recognition_loop, daemon=True)
            self.recognition_thread.start()
            
    def stop_recognition(self):
        """Stop face recognition loop"""
        self.is_running = False
        self.start_button.configure(state="normal")
        self.stop_button.configure(state="disabled")
        self.status_label.configure(text="Status: Stopped")
        
    def recognition_loop(self):
        """Main recognition loop"""
        while self.is_running:
            try:
                ret, frame = self.cap.read()
                if not ret:
                    continue
                    
                # Convert to RGB for display
                frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
                
                # Detect faces
                gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
                faces = self.face_cascade.detectMultiScale(gray, 1.1, 4)
                
                # Draw rectangles around detected faces
                for (x, y, w, h) in faces:
                    cv2.rectangle(frame_rgb, (x, y), (x+w, y+h), (0, 255, 0), 2)
                    
                    # Add corner indicators
                    corner_size = 20
                    # Top-left corner
                    cv2.line(frame_rgb, (x, y), (x + corner_size, y), (0, 255, 0), 3)
                    cv2.line(frame_rgb, (x, y), (x, y + corner_size), (0, 255, 0), 3)
                    # Top-right corner
                    cv2.line(frame_rgb, (x + w - corner_size, y), (x + w, y), (0, 255, 0), 3)
                    cv2.line(frame_rgb, (x + w, y), (x + w, y + corner_size), (0, 255, 0), 3)
                    # Bottom-left corner
                    cv2.line(frame_rgb, (x, y + h - corner_size), (x, y + h), (0, 255, 0), 3)
                    cv2.line(frame_rgb, (x, y + h), (x + corner_size, y + h), (0, 255, 0), 3)
                    # Bottom-right corner
                    cv2.line(frame_rgb, (x + w - corner_size, y + h), (x + w, y + h), (0, 255, 0), 3)
                    cv2.line(frame_rgb, (x + w, y + h - corner_size), (x + w, y + h), (0, 255, 0), 3)
                    
                    # Perform recognition on the first detected face
                    if self.recognizer and len(faces) > 0:
                        face_roi = gray[y:y+h, x:x+w]
                        face_roi = cv2.resize(face_roi, (200, 200))
                        face_roi = cv2.equalizeHist(face_roi)
                        
                        try:
                            # Predict
                            user_id, confidence = self.recognizer.predict(face_roi)
                            
                            # Get user name
                            user_name = self.user_data.get(str(user_id), f"User {user_id}")
                            
                            # Add recognition info to frame
                            text = f"{user_name} ({confidence:.1f})"
                            cv2.putText(frame_rgb, text, (x, y-10), 
                                      cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
                            
                            # Add to history (only if confidence is good)
                            if confidence < 120:  # Lower is better for LBPH
                                self.add_to_history(user_name, confidence, "Recognized")
                            
                        except Exception as e:
                            print(f"Recognition error: {e}")
                
                # Convert to PhotoImage for display
                frame_pil = Image.fromarray(frame_rgb)
                frame_tk = ImageTk.PhotoImage(frame_pil)
                
                # Update camera label
                self.camera_label.configure(image=frame_tk, text="")
                self.camera_label.image = frame_tk
                
                # Control frame rate
                cv2.waitKey(30)
                
            except Exception as e:
                print(f"Error in recognition loop: {e}")
                continue
                
    def add_to_history(self, user_name, confidence, status):
        """Add recognition result to history"""
        try:
            timestamp = datetime.now().strftime("%H:%M:%S")
            
            # Insert at the top
            self.history_tree.insert("", 0, values=(timestamp, user_name, f"{confidence:.1f}", status))
            
            # Limit history to 100 entries
            if len(self.history_tree.get_children()) > 100:
                self.history_tree.delete(self.history_tree.get_children()[-1])
                
        except Exception as e:
            print(f"Error adding to history: {e}")
            
    def clear_history(self):
        """Clear recognition history"""
        for item in self.history_tree.get_children():
            self.history_tree.delete(item)
            
    def open_settings(self):
        """Open settings dialog"""
        settings_window = ctk.CTkToplevel(self.root)
        settings_window.title("Settings")
        settings_window.geometry("500x500")
        settings_window.transient(self.root)
        settings_window.grab_set()
        
        # Settings content
        title = ctk.CTkLabel(settings_window, text="Recognition Settings", 
                           font=ctk.CTkFont(size=18, weight="bold"))
        title.pack(pady=20)
        
        # Camera selection frame
        camera_frame = ctk.CTkFrame(settings_window)
        camera_frame.pack(fill="x", padx=20, pady=10)
        
        ctk.CTkLabel(camera_frame, text="Camera Selection:", 
                    font=ctk.CTkFont(size=14, weight="bold")).pack(pady=5)
        
        # Get available cameras
        available_cameras = self.get_available_cameras()
        
        if available_cameras:
            # Camera dropdown
            camera_var = ctk.StringVar(value=str(self.current_camera_index))
            camera_dropdown = ctk.CTkOptionMenu(camera_frame, 
                                              values=available_cameras,
                                              variable=camera_var,
                                              command=self.on_camera_change)
            camera_dropdown.pack(pady=5)
            
            # Camera info
            camera_info = ctk.CTkLabel(camera_frame, 
                                     text=f"Current: {self.get_camera_display_name(self.current_camera_index, 0, 0)}",
                                     font=ctk.CTkFont(size=12))
            camera_info.pack(pady=5)
            
            # Refresh cameras button
            refresh_button = ctk.CTkButton(camera_frame, text="Refresh Cameras", 
                                        command=self.refresh_cameras)
            refresh_button.pack(pady=5)
        else:
            ctk.CTkLabel(camera_frame, text="No cameras detected", 
                        font=ctk.CTkFont(size=12)).pack(pady=5)
        
        # Confidence threshold
        confidence_frame = ctk.CTkFrame(settings_window)
        confidence_frame.pack(fill="x", padx=20, pady=10)
        
        ctk.CTkLabel(confidence_frame, text="Confidence Threshold:", 
                    font=ctk.CTkFont(size=14, weight="bold")).pack(pady=5)
        confidence_slider = ctk.CTkSlider(confidence_frame, from_=50, to=200, number_of_steps=150)
        confidence_slider.set(120)
        confidence_slider.pack(pady=5)
        
        # Detection sensitivity
        sensitivity_frame = ctk.CTkFrame(settings_window)
        sensitivity_frame.pack(fill="x", padx=20, pady=10)
        
        ctk.CTkLabel(sensitivity_frame, text="Detection Sensitivity:", 
                    font=ctk.CTkFont(size=14, weight="bold")).pack(pady=5)
        sensitivity_slider = ctk.CTkSlider(sensitivity_frame, from_=1.0, to=2.0, number_of_steps=100)
        sensitivity_slider.set(1.1)
        sensitivity_slider.pack(pady=5)
        
        # Close button
        close_button = ctk.CTkButton(settings_window, text="Close", 
                                   command=settings_window.destroy)
        close_button.pack(pady=20)
        
    def get_available_cameras(self):
        """Get list of available cameras with meaningful names"""
        available_cameras = []
        
        # Test cameras from index 0 to 10
        for i in range(10):
            cap = cv2.VideoCapture(i)
            if cap.isOpened():
                # Get camera properties
                width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
                height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
                
                # Try to get more detailed camera info
                camera_name = self.get_camera_display_name(i, width, height)
                
                available_cameras.append(camera_name)
                cap.release()
                
        return available_cameras
        
    def get_camera_display_name(self, index, width, height):
        """Get a meaningful display name for the camera"""
        try:
            # Try to get camera backend info
            backend = cv2.getBackendName()
            
            # Common camera patterns
            if index == 0:
                # Camera 0 is usually the default/built-in camera
                if width >= 1920 or height >= 1080:  # High resolution
                    return f"MacBook Built-in Camera ({width}x{height})"
                else:
                    return f"Built-in Camera ({width}x{height})"
            elif index == 1:
                # Camera 1 is often external devices
                if "AVFoundation" in backend or "Darwin" in backend:
                    return f"iPhone/External Camera ({width}x{height})"
                else:
                    return f"External Camera ({width}x{height})"
            else:
                # Other cameras
                return f"Camera {index} ({width}x{height})"
                
        except Exception as e:
            # Fallback to basic naming
            return f"Camera {index} ({width}x{height})"
            
    def on_camera_change(self, camera_selection):
        """Handle camera selection change"""
        try:
            # Extract camera index from selection (more robust parsing)
            camera_index = self.extract_camera_index(camera_selection)
            
            if camera_index is not None and camera_index != self.current_camera_index:
                # Stop current camera
                if self.cap and self.cap.isOpened():
                    self.cap.release()
                
                # Update camera index
                self.current_camera_index = camera_index
                
                # Restart camera with new selection
                self.start_camera()
                
                # Update status with camera name
                camera_name = self.get_camera_display_name(camera_index, 0, 0)
                self.status_label.configure(text=f"Status: Switched to {camera_name}")
                
        except Exception as e:
            print(f"Error changing camera: {e}")
            
    def extract_camera_index(self, camera_selection):
        """Extract camera index from camera selection string"""
        try:
            # Handle different formats: "Camera 0 (640x480)", "Built-in Camera (640x480)", etc.
            if "Camera" in camera_selection:
                # Extract number after "Camera"
                parts = camera_selection.split()
                for i, part in enumerate(parts):
                    if part == "Camera" and i + 1 < len(parts):
                        return int(parts[i + 1])
            elif "Built-in" in camera_selection:
                return 0  # Built-in is usually index 0
            elif "iPhone" in camera_selection or "External" in camera_selection:
                return 1  # External devices are usually index 1
                
            # Fallback: try to find any number in the string
            import re
            numbers = re.findall(r'\d+', camera_selection)
            if numbers:
                return int(numbers[0])
                
        except Exception as e:
            print(f"Error extracting camera index: {e}")
            
        return None
            
    def refresh_cameras(self):
        """Refresh available cameras list"""
        # This would typically refresh the camera dropdown
        # For now, just show a message
        messagebox.showinfo("Refresh", "Camera list refreshed. Close and reopen settings to see changes.")
        
    def on_closing(self):
        """Handle application closing"""
        self.is_running = False
        if self.cap:
            self.cap.release()
        self.root.destroy()
        
    def run(self):
        """Run the application"""
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
        self.root.mainloop()

if __name__ == "__main__":
    app = FaceRecognitionApp()
    app.run()
