#!/usr/bin/env python3
"""
Project Borealis (Fixed Progress Bar)
=====================================
• Uses Tesseract OCR only, storing up to 7 data points.
• Rollover logic (reset if new < old).
• Skips duplicates.
• Shows a Rich table of historical data (2+ points).
• Then displays a Rich progress bar for the current EXP, plus predicted time to level
  in hours/min/seconds if over 60 minutes.

Key fix: we use "with Progress(...) as progress:" *without* transient,
so the bar remains in the console after the update.
"""

import sys
import os
import time
import re
import pytesseract
import numpy as np

from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QRect, QPoint, QTimer
from PyQt5.QtGui import QPainter, QPen, QColor, QFont
from PIL import Image, ImageGrab, ImageEnhance, ImageFilter

from rich.console import Console
from rich.table import Table
from rich.progress import Progress, BarColumn, TextColumn

# ---------------- [ Global Config ] ----------------
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"

OCR_ENGINE = "Tesseract"  # Hard-coded since we removed EasyOCR
POLLING_RATE_MS = 1000
MAX_DATA_POINTS = 7

GREEN_HEADER_STYLE = "bold green"
HANDLE_SIZE = 10
LABEL_HEIGHT = 20
DEFAULT_WIDTH = 150
DEFAULT_HEIGHT = 50

def sanitize_experience_string(raw_text):
    """Extract a float from raw OCR text, removing extraneous symbols."""
    text_no_percent = raw_text.replace('%', '')
    text_no_spaces = text_no_percent.replace(' ', '')
    cleaned = re.sub(r'[^0-9\.]', '', text_no_spaces)
    match = re.search(r'\d+(?:\.\d+)?', cleaned)
    if not match:
        return None
    val = float(match.group(0))
    return round(val, 4)

def format_experience_value(value):
    """Format float to 'XX.XXXX' with leading zeros if needed."""
    if value < 0:
        value = 0
    elif value > 100:
        value = 100

    float_4 = round(value, 4)
    raw_str = f"{float_4:.4f}"
    int_part, dec_part = raw_str.split('.')
    if int_part == "100":
        pass
    elif len(int_part) == 1 and int_part != "0":
        int_part = "0" + int_part
    elif int_part == "0":
        int_part = "00"
    return f"{int_part}.{dec_part}"

def format_duration(seconds):
    """
    Convert total seconds into hours / min / sec:
      - If hours > 0 => "Xh Ym Zs"
      - Otherwise => "Xm Ys"
    """
    seconds = int(seconds)
    hours = seconds // 3600
    leftover = seconds % 3600
    mins = leftover // 60
    secs = leftover % 60

    if hours > 0:
        return f"{hours}h {mins}m {secs}s"
    else:
        return f"{mins}m {secs}s"

class Region:
    def __init__(self, x, y, label="Region", color=QColor(0, 0, 255)):
        self.x = x
        self.y = y
        self.w = DEFAULT_WIDTH
        self.h = DEFAULT_HEIGHT
        self.label = label
        self.color = color
        self.visible = True
        self.data = ""

    def rect(self):
        return QRect(self.x, self.y, self.w, self.h)

    def label_rect(self):
        return QRect(self.x, self.y - LABEL_HEIGHT, self.w, LABEL_HEIGHT)

    def resize_handles(self):
        return [
            QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
            QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
            QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
            QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),
        ]

