#!/usr/bin/env python3

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

try:
    import winsound
    HAS_WINSOUND = True
except ImportError:
    HAS_WINSOUND = False

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, Group
from rich.table import Table
from rich.progress import Progress, BarColumn, TextColumn
from rich.text import Text
from rich.live import Live

# =============================================================================
#                          Global Config
# =============================================================================

pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"

POLLING_RATE_MS = 500
MAX_DATA_POINTS = 8

# We still use these defaults for Region size. 
DEFAULT_WIDTH = 180
DEFAULT_HEIGHT = 130
HANDLE_SIZE = 8
LABEL_HEIGHT = 20

GREEN_HEADER_STYLE = "bold green"

BEEP_INTERVAL_SECONDS = 1.0  # Only beep once every 1 second

# STATUS BAR AUTO-LOCATOR LOGIC (WILL BE BUILT-OUT TO BE MORE ROBUST LATER)
TEMPLATE_PATH = "G:\\Nextcloud\\Projects\\Scripting\\bars_template.png"  # Path to your bars template file
MATCH_THRESHOLD = 0.4  # The correlation threshold to consider a "good" match

# =============================================================================
#                           Helper Functions
# =============================================================================

def beep_hp_warning():
    """
    Only beep if enough time has elapsed since the last beep (BEEP_INTERVAL_SECONDS).
    """
    current_time = time.time()
    if (beep_hp_warning.last_beep_time is None or
        (current_time - beep_hp_warning.last_beep_time >= BEEP_INTERVAL_SECONDS)):

        beep_hp_warning.last_beep_time = current_time
        if HAS_WINSOUND:
            # frequency=376 Hz, duration=100 ms
            winsound.Beep(376, 100)
        else:
            # Attempt terminal bell
            print('\a', end='')

beep_hp_warning.last_beep_time = None


def locate_bars_opencv(template_path, threshold=MATCH_THRESHOLD):
    """
    Attempt to locate the bars via OpenCV template matching:
      1) Grab the full screen using PIL.ImageGrab.
      2) Convert to NumPy array in BGR format for cv2.
      3) Load template from `template_path`.
      4) Use cv2.matchTemplate to find the best match location.
      5) If max correlation > threshold, return (x, y, w, h).
      6) Else return None.
    """
    # 1) Capture full screen
    screenshot_pil = ImageGrab.grab()
    screenshot_np = np.array(screenshot_pil)  # shape (H, W, 4) possibly
    # Convert RGBA or RGB to BGR
    screenshot_bgr = cv2.cvtColor(screenshot_np, cv2.COLOR_RGB2BGR)

    # 2) Load template from file
    template_bgr = cv2.imread(template_path, cv2.IMREAD_COLOR)
    if template_bgr is None:
        print(f"[WARN] Could not load template file: {template_path}")
        return None

    # 3) Template matching
    result = cv2.matchTemplate(screenshot_bgr, template_bgr, cv2.TM_CCOEFF_NORMED)

    # 4) Find best match
    min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
    # template width/height
    th, tw, _ = template_bgr.shape

    if max_val >= threshold:
        # max_loc is top-left corner of the best match
        found_x, found_y = max_loc
        return (found_x, found_y, tw, th)
    else:
        return None


def format_duration(seconds):
    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):
    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):
    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}"

# -----------------------------------------------------------------------------
#                              Region Class
# -----------------------------------------------------------------------------
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 [
            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),
        ]

# -----------------------------------------------------------------------------
#                           OverlayCanvas Class
# -----------------------------------------------------------------------------
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
        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:
                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):
                for i, handle in enumerate(region.resize_handles()):
                    if handle.contains(event.pos()):
                        self.selected_region = region
                        self.selected_handle = i
                        return
                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:
            self.selected_region.x = event.x() - self.drag_offset.x()
            self.selected_region.y = event.y() - self.drag_offset.y()
        else:
            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

