#!/usr/bin/env python3
"""
Project Borealis (Single Region: HP/MP/FP/EXP)
==============================================
• One region labeled "Player Stats" capturing 4 lines:
  1) HP: current / max
  2) MP: current / max
  3) FP: current / max
  4) EXP (percentage)

• HP, MP, FP each have Rich progress bars that stay their assigned color at 100%.
• The 4th line (EXP) feeds into historical EXP logic (table + predicted time).
• Region is resizable & draggable (edit_mode = True).
• Tesseract uses --psm 4 for multi-line segmentation.

Adjust region.x, region.y, region.w, region.h to match your UI.
"""

import sys
import os
import time
import re
import pytesseract
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, 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"  
POLLING_RATE_MS = 1000    
MAX_DATA_POINTS = 7

DEFAULT_WIDTH = 150
DEFAULT_HEIGHT = 120  # taller to ensure line 4 is captured
HANDLE_SIZE = 10
LABEL_HEIGHT = 20

GREEN_HEADER_STYLE = "bold green"

def format_duration(seconds):
    """
    Convert total seconds into hours/min/seconds (e.g., "Xh Ym Zs").
    Returns '???' if None.
    """
    if seconds is None:
        return "???"
    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"

def sanitize_experience_string(raw_text):
    """
    Extracts a float from raw OCR text for EXP (0-100%).
    Handles e.g. "25.5682%", "77.8649" etc.
    """
    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))
    if val < 0:
        val = 0
    elif val > 100:
        val = 100
    return round(val, 4)

def format_experience_value(value):
    """
    Format a float 0-100 to XX.XXXX for display in table output.
    """
    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}"

