
import cv2
import numpy as np
import os
import json
import time
import sys
import multiprocessing as mp
from datetime import datetime
from colorama import Fore, Back, Style, init
from pathlib import Path
from typing import Tuple, List, Optional, Dict, Any

# Obsługa klawiatury: msvcrt na Windows, fallback dla Unix
try:
    import msvcrt
    _HAS_MSVCRT = True
except Exception:
    import select
    import tty
    import termios
    _HAS_MSVCRT = False

# Inicjalizacja colorama
init(autoreset=True)

# ==================== KONFIGURACJA ====================

CONFIG_DIR = Path("ascii_config")
CONFIG_FILE = CONFIG_DIR / "settings.json"
RECORDINGS_DIR = Path("ascii_recordings")

CONFIG_DIR.mkdir(exist_ok=True)
RECORDINGS_DIR.mkdir(exist_ok=True)

# Zestawy znaków ASCII – rozszerzone opcje
ASCII_SETS = {
    "standard": "@%#*+=-:. ",
    "detailed": "$@B%8&WM#*oahkbdpqwmZO0QLCJUYXzcvunxrjft/\\|()1{}[]?-_+~<>i!lI;:,\"^`'. ",
    "simple": "#=-. ",
    "block": "▓▒░ ",
    "minimal": "██▉▊▋▌▅▴▃▂▁ ",
    "circuit": "█▓▒░░ ",
    "inverse": " .:-=+*#%@█",
    "heavy": "█▓▒░",
    "gradients": "░▒▓█",
}

# Predefiniowane profile ustawień
PROFILES = {
    "normal": {
        "width": 120,
        "char_set": "standard",
        "use_color": False,
        "outline_only": False,
        "invert_colors": False,
        "brightness": 1.0,
        "contrast": 1.0,
        "sharpen": False,
        "target_fps": 30,
    },
    "detailed": {
        "width": 150,
        "char_set": "detailed",
        "use_color": True,
        "outline_only": False,
        "invert_colors": False,
        "brightness": 1.1,
        "contrast": 1.2,
        "sharpen": True,
        "target_fps": 20,
    },
    "performance": {
        "width": 80,
        "char_set": "simple",
        "use_color": False,
        "outline_only": False,
        "invert_colors": False,
        "brightness": 1.0,
        "contrast": 1.0,
        "sharpen": False,
        "target_fps": 60,
    },
    "artistic": {
        "width": 100,
        "char_set": "block",
        "use_color": False,
        "outline_only": True,
        "invert_colors": False,
        "brightness": 0.9,
        "contrast": 1.3,
        "sharpen": False,
        "target_fps": 30,
    },
    "highcontrast": {
        "width": 100,
        "char_set": "simple",
        "use_color": False,
        "outline_only": False,
        "invert_colors": False,
        "brightness": 1.2,
        "contrast": 1.8,
        "sharpen": True,
        "target_fps": 30,
    },
}

# ==================== FUNKCJE POMOCNICZE ====================

def save_config(config: Dict[str, Any]) -> bool:
    """Zapisz config do pliku JSON"""
    try:
        with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
            json.dump(config, f, indent=4, ensure_ascii=False)
        return True
    except Exception as e:
        print(Fore.RED + f"Błąd przy zapisywaniu: {e}")
        return False

def load_config() -> Dict[str, Any]:
    """Załaduj config z pliku JSON lub zwróć domyślny"""
    try:
        if CONFIG_FILE.exists():
            with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
                return json.load(f)
    except Exception as e:
        print(Fore.YELLOW + f"Nie można załadować konfigu: {e}")
    return {}

def get_timestamp() -> str:
    """Zwróć timestamp dla nazwy pliku"""
    return datetime.now().strftime("%Y%m%d_%H%M%S")

def clear_screen():
    """Wyczyść ekran – cross-platform"""
    os.system('cls' if os.name == 'nt' else 'clear')

def print_header(title: str):
    """Wydrukuj nagłówek z ramką"""
    width = 40
    border = "═" * width
    print(Fore.CYAN + border)
    print(f"║ {title.center(width-4)} ║")
    print(border + Style.RESET_ALL)

def print_menu_option(number: int, text: str, current_value: Any = None):
    """Wydrukuj opcję menu"""
    if current_value is not None:
        value_str = str(current_value)
        if isinstance(current_value, bool):
            value_str = Fore.GREEN + "WŁ" if current_value else Fore.RED + "WYŁ"
        print(f"{number}. {text:<30} {Fore.YELLOW}: {value_str}{Style.RESET_ALL}")
    else:
        print(f"{number}. {text}")

