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

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

import difflib
import time


class TabCompletionManager:
    def __init__(self, timeout=0.5):
        self.timeout = timeout

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

    def _ensure_state(self):
        if "tabCompletion" not in self.env["commandBuffer"]:
            self.env["commandBuffer"]["tabCompletion"] = {}
        state = self.env["commandBuffer"]["tabCompletion"]
        state.setdefault("pending", None)
        state.setdefault("lastProcessedDelta", "")
        state.setdefault("lastProcessedTime", 0)

    def capture_if_tab(self):
        if not self._last_event_is_tab_press():
            return

        self._ensure_state()
        self.env["commandBuffer"]["tabCompletion"]["pending"] = {
            "timestamp": time.time(),
            "cursor": self.env["screen"]["new_cursor"].copy(),
            "content": self.env["screen"]["new_content_text"],
            "screen": self.env["screen"]["newTTY"],
        }

    def process_update(self):
        self._ensure_state()
        state = self.env["commandBuffer"]["tabCompletion"]
        if self._was_recently_processed(state):
            return ""

        pending = state.get("pending")
        if not pending:
            pending = self._build_pending_from_recent_tab_update()
            if not pending:
                return ""

        if time.time() - pending["timestamp"] > self.timeout:
            state["pending"] = None
            return ""

        if pending.get("screen") != self.env["screen"]["newTTY"]:
            state["pending"] = None
            return ""

        spoken_text = self._get_completion_text(pending)
        if not spoken_text:
            return ""

        state["pending"] = None
        state["lastProcessedDelta"] = self.env["screen"]["new_delta"]
        state["lastProcessedTime"] = time.time()
        return spoken_text

    def _was_recently_processed(self, state):
        if state.get("lastProcessedDelta") != self.env["screen"]["new_delta"]:
            return False
        last_time = state.get("lastProcessedTime")
        if not last_time:
            return False
        return time.time() - last_time <= self.timeout

    def _build_pending_from_recent_tab_update(self):
        if not self._recent_tab_input():
            return None
        return {
            "timestamp": time.time(),
            "cursor": self.env["screen"]["old_cursor"].copy(),
            "content": self.env["screen"]["old_content_text"],
            "screen": self.env["screen"]["newTTY"],
        }

    def _recent_tab_input(self):
        input_manager = self.env["runtime"]["InputManager"]
        if input_manager.get_last_deepest_input() in [["KEY_TAB"]]:
            return True

        last_event = input_manager.get_last_event()
        if not last_event or last_event.get("event_name") != "KEY_TAB":
            return False

        try:
            return (
                time.time() - input_manager.get_last_input_time()
                <= self.timeout
            )
        except Exception:
            return False

    def _last_event_is_tab_press(self):
        input_manager = self.env["runtime"]["InputManager"]
        last_event = input_manager.get_last_event()
        if not last_event:
            return False
        return (
            last_event.get("event_name") == "KEY_TAB"
            and last_event.get("event_state") == 1
        )

    def _get_completion_text(self, pending):
        old_lines = pending["content"].split("\n")
        new_lines = self.env["screen"]["new_content_text"].split("\n")
        cursor = pending["cursor"]
        cursor_y = cursor["y"]

        if cursor_y < len(old_lines) and cursor_y < len(new_lines):
            inserted_text = self._get_cursor_line_inserted_text(
                old_lines[cursor_y],
                new_lines[cursor_y],
                cursor["x"],
                self.env["screen"]["new_cursor"]["x"],
            )
            if inserted_text:
                return self._clean_text(inserted_text)

        candidate_text = self._get_candidate_text(
            old_lines, new_lines, cursor_y
        )
        if candidate_text:
            return self._clean_text(candidate_text)

        delta_text = self.env["screen"]["new_delta"]
        if (
            delta_text
            and not self.env["screen"].get("new_delta_is_typing", False)
        ):
            return self._clean_text(delta_text)

        return ""

    def _get_cursor_line_inserted_text(
        self, old_line, new_line, old_cursor_x, new_cursor_x
    ):
        if new_cursor_x <= old_cursor_x:
            return ""
        if old_line == new_line:
            return ""

        matcher = difflib.SequenceMatcher(
            None, old_line, new_line, autojunk=False
        )
        inserted_parts = []
        for (
            tag,
            old_start,
            old_end,
            new_start,
            new_end,
        ) in matcher.get_opcodes():
            if tag == "equal":
                continue
            if tag == "insert" and old_cursor_x <= new_start <= new_cursor_x:
                inserted_parts.append(new_line[new_start:new_end])
                continue
            if (
                tag == "replace"
                and old_line[old_start:old_end].strip() == ""
                and old_cursor_x <= new_start <= new_cursor_x
            ):
                inserted_parts.append(new_line[new_start:new_end])
                continue
            if (
                tag == "delete"
                and old_line[old_start:old_end].strip() == ""
                and old_start >= old_cursor_x
            ):
                continue
            if old_start >= old_cursor_x:
                return ""

        return "".join(inserted_parts)

    def _get_candidate_text(self, old_lines, new_lines, cursor_y):
        if len(old_lines) != len(new_lines):
            return self._get_inserted_lines(old_lines, new_lines, cursor_y)

        changed_lines = []
        old_cursor_line = (
            old_lines[cursor_y].strip() if cursor_y < len(old_lines) else ""
        )
        for index, old_line in enumerate(old_lines):
            if index == cursor_y:
                continue
            if index < len(new_lines) and old_line != new_lines[index]:
                if new_lines[index].strip() == old_cursor_line:
                    continue
                changed_lines.append(new_lines[index])

        return "\n".join(
            line.rstrip() for line in changed_lines if line.strip()
        )

    def _get_inserted_lines(self, old_lines, new_lines, cursor_y):
        matcher = difflib.SequenceMatcher(
            None, old_lines, new_lines, autojunk=False
        )
        inserted_lines = []
        for (
            tag,
            old_start,
            old_end,
            new_start,
            new_end,
        ) in matcher.get_opcodes():
            if tag not in ["insert", "replace"]:
                continue
            if new_end <= cursor_y:
                continue
            for line in new_lines[new_start:new_end]:
                if line.strip():
                    inserted_lines.append(line.rstrip())
        return "\n".join(inserted_lines)

    def _clean_text(self, text):
        if not text:
            return ""
        if len(text.strip()) != len(text) and text.strip() != "":
            return text.strip()
        return text