class Region:
    """
    Defines a draggable/resizable screen region for OCR capture.
    """
    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 four small rectangles (handles) for resizing each corner.
        """
        return [
            QRect(self.x - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),  # top-left
            QRect(self.x + self.w - HANDLE_SIZE // 2, self.y - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),  # top-right
            QRect(self.x - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),  # bottom-left
            QRect(self.x + self.w - HANDLE_SIZE // 2, self.y + self.h - HANDLE_SIZE // 2, HANDLE_SIZE, HANDLE_SIZE),  # bottom-right
        ]

class OverlayCanvas(QWidget):
    """
    Renders the overlay & handles region dragging/resizing.
    """
    def __init__(self, regions, parent=None):
        super().__init__(parent)
        self.regions = regions
        self.edit_mode = True   # allow editing by default
        self.selected_region = None
        self.selected_handle = None
        self.drag_offset = QPoint()

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

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

                # If in edit mode, show corner handles
                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:
            # Check topmost region first (reverse if multiple)
            for region in reversed(self.regions):
                # Check each resize handle
                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 main rect for dragging
                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 entire 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

            # Enforce min size
            sr.w = max(sr.w, 10)
            sr.h = max(sr.h, 10)

        self.update()  # repaint

    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):
    """
    Single Region Overlay for Player Stats (HP/MP/FP/EXP)
    """
    def __init__(self):
        super().__init__()
        screen_geo = QApplication.primaryScreen().geometry()
        self.setGeometry(screen_geo)
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)

        # Single region, with an increased height (120)
        region = Region(250, 50, label="Player Stats")
        region.h = 120
        self.regions = [region]

        self.canvas = OverlayCanvas(self.regions, self)
        self.canvas.setGeometry(self.rect())

        # Tesseract
        self.engine = pytesseract

        # Keep history of EXP data
        self.points = []

        # Timer for periodic OCR scanning
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.collect_ocr_data)
        self.timer.start(POLLING_RATE_MS)

    # ---------------------------------------------------------------------
    #                           OCR
    # ---------------------------------------------------------------------
    def collect_ocr_data(self):
        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 = self.preprocess_image(screenshot)

                # Use psm=4 for multi-line
                text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
                region.data = text.strip()

        self.display_ocr_data_in_terminal()

    def preprocess_image(self, image):
        """
        Convert to grayscale, scale up, threshold, median filter
        for improved Tesseract accuracy.
        """
        gray = image.convert("L")
        scaled = gray.resize((gray.width * 3, gray.height * 3))
        thresh = scaled.point(lambda p: p > 200 and 255)
        return thresh.filter(ImageFilter.MedianFilter(3))

    # ---------------------------------------------------------------------
    #                           Parsing
    # ---------------------------------------------------------------------
    def parse_all_stats(self, raw_text):
        """
        Expect up to 4 lines: HP, MP, FP, EXP.
        Returns dict with keys "hp", "mp", "fp", "exp".
        """
        raw_lines = raw_text.splitlines()
        lines = [l.strip() for l in raw_lines if l.strip()]  # remove empty lines

        stats_dict = {
            "hp":  (0, 1),
            "mp":  (0, 1),
            "fp":  (0, 1),
            "exp": None
        }

        if len(lines) < 4:
            return stats_dict

        # HP
        hp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[0])
        if hp_match:
            stats_dict["hp"] = (int(hp_match.group(1)), int(hp_match.group(2)))

        # MP
        mp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[1])
        if mp_match:
            stats_dict["mp"] = (int(mp_match.group(1)), int(mp_match.group(2)))

        # FP
        fp_match = re.search(r"(\d+)\s*/\s*(\d+)", lines[2])
        if fp_match:
            stats_dict["fp"] = (int(fp_match.group(1)), int(fp_match.group(2)))

        # EXP
        exp_val = sanitize_experience_string(lines[3])
        stats_dict["exp"] = exp_val

        return stats_dict

    def update_points(self, new_val):
        """
        Track historical EXP changes for table & predicted time to level.
        """
        now = time.time()
        if self.points:
            _, last_v = self.points[-1]
            # skip duplicates
            if abs(new_val - last_v) < 1e-6:
                return
            # if new_val < last_v, assume rollover
            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)

    # ---------------------------------------------------------------------
    #                           Display
    # ---------------------------------------------------------------------
    def compute_time_to_100(self):
        """
        Estimate time to reach 100% from current EXP data.
        """
        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):
        """
        Clears terminal, prints HP/MP/FP bars, EXP table, predicted time.
        """
        console = Console()
        os.system('cls' if os.name == 'nt' else 'clear')

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

        # Parse stats
        raw_text = self.regions[0].data
        stats = self.parse_all_stats(raw_text)
        hp_cur, hp_max = stats["hp"]
        mp_cur, mp_max = stats["mp"]
        fp_cur, fp_max = stats["fp"]
        exp_val = stats["exp"]

        # Show HP / MP / FP bars, forcing each color to remain even at 100%
        with Progress(
            "{task.description}",
            BarColumn(bar_width=30),
            TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"),
            console=console,
            transient=False,
            auto_refresh=True,
        ) as progress:

            progress.add_task(
                "[bold red]HP[/bold red]",
                total=hp_max,
                completed=hp_cur,
                style="red",
                complete_style="red"    # remain red at 100%
            )
            progress.add_task(
                "[bold blue]MP[/bold blue]",
                total=mp_max,
                completed=mp_cur,
                style="blue",
                complete_style="blue"   # remain blue at 100%
            )
            progress.add_task(
                "[bold green]FP[/bold green]",
                total=fp_max,
                completed=fp_cur,
                style="green",
                complete_style="green"  # remain green at 100%
            )

            progress.refresh()
            progress.stop()

        console.print()  # blank line after bars

        # If we have an EXP value, update historical data
        if exp_val is not None:
            self.update_points(exp_val)

        # Build the Historical EXP 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 == 0:
            table.add_row("N/A", "N/A", "N/A", "N/A")
        elif n == 1:
            _, 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]

                delta_v = v_cur - v_prev
                delta_str = f"{delta_v:+.4f}%"
                exp_main = format_experience_value(v_cur)
                exp_str = f"[green]{exp_main}%[/green] [dim]({delta_str})[/dim]"

                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()  # blank line

        # Predicted time to level
        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)

        with Progress(
            TextColumn("[bold white]Predicted Time to Level:[/bold white] "),
            BarColumn(bar_width=30, complete_style="magenta"),
            TextColumn(" [green]{task.percentage:>5.2f}%[/green] "),
            TextColumn(f"[magenta]{time_str}[/magenta] until 100%", justify="right"),
            console=console,
            transient=False,
        ) as progress:
            progress.add_task("", total=100, completed=current_exp)
            progress.refresh()

def main():
    """
    Launches the PyQt5 overlay, starts the event loop.
    """
    app = QApplication(sys.argv)
    window = BorealisOverlay()
    window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)")
    window.show()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()