# ==================== KONWERSJA ASCII ====================

def frame_to_ascii(frame, width: int, use_color=False, outline_only=False, 
                   invert_colors=False, brightness=1.0, contrast=1.0, 
                   sharpen=False, char_set="standard") -> str:
    """Konwertuj klatkę wideo na ASCII art"""
    try:
        # Odwrócenie poziome (usuwa efekt lustra)
        frame = cv2.flip(frame, 1)

        # Dostosowanie jasności i kontrastu
        frame = cv2.convertScaleAbs(frame, alpha=contrast, beta=(brightness - 1.0) * 100)

        # Wyostrzanie (opcjonalne)
        if sharpen:
            kernel = np.array([[0, -1, 0],
                               [-1, 5, -1],
                               [0, -1, 0]])
            frame = cv2.filter2D(frame, -1, kernel)

        # Obliczenie nowej wysokości z zachowaniem proporcji
        h, w = frame.shape[:2]
        ratio = h / w / 0.55
        new_height = max(1, int(width * ratio))
        resized = cv2.resize(frame, (width, new_height), interpolation=cv2.INTER_AREA)

        # Konwersja do szarości
        gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)

        # Odwrócenie kolorów (opcjonalne)
        if invert_colors:
            gray = 255 - gray

        chars = ASCII_SETS.get(char_set, ASCII_SETS["standard"])
        ascii_image = []

        for i in range(gray.shape[0]):
            line = []
            for j in range(gray.shape[1]):
                pixel = int(gray[i, j])
                char = chars[pixel * len(chars) // 256]

                if outline_only:
                    char = "#" if pixel < 128 else " "

                if use_color:
                    b, g, r = resized[i, j]
                    line.append(f"\033[38;2;{int(r)};{int(g)};{int(b)}m{char}")
                else:
                    line.append(char)
            ascii_image.append("".join(line))

        return "\n".join(ascii_image)
    except Exception as e:
        return f"Error: {e}"

def render_chunk(args) -> str:
    """Renderuj fragment obrazu w osobnym procesie"""
    frame, width, use_color, outline_only, invert_colors, brightness, contrast, sharpen, char_set = args
    return frame_to_ascii(
        frame, width, use_color, outline_only, invert_colors,
        brightness, contrast, sharpen, char_set
    )

# ==================== GŁÓWNA APLIKACJA ====================

class ASCIICamV2:
    """Zaawansowana aplikacja ASCII Camera w wersji 1.5"""
    
    def __init__(self):
        self.load_settings()
        self.recording = False
        self.recording_file = None
        self.frame_buffer = []
        
    def load_settings(self):
        """Załaduj ustawienia z pliku lub ustaw domyślne"""
        saved = load_config()
        self.width = saved.get('width', 120)
        self.use_color = saved.get('use_color', False)
        self.outline_only = saved.get('outline_only', False)
        self.invert_colors = saved.get('invert_colors', False)
        self.brightness = saved.get('brightness', 1.0)
        self.contrast = saved.get('contrast', 1.0)
        self.sharpen = saved.get('sharpen', False)
        self.split = saved.get('split', 4)
        self.target_fps = saved.get('target_fps', 30)
        self.show_fps = saved.get('show_fps', True)
        self.camera_index = saved.get('camera_index', 0)
        self.char_set = saved.get('char_set', 'standard')
        self.show_help = saved.get('show_help', True)

    def save_settings(self):
        """Zapisz aktualne ustawienia"""
        config = {
            'width': self.width,
            'use_color': self.use_color,
            'outline_only': self.outline_only,
            'invert_colors': self.invert_colors,
            'brightness': self.brightness,
            'contrast': self.contrast,
            'sharpen': self.sharpen,
            'split': self.split,
            'target_fps': self.target_fps,
            'show_fps': self.show_fps,
            'camera_index': self.camera_index,
            'char_set': self.char_set,
            'show_help': self.show_help,
        }
        save_config(config)

    def main_menu(self):
        """Główne menu aplikacji"""
        while True:
            clear_screen()
            print_header("ASCII CAMERA v1.5")
            print()
            print_menu_option(1, "▶ Uruchom kamerę")
            print_menu_option(2, "⚙ Ustawienia")
            print_menu_option(3, "📋 Profile")
            print_menu_option(4, "💾 Wskazówki")
            print_menu_option(5, "❌ Wyjście")
            print()
            
            choice = input(Fore.CYAN + "Wybierz (1-5): " + Style.RESET_ALL).strip()

            if choice == "1":
                self.start_camera()
            elif choice == "2":
                self.settings_menu()
            elif choice == "3":
                self.profiles_menu()
            elif choice == "4":
                self.help_menu()
            elif choice == "5":
                print(Fore.GREEN + "\n👋 Do zobaczenia!" + Style.RESET_ALL)
                self.save_settings()
                break
            else:
                print(Fore.RED + "❌ Niepoprawny wybór!")
                input("Naciśnij Enter...")

    def help_menu(self):
        """Menu z instrukcjami"""
        clear_screen()
        print_header("INSTRUKCJE")
        print("""
╔ KONTROLA PODCZAS NAGRYWANIA ╗
 [Q] - Wyjście z kamery
 [R] - Zapisz klatkę na dysk
 [S] - Zapisz ASCII do pliku
 
╔ KLAWISZE ╗
 W/S - Zmień jasność
 A/D - Zmień kontrast
 Space - Przełącz kolorowanie
 
╔ USTAWIENIA ╗
 Wszystkie ustawienia są automatycznie zapisywane
 w folderze 'ascii_config' jako JSON
 
╔ EKSPORT ╗
 Nagrania i eksporty znajdują się w folderze
 'ascii_recordings'

╔ PROFILE ╗
 Predefiniowane konfiguracje do szybkiego wyboru:
 - normal: Standardowe ustawienia
 - detailed: Wysokiej jakości z kolorami
 - performance: Szybkie nagrywanie
 - artistic: Artystyczne efekty
 - highcontrast: Wysoki kontrast

""")
        input(Fore.YELLOW + "Naciśnij Enter, aby wrócić..." + Style.RESET_ALL)

    def profiles_menu(self):
        """Menu wyboru profilów"""
        while True:
            clear_screen()
            print_header("PROFILE PREDEFINIOWANE")
            print()
            
            for i, (name, profile) in enumerate(PROFILES.items(), 1):
                current = "✓" if self.char_set == profile['char_set'] else " "
                print(f"{i}. [{current}] {name.upper():<15} (width: {profile['width']}, fps: {profile['target_fps']})")
            
            print(f"{len(PROFILES) + 1}. Powrót")
            print()
            
            choice = input(Fore.CYAN + "Wybierz profil (1-{}): ".format(len(PROFILES)+1) + Style.RESET_ALL).strip()
            
            try:
                idx = int(choice) - 1
                if idx == len(PROFILES):
                    break
                if 0 <= idx < len(PROFILES):
                    profile_name = list(PROFILES.keys())[idx]
                    profile = PROFILES[profile_name]
                    self.apply_profile(profile)
                    print(Fore.GREEN + f"✓ Profil '{profile_name}' załadowany!")
                    input("Naciśnij Enter...")
                else:
                    print(Fore.RED + "❌ Niepoprawny wybór!")
                    input("Naciśnij Enter...")
            except ValueError:
                print(Fore.RED + "❌ Wpisz liczbę!")
                input("Naciśnij Enter...")

    def apply_profile(self, profile: Dict[str, Any]):
        """Zastosuj profil ustawień"""
        self.width = profile.get('width', self.width)
        self.char_set = profile.get('char_set', self.char_set)
        self.use_color = profile.get('use_color', self.use_color)
        self.outline_only = profile.get('outline_only', self.outline_only)
        self.invert_colors = profile.get('invert_colors', self.invert_colors)
        self.brightness = profile.get('brightness', self.brightness)
        self.contrast = profile.get('contrast', self.contrast)
        self.sharpen = profile.get('sharpen', self.sharpen)
        self.target_fps = profile.get('target_fps', self.target_fps)
        self.save_settings()

    def settings_menu(self):
        """Zaawansowane menu ustawień"""
        while True:
            clear_screen()
            print_header("USTAWIENIA")
            print()
            
            print_menu_option(1, "Szerokość ASCII", self.width)
            print_menu_option(2, "Zestaw znaków", self.char_set)
            print_menu_option(3, "Kolory", self.use_color)
            print_menu_option(4, "Tylko kontur", self.outline_only)
            print_menu_option(5, "Odwróć kolory", self.invert_colors)
            print_menu_option(6, "Jasność", f"{self.brightness:.2f}")
            print_menu_option(7, "Kontrast", f"{self.contrast:.2f}")
            print_menu_option(8, "Wyostrzanie", self.sharpen)
            print_menu_option(9, "Liczba rdzeni", self.split)
            print_menu_option(10, "Cel FPS", self.target_fps)
            print_menu_option(11, "Pokazuj FPS", self.show_fps)
            print_menu_option(12, "Kamera (indeks)", self.camera_index)
            print_menu_option(13, "Powrót")
            print()

            choice = input(Fore.CYAN + "Wybierz (1-13): " + Style.RESET_ALL).strip()

            if choice == "1":
                self._set_int_value("Szerokość ASCII", 10, 300, lambda x: setattr(self, 'width', x))
            elif choice == "2":
                self._set_charset()
            elif choice == "3":
                self.use_color = not self.use_color
            elif choice == "4":
                self.outline_only = not self.outline_only
            elif choice == "5":
                self.invert_colors = not self.invert_colors
            elif choice == "6":
                self._set_float_value("Jasność", 0.1, 3.0, lambda x: setattr(self, 'brightness', x))
            elif choice == "7":
                self._set_float_value("Kontrast", 0.1, 3.0, lambda x: setattr(self, 'contrast', x))
            elif choice == "8":
                self.sharpen = not self.sharpen
            elif choice == "9":
                self._set_int_value("Liczba rdzeni", 1, 16, lambda x: setattr(self, 'split', x))
            elif choice == "10":
                self._set_int_value("Cel FPS", 1, 60, lambda x: setattr(self, 'target_fps', x))
            elif choice == "11":
                self.show_fps = not self.show_fps
            elif choice == "12":
                self._set_int_value("Indeks kamery", 0, 10, lambda x: setattr(self, 'camera_index', x))
            elif choice == "13":
                break
            
            self.save_settings()

    def _set_int_value(self, name: str, min_val: int, max_val: int, setter):
        """Ustaw wartość int z walidacją"""
        try:
            val = int(input(f"{name} ({min_val}–{max_val}): "))
            val = max(min_val, min(max_val, val))
            setter(val)
            print(Fore.GREEN + f"✓ Zmieniono na {val}")
        except ValueError:
            print(Fore.RED + "❌ Wpisz liczbę!")
        input("Naciśnij Enter...")

    def _set_float_value(self, name: str, min_val: float, max_val: float, setter):
        """Ustaw wartość float z walidacją"""
        try:
            val = float(input(f"{name} ({min_val}–{max_val}): "))
            val = max(min_val, min(max_val, val))
            setter(val)
            print(Fore.GREEN + f"✓ Zmieniono na {val:.2f}")
        except ValueError:
            print(Fore.RED + "❌ Wpisz liczbę!")
        input("Naciśnij Enter...")

    def _set_charset(self):
        """Ustaw zestaw znaków"""
        clear_screen()
        print_header("ZESTAWY ZNAKÓW")
        print()
        
        for i, name in enumerate(ASCII_SETS.keys(), 1):
            current = "✓" if name == self.char_set else " "
            preview = ASCII_SETS[name][:20]
            print(f"{i}. [{current}] {name:<15} Podgląd: {preview}")
        
        print()
        try:
            idx = int(input("Wybierz (1-{}): ".format(len(ASCII_SETS)))) - 1
            keys = list(ASCII_SETS.keys())
            if 0 <= idx < len(keys):
                self.char_set = keys[idx]
                print(Fore.GREEN + f"✓ Zmieniono na '{self.char_set}'")
            else:
                print(Fore.RED + "❌ Niepoprawny wybór!")
        except ValueError:
            print(Fore.RED + "❌ Wpisz liczbę!")
        input("Naciśnij Enter...")

    def start_camera(self):
        """Uruchom kamerę z podglądem na żywo"""
        cap = None
        pool = None
        try:
            cap = cv2.VideoCapture(self.camera_index)
            if not cap.isOpened():
                print(Fore.RED + "❌ Nie udało się uruchomić kamery!")
                input("Naciśnij Enter...")
                return

            cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
            cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
            pool = mp.Pool(processes=self.split)

            old_term_settings = None
            if not _HAS_MSVCRT:
                try:
                    fd = sys.stdin.fileno()
                    old_term_settings = termios.tcgetattr(fd)
                    tty.setcbreak(fd)
                except Exception:
                    old_term_settings = None

            frame_count = 0
            start_time = time.time()
            last_time = start_time

            print(Fore.YELLOW + "\n[Q] - Wyjście | [R] - Zapisz klatkę | [S] - Eksport ASCII\n" + Style.RESET_ALL)
            time.sleep(2)

            while True:
                ret, frame = cap.read()
                if not ret:
                    print(Fore.RED + "❌ Błąd odczytu kamery!")
                    break

                chunks = np.array_split(frame, self.split, axis=0)
                args = [
                    (
                        chunk, self.width, self.use_color, self.outline_only,
                        self.invert_colors, self.brightness, self.contrast,
                        self.sharpen, self.char_set
                    )
                    for chunk in chunks
                ]

                try:
                    results = pool.map(render_chunk, args)
                except Exception as e:
                    print(Fore.RED + f"❌ Błąd: {e}")
                    break

                ascii_frame = "\n".join(results)

                frame_count += 1
                current_time = time.time()
                elapsed = current_time - last_time
                fps = 1.0 / elapsed if elapsed > 0 else 0

                target_frame_time = 1.0 / self.target_fps
                sleep_time = target_frame_time - elapsed
                if sleep_time > 0:
                    time.sleep(sleep_time)
                last_time = time.time()

                clear_screen()

                if self.show_fps:
                    ascii_frame += f"\n{Fore.YELLOW}FPS: {fps:.1f}{Style.RESET_ALL}"

                print(ascii_frame)

                # Obsługa klawiszy
                try:
                    if _HAS_MSVCRT:
                        if msvcrt.kbhit():
                            ch = msvcrt.getwch().lower()
                            if ch == 'q':
                                break
                            elif ch == 'r':
                                self._save_frame(frame)
                            elif ch == 's':
                                self._export_ascii(ascii_frame)
                    else:
                        dr, _, _ = select.select([sys.stdin], [], [], 0)
                        if dr:
                            ch = sys.stdin.read(1).lower()
                            if ch == 'q':
                                break
                            elif ch == 'r':
                                self._save_frame(frame)
                            elif ch == 's':
                                self._export_ascii(ascii_frame)
                except Exception:
                    pass

                if cv2.waitKey(1) & 0xFF == ord('q'):
                    break

        except Exception as e:
            print(Fore.RED + f"❌ Błąd: {e}")
            input("Naciśnij Enter...")
        finally:
            if cap:
                cap.release()
            if pool:
                pool.close()
                pool.join()

            if not _HAS_MSVCRT and old_term_settings is not None:
                try:
                    termios.tcsetattr(fd, termios.TCSADRAIN, old_term_settings)
                except Exception:
                    pass

            cv2.destroyAllWindows()

    def _save_frame(self, frame):
        """Zapisz klatkę do pliku"""
        try:
            filename = RECORDINGS_DIR / f"frame_{get_timestamp()}.png"
            cv2.imwrite(str(filename), frame)
            print(Fore.GREEN + f"\n✓ Zapisano: {filename.name}")
            time.sleep(1)
        except Exception as e:
            print(Fore.RED + f"\n❌ Błąd: {e}")
            time.sleep(1)

    def _export_ascii(self, ascii_text: str):
        """Eksportuj ASCII art do pliku"""
        try:
            filename = RECORDINGS_DIR / f"ascii_{get_timestamp()}.txt"
            with open(filename, 'w', encoding='utf-8') as f:
                f.write(ascii_text)
            print(Fore.GREEN + f"\n✓ Eksport: {filename.name}")
            time.sleep(1)
        except Exception as e:
            print(Fore.RED + f"\n❌ Błąd: {e}")
            time.sleep(1)


# ==================== PUNKT WEJŚCIA ====================

if __name__ == "__main__":
    try:
        print(Fore.CYAN + "\n╔══════════════════════════════════════════╗")
        print("║     ASCII CAMERA v1.5 - Zaawansowany     ║")
        print("║         Konwerter Wideo na ASCII Art      ║")
        print("╚══════════════════════════════════════════╝\n" + Style.RESET_ALL)
        time.sleep(1)
        
        app = ASCIICamV2()
        app.main_menu()
    except KeyboardInterrupt:
        print(Fore.YELLOW + "\n\n⚠ Program przerwany przez użytkownika.")
        print("Ustawienia zapisane.\n" + Style.RESET_ALL)
    except Exception as e:
        print(Fore.RED + f"\n\n❌ Krytyczny błąd: {e}\n" + Style.RESET_ALL)
