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

# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.

import difflib
import os
import re
import time

from fenrirscreenreader.core import debug
from fenrirscreenreader.utils import screen_utils


class ScreenManager:
    def __init__(self):
        self.differ = difflib.Differ()
        self.currScreenIgnored = False
        self.prevScreenIgnored = False
        self.prevScreenText = ""
        self.currScreenText = ""
        self.colums = None
        self.rows = None
        # Compile regex once for better performance
        self._space_normalize_regex = re.compile(" +")

    def get_rows(self):
        return self.rows

    def get_columns(self):
        return self.colums

    def initialize(self, environment):
        self.env = environment
        self.env["runtime"]["SettingsManager"].load_driver(
            self.env["runtime"]["SettingsManager"].get_setting(
                "screen", "driver"
            ),
            "ScreenDriver",
        )
        self.get_curr_screen()
        self.get_curr_screen()
        self.get_session_information()
        self.update_screen_ignored()
        self.update_screen_ignored()

    def reset_screen_text(self, screen_text):
        self.prevScreenText = ""
        self.currScreenText = screen_text

    def set_screen_text(self, screen_text):
        self.prevScreenText = self.currScreenText
        self.currScreenText = screen_text

    def get_screen_text(self):
        return self.currScreenText
    
    def get_line_text(self, line_number):
        """Get text from a specific line (0-based index)"""
        if not self.env["screen"]["new_content_text"]:
            return ""
        
        lines = self.env["screen"]["new_content_text"].split("\n")
        if line_number < 0 or line_number >= len(lines):
            return ""
        
        return lines[line_number]

    def get_curr_screen(self):
        try:
            self.env["runtime"]["ScreenDriver"].get_curr_screen()
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "ScreenManager get_curr_screen: Error getting current screen: "
                + str(e),
                debug.DebugLevel.ERROR,
            )

    def get_session_information(self):
        try:
            self.env["runtime"]["ScreenDriver"].get_session_information()
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "ScreenManager get_session_information: Error getting session info: "
                + str(e),
                debug.DebugLevel.ERROR,
            )

    def shutdown(self):
        self.env["runtime"]["SettingsManager"].shutdown_driver("ScreenDriver")

    def is_curr_screen_ignored_changed(self):
        return self.get_curr_screen_ignored() != self.get_prev_screen_ignored()

    def handle_screen_change(self, event_data):
        self.get_curr_screen()
        self.get_session_information()
        self.update_screen_ignored()
        if self.is_curr_screen_ignored_changed():
            self.env["runtime"]["InputManager"].set_execute_device_grab()
        self.env["runtime"]["InputManager"].handle_device_grab()
        
        # Update current application detection on screen change
        try:
            self.env["runtime"]["ScreenDriver"].get_curr_application()
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                f"Application detection failed: {str(e)}", debug.DebugLevel.ERROR
            )
            
        if not self.is_ignored_screen(self.env["screen"]["newTTY"]):
            self.update(event_data, "onScreenChange")
            self.env["screen"]["lastScreenUpdate"] = time.time()
        else:
            self.env["runtime"]["OutputManager"].interrupt_output()

    def handle_screen_update(self, event_data):
        self.env["screen"]["oldApplication"] = self.env["screen"][
            "new_application"
        ]
        self.update_screen_ignored()
        if self.is_curr_screen_ignored_changed():
            self.env["runtime"]["InputManager"].set_execute_device_grab()
        self.env["runtime"]["InputManager"].handle_device_grab()
        if not self.get_curr_screen_ignored():
            self.update(event_data, "onScreenUpdate")
            self.env["screen"]["lastScreenUpdate"] = time.time()
        elif self.is_curr_screen_ignored_changed():
            self.env["runtime"]["OutputManager"].interrupt_output()

    def get_curr_screen_ignored(self):
        return self.currScreenIgnored

    def get_prev_screen_ignored(self):
        return self.prevScreenIgnored

    def update_screen_ignored(self):
        self.prevScreenIgnored = self.currScreenIgnored
        self.currScreenIgnored = self.is_ignored_screen(
            self.env["screen"]["newTTY"]
        )

    def update(self, event_data, trigger="onUpdate"):
        # set new "old" values
        self.env["screen"]["oldContentBytes"] = self.env["screen"][
            "newContentBytes"
        ]
        self.env["screen"]["old_content_text"] = self.env["screen"][
            "new_content_text"
        ]
        self.env["screen"]["old_cursor"] = self.env["screen"][
            "new_cursor"
        ].copy()
        self.env["screen"]["oldDelta"] = self.env["screen"]["new_delta"]
        self.env["screen"]["oldNegativeDelta"] = self.env["screen"][
            "newNegativeDelta"
        ]
        self.env["screen"]["newContentBytes"] = event_data["bytes"]

        # get metadata like cursor or screensize
        self.env["screen"]["lines"] = int(event_data["lines"])
        self.env["screen"]["columns"] = int(event_data["columns"])
        self.colums = int(event_data["columns"])
        self.rows = int(event_data["lines"])
        self.env["screen"]["new_cursor"]["x"] = int(
            event_data["textCursor"]["x"]
        )
        self.env["screen"]["new_cursor"]["y"] = int(
            event_data["textCursor"]["y"]
        )
        self.env["screen"]["newTTY"] = event_data["screen"]
        self.env["screen"]["new_content_text"] = event_data["text"]


        # screen change
        if self.is_screen_change():
            self.env["screen"]["oldContentBytes"] = b""
            self.reset_screen_text(event_data["text"])
            self.env["runtime"]["AttributeManager"].reset_attributes(
                event_data["attributes"]
            )
            self.env["runtime"]["AttributeManager"].reset_attribute_cursor()
            self.env["screen"]["old_content_text"] = ""
            self.env["screen"]["old_cursor"]["x"] = 0
            self.env["screen"]["old_cursor"]["y"] = 0
            self.env["screen"]["oldDelta"] = ""
            self.env["screen"]["oldNegativeDelta"] = ""
        else:
            self.set_screen_text(event_data["text"])
            self.env["runtime"]["AttributeManager"].set_attributes(
                event_data["attributes"]
            )
        # initialize current deltas
        self.env["screen"]["newNegativeDelta"] = ""
        self.env["screen"]["new_delta"] = ""
        self.env["runtime"]["AttributeManager"].reset_attribute_delta()

        # Diff generation - critical for screen reader functionality
        # This code detects and categorizes screen content changes to provide appropriate
        # speech feedback (typing echo vs incoming text vs screen updates)

        # Track whether this appears to be typing (user input) vs other screen
        # changes
        typing = False
        diff_list = []

        if (
            self.env["screen"]["old_content_text"]
            != self.env["screen"]["new_content_text"]
        ):
            # Special case: Initial screen content (going from empty to populated)
            # This handles first screen load or TTY switch scenarios
            if (
                self.env["screen"]["new_content_text"] != ""
                and self.env["screen"]["old_content_text"] == ""
            ):
                # Pre-process screen text for comparison - collapse multiple spaces to single space
                # This normalization prevents spurious diffs from spacing
                # inconsistencies
                old_screen_text = self._space_normalize_regex.sub(
                    " ",
                    self.env["runtime"][
                        "ScreenManager"
                    ].get_window_area_in_text(
                        self.env["screen"]["old_content_text"]
                    ),
                )
                new_screen_text = self._space_normalize_regex.sub(
                    " ",
                    self.env["runtime"][
                        "ScreenManager"
                    ].get_window_area_in_text(
                        self.env["screen"]["new_content_text"]
                    ),
                )
                if old_screen_text == "" and new_screen_text != "":
                    self.env["screen"]["new_delta"] = new_screen_text
            else:
                # Calculate byte positions for the current cursor's line in the flat text buffer
                # Formula: (line_number * columns) + line_number accounts for newlines
                # Each line contributes 'columns' chars + 1 newline char
                cursor_line_start = (
                    self.env["screen"]["new_cursor"]["y"]
                    * self.env["screen"]["columns"]
                    + self.env["screen"]["new_cursor"]["y"]
                )
                cursor_line_end = (
                    cursor_line_start + self.env["screen"]["columns"]
                )

                # TYPING DETECTION ALGORITHM
                # Determines if this screen change is likely user typing vs other content changes
                # All conditions must be met for typing detection:
                if (
                    abs(
                        self.env["screen"]["old_cursor"]["x"]
                        - self.env["screen"]["new_cursor"]["x"]
                    )
                    >= 1
                    and self.env["screen"]["old_cursor"]["y"]
                    == self.env["screen"]["new_cursor"]["y"]
                    and self.env["screen"]["new_content_text"][
                        :cursor_line_start
                    ]
                    == self.env["screen"]["old_content_text"][
                        :cursor_line_start
                    ]
                    and self.env["screen"]["new_content_text"][
                        cursor_line_end:
                    ]
                    == self.env["screen"]["old_content_text"][cursor_line_end:]
                ):
                    # Condition 1: Cursor moved horizontally by at least 1 position (typical of typing)
                    # Condition 2: Cursor stayed on same line (typing doesn't usually change lines)
                    # Condition 3: Content BEFORE cursor line is unchanged (text above typing area)
                    # Condition 4: Content AFTER cursor line is unchanged (text below typing area)
                    # Together: only the current line changed, cursor moved
                    # horizontally = likely typing

                    # Optimize diff calculation for typing by focusing on a
                    # small window around cursor
                    cursor_line_start_offset = cursor_line_start
                    cursor_line_end_offset = cursor_line_end

                    # Limit analysis window to avoid processing entire long lines
                    # +3 provides safety buffer beyond cursor position to catch edge cases
                    if (
                        cursor_line_end
                        > cursor_line_start
                        + self.env["screen"]["new_cursor"]["x"]
                        + 3
                    ):
                        cursor_line_end_offset = (
                            cursor_line_start
                            + self.env["screen"]["new_cursor"]["x"]
                            + 3
                        )

                    # Extract just the relevant text sections for
                    # character-level diff
                    old_screen_text = self.env["screen"]["old_content_text"][
                        cursor_line_start_offset:cursor_line_end_offset
                    ]
                    new_screen_text = self.env["screen"]["new_content_text"][
                        cursor_line_start_offset:cursor_line_end_offset
                    ]

                    # Character-level diff for typing detection (not
                    # line-level)
                    diff = self.differ.compare(
                        old_screen_text, new_screen_text
                    )
                    diff_list = list(diff)
                    typing = True

                    # Validate typing assumption by checking if detected
                    # changes match cursor movement
                    temp_new_delta = "".join(
                        x[2:] for x in diff_list if x[0] == "+"
                    )
                    if temp_new_delta.strip() != "":
                        # Compare diff result against expected typing pattern
                        # Expected: characters between old and new cursor
                        # positions
                        expected_typing = "".join(
                            new_screen_text[
                                self.env["screen"]["old_cursor"][
                                    "x"
                                ] : self.env["screen"]["new_cursor"]["x"]
                            ].rstrip()
                        )

                        # If diff doesn't match expected typing pattern, treat
                        # as general screen change
                        if temp_new_delta != expected_typing:
                            # Fallback: treat entire current line as new
                            # content
                            diff_list = [
                                "+ "
                                + self.env["screen"]["new_content_text"].split(
                                    "\n"
                                )[self.env["screen"]["new_cursor"]["y"]]
                                + "\n"
                            ]
                            typing = False
                else:
                    # GENERAL SCREEN CHANGE DETECTION
                    # Not typing - handle as line-by-line content change
                    # This catches: incoming messages, screen updates,
                    # application output, etc.

                    # Pre-process screen text for comparison - collapse multiple spaces to single space
                    # This normalization prevents spurious diffs from spacing
                    # inconsistencies
                    old_screen_text = self._space_normalize_regex.sub(
                        " ",
                        self.env["runtime"][
                            "ScreenManager"
                        ].get_window_area_in_text(
                            self.env["screen"]["old_content_text"]
                        ),
                    )
                    new_screen_text = self._space_normalize_regex.sub(
                        " ",
                        self.env["runtime"][
                            "ScreenManager"
                        ].get_window_area_in_text(
                            self.env["screen"]["new_content_text"]
                        ),
                    )

                    diff = self.differ.compare(
                        old_screen_text.split("\n"),
                        new_screen_text.split("\n"),
                    )
                    diff_list = list(diff)

                # Extract added and removed content from diff results
                # Output format depends on whether this was detected as typing
                # or general change
                if not typing:
                    # Line-based changes: join with newlines for proper speech
                    # cadence
                    self.env["screen"]["new_delta"] = "\n".join(
                        x[2:] for x in diff_list if x[0] == "+"
                    )
                else:
                    # Character-based changes: no newlines for smooth typing
                    # echo
                    self.env["screen"]["new_delta"] = "".join(
                        x[2:] for x in diff_list if x[0] == "+"
                    )

                # Negative delta (removed content) - used for backspace/delete
                # detection
                self.env["screen"]["newNegativeDelta"] = "".join(
                    x[2:] for x in diff_list if x[0] == "-"
                )

        # track highlighted
        try:
            if self.env["runtime"]["AttributeManager"].is_attribute_change():
                if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
                    "focus", "highlight"
                ):
                    attribute_delta, attributeCursor = self.env["runtime"][
                        "AttributeManager"
                    ].track_highlights()
                    if attributeCursor:
                        self.env["runtime"][
                            "AttributeManager"
                        ].set_attribute_cursor(attributeCursor)
                        self.env["runtime"][
                            "AttributeManager"
                        ].set_attribute_delta(attribute_delta)
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "ScreenManager:update:highlight: " + str(e),
                debug.DebugLevel.ERROR,
            )

    def is_ignored_screen(self, screen=None):
        if screen is None:
            screen = self.env["screen"]["newTTY"]
        # Check if force all screens flag is set
        if self.env["runtime"].get("force_all_screens", False):
            return False
        ignore_screens = []
        fix_ignore_screens = self.env["runtime"][
            "SettingsManager"
        ].get_setting("screen", "ignoreScreen")
        if fix_ignore_screens != "":
            ignore_screens.extend(fix_ignore_screens.split(","))
        if self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "screen", "autodetectIgnoreScreen"
        ):
            ignore_screens.extend(self.env["screen"]["autoIgnoreScreens"])
        self.env["runtime"]["DebugManager"].write_debug_out(
            "ScreenManager:is_ignored_screen ignore:"
            + str(ignore_screens)
            + " current:"
            + str(screen),
            debug.DebugLevel.INFO,
        )
        return screen in ignore_screens

    def is_screen_change(self):
        if not self.env["screen"]["oldTTY"]:
            return False
        return self.env["screen"]["newTTY"] != self.env["screen"]["oldTTY"]

    def is_delta(self, ignoreSpace=False):
        new_delta = self.env["screen"]["new_delta"]
        if ignoreSpace:
            new_delta = new_delta.strip()
        return new_delta != ""

    def is_negative_delta(self):
        return self.env["screen"]["newNegativeDelta"] != ""

    def get_window_area_in_text(self, text):
        if not self.env["runtime"][
            "CursorManager"
        ].is_application_window_set():
            return text
        window_text = ""
        window_list = text.split("\n")
        curr_app = self.env["runtime"][
            "ApplicationManager"
        ].get_current_application()
        window_list = window_list[
            self.env["commandBuffer"]["windowArea"][curr_app]["1"][
                "y"
            ] : self.env["commandBuffer"]["windowArea"][curr_app]["2"]["y"]
            + 1
        ]
        for line in window_list:
            window_text += (
                line[
                    self.env["commandBuffer"]["windowArea"][curr_app]["1"][
                        "x"
                    ] : self.env["commandBuffer"]["windowArea"][curr_app]["2"][
                        "x"
                    ]
                    + 1
                ]
                + "\n"
            )
        return window_text

    def inject_text_to_screen(self, text, screen=None):
        try:
            self.env["runtime"]["ScreenDriver"].inject_text_to_screen(
                text, screen
            )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "ScreenManager:inject_text_to_screen " + str(e),
                debug.DebugLevel.ERROR,
            )