# -----------------------------------------------------------------------------
#                           BorealisOverlay Class
# -----------------------------------------------------------------------------
class BorealisOverlay(QWidget):
    """
    Single Region Overlay for Player Stats (HP/MP/FP/EXP) with:
      - Automatic location via OpenCV template matching at startup
      - OCR scanning
      - Low-HP beep
      - Rich Live updates in terminal
    """
    def __init__(self, live=None):
        super().__init__()
        screen_geo = QApplication.primaryScreen().geometry()
        self.setGeometry(screen_geo)
        self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint)
        self.setAttribute(Qt.WA_TranslucentBackground, True)

        # Try to find the bars automatically
        # If found => use that location, else default
        initial_x, initial_y = 250, 50
        region_w, region_h = DEFAULT_WIDTH, DEFAULT_HEIGHT

        match_result = locate_bars_opencv(TEMPLATE_PATH, MATCH_THRESHOLD)
        if match_result is not None:
            found_x, found_y, w, h = match_result
            print(f"Character Status Located at {found_x}, {found_y} with confidence >= {MATCH_THRESHOLD}.")
            initial_x, initial_y = found_x, found_y
            # Optionally override region size with template size
            region_w, region_h = w, h
        else:
            print("Could not auto-locate the character status page. Set your theme to Masquerade and Interface Scale to 140%, and browser zoom level to 110%.  Using default region.")

        region = Region(initial_x, initial_y, label="Character Status")
        region.w = region_w
        region.h = region_h
        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 = []

        self.live = live

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

    def set_live(self, live):
        self.live = live

    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)
                text = pytesseract.image_to_string(processed, config='--psm 4 --oem 1')
                region.data = text.strip()

        if self.live is not None:
            renderable = self.build_renderable()
            self.live.update(renderable)

    def preprocess_image(self, image):
        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))

    def parse_all_stats(self, raw_text):
        raw_lines = raw_text.splitlines()
        lines = [l.strip() for l in raw_lines if l.strip()]
        stats_dict = {
            "hp":  (0,1),
            "mp":  (0,1),
            "fp":  (0,1),
            "exp": None
        }
        if len(lines) < 4:
            return stats_dict

        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_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_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_val = sanitize_experience_string(lines[3])
        stats_dict["exp"] = exp_val
        return stats_dict

    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):
        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 build_renderable(self):
        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"]

        # HP beep logic
        if hp_max > 0:
            hp_ratio = hp_cur / hp_max
            if 0 < hp_ratio <= 0.40:
                beep_hp_warning()

        if exp_val is not None:
            self.update_points(exp_val)
        current_exp = self.points[-1][1] if self.points else 0.0

        # Title
        title_text = Text("Project Borealis\n", style="bold white")
        subtitle_text = Text("Flyff Information Overlay\n\n", style="dim")

        # HP / MP / FP bars
        bar_progress = Progress(
            "{task.description}",
            BarColumn(bar_width=30),
            TextColumn(" {task.completed}/{task.total} ({task.percentage:>5.2f}%)"),
            transient=False,
            auto_refresh=False
        )
        bar_progress.add_task("[bold red]HP[/bold red]", total=hp_max, completed=hp_cur,
                              style="red", complete_style="red")
        bar_progress.add_task("[bold blue]MP[/bold blue]", total=mp_max, completed=mp_cur,
                              style="blue", complete_style="blue")
        bar_progress.add_task("[bold green]FP[/bold green]", total=fp_max, completed=fp_cur,
                              style="green", complete_style="green")
        bar_progress.refresh()

        # 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)

        # Predicted Time to Level
        secs_left = self.compute_time_to_100()
        time_str = format_duration(secs_left)

        time_bar = 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"),
            transient=False,
            auto_refresh=False
        )
        time_bar.add_task("", total=100, completed=current_exp)
        time_bar.refresh()

        return Group(
            title_text,
            subtitle_text,
            bar_progress,
            table,
            time_bar
        )

# -----------------------------------------------------------------------------
#                                     main
# -----------------------------------------------------------------------------
def main():
    """
    1) Attempt to locate HP/MP/FP/Exp bars using OpenCV template matching.
    2) Position overlay region accordingly if found, else default.
    3) Start PyQt, periodically OCR the region, update Rich Live in terminal.
    """
    app = QApplication(sys.argv)
    window = BorealisOverlay()
    window.setWindowTitle("Project Borealis Overlay (HP/MP/FP/EXP)")
    window.show()

    console = Console()

    with Live(console=console, refresh_per_second=4) as live:
        window.set_live(live)
        exit_code = app.exec_()

    sys.exit(exit_code)

if __name__ == "__main__":
    main()