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

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

import difflib
import re
import time
from fenrirscreenreader.core.i18n import _


class command:
    def __init__(self):
        self._update_times = []
        self._line_count_times = []
        self._batched_text = []
        self._last_flush_time = 0
        self._in_flood_mode = False
        self._pending_focus_text = ""
        self._pending_focus_time = 0

    def initialize(self, environment):
        self.env = environment

    def shutdown(self):
        pass

    def get_description(self):
        return _("Announces incoming text changes")

    def _reset_flood_state(self):
        self._update_times = []
        self._line_count_times = []
        self._batched_text = []
        self._last_flush_time = 0
        self._in_flood_mode = False

    def _is_rapid_updates(self):
        current_time = time.time()
        window = self.env["runtime"]["SettingsManager"].get_setting_as_float(
            "speech", "rapid_update_window"
        )
        threshold = self.env["runtime"]["SettingsManager"].get_setting_as_int(
            "speech", "rapid_update_threshold"
        )

        self._update_times = [
            ts for ts in self._update_times if current_time - ts < window
        ]
        self._update_times.append(current_time)

        return len(self._update_times) >= threshold

    def _is_high_volume(self, delta_text):
        current_time = time.time()
        window = self.env["runtime"]["SettingsManager"].get_setting_as_float(
            "speech", "rapid_update_window"
        )
        threshold = self.env["runtime"]["SettingsManager"].get_setting_as_int(
            "speech", "flood_line_threshold"
        )

        line_count = max(1, delta_text.count("\n") + 1)
        self._line_count_times = [
            (ts, count)
            for ts, count in self._line_count_times
            if current_time - ts < window
        ]
        self._line_count_times.append((current_time, line_count))

        total_lines = sum(count for _, count in self._line_count_times)
        return total_lines >= threshold

    def _add_to_batch(self, text):
        new_lines = text.splitlines()
        if text.endswith("\n"):
            new_lines.append("")
        self._batched_text.extend(new_lines)

        max_lines = self.env["runtime"]["SettingsManager"].get_setting_as_int(
            "speech", "max_batch_lines"
        )
        if len(self._batched_text) > max_lines:
            self._batched_text = self._batched_text[-max_lines:]

    def _flush_batch(self):
        if not self._batched_text:
            return

        text = "\n".join(self._batched_text)
        self._batched_text = []
        self._last_flush_time = time.time()

        self.env["runtime"]["OutputManager"].present_text(
            text, interrupt=False, flush=False
        )

    def _was_handled_by_tab_completion(self, delta_text):
        """Check if this delta was already handled by tab completion to avoid duplicates"""
        if "tabCompletion" not in self.env["commandBuffer"]:
            return False

        tab_state = self.env["commandBuffer"]["tabCompletion"]

        # Check if this exact delta was processed recently by tab completion
        if (tab_state.get("lastProcessedDelta") == delta_text and
            tab_state.get("lastProcessedTime")):

            # Only suppress if processed within the last 50ms to avoid stale suppression
            # Reduced from 100ms to minimize false positives with rapid multi-line updates
            time_since_processed = time.time() - tab_state["lastProcessedTime"]
            if time_since_processed <= 0.05:
                return True

        return False

    def _normalize_line(self, line):
        return " ".join(line.split())

    def _is_focus_control_line(self, text):
        stripped = self._normalize_line(text)
        if not stripped:
            return False
        if re.fullmatch(r"<\s*[A-Za-z][A-Za-z0-9 _.-]{0,30}\s*>", stripped):
            return True
        return stripped.lower() in {
            "ok",
            "cancel",
            "yes",
            "no",
            "retry",
            "abort",
            "ignore",
            "continue",
        }

    def _is_transient_focus_only_update(self, delta_text):
        delta_lines = [
            self._normalize_line(line)
            for line in delta_text.splitlines()
            if line.strip()
        ]
        if len(delta_lines) != 1:
            return False
        if not self._is_focus_control_line(delta_lines[0]):
            return False

        screen_lines = [
            self._normalize_line(line)
            for line in self.env["screen"]["new_content_text"].split("\n")
            if line.strip()
        ]
        return screen_lines == delta_lines

    def _store_pending_focus_text(self, delta_text):
        delta_lines = [
            self._normalize_line(line)
            for line in delta_text.splitlines()
            if line.strip()
        ]
        self._pending_focus_text = "\n".join(delta_lines)
        self._pending_focus_time = time.time()

    def _take_pending_focus_text(self):
        if not self._pending_focus_text:
            return ""
        if time.time() - self._pending_focus_time > 0.5:
            self._pending_focus_text = ""
            self._pending_focus_time = 0
            return ""
        pending = self._pending_focus_text
        self._pending_focus_text = ""
        self._pending_focus_time = 0
        return pending

    def _is_subsequence(self, subset_lines, source_lines):
        subset_index = 0
        for source_line in source_lines:
            if source_line == subset_lines[subset_index]:
                subset_index += 1
                if subset_index == len(subset_lines):
                    return True
        return False

    def _prefer_inserted_lower_screen_text(self, delta_text):
        delta_lines = [
            line for line in delta_text.splitlines() if line.strip() != ""
        ]
        if len(delta_lines) < 2:
            return delta_text

        old_lines = self.env["screen"]["old_content_text"].split("\n")
        new_lines = self.env["screen"]["new_content_text"].split("\n")
        if len(old_lines) != len(new_lines) or len(new_lines) < 4:
            return delta_text

        normalized_old_lines = [
            self._normalize_line(line) for line in old_lines
        ]
        normalized_new_lines = [
            self._normalize_line(line) for line in new_lines
        ]
        matcher = difflib.SequenceMatcher(
            None, normalized_old_lines, normalized_new_lines, autojunk=False
        )

        top_lines_changed = False
        inserted_lines = []
        lower_screen_start = max(2, len(new_lines) // 2)

        for tag, old_start, old_end, new_start, new_end in matcher.get_opcodes():
            if tag in {"replace", "delete"} and old_start < 2:
                top_lines_changed = True
            if tag != "insert" or new_start < lower_screen_start:
                continue
            inserted_lines.extend(
                line for line in new_lines[new_start:new_end] if line.strip() != ""
            )

        if not top_lines_changed or not inserted_lines:
            return delta_text
        if all(
            self._is_focus_control_line(line) for line in inserted_lines
        ):
            return delta_text

        normalized_delta_lines = [
            self._normalize_line(line) for line in delta_lines
        ]
        normalized_inserted_lines = [
            self._normalize_line(line) for line in inserted_lines
        ]
        if not self._is_subsequence(
            normalized_inserted_lines, normalized_delta_lines
        ):
            return delta_text
        if normalized_delta_lines == normalized_inserted_lines:
            return delta_text

        return "\n".join(inserted_lines)

    def run(self):
        if not self.env["runtime"]["SettingsManager"].get_setting_as_bool(
            "speech", "auto_read_incoming"
        ):
            return
        # is there something to read?
        if not self.env["runtime"]["ScreenManager"].is_delta(ignoreSpace=True):
            return
        if self.env["screen"].get("new_delta_is_typing", False):
            return

        delta_text = self.env["screen"]["new_delta"]

        if self.env["runtime"]["ScreenManager"].is_screen_change():
            self._reset_flood_state()
            self._pending_focus_text = ""
            self._pending_focus_time = 0
        
        # Skip if tab completion already handled this delta
        if self._was_handled_by_tab_completion(delta_text):
            return

        # this must be a keyecho or something
        # if len(self.env['screen']['new_delta'].strip(' \n\t')) <= 1:
        x_move = abs(
            self.env["screen"]["new_cursor"]["x"]
            - self.env["screen"]["old_cursor"]["x"]
        )
        y_move = abs(
            self.env["screen"]["new_cursor"]["y"]
            - self.env["screen"]["old_cursor"]["y"]
        )

        if (x_move >= 1) and x_move == len(delta_text):
            # if len(self.env['screen']['new_delta'].strip(' \n\t0123456789'))
            # <= 2:
            if "\n" not in delta_text:
                return

        if self._is_transient_focus_only_update(delta_text):
            self._store_pending_focus_text(delta_text)
            return

        rapid = self._is_rapid_updates()
        high_volume = self._is_high_volume(delta_text)

        if (rapid and high_volume) or self._in_flood_mode:
            if not self._in_flood_mode:
                self._last_flush_time = time.time()
                self._in_flood_mode = True

            self._add_to_batch(delta_text)

            interval = self.env["runtime"][
                "SettingsManager"
            ].get_setting_as_float("speech", "batch_flush_interval")
            if time.time() - self._last_flush_time >= interval:
                self._flush_batch()

            if not rapid or not high_volume:
                if self._batched_text:
                    self._flush_batch()
                self._in_flood_mode = False
            return

        # print(x_move, y_move, len(self.env['screen']['new_delta']), len(self.env['screen']['newNegativeDelta']))
        delta_text = self._prefer_inserted_lower_screen_text(delta_text)
        pending_focus_text = self._take_pending_focus_text()
        if pending_focus_text:
            delta_text = "\n".join([delta_text, pending_focus_text])
        self.env["runtime"]["OutputManager"].present_text(
            delta_text, interrupt=False, flush=False
        )

    def set_callback(self, callback):
        pass
