import curses
import sys
import time
import re
import argparse

# --- CONFIGURATION ---
RETICLE_OPACITY_PERCENT = 20  # 0-100% (lower is dimmer)
X_OFFSET = 0                  # Fine-tune horizontal centering
Y_OFFSET = 0                  # Fine-tune vertical centering

# Smart Pause Factors
PAUSE_PUNCTUATION_FACTOR = 1.5  # Multiplier for words ending in . ! ?
PAUSE_LONG_WORD_FACTOR = 1.1    # Multiplier for words > 10 chars

# Motion Blur Configuration
MB_OPACITY_PERCENT = 10        # 0-100% (Trail intensity)
# ---------------------

# Custom 5x7 Bitmap Font for Pixel-Precision Graphics (Sub-character resolution)
# ... (BITMAP_FONT remains as is) ...
# Each entry is 7 rows of 5 bits each.
BITMAP_FONT = {
    'A': [0x0E,0x11,0x11,0x1F,0x11,0x11,0x11], 'B': [0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E],
    'C': [0x0F,0x10,0x10,0x10,0x10,0x10,0x0F], 'D': [0x1E,0x11,0x11,0x11,0x11,0x11,0x1E],
    'E': [0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F], 'F': [0x1F,0x10,0x10,0x1E,0x10,0x10,0x10],
    'G': [0x0F,0x10,0x10,0x17,0x11,0x11,0x0F], 'H': [0x11,0x11,0x11,0x1F,0x11,0x11,0x11],
    'I': [0x0E,0x04,0x04,0x04,0x04,0x04,0x0E], 'J': [0x07,0x02,0x02,0x02,0x02,0x12,0x0C],
    'K': [0x11,0x12,0x14,0x18,0x14,0x12,0x11], 'L': [0x10,0x10,0x10,0x10,0x10,0x10,0x1F],
    'M': [0x11,0x1B,0x15,0x11,0x11,0x11,0x11], 'N': [0x11,0x11,0x19,0x15,0x13,0x11,0x11],
    'O': [0x0E,0x11,0x11,0x11,0x11,0x11,0x0E], 'P': [0x1E,0x11,0x11,0x1E,0x10,0x10,0x10],
    'Q': [0x0E,0x11,0x11,0x11,0x15,0x12,0x0D], 'R': [0x1E,0x11,0x11,0x1E,0x14,0x12,0x11],
    'S': [0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E], 'T': [0x1F,0x04,0x04,0x04,0x04,0x04,0x04],
    'U': [0x11,0x11,0x11,0x11,0x11,0x11,0x0E], 'V': [0x11,0x11,0x11,0x11,0x11,0x0A,0x04],
    'W': [0x11,0x11,0x11,0x15,0x15,0x1B,0x11], 'X': [0x11,0x11,0x0A,0x04,0x0A,0x11,0x11],
    'Y': [0x11,0x11,0x0A,0x04,0x04,0x04,0x04], 'Z': [0x1F,0x01,0x02,0x04,0x08,0x10,0x1F],
    '0': [0x0E,0x11,0x13,0x15,0x19,0x11,0x0E], '1': [0x04,0x0C,0x04,0x04,0x04,0x04,0x0E],
    '2': [0x0E,0x11,0x01,0x02,0x04,0x08,0x1F], '3': [0x1F,0x01,0x02,0x0E,0x01,0x11,0x0E],
    '4': [0x02,0x06,0x0A,0x12,0x1F,0x02,0x02], '5': [0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E],
    '6': [0x0E,0x11,0x10,0x1E,0x11,0x11,0x0E], '7': [0x1F,0x01,0x02,0x04,0x04,0x04,0x04],
    '8': [0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E], '9': [0x0E,0x11,0x11,0x0F,0x01,0x01,0x0E],
    ' ': [0x00,0x00,0x00,0x00,0x00,0x00,0x00], '.': [0x00,0x00,0x00,0x00,0x00,0x0C,0x0C],
    ',': [0x00,0x00,0x00,0x00,0x00,0x0C,0x04], '?': [0x0E,0x11,0x01,0x02,0x04,0x00,0x04],
    '!': [0x04,0x04,0x04,0x04,0x04,0x00,0x04], '-': [0x00,0x00,0x00,0x1F,0x00,0x00,0x00]
}
# Lowercase mapping to Uppercase for simplified RSVP readability in high-res
for c in 'abcdefghijklmnopqrstuvwxyz':
    BITMAP_FONT[c] = BITMAP_FONT[c.upper()]