class OverlayCanvas(QWidget):
    def __init__(self, regions, parent=None):
        super().__init__(parent)
        self.regions = regions
        self.edit_mode = True
        self.selected_region = None
        self.selected_handle = None
        self.drag_offset = QPoint()

    def paintEvent(self, event):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        for region in self.regions:
            if region.visible:
                pen = QPen(region.color)
                pen.setWidth(3)
                painter.setPen(pen)
                painter.drawRect(region.x, region.y, region.w, region.h)

                painter.setFont(QFont("Arial", 12, QFont.Bold))
                painter.setPen(region.color)
                painter.drawText(region.x, region.y - 5, region.label)

                if self.edit_mode:
                    for handle in region.resize_handles():
                        painter.fillRect(handle, region.color)

    def mousePressEvent(self, event):
        if not self.edit_mode:
            return

        if event.button() == Qt.LeftButton:
            for region in reversed(self.regions):
                # Check handles
                for i, handle in enumerate(region.resize_handles()):
                    if handle.contains(event.pos()):
                        self.selected_region = region
                        self.selected_handle = i
                        return
                # Check label or rect
                if region.label_rect().contains(event.pos()):
                    self.selected_region = region
                    self.selected_handle = None
                    self.drag_offset = event.pos() - QPoint(region.x, region.y)
                    return
                if region.rect().contains(event.pos()):
                    self.selected_region = region
                    self.selected_handle = None
                    self.drag_offset = event.pos() - QPoint(region.x, region.y)
                    return

    def mouseMoveEvent(self, event):
        if not self.edit_mode or self.selected_region is None:
            return

        if self.selected_handle is None:
            # Drag rectangle
            self.selected_region.x = event.x() - self.drag_offset.x()
            self.selected_region.y = event.y() - self.drag_offset.y()
        else:
            # Resize
            sr = self.selected_region
            if self.selected_handle == 0:  # Top-left
                sr.w += sr.x - event.x()
                sr.h += sr.y - event.y()
                sr.x = event.x()
                sr.y = event.y()
            elif self.selected_handle == 1:  # Top-right
                sr.w = event.x() - sr.x
                sr.h += sr.y - event.y()
                sr.y = event.y()
            elif self.selected_handle == 2:  # Bottom-left
                sr.w += sr.x - event.x()
                sr.h = event.y() - sr.y
                sr.x = event.x()
            elif self.selected_handle == 3:  # Bottom-right
                sr.w = event.x() - sr.x
                sr.h = event.y() - sr.y

            sr.w = max(sr.w, 10)
            sr.h = max(sr.h, 10)
        self.update()

    def mouseReleaseEvent(self, event):
        if not self.edit_mode:
            return
        if event.button() == Qt.LeftButton:
            self.selected_region = None
            self.selected_handle = None

