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

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

from collections import Counter

from fenrirscreenreader.core import debug
from fenrirscreenreader.core.i18n import _


class AttributeManager:
    def __init__(self):
        self.currAttributes = None
        self.prevAttributes = None
        self.currAttributeDelta = ""
        self.currAttributeCursor = None
        self.prefAttributeCursor = None
        self.init_default_attributes()
        self.prevLastCursorAttribute = None
        self.currLastCursorAttribute = None

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

    def shutdown(self):
        pass

    def set_last_cursor_attribute(self, lastCursorAttribute):
        self.prevLastCursorAttribute = self.currLastCursorAttribute
        self.currLastCursorAttribute = lastCursorAttribute

    def reset_last_cursor_attribute(self):
        self.prevLastCursorAttribute = None
        self.currLastCursorAttribute = None

    def is_last_cursor_attribute_change(self):
        if self.prevLastCursorAttribute is None:
            return False
        return self.prevLastCursorAttribute != self.currLastCursorAttribute

    def get_curr_attribute_cursor(self):
        return self.currAttributeCursor

    def is_attribute_cursor_active(self):
        return self.currAttributeCursor is not None

    def is_attribute_change(self):
        if not self.prevAttributes:
            return False
        return self.currAttributes != self.prevAttributes

    def reset_attribute_all(self):
        self.reset_attribute_delta()
        self.reset_attribute_cursor()

    def get_attribute_delta(self):
        return self.currAttributeDelta

    def reset_attribute_delta(self):
        self.currAttributeDelta = ""

    def set_attribute_delta(self, currAttributeDelta):
        self.currAttributeDelta = currAttributeDelta

    def reset_attribute_cursor(self):
        self.currAttributeCursor = None
        self.prefAttributeCursor = None

    def set_attribute_cursor(self, currAttributeCursor):
        self.prefAttributeCursor = self.currAttributeCursor
        self.currAttributeCursor = currAttributeCursor.copy()

    def reset_attributes(self, currAttributes):
        self.prevAttributes = None
        self.currAttributes = currAttributes

    def set_attributes(self, currAttributes):
        self.prevAttributes = self.currAttributes
        self.currAttributes = currAttributes.copy()

    def get_attribute_by_xy(self, x, y):
        if not self.currAttributes:
            return None
        if len(self.currAttributes) < y:
            return None
        if len(self.currAttributes[y]) < x - 1:
            return None
        try:
            return self.currAttributes[y][x]
        except KeyError:
            try:
                return self.defaultAttributes[0]
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "AttributeManager get_attribute_by_xy: Error accessing default attributes: "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
        return None

    def append_default_attributes(self, attribute):
        if not attribute:
            return
        if len(attribute) != 10:
            return
        self.defaultAttributes.append(attribute)

    def init_default_attributes(self):
        self.defaultAttributes = [None]
        self.defaultAttributes.append(
            [
                "default",  # fg
                "default",  # bg
                False,  # bold
                False,  # italics
                False,  # underscore
                False,  # strikethrough
                False,  # reverse
                False,  # blink
                "default",  # fontsize
                "default",  # fontfamily
            ]
        )  # end attribute

    def is_default_attribute(self, attribute):
        return attribute in self.defaultAttributes

    def has_attributes(self, cursor, update=True):
        if not cursor:
            return False
        cursor_pos = cursor.copy()
        try:
            attribute = self.get_attribute_by_xy(
                cursor_pos["x"], cursor_pos["y"]
            )

            if update:
                self.set_last_cursor_attribute(attribute)
            if not self.is_last_cursor_attribute_change():
                return False

            if self.is_default_attribute(attribute):
                return False

        except Exception as e:
            return False
        return True

    def format_attributes(self, attribute, attribute_format_string=""):
        # "black",
        # "red",
        # "green",
        # "brown",
        # "blue",
        # "magenta",
        # "cyan",
        # "white",
        # "default" # white.
        # _order_
        # "fg",
        # "bg",
        # "bold",
        # "italics",
        # "underscore",
        # "strikethrough",
        # "reverse",
        # "blink"
        # "fontsieze"
        # "fontfamily"
        if attribute_format_string == "":
            attribute_format_string = self.env["runtime"][
                "SettingsManager"
            ].get_setting("general", "attribute_format_string")
        if not attribute_format_string:
            return ""
        if attribute_format_string == "":
            return ""
        if not attribute:
            return ""
        if len(attribute) != 10:
            return ""

        # 0 FG color (name)
        try:
            attribute_format_string = attribute_format_string.replace(
                "fenrirFGColor", _(attribute[0])
            )
        except Exception as e:
            attribute_format_string = attribute_format_string.replace(
                "fenrirFGColor", ""
            )

        # 1 BG color (name)
        try:
            attribute_format_string = attribute_format_string.replace(
                "fenrirBGColor", _(attribute[1])
            )
        except Exception as e:
            attribute_format_string = attribute_format_string.replace(
                "fenrirBGColor", ""
            )

        # 2 bold (True/ False)
        try:
            if attribute[2]:
                attribute_format_string = attribute_format_string.replace(
                    "fenrirBold", _("bold")
                )
        except Exception as e:
            pass
        attribute_format_string = attribute_format_string.replace(
            "fenrirBold", ""
        )

        # 3 italics (True/ False)
        try:
            if attribute[3]:
                attribute_format_string = attribute_format_string.replace(
                    "fenrirItalics", _("italic")
                )
        except Exception as e:
            pass
        attribute_format_string = attribute_format_string.replace(
            "fenrirItalics", ""
        )

        # 4 underline (True/ False)
        try:
            if attribute[4]:
                attribute_format_string = attribute_format_string.replace(
                    "fenrirUnderline", _("underline")
                )
        except Exception as e:
            pass
        attribute_format_string = attribute_format_string.replace(
            "fenrirUnderline", ""
        )

        # 5 strikethrough (True/ False)
        try:
            if attribute[5]:
                attribute_format_string = attribute_format_string.replace(
                    "fenrirStrikethrough", _("strikethrough")
                )
        except Exception as e:
            pass
        attribute_format_string = attribute_format_string.replace(
            "fenrirStrikethrough", ""
        )

        # 6 reverse (True/ False)
        try:
            if attribute[6]:
                attribute_format_string = attribute_format_string.replace(
                    "fenrirReverse", _("reverse")
                )
        except Exception as e:
            pass
        attribute_format_string = attribute_format_string.replace(
            "fenrirReverse", ""
        )

        # 7 blink (True/ False)
        try:
            if attribute[7]:
                attribute_format_string = attribute_format_string.replace(
                    "fenrirBlink", _("blink")
                )
        except Exception as e:
            pass
        attribute_format_string = attribute_format_string.replace(
            "fenrirBlink", ""
        )

        # 8 font size (int/ string)
        try:
            try:
                attribute_format_string = attribute_format_string.replace(
                    "fenrirFontSize", int(attribute[8])
                )
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "AttributeManager formatAttributeToString: Error formatting font size as int: "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
            try:
                attribute_format_string = attribute_format_string.replace(
                    "fenrirFontSize", str(attribute[8])
                )
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "AttributeManager formatAttributeToString: Error formatting font size as string: "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
        except Exception as e:
            pass
        attribute_format_string = attribute_format_string.replace(
            "fenrirFontSize", _("default")
        )

        # 9 font family (string)
        try:
            attribute_format_string = attribute_format_string.replace(
                "fenrirFont", attribute[9]
            )
        except Exception as e:
            pass
        attribute_format_string = attribute_format_string.replace(
            "fenrirFont", _("default")
        )

        return attribute_format_string

    def track_highlights(self):
        """
        Detects text with changed attributes (highlighting) between screen updates.

        This is crucial for screen readers to announce when text becomes highlighted,
        selected, or changes visual emphasis (bold, reverse video, color changes, etc.)

        Enhanced version includes bracket pattern detection for better context in applications
        like ninjam that use patterns like [X]mute or [ ]mute.

        Returns:
            tuple: (highlighted_text, cursor_position)
                - highlighted_text: string of characters that gained highlighting
                - cursor_position: dict {'x': col, 'y': row} of first highlighted char
        """
        result = ""
        curr_cursor = None

        # Early exit conditions - no attribute comparison possible
        if self.prevAttributes is None:
            # First screen load - no previous attributes to compare against
            return result, curr_cursor
        if self.prevAttributes == self.currAttributes:
            # No attribute changes detected
            return result, curr_cursor
        if self.currAttributes is None:
            # Error condition - current attributes missing
            return result, curr_cursor
        if len(self.currAttributes) == 0:
            # Special case for PTY environments with no text content
            return result, curr_cursor

        # Get current screen text to correlate with attribute changes
        text = self.env["runtime"]["ScreenManager"].get_screen_text()
        text_lines = text.split("\n")

        # Sanity check: text lines must match attribute array dimensions
        if len(text_lines) != len(self.currAttributes):
            return result, curr_cursor

        # Track highlighted positions for context analysis
        highlighted_positions = []
        
        # Compare attributes line by line, character by character
        for line in range(len(self.prevAttributes)):
            if self.prevAttributes[line] != self.currAttributes[line]:
                # This line has attribute changes - examine each character
                # position
                for column in range(len(self.prevAttributes[line])):
                    if (
                        self.prevAttributes[line][column]
                        == self.currAttributes[line][column]
                    ):
                        # No change at this position
                        continue
                    # Attribute changed at this position - check if it's worth
                    # announcing
                    if self.is_useful_for_tracking(
                        line, column, self.currAttributes, self.prevAttributes
                    ):
                        # First highlighted character becomes cursor position
                        # for navigation
                        if not curr_cursor:
                            curr_cursor = {"x": column, "y": line}
                        # Store position for context analysis
                        highlighted_positions.append((line, column))
                        # Accumulate highlighted characters
                        result += text_lines[line][column]
                # Add space between lines of highlighted text for speech
                # clarity
                result += " "
        
        # Enhanced bracket pattern detection for better context
        if highlighted_positions:
            try:
                enhanced_result = self._detect_bracket_context(text_lines, highlighted_positions, result)
                if enhanced_result and enhanced_result != result:
                    # Debug logging for bracket detection
                    try:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            f"AttributeManager bracket detection: Original='{result}' Enhanced='{enhanced_result}'",
                            debug.DebugLevel.INFO
                        )
                    except Exception:
                        pass  # Don't let debug logging break functionality
                    result = enhanced_result
            except Exception as e:
                # If bracket detection fails, fall back to original result
                try:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        f"AttributeManager bracket detection error: {e}",
                        debug.DebugLevel.ERROR
                    )
                except Exception:
                    pass  # Don't let debug logging break functionality
        
        return result, curr_cursor

    def _detect_bracket_context(self, text_lines, highlighted_positions, original_result):
        """
        Analyzes highlighted positions to detect bracket patterns and provide better context.
        
        This method specifically looks for patterns like [X]mute, [ ]mute, [X]xmit, etc.
        that are common in applications like ninjam where the bracket content indicates
        state but the meaningful context is the surrounding text.
        
        Args:
            text_lines: List of text lines from the screen
            highlighted_positions: List of (line, column) tuples of highlighted characters
            original_result: The original highlighted text result
            
        Returns:
            str: Enhanced result with context, or None if no bracket pattern detected
        """
        if not highlighted_positions:
            return None
            
        # Group positions by line for easier analysis
        positions_by_line = {}
        for line, column in highlighted_positions:
            if line not in positions_by_line:
                positions_by_line[line] = []
            positions_by_line[line].append(column)
        
        enhanced_results = []
        
        for line_num, columns in positions_by_line.items():
            if line_num >= len(text_lines):
                continue
                
            line_text = text_lines[line_num]
            columns.sort()  # Process columns in order
            
            # Look for bracket patterns in this line
            bracket_context = self._analyze_bracket_pattern(line_text, columns)
            if bracket_context:
                enhanced_results.append(bracket_context)
        
        # If we found enhanced context, return it; otherwise return None to use original
        if enhanced_results:
            # Remove duplicates while preserving order
            unique_results = []
            seen = set()
            for result in enhanced_results:
                if result not in seen:
                    unique_results.append(result)
                    seen.add(result)
            return " ".join(unique_results)
        return None
    
    def _analyze_bracket_pattern(self, line_text, highlighted_columns):
        """
        Analyzes a single line for bracket patterns around highlighted positions.
        
        Looks for patterns like:
        - master: [X]mute -> "master mute on"
        - metronome: [ ]mute -> "metronome mute off"  
        - [Left] [X]xmit -> "Left xmit on"
        
        Args:
            line_text: The text of the line
            highlighted_columns: List of column positions that are highlighted
            
        Returns:
            str: Context-aware description, or None if no pattern found
        """
        if not line_text or not highlighted_columns:
            return None
            
        # Look for bracket patterns around highlighted positions
        # Process columns in order and only return the first meaningful match
        for col in highlighted_columns:
            bracket_info = self._find_bracket_at_position(line_text, col)
            if bracket_info:
                bracket_start, bracket_end, bracket_content = bracket_info
                
                # Get context before and after the bracket
                context_before = self._get_context_before(line_text, bracket_start)
                context_after = self._get_context_after(line_text, bracket_end)
                
                # Build meaningful description
                description = self._build_bracket_description(
                    context_before, bracket_content, context_after
                )
                
                # Only return if we have meaningful context (not just state)
                if description and (context_before or context_after):
                    return description
        
        return None
    
    def _find_bracket_at_position(self, line_text, position):
        """
        Determines if a position is within a bracket pattern like [X] or [ ].
        
        Args:
            line_text: The text line
            position: Column position to check
            
        Returns:
            tuple: (bracket_start, bracket_end, bracket_content) or None
        """
        if position >= len(line_text):
            return None
            
        # Look for opening bracket before or at position
        bracket_start = None
        for i in range(max(0, position - 2), min(len(line_text), position + 3)):
            if line_text[i] == '[':
                bracket_start = i
                break
        
        if bracket_start is None:
            return None
            
        # Look for closing bracket after bracket_start
        bracket_end = None
        for i in range(bracket_start + 1, min(len(line_text), bracket_start + 5)):
            if line_text[i] == ']':
                bracket_end = i
                break
        
        if bracket_end is None:
            return None
            
        # Check if our position is within the bracket
        if bracket_start <= position <= bracket_end:
            bracket_content = line_text[bracket_start + 1:bracket_end]
            
            # Filter out brackets that are likely not controls
            # Skip brackets that contain complex content like "[-10.5dB center]"
            if len(bracket_content) > 3 and ('dB' in bracket_content or 'Hz' in bracket_content):
                return None
                
            return bracket_start, bracket_end, bracket_content
        
        return None
    
    def _get_context_before(self, line_text, bracket_start):
        """
        Gets meaningful context words before a bracket.
        
        Args:
            line_text: The text line
            bracket_start: Position of opening bracket
            
        Returns:
            str: Context words before bracket, or empty string
        """
        if bracket_start <= 0:
            return ""
            
        # Get text before bracket
        before_text = line_text[:bracket_start].rstrip()
        
        # Special handling for common ninjam patterns
        # Look for patterns like "master: " or "metronome: " or bracketed labels
        
        # Check if we have a colon-separated label immediately before
        if ':' in before_text:
            # Get the last colon-separated part
            parts = before_text.split('|')  # Split by pipe first
            last_part = parts[-1].strip()
            
            if ':' in last_part:
                # Extract the label before the colon
                label_parts = last_part.split(':')
                if len(label_parts) >= 2:
                    return label_parts[-2].strip()
        
        # Look for bracketed content that might be a label
        # Pattern: [Something] [X]target -> "Something"
        bracket_matches = []
        i = 0
        while i < len(before_text):
            if before_text[i] == '[':
                start = i
                i += 1
                while i < len(before_text) and before_text[i] != ']':
                    i += 1
                if i < len(before_text):  # Found closing bracket
                    content = before_text[start+1:i]
                    if content.strip():
                        bracket_matches.append(content.strip())
            i += 1
        
        # If we found bracketed content, use the last one
        if bracket_matches:
            return bracket_matches[-1]
        
        # Fall back to removing bracketed content and getting words
        cleaned_text = ""
        bracket_level = 0
        for char in before_text:
            if char == '[':
                bracket_level += 1
            elif char == ']':
                bracket_level -= 1
            elif bracket_level == 0:
                cleaned_text += char
        
        # Clean up separators and get meaningful words
        words = []
        for word in cleaned_text.replace(':', '').replace('|', '').strip().split():
            if word and not word.startswith('[') and not word.endswith(']'):
                words.append(word)
        
        # Return last word as context
        if words:
            return words[-1]
        
        return ""
    
    def _get_context_after(self, line_text, bracket_end):
        """
        Gets meaningful context words after a bracket.
        
        Args:
            line_text: The text line  
            bracket_end: Position of closing bracket
            
        Returns:
            str: Context words after bracket, or empty string
        """
        if bracket_end >= len(line_text) - 1:
            return ""
            
        # Get text after bracket and find first meaningful word
        after_text = line_text[bracket_end + 1:].lstrip()
        
        # Get first word, removing common separators
        words = after_text.replace(':', '').replace('|', '').strip().split()
        
        if words:
            return words[0]
        
        return ""
    
    def _build_bracket_description(self, context_before, bracket_content, context_after):
        """
        Builds a human-readable description from bracket context.
        
        Args:
            context_before: Words before the bracket
            bracket_content: Content inside brackets (X, space, etc.)
            context_after: Words after the bracket
            
        Returns:
            str: Human-readable description
        """
        # Clean up bracket content
        bracket_content = bracket_content.strip()
        
        # Determine state based on bracket content
        if bracket_content == 'X':
            state = "on"
        elif bracket_content == '' or bracket_content == ' ':
            state = "off"
        else:
            state = bracket_content  # For other patterns
        
        # Build description prioritizing context_before, then context_after
        # Add spaces between components for better speech clarity
        components = []
        
        if context_before:
            components.append(context_before)
        if context_after:
            components.append(context_after)
        components.append(state)
        
        # Join with spaces for better speech flow
        return " ".join(components)

    def is_useful_for_tracking(
        self,
        line,
        column,
        currAttributes,
        prevAttributes,
        attribute=1,
        mode="zaxe",
    ):
        """
        Determines if an attribute change at a specific position is worth announcing.

        This prevents announcing every minor attribute change and focuses on meaningful
        highlighting that indicates user selections, focus changes, or important emphasis.

        Args:
            line, column: Position of the attribute change
            currAttributes, prevAttributes: Current and previous attribute arrays
            attribute: Which attribute to examine (1=background color by default)
            mode: Detection algorithm ('zaxe', 'default', 'barrier')

        Returns:
            bool: True if this attribute change should be announced to user
        """
        # Sanity checks for valid position and sufficient screen content
        if len(currAttributes) <= 3:
            return False
        if line < 0:
            return False
        if line > len(currAttributes):
            return False

        useful = False

        if mode == "default":
            # Simple mode: announce any non-default attribute
            useful = not self.is_default_attribute(
                currAttributes[line][column]
            )

        elif (mode == "zaxe") or (mode == ""):
            # Context-aware mode: only announce attributes that stand out from surroundings
            # This prevents announcing entire blocks of highlighted text character by character
            # by checking if the attribute differs from adjacent lines

            if line == 0:
                # Top line: compare against lines below
                useful = (
                    currAttributes[line][column][attribute]
                    != currAttributes[line + 1][column][attribute]
                ) and (
                    currAttributes[line][column][attribute]
                    != currAttributes[line + 2][column][attribute]
                )
            elif line >= len(prevAttributes):
                # Bottom line: compare against lines above
                useful = (
                    currAttributes[line][column][attribute]
                    != currAttributes[line - 1][column][attribute]
                ) and (
                    currAttributes[line][column][attribute]
                    != currAttributes[line - 2][column][attribute]
                )
            else:
                # Middle lines: compare against both directions
                useful = (
                    currAttributes[line][column][attribute]
                    != currAttributes[line + 1][column][attribute]
                ) and (
                    currAttributes[line][column][attribute]
                    != currAttributes[line - 1][column][attribute]
                )

        elif mode == "barrier":
            # Barrier mode: future enhancement for detecting screen
            # boundaries/separators
            useful = True

        return useful