def strip_markdown(text):
    """
    Removes common Markdown syntax for a cleaner reading experience.
    Strips code blocks, inline code, headers, bold/italic, links, images, and blockquotes.

    Args:
        text (str): The raw markdown string to be cleaned.

    Returns:
        str: The cleaned text without markdown artifacts.
    """
    # Remove code blocks
    text = re.sub(r'```.*?```', '', text, flags=re.DOTALL)
    # Remove inline code marks
    text = re.sub(r'`(.*?)`', r'\1', text)
    # Remove headers
    text = re.sub(r'#+\s?', '', text)
    # Remove bold/italic
    text = re.sub(r'\*\*(.*?)\*\*', r'\1', text)
    text = re.sub(r'\*(.*?)\*', r'\1', text)
    text = re.sub(r'__(.*?)__', r'\1', text)
    text = re.sub(r'_(.*?)_', r'\1', text)
    # Remove links
    text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text)
    # Remove images
    text = re.sub(r'!\[([^\]]+)\]\([^\)]+\)', r'\1', text)
    # Remove blockquotes
    text = re.sub(r'>\s?', '', text)
    return text

class RetiCLI:
    """
    Main application class for the RetiCLI RSVP speed reader.
    Handles file loading, state management, and the curses-based UI loop.
    """

    def __init__(self, filepath, accel=None, mb=False):
        """
        Initializes the RetiCLI application.

        Args:
            filepath (str): Path to the text file to read.
            accel (tuple, optional): (start_wpm, end_wpm, [peak_pct]) for progressive speed scaling.
            mb (bool): Enable Motion Blur (Phosphor Persistence) mode.
        """
        self.words = self.load_file(filepath)
        self.index = 0
        self.wpm = 300
        self.paused = True
        self.font_scale = 1 # 1=Normal, 2=Big
        self.accel = accel # (start, end, [peak]) or None
        self.mb = mb
        
        # State for Motion Blur
        self.prev_word = ""
        self.prev_orp_idx = 0
        
        # If accel is set, start at the beginning speed
        if self.accel:
            self.wpm = self.accel[0]

    def load_file(self, filepath):
        """
        Loads and tokenizes the text file. 
        Splits by whitespace to create a simple stream of words.
        """
        try:
            with open(filepath, 'r', encoding='utf-8') as f:
                text = f.read()
            text = strip_markdown(text)
            words = [w for w in text.split() if w]
            if not words:
                print("File is empty or contains no readable text.")
                sys.exit(1)
            return words
        except FileNotFoundError:
            print(f"Error: File '{filepath}' not found.")
            sys.exit(1)
        except Exception as e:
            print(f"Error reading file: {e}")
            sys.exit(1)

    def get_orp_index(self, word):
        """
        Determines the Optimal Recognition Point (ORP) index for a word.
        Uses a scientific OVP (Optimal Viewing Position) of ~35% into the word.
        
        "FOLLOWING" (9) -> 3 ('L') [User Verified]
        "INSTRUCTIONS" (12) -> 4 ('R')
        """
        length = len(word)
        if length <= 1: return 0
        if length <= 5: return int((length - 1) / 2) # Keep short words centered
        return int(round(length * 0.35))

    def get_char_center_px(self, char):
        """
        Calculates the true visual center (in pixels) using the 
        Geometric Center of the Visual Bounding Box.
        We use floor/int() rounding to prefer left-center for even-width glyphs,
        solving the off-center feel for letters like 'P' (width 4) or 'L' (width 5).
        """
        bitmap = BITMAP_FONT.get(char, BITMAP_FONT[' '])
        combined = 0
        for row in bitmap: combined |= row
        if combined == 0: return 2.0
        
        first = -1
        last = -1
        for col in range(5):
            if (combined >> (4 - col)) & 1:
                if first == -1: first = col
                last = col
        
        # Use simple average (Geometric Center)
        # int() acts as floor(), aligning X.5 to X (left-bias)
        # P (0-4) -> 2.0 -> 2 (Correct) [Uses full 5px width]
        # L (0-4) -> 2.0 -> 2 (Correct)
        return int((first + last) / 2.0)

    def draw_bitmap_word(self, stdscr, word, orp_idx, center_x, center_y, h, w, is_ghost=False):
        """
        Helper to draw a word in Big Mode (bitmap).
        """
        char_w = 6
        
        # Calculate start_x based on ORP pixel center
        orp_visual_mid = self.get_char_center_px(word[orp_idx])
        orp_px_center = int(round(orp_idx * char_w + orp_visual_mid))
        start_x = center_x - orp_px_center
        start_y = center_y - 2 

        # Ghost rendering uses shade blocks and dimmer color
        base_attr = curses.A_DIM if is_ghost else 0
        
        # Determine ghost glyphs based on opacity
        ghost_full = "▒" # Default
        ghost_half = "░"
        
        if is_ghost:
            if MB_OPACITY_PERCENT <= 25:
                ghost_full, ghost_half = "░", " "
            elif MB_OPACITY_PERCENT <= 50:
                ghost_full, ghost_half = "▒", "░"
            elif MB_OPACITY_PERCENT <= 75:
                ghost_full, ghost_half = "▓", "▒"
            else:
                ghost_full, ghost_half = "█", "▓"
        
        for row_pair in range(4):
            curr_y = start_y + row_pair
            if not (0 <= curr_y < h - 1): continue
            
            for i, char in enumerate(word):
                char_start_x = start_x + (i * char_w)
                bitmap = BITMAP_FONT.get(char, BITMAP_FONT[' '])
                bits1 = bitmap[row_pair * 2] if (row_pair * 2) < 7 else 0
                bits2 = bitmap[row_pair * 2 + 1] if (row_pair * 2 + 1) < 7 else 0
                
                for col in range(5):
                    px_x = char_start_x + col
                    if 0 <= px_x < w:
                        b1 = (bits1 >> (4 - col)) & 1
                        b2 = (bits2 >> (4 - col)) & 1
                        
                        glyph = " "
                        if is_ghost:
                            if b1 and b2: glyph = ghost_full
                            elif b1 or b2: glyph = ghost_half
                        else:
                            if b1 and b2: glyph = "█"
                            elif b1: glyph = "▀"
                            elif b2: glyph = "▄"
                        
                        if glyph != " ":
                            attr = base_attr
                            # Highlight ORP only for the main word
                            if not is_ghost and i == orp_idx:
                                attr |= curses.color_pair(1) | curses.A_BOLD
                            stdscr.addstr(curr_y, px_x, glyph, attr)

    def draw_screen(self, stdscr):
        """
        Renders the application state with sub-character graphics and 
        surgical alignment calibration.
        """
        stdscr.clear()
        h, w = stdscr.getmaxyx()
        
        if h < 8 or w < 20:
            stdscr.addstr(0, 0, "Terminal small!")
            stdscr.refresh()
            return

        # Core Coordinates with Calibration Offsets
        # (w-1)//2 is the visual center index (e.g., 39 for 80 units)
        center_x = ((w - 1) // 2) + X_OFFSET
        center_y = (h // 2) + Y_OFFSET
        
        word = self.words[self.index]
        orp_idx = self.get_orp_index(word)

        if self.font_scale == 1:
            # === NORMAL MODE ===
            start_x = center_x - orp_idx
            stdscr.addstr(center_y, start_x, word[:orp_idx])
            stdscr.addstr(center_y, start_x + orp_idx, word[orp_idx], 
                          curses.color_pair(1) | curses.A_BOLD)
            stdscr.addstr(center_y, start_x + orp_idx + 1, word[orp_idx+1:])
        else:
            # === PIXEL-PRECISION BIG MODE ===
            if self.mb and self.prev_word:
                 # Draw Ghost Logic
                 # We render the previous word first, "underneath"
                 self.draw_bitmap_word(stdscr, self.prev_word, self.prev_orp_idx, 
                                       center_x, center_y, h, w, is_ghost=True)

            # Draw Current Word (Main Layer)
            char_w = 6
            total_px_w = len(word) * char_w - 1
            
            if total_px_w > (w - 4):
                 # Auto-Fallback for huge words
                 start_x = center_x - orp_idx
                 stdscr.addstr(center_y, start_x, word[:orp_idx])
                 stdscr.addstr(center_y, start_x + orp_idx, word[orp_idx], 
                               curses.color_pair(1) | curses.A_BOLD)
                 stdscr.addstr(center_y, start_x + orp_idx + 1, word[orp_idx+1:])
            else:
                 self.draw_bitmap_word(stdscr, word, orp_idx, center_x, center_y, h, w, is_ghost=False)

        # --- RETICLE GUIDES ---
        reticle_attr = curses.color_pair(2)
        if RETICLE_OPACITY_PERCENT < 60: reticle_attr |= curses.A_DIM
        
        gap = 3 if self.font_scale == 2 else 1
        stdscr.attron(reticle_attr)
        stdscr.addch(center_y - gap, center_x, '|')
        stdscr.addch(center_y + gap, center_x, '|')
        stdscr.attroff(reticle_attr)

        # --- INFO BARS: HEADER ---
        words_left = len(self.words) - self.index
        minutes_left = words_left / self.wpm if self.wpm > 0 else 0
        
        wpm_display = f"{int(self.wpm)}"
        if self.accel:
            start, end = self.accel[0], self.accel[1]
            if len(self.accel) > 2:
                peak = self.accel[2]
                wpm_display = f"ACCEL: {start}->{end}@{peak}% ({int(self.wpm)})"
            else:
                wpm_display = f"ACCEL: {start}->{end} ({int(self.wpm)})"
        
        mb_status = "| MB " if self.mb else ""
        title_bar = f" RetiCLI | WPM: {wpm_display} {mb_status}| {'PAUSED' if self.paused else 'READING'}"
        time_info = f"Left: {int(minutes_left)}m {int((minutes_left%1)*60)}s "
        
        stdscr.attron(curses.A_REVERSE)
        stdscr.addstr(0, 0, title_bar[:w-1])
        if w > len(title_bar) + len(time_info) + 2:
            stdscr.addstr(0, w - len(time_info) - 1, time_info)
        stdscr.attroff(curses.A_REVERSE)

        # --- INFO BARS: FOOTER ---
        try:
            pct = self.index / len(self.words)
            bar_w = w - 4
            filled = int(bar_w * pct)
            bar = "[" + "=" * filled + "-" * (bar_w - filled) + "]"
            stdscr.addstr(h-2, 2, bar[:w-4], curses.A_DIM)
            
            ctrls = " [SPC] Pause | [UP/DN] Speed | [LF/RT] Skip | [+/-] Size | [Q] Quit "
            stdscr.addstr(h-1, 0, ctrls[:w-1], curses.A_DIM)
        except: pass

        stdscr.refresh()

    def run(self, stdscr):
        """
        The main execution loop of the application. Handles user input, 
        timing, and screen refreshing.

        Args:
            stdscr: The curses window object provided by curses.wrapper.
        """
        # Colors: 1=Red(Focus), 2=Cyan(Guides)
        curses.start_color()
        curses.use_default_colors()
        curses.init_pair(1, curses.COLOR_RED, -1)
        curses.init_pair(2, curses.COLOR_CYAN, -1)
        curses.curs_set(0)
        stdscr.nodelay(True)

        # Initial draw to prevent blank screen
        self.draw_screen(stdscr)

        last_update = time.time()
        
        while True:
            # --- DYNAMIC ACCELERATION LOGIC ---
            if self.accel and not self.paused:
                start_wpm = self.accel[0]
                end_wpm = self.accel[1]
                peak_pct = self.accel[2] / 100.0 if len(self.accel) > 2 else 1.0
                
                # Calculate progress purely based on location
                # Avoid division by zero if peak_pct is weirdly 0
                if peak_pct <= 0: peak_pct = 1.0
                
                file_progress = self.index / len(self.words)
                ramp_progress = min(1.0, file_progress / peak_pct)
                
                self.wpm = start_wpm + (end_wpm - start_wpm) * ramp_progress

            try:
                key = stdscr.getch()
            except:
                key = -1

            if key == ord('q') or key == 3: # Quit
                break
            elif key == ord(' '): # Pause
                self.paused = not self.paused
                self.draw_screen(stdscr)
            elif key == curses.KEY_UP: # Faster
                self.wpm = min(2000, self.wpm + 25)
                # Check for Accel override? User manual adjustment breaks accel curve temporarily
            elif key == curses.KEY_DOWN: # Slower
                self.wpm = max(25, self.wpm - 25)
            elif key == curses.KEY_LEFT: # Back
                self.index = max(0, self.index - 20)
                self.draw_screen(stdscr)
            elif key == curses.KEY_RIGHT: # Forward
                self.index = min(len(self.words)-1, self.index + 20)
                self.draw_screen(stdscr)
            elif key == ord('+') or key == ord('='): # Big Text
                self.font_scale = 2
                self.draw_screen(stdscr)
            elif key == ord('-') or key == ord('_'): # Normal Text
                self.font_scale = 1
                self.draw_screen(stdscr)
            
            # --- SMART DELAY CALCULATION ---
            # Base Delay
            delay = 60.0 / self.wpm
            
            # Smart Modifiers
            current_word = self.words[self.index]
            
            # 1. Punctuation Pause
            if current_word[-1] in '.!?':
                delay *= PAUSE_PUNCTUATION_FACTOR
            
            # 2. Long Word Delay
            if len(current_word) > 10:
                delay *= PAUSE_LONG_WORD_FACTOR

            current_time = time.time()
            
            if not self.paused:
                if current_time - last_update >= delay:
                    # Capture State for Motion Blur BEFORE updating index
                    self.prev_word = current_word
                    self.prev_orp_idx = self.get_orp_index(current_word)
                    
                    self.index += 1
                    if self.index >= len(self.words):
                        self.index = len(self.words) - 1
                        self.paused = True
                    last_update = current_time
                    self.draw_screen(stdscr)
            elif key == curses.KEY_RESIZE:
                curses.update_lines_cols()
                self.draw_screen(stdscr)
            else:
                time.sleep(0.05)

            if not self.paused:
                time.sleep(0.001)

def main():
    """
    Entry point for the application. Parses command-line arguments 
    and starts the curses event loop.
    """
    parser = argparse.ArgumentParser(description="RetiCLI: Terminal RSVP Speed Reader")
    parser.add_argument("--file", type=str, required=True, help="Path to text/markdown file")
    parser.add_argument("--accel", type=int, nargs='+', metavar='ARG',
                        help="Enable progressive speed ramp: START END [PEAK_PERCENT] (e.g. 300 900 40)")
    parser.add_argument("--mb", action="store_true", help="Enable Motion Blur (Phosphor Persistence)")
    args = parser.parse_args()

    if args.accel and len(args.accel) not in [2, 3]:
        print("Error: --accel requires 2 or 3 arguments (START END [PEAK_PERCENT])")
        sys.exit(1)

    app = RetiCLI(args.file, accel=args.accel, mb=args.mb)
    try:
        curses.wrapper(app.run)
    except KeyboardInterrupt:
        pass
    except Exception as e:
        curses.endwin()
        print(f"Error: {e}")

if __name__ == "__main__":
    main()