class BorealisOverlay(QWidget):
    """
    Project Borealis with Tesseract + Rich table + Rich progress bar,
    properly displayed with "with Progress(...) as progress".
    """
    def __init__(self):
        super().__init__()

        # Fullscreen overlay
        screen_geometry = QApplication.primaryScreen().geometry()
        self.setGeometry(screen_geometry)
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)

        self.regions = [
            Region(250, 50, label="Experience"),
        ]
        self.canvas = OverlayCanvas(self.regions, self)
        self.canvas.setGeometry(self.rect())

        # Tesseract
        self.engine = pytesseract

        self.points = []
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.collect_ocr_data)
        self.timer.start(POLLING_RATE_MS)

    def collect_ocr_data(self):
        if not self.engine:
            return

        for region in self.regions:
            if region.visible:
                screenshot = ImageGrab.grab(
                    bbox=(region.x, region.y, region.x + region.w, region.y + region.h)
                )
                processed_image = self.preprocess_image(screenshot)

                text = pytesseract.image_to_string(processed_image, config='--psm 6 --oem 1')
                region.data = text.strip()

                if region.label.lower() == "experience":
                    val = sanitize_experience_string(region.data)
                    if val is not None:
                        self.update_points(val)
                        region.data = format_experience_value(val) + "%"
                    else:
                        region.data = "N/A"

        self.display_ocr_data_in_terminal()

    def preprocess_image(self, image):
        gray = image.convert("L")
        scaled = gray.resize(
            (gray.width * 3, gray.height * 3),
            resample=Image.Resampling.LANCZOS
        )
        thresh = scaled.point(lambda p: p > 200 and 255)
        return thresh.filter(ImageFilter.MedianFilter(3))

    def update_points(self, new_val):
        now = time.time()
        if self.points:
            _, last_v = self.points[-1]
            if abs(new_val - last_v) < 1e-6:
                return
            if new_val < last_v:
                self.points.clear()
        self.points.append((now, new_val))
        if len(self.points) > MAX_DATA_POINTS:
            self.points.pop(0)

    def compute_time_to_100(self):
        """Return integer seconds until 100% or None if not feasible."""
        n = len(self.points)
        if n < 2:
            return None
        first_t, first_v = self.points[0]
        last_t, last_v = self.points[-1]
        diff_v = last_v - first_v
        if diff_v <= 0:
            return None

        steps = n - 1
        total_time = last_t - first_t
        if total_time <= 0:
            return None

        avg_change = diff_v / steps
        remain = 100.0 - last_v
        if remain <= 0:
            return None

        avg_time = total_time / steps
        rate_per_s = avg_change / avg_time if avg_time > 0 else 0
        if rate_per_s <= 0:
            return None

        return int(remain / rate_per_s)

    def display_ocr_data_in_terminal(self):
        console = Console()
        os.system('cls' if os.name == 'nt' else 'clear')

        console.print("[bold white]Project Borealis[/bold white]")
        console.print("[dim]Flyff Information Overlay[/dim]\n")

        console.print(f"[bold]OCR Engine[/bold]: {OCR_ENGINE}")
        console.print(f"[bold]Data Polling Rate[/bold]: {POLLING_RATE_MS / 1000}s\n")

        # Build the historical table
        table = Table(show_header=True, header_style=GREEN_HEADER_STYLE, style=None)
        table.add_column("Historical EXP", justify="center", style="green")
        table.add_column("Time Since Last Kill", justify="center", style="green")
        table.add_column("Average EXP Per Kill", justify="center", style="green")
        table.add_column("Average Time Between Kills", justify="center", style="green")

        n = len(self.points)
        if n == 1:
            t0, v0 = self.points[0]
            exp_str = f"[green]{format_experience_value(v0)}%[/green]"
            table.add_row(exp_str, "N/A", "N/A", "N/A")
        else:
            for i in range(1, n):
                t_cur, v_cur = self.points[i]
                t_prev, v_prev = self.points[i - 1]

                exp_str = f"[green]{format_experience_value(v_cur)}%[/green]"
                delta_t = t_cur - t_prev
                t_since_str = f"{delta_t:.1f}s"

                diff_v = v_cur - self.points[0][1]
                steps = i
                avg_exp_str = f"{diff_v/steps:.4f}%"

                total_time = t_cur - self.points[0][0]
                avg_kill_time = total_time / steps
                avg_time_str = f"{avg_kill_time:.1f}s"

                table.add_row(exp_str, t_since_str, avg_exp_str, avg_time_str)

        console.print(table)
        console.print()

        # Prepare the progress bar
        current_exp = self.points[-1][1] if self.points else 0.0
        secs_left = self.compute_time_to_100()
        time_str = format_duration(secs_left) if secs_left is not None else "???"

        # We'll display a single progress bar line in a context manager
        # Setting transient=False ensures it remains in the console
        with Progress(
            TextColumn("[bold white]Predicted Time to Level:[/bold white] "),
            BarColumn(bar_width=30),
            TextColumn(" [green]{task.percentage:>5.2f}%[/green] "),
            TextColumn(f"[orange]{time_str}[/orange] until 100%", justify="right"),
            console=console,
            transient=False,
        ) as progress:
            task_id = progress.add_task("", total=100, completed=current_exp)
            # Force a manual refresh so the bar appears immediately
            progress.refresh()

def main():
    app = QApplication(sys.argv)
    window = BorealisOverlay()
    window.setWindowTitle("Project Borealis Overlay (Fixed Progress Bar)")
    window.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()