import sys, os, json, threading, queue, time, subprocess, random
import cv2, numpy as np
from PyQt6.QtWidgets import (
    QApplication, QWidget, QLabel, QPushButton,
    QVBoxLayout, QHBoxLayout, QListWidget, QListWidgetItem,
    QFileDialog, QMessageBox, QTextEdit, QDialog,
    QLineEdit, QInputDialog, QComboBox, QScrollArea,
    QFormLayout, QSpinBox, QDoubleSpinBox, QMenu,
    QRubberBand, QSplitter, QMainWindow, QCheckBox
)
from PyQt6.QtCore import Qt, QTimer, QRect, QSize
from PyQt6.QtGui import QPixmap, QImage, QIcon, QAction

import ctypes

CREATE_NO_WINDOW = 0x08000000
def bgr_to_hs(img):
    """
    BGR → HSV → H+S 2채널로 변환
    """
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
    h, s, _ = cv2.split(hsv)
    return cv2.merge([h, s])

def run_hidden(cmd, **kwargs):
    return subprocess.run(
        cmd,
        creationflags=CREATE_NO_WINDOW,
        **kwargs
    )

# === Windows 작업표시줄 AppUserModelID ===
try:
    ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("VegaMax")
except Exception:
    pass

# =====================================================
CONFIG_FILE = "config.json"
DEFAULT_ADB = r"C:\\LDPlayer\\LDPlayer9\\adb.exe"
DEFAULT_THRESHOLD = 0.85


def load_config():
    if os.path.exists(CONFIG_FILE):
        try:
            return json.load(open(CONFIG_FILE, encoding="utf-8"))
        except:
            pass
    return {}


def save_config(cfg):
    json.dump(cfg, open(CONFIG_FILE, "w", encoding="utf-8"), indent=2)


CONFIG = load_config()

ADB_PATH = CONFIG.get("adb_path", DEFAULT_ADB)


def check_adb(path):
    try:
        run_hidden(
            [path, "version"],
            stdout=subprocess.DEVNULL,
            stderr=subprocess.DEVNULL,
            timeout=3
        )
        return True
    except:
        return False


# =====================================================
# SAFE IMREAD
# =====================================================
def cv2_imread_safe(path, flags=cv2.IMREAD_COLOR):
    try:
        data = np.fromfile(path, dtype=np.uint8)
        img = cv2.imdecode(data, flags)
        if img is None:
            return None
        return img
    except Exception:
        return None
# -----------------------
# HSV 기반 템플릿 전처리 & 일괄 매칭 헬퍼 (H+S 채널 사용)
# -----------------------
def preprocess_template_hs(t):
    """
    target dict t에 HSV(H+S) 템플릿과 템플릿 크기 정보를 추가합니다.
    """
    try:
        img = t.get("img")
        if img is None:
            t["img_hs"] = None
            t["tpl_h"] = 0
            t["tpl_w"] = 0
            return
        # BGR -> HSV
        hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
        h, s, _ = cv2.split(hsv)
        # H+S 두 채널로 합침 (dtype은 uint8)
        hs = cv2.merge([h, s])
        t["img_hs"] = hs
        t["tpl_h"], t["tpl_w"] = hs.shape[:2]
    except Exception:
        t["img_hs"] = None
        t["tpl_h"] = 0
        t["tpl_w"] = 0


def match_all_templates_hs(search_bgr, targets, default_threshold=DEFAULT_THRESHOLD):
    """
    search_bgr: BGR 이미지 (ROI 적용된 상태일 수 있음)
    targets: {path: target_dict, ...}
    반환: [{'path': path, 'score': float, 'center': (cx,cy), 'loc': (x,y)}, ...]
    """
    results = []
    if search_bgr is None:
        return results

    try:
        hsv = cv2.cvtColor(search_bgr, cv2.COLOR_BGR2HSV)
    except Exception:
        return results

    sh, sw = hsv.shape[:2]
    sh_ch, ss_ch = cv2.split(hsv)[0], cv2.split(hsv)[1]
    # 미리 합쳐놓기 (2채널)
    try:
        search_hs = cv2.merge([sh_ch, ss_ch])
    except Exception:
        return results

    # 안전 순회
    for path, t in list(targets.items()):
        try:
            tpl_hs = t.get("img_hs")
            if tpl_hs is None:
                preprocess_template_hs(t)
                tpl_hs = t.get("img_hs")
                if tpl_hs is None:
                    continue

            th = t.get("tpl_h", tpl_hs.shape[0])
            tw = t.get("tpl_w", tpl_hs.shape[1])

            if sh < th or sw < tw:
                # 검색 대상이 템플릿보다 작으면 건너뜀
                continue

            # matchTemplate (H+S 2채널 이미지로)
            try:
                res = cv2.matchTemplate(search_hs, tpl_hs, cv2.TM_CCOEFF_NORMED)
                _, max_val, _, max_loc = cv2.minMaxLoc(res)
            except Exception:
                continue

            threshold = float(t.get("threshold", default_threshold))
            if max_val >= threshold:
                cx = max_loc[0] + tw // 2
                cy = max_loc[1] + th // 2
                results.append({
                    "path": path,
                    "score": float(max_val),
                    "center": (cx, cy),
                    "loc": max_loc
                })
        except Exception:
            continue

    # 점수 내림차순
    results.sort(key=lambda x: x["score"], reverse=True)
    return results


# =====================================================
# ADB helpers (device-aware)
# =====================================================

def adb_input_text(device, text):
    run_hidden(
        [ADB_PATH, "-s", device, "shell", "input", "text", str(text)],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL
    )


def adb_keyevent(device, keycode):
    run_hidden(
        [ADB_PATH, "-s", device, "shell", "input", "keyevent", str(keycode)],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL
    )


def adb_force_stop(device, package):
    if not package:
        return
    run_hidden([
        ADB_PATH, "-s", device,
        "shell", "am", "force-stop", package
    ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def list_adb_devices():
    try:
        p = run_hidden(
            [ADB_PATH, "devices"],
            stdout=subprocess.PIPE,
            text=True
        )
        return [l.split("\t")[0] for l in p.stdout.splitlines() if "\tdevice" in l]
    except Exception:
        return []


def adb_swipe(device, x1, y1, x2, y2, ms):
    run_hidden([ADB_PATH, "-s", device, "shell", "input", "swipe", str(x1), str(y1), str(x2), str(y2), str(ms)])


def adb_launch_app(device, package):
    if not package:
        return
    run_hidden([
        ADB_PATH, "-s", device,
        "shell", "monkey", "-p", package,
        "-c", "android.intent.category.LAUNCHER", "1"
    ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


def adb_screencap(device):
    try:
        p = run_hidden(
            [ADB_PATH, "-s", device, "exec-out", "screencap", "-p"],
            stdout=subprocess.PIPE
        )
        if not p.stdout:
            return None
        return cv2.imdecode(np.frombuffer(p.stdout, np.uint8), cv2.IMREAD_COLOR)
    except Exception:
        return None


def adb_tap(device, x, y):
    run_hidden([ADB_PATH, "-s", device, "shell", "input", "tap", str(x), str(y)])


def adb_long(device, x, y, ms):
    run_hidden([
        ADB_PATH, "-s", device, "shell", "input", "swipe",
        str(x), str(y), str(x), str(y), str(ms)
    ])


# =====================================================
# Screenshot / ROI Pickers (device-aware)
# =====================================================
class ScreenshotPickerAbsolute(QDialog):
    def __init__(self, parent, device, result_cb):
        super().__init__(parent)
        self.result_cb = result_cb
        self.setWindowTitle(f"Pick Absolute Position ({device})")

        img = adb_screencap(device)
        if img is None:
            QMessageBox.critical(self, "ADB Error", f"Unable to capture screen for device {device}")
            self.reject()
            return

        h, w = img.shape[:2]
        rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        qimg = QImage(rgb.data, w, h, QImage.Format.Format_RGB888)

        self.scale = min(1.0, 1200 / w)
        pix = QPixmap.fromImage(qimg).scaled(
            int(w * self.scale),
            int(h * self.scale),
            Qt.AspectRatioMode.KeepAspectRatio
        )

        label = QLabel()
        label.setPixmap(pix)
        label.mousePressEvent = self.on_click

        layout = QVBoxLayout(self)
        layout.addWidget(label)

    def on_click(self, e):
        x = int(e.position().x() / self.scale)
        y = int(e.position().y() / self.scale)
        self.result_cb(x, y)
        self.accept()

class RoiPickerSave(QDialog):
    def __init__(self, parent, device, img):
        super().__init__(parent)
        self.device = device
        self.img = img
        self.start_pos = None
        self.setWindowTitle("Capture Area")

        h, w = img.shape[:2]
        rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        qimg = QImage(rgb.data, w, h, QImage.Format.Format_RGB888)

        # 메인 윈도우 크기에 맞춤
        mw = parent.size().width()
        self.scale = min(1.0, mw / w)

        pix = QPixmap.fromImage(qimg).scaled(
            int(w * self.scale),
            int(h * self.scale),
            Qt.AspectRatioMode.KeepAspectRatio
        )

        self.label = QLabel()
        self.label.setPixmap(pix)
        self.label.setMouseTracking(True)

        layout = QVBoxLayout(self)
        layout.addWidget(self.label)

        self.rubber = QRubberBand(QRubberBand.Shape.Rectangle, self.label)

        self.label.mousePressEvent = self.on_press
        self.label.mouseMoveEvent = self.on_move
        self.label.mouseReleaseEvent = self.on_release

    def on_press(self, e):
        self.start_pos = e.position().toPoint()
        self.rubber.setGeometry(QRect(self.start_pos, QSize()))
        self.rubber.show()

    def on_move(self, e):
        if not self.start_pos:
            return
        rect = QRect(self.start_pos, e.position().toPoint()).normalized()
        self.rubber.setGeometry(rect)

    def on_release(self, e):
        self.rubber.hide()
        end = e.position().toPoint()

        x1 = int(min(self.start_pos.x(), end.x()) / self.scale)
        y1 = int(min(self.start_pos.y(), end.y()) / self.scale)
        x2 = int(max(self.start_pos.x(), end.x()) / self.scale)
        y2 = int(max(self.start_pos.y(), end.y()) / self.scale)

        if x2 - x1 > 10 and y2 - y1 > 10:
            crop = self.img[y1:y2, x1:x2]
            self.save_crop(crop)

        self.accept()

    def save_crop(self, crop):
        base = os.path.dirname(os.path.abspath(sys.argv[0]))
        ts = time.strftime("%Y%m%d_%H%M%S")
        path = os.path.join(base, f"{ts}.png")
        cv2.imwrite(path, crop)
        QMessageBox.information(self, "Saved", f"Screenshot saved:\n{path}")

class RoiPicker(QDialog):
    def __init__(self, parent, device, cb):
        super().__init__(parent)
        self.cb = cb
        self.start_pos = None

        img = adb_screencap(device)
        if img is None:
            QMessageBox.critical(self, "ADB Error", f"Unable to capture screen for device {device}")
            self.reject()
            return

        h, w = img.shape[:2]
        rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        qimg = QImage(rgb.data, w, h, QImage.Format.Format_RGB888)

        self.scale = min(1.0, 1200 / w)
        pix = QPixmap.fromImage(qimg).scaled(
            int(w * self.scale), int(h * self.scale), Qt.AspectRatioMode.KeepAspectRatio
        )

        self.label = QLabel()
        self.label.setPixmap(pix)
        self.label.setMouseTracking(True)

        layout = QVBoxLayout(self)
        layout.addWidget(self.label)

        self.rubber = QRubberBand(QRubberBand.Shape.Rectangle, self.label)

        self.label.mousePressEvent = self.on_mouse_press
        self.label.mouseMoveEvent = self.on_mouse_move
        self.label.mouseReleaseEvent = self.on_mouse_release

    def on_mouse_press(self, e):
        self.start_pos = e.position().toPoint()
        self.rubber.setGeometry(QRect(self.start_pos, QSize()))
        self.rubber.show()

    def on_mouse_move(self, e):
        if not self.start_pos:
            return
        current = e.position().toPoint()
        rect = QRect(self.start_pos, current).normalized()
        self.rubber.setGeometry(rect)

    def on_mouse_release(self, e):
        self.rubber.hide()

        end_pos = e.position().toPoint()

        x1 = int(min(self.start_pos.x(), end_pos.x()) / self.scale)
        y1 = int(min(self.start_pos.y(), end_pos.y()) / self.scale)
        x2 = int(max(self.start_pos.x(), end_pos.x()) / self.scale)
        y2 = int(max(self.start_pos.y(), end_pos.y()) / self.scale)

        w = x2 - x1
        h = y2 - y1

        if w > 5 and h > 5:
            self.cb((x1, y1, w, h))

        self.accept()


# =====================================================
# Test result popup
# =====================================================
class TestResultDialog(QDialog):
    def __init__(self, parent, scr_bgr, match_info, tpl_img=None):
        super().__init__(parent)
        self.setWindowTitle("Test Match Result")

        root_layout = QVBoxLayout(self)

        # ===== Scroll Area =====
        scroll = QScrollArea()
        scroll.setWidgetResizable(True)
        root_layout.addWidget(scroll)

        container = QWidget()
        scroll.setWidget(container)

        layout = QVBoxLayout(container)

        # ---------------------------
        # 1. 스크린샷에 매칭 박스 그리기
        # ---------------------------
        scr_copy = scr_bgr.copy()
        p = match_info["path"]
        loc = match_info["loc"]
        score = match_info["score"]

        t_w, t_h = None, None
        if tpl_img is not None:
            t_h, t_w = tpl_img.shape[:2]

        dev = parent.current_device
        if dev and p in parent.devices.get(dev, {}).get("targets", {}):
            t = parent.devices[dev]["targets"][p]
            t_w = t.get("tpl_w", t_w)
            t_h = t.get("tpl_h", t_h)

        if t_w is None or t_h is None:
            try:
                tmp = cv2_imread_safe(p)
                if tmp is not None:
                    t_h, t_w = tmp.shape[:2]
            except:
                t_w, t_h = 50, 50

        x, y = int(loc[0]), int(loc[1])
        x2, y2 = x + int(t_w), y + int(t_h)

        cv2.rectangle(scr_copy, (x, y), (x2, y2), (0, 0, 255), 3)
        cx, cy = match_info.get("center", (x + (x2 - x) // 2, y + (y2 - y) // 2))
        cv2.circle(scr_copy, (int(cx), int(cy)), 6, (0, 0, 255), -1)

        # ---------------------------
        # 2. QPixmap
        # ---------------------------
        rgb = cv2.cvtColor(scr_copy, cv2.COLOR_BGR2RGB)
        h, w = rgb.shape[:2]
        qimg = QImage(rgb.data, w, h, QImage.Format.Format_RGB888)
        pix = QPixmap.fromImage(qimg)

        # ---------------------------
        # 3. ADB 해상도 기준 스케일
        # ---------------------------
        max_w, max_h = w, h
        if dev:
            devinfo = parent.devices.get(dev, {})
            max_w = devinfo.get("screen_w", w)
            max_h = devinfo.get("screen_h", h)

        scale = min(max_w / w, max_h / h, 1.0)
        disp_w = int(w * scale)
        disp_h = int(h * scale)

        pix = pix.scaled(
            disp_w,
            disp_h,
            Qt.AspectRatioMode.KeepAspectRatio,
            Qt.TransformationMode.SmoothTransformation
        )

        lbl = QLabel()
        lbl.setPixmap(pix)
        lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(lbl)

        # ---------------------------
        # 4. 정보
        # ---------------------------
        info = QLabel(
            f"Match: {os.path.basename(p)}\n"
            f"Score: {score:.4f}\n"
            f"Top-left: ({x}, {y})  Size: ({t_w} x {t_h})\n"
            f"Center: ({int(cx)}, {int(cy)})\n"
            f"Screen: {w} x {h}"
        )
        info.setWordWrap(True)
        layout.addWidget(info)

        # ---------------------------
        # 5. Close 버튼 (스크롤 밖에 둠)
        # ---------------------------
        btn = QPushButton("Close")
        btn.clicked.connect(self.accept)
        root_layout.addWidget(btn)

        # ---------------------------
        # 6. Dialog size = ADB 화면 기준
        # ---------------------------
        self.resize(disp_w + 40, min(disp_h + 200, max_h + 100))




# =====================================================
# Action Editor (device-aware pickers)
# =====================================================
class ActionEditor(QDialog):
    def __init__(self, parent, action=None, device=None):
        super().__init__(parent)
        self.result = None
        self.absolute = False
        self.device = device
        self.parent = parent
        self.setWindowTitle("Edit Action")

        self.type = QComboBox()
        self.type.addItems([
            "tap", "long", "drag", "random_wait",
            "type_text", "hold_until_gone",
            "enable_image", "disable_image"
        ])
        self.type.currentTextChanged.connect(self.build)

        self.wait_min = QDoubleSpinBox()
        self.wait_min.setRange(0, 30)
        self.wait_min.setDecimals(2)

        self.wait_max = QDoubleSpinBox()
        self.wait_max.setRange(0, 30)
        self.wait_max.setDecimals(2)

        self.x = QSpinBox(); self.x.setRange(-9999, 9999)
        self.y = QSpinBox(); self.y.setRange(-9999, 9999)
        self.x2 = QSpinBox(); self.x2.setRange(-9999, 9999)
        self.y2 = QSpinBox(); self.y2.setRange(-9999, 9999)

        self.dur = QSpinBox(); self.dur.setMaximum(10000)
        self.delay = QDoubleSpinBox(); self.delay.setMaximum(10)

        self.text_input = QLineEdit()
        self.enter_check = QCheckBox("Press Enter after typing")

        # 🔥 이미지 선택 콤보박스
        self.image_select = QComboBox()

        # 현재 디바이스의 이미지들 로드
        if self.device and self.device in self.parent.devices:
            for path in self.parent.devices[self.device]["targets"].keys():
                self.image_select.addItem(path)

        self.pick_start = QPushButton("Pick START (ABS)")
        self.pick_end = QPushButton("Pick END (ABS)")
        self.pick_start.clicked.connect(lambda: self.pick_offset("start"))
        self.pick_end.clicked.connect(lambda: self.pick_offset("end"))

        self.form = QFormLayout()
        self.form.addRow("Type", self.type)
        self.form.addRow("X", self.x)
        self.form.addRow("Y", self.y)
        self.form.addRow("X2", self.x2)
        self.form.addRow("Y2", self.y2)
        self.form.addRow("Duration (ms)", self.dur)
        self.form.addRow("Delay", self.delay)
        self.form.addRow("Wait Min (sec)", self.wait_min)
        self.form.addRow("Wait Max (sec)", self.wait_max)
        self.form.addRow("Text", self.text_input)
        self.form.addRow("", self.enter_check)

        # 🔥 enable/disable 이미지 선택 UI
        self.form.addRow("Target Image", self.image_select)

        layout = QVBoxLayout(self)
        layout.addLayout(self.form)
        layout.addWidget(self.pick_start)
        layout.addWidget(self.pick_end)

        btns = QHBoxLayout()
        btns.addWidget(QPushButton("Save", clicked=self.save))
        btns.addWidget(QPushButton("Cancel", clicked=self.reject))
        layout.addLayout(btns)

        # 기존 액션 로드
        if action:
            self.type.setCurrentText(action["type"])
            self.x.setValue(action.get("x", 0))
            self.y.setValue(action.get("y", 0))
            self.x2.setValue(action.get("x2", 0))
            self.y2.setValue(action.get("y2", 0))
            self.dur.setValue(action.get("duration", 500))
            self.delay.setValue(action.get("delay", 0))
            self.absolute = action.get("absolute", False)

            if action["type"] == "type_text":
                self.text_input.setText(action.get("text", ""))
                self.enter_check.setChecked(action.get("enter", False))

            # 🔥 enable/disable 이미지 선택 복원
            if action["type"] in ("enable_image", "disable_image"):
                tgt = action.get("target")
                if tgt:
                    idx = self.image_select.findText(tgt)
                    if idx >= 0:
                        self.image_select.setCurrentIndex(idx)

        self.build()


    def build(self):
        t = self.type.currentText()

        is_tap = t == "tap"
        is_long = t == "long"
        is_drag = t == "drag"
        is_wait = t == "random_wait"
        is_text = t == "type_text"
        is_hold = t == "hold_until_gone"  # 새로 추가

        # 모든 위젯 숨기기
        for w in (
            self.x, self.y, self.x2, self.y2,
            self.dur, self.wait_min, self.wait_max,
            self.text_input, self.enter_check
        ):
            w.setVisible(False)

        self.pick_start.setVisible(False)
        self.pick_end.setVisible(False)

        # tap/long/drag
        if is_tap or is_long or is_drag:
            self.x.setVisible(True)
            self.y.setVisible(True)
            self.pick_start.setVisible(True)

        # long
        if is_long:
            self.dur.setVisible(True)

        # drag
        if is_drag:
            self.x2.setVisible(True)
            self.y2.setVisible(True)
            self.dur.setVisible(True)
            self.pick_end.setVisible(True)

        # random_wait
        if is_wait:
            self.wait_min.setVisible(True)
            self.wait_max.setVisible(True)

        # type_text
        if is_text:
            self.text_input.setVisible(True)
            self.enter_check.setVisible(True)

        # hold_until_gone
        if is_hold:
            self.x.setVisible(True)
            self.y.setVisible(True)
            self.dur.setVisible(True)
            self.pick_start.setVisible(True)


    def pick_offset(self, mode):
        if not self.device:
            QMessageBox.warning(self, "Device Required", "No device selected for absolute picking.")
            return
        self._pick_mode = mode
        ScreenshotPickerAbsolute(self, self.device, self._set_abs).exec()

    def _set_abs(self, x, y):
        if self._pick_mode == "start":
            self.x.setValue(x)
            self.y.setValue(y)
        elif self._pick_mode == "end":
            self.x2.setValue(x)
            self.y2.setValue(y)
        self.absolute = True

    def save(self):
        t = self.type.currentText()

        a = {
            "type": t,
            "delay": self.delay.value(),
            "absolute": self.absolute
        }
        if t in ("enable_image", "disable_image"):
            a["target"] = self.image_select.currentText()
        if t in ("tap", "long", "drag", "hold_until_gone"):
            a["x"] = self.x.value()
            a["y"] = self.y.value()

        if t == "long":
            a["duration"] = self.dur.value()

        if t == "drag":
            a["x2"] = self.x2.value()
            a["y2"] = self.y2.value()
            a["duration"] = self.dur.value()

        if t == "random_wait":
            a["min"] = self.wait_min.value()
            a["max"] = self.wait_max.value()

        if t == "type_text":
            a["text"] = self.text_input.text()
            a["enter"] = self.enter_check.isChecked()

        self.result = a
        self.accept()


# =====================================================
# Main Window
# =====================================================
class MacroWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("VegaMax: Re")

        self.resize(1500, 750)
        self.setStyleSheet("""
/* =========================
   GLOBAL
   ========================= */
QWidget {
    background-color: #0f172a;
    color: #e5e7eb;
    font-size: 13px;
}

/* =========================
   LABEL
   ========================= */
QLabel {
    font-weight: 600;
}

/* =========================
   LIST / TEXT
   ========================= */
QListWidget, QTextEdit {
    background-color: #020617;
    border: 1px solid #1e293b;
    border-radius: 6px;
    padding: 6px;
}

QListWidget::item {
    padding: 6px;
}

QListWidget::item:selected {
    background-color: #1e293b;
    border-left: 3px solid #38bdf8;
}

/* =========================
   BUTTON
   ========================= */
QPushButton {
    background-color: #020617;
    border: 1px solid #1e293b;
    border-radius: 6px;
    padding: 6px 10px;
}

QPushButton:hover {
    background-color: #1e293b;
}

QPushButton:pressed {
    background-color: #334155;
}

/* =========================
   INPUT
   ========================= */
QSpinBox, QDoubleSpinBox, QComboBox {
    background-color: #020617;
    border: 1px solid #1e293b;
    border-radius: 6px;
    padding: 4px;
}

/* =========================
   SCROLL BAR (깔끔한 얇은 스타일)
   ========================= */
/* =========================
   SCROLL BAR (ADVANCED)
   ========================= */

/* 공통 */
QScrollBar {
    background: transparent;
}

/* ---------- 세로 ---------- */
QScrollBar:vertical {
    width: 8px;
    margin: 2px;
}

QScrollBar::handle:vertical {
    background: rgba(148, 163, 184, 0.35);  /* slate-300 느낌 */
    border-radius: 4px;
    min-height: 32px;
}

QScrollBar::handle:vertical:hover {
    background: rgba(148, 163, 184, 0.6);
}

QScrollBar::handle:vertical:pressed {
    background: rgba(226, 232, 240, 0.9);
}

/* 위/아래 버튼 제거 */
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
    height: 0px;
    background: none;
}

/* 트랙 */
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
    background: none;
}

/* ---------- 가로 ---------- */
QScrollBar:horizontal {
    height: 8px;
    margin: 2px;
}

QScrollBar::handle:horizontal {
    background: rgba(148, 163, 184, 0.35);
    border-radius: 4px;
    min-width: 32px;
}

QScrollBar::handle:horizontal:hover {
    background: rgba(148, 163, 184, 0.6);
}

QScrollBar::handle:horizontal:pressed {
    background: rgba(226, 232, 240, 0.9);
}

QScrollBar::add-line:horizontal,
QScrollBar::sub-line:horizontal {
    width: 0px;
    background: none;
}

QScrollBar::add-page:horizontal,
QScrollBar::sub-page:horizontal {
    background: none;
}

/* ---------- 코너 (QSplitter + QTextEdit 조합 대비) ---------- */
QScrollBar::corner {
    background: transparent;
}


/* =========================
   STATUS LABEL 강조용
   ========================= */
QLabel#status {
    font-size: 14px;
    font-weight: bold;
}
""")

        self.devices = {}  # device_id -> {targets, running, last_center, log_q, thread}
        self.current_device = None
        self.current_target = None  # path string or None

        self.build_ui()
        self.start_log_timer()
        self.set_app_icon()

    def set_app_icon(self):
        try:
            if getattr(sys, "frozen", False):
                base_path = sys._MEIPASS
            else:
                base_path = os.path.dirname(os.path.abspath(__file__))

            icon_path = os.path.join(base_path, "vegamax.ico")

            if os.path.exists(icon_path):
                icon = QIcon(icon_path)
                self.setWindowIcon(icon)
                QApplication.instance().setWindowIcon(icon)
            else:
                print("아이콘 파일 없음:", icon_path)
        except Exception as e:
            print("아이콘 설정 실패:", e)
    def device_menu(self, pos):
      it = self.device_list.itemAt(pos)
      if not it:
          return

      menu = QMenu(self)
      menu.addAction("Delete", self.delete_device)
      menu.exec(self.device_list.mapToGlobal(pos))
    def delete_device(self):
      it = self.device_list.currentItem()
      if not it:
          QMessageBox.warning(self, "No Selection", "Select a device to delete")
          return

      device = it.text()

      # 실행 중이면 중지
      if self.devices.get(device, {}).get("running"):
          self.devices[device]["running"] = False

      # 확인
      if QMessageBox.question(
          self,
          "Delete Device",
          f"Remove device '{device}' from list?\n(ADB 연결은 유지됩니다)",
          QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
      ) != QMessageBox.StandardButton.Yes:
          return

      # 내부 데이터 삭제
      del self.devices[device]

      # UI 삭제
      row = self.device_list.row(it)
      self.device_list.takeItem(row)

      # 현재 선택 장치면 초기화
      if self.current_device == device:
          self.current_device = None
          self.current_target = None
          self.status.setText("NO DEVICE SELECTED")
          self.target_list.clear()
          self.action_list.clear()

      self.log.append(f"[SYSTEM] Device removed: {device}")
    def capture_screen(self):
      if not self.current_device:
          QMessageBox.warning(self, "No Device", "Select a device first")
          return

      img = adb_screencap(self.current_device)
      if img is None:
          QMessageBox.critical(self, "ADB Error", "Failed to capture screen")
          return

      RoiPickerSave(self, self.current_device, img).exec()

    # ---------------- UI ----------------
    def build_ui(self):
        splitter = QSplitter(Qt.Orientation.Horizontal)
        self.setCentralWidget(splitter)

        # LEFT (Devices)
        left = QWidget()
        lv = QVBoxLayout(left)
        self.device_list = QListWidget()
        self.device_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.device_list.customContextMenuRequested.connect(self.device_menu)
        self.device_list.currentItemChanged.connect(self.on_device_selected)
        lv.addWidget(QLabel("Devices"))
        lv.addWidget(self.device_list)

        btn_row = QHBoxLayout()
        btn_row.addWidget(QPushButton("Add Device", clicked=self.add_device))
        btn_row.addWidget(QPushButton("Refresh", clicked=self.refresh_device_list))
        lv.addLayout(btn_row)

        # MID (Images)
        mid = QWidget()
        mv = QVBoxLayout(mid)
        self.target_list = QListWidget()
        mv.addWidget(QLabel("Images"))
        mv.addWidget(self.target_list)
        mv.addWidget(QPushButton("Add Image", clicked=self.add_target))
        mv.addWidget(QPushButton("Remove Image", clicked=self.del_target))

        # MID2 (Actions)
        mid2 = QWidget()
        mv2 = QVBoxLayout(mid2)
        self.action_list = QListWidget()
        mv2.addWidget(QLabel("Actions"))
        mv2.addWidget(self.action_list)
        mv2.addWidget(QPushButton("Add Action", clicked=self.add_action))
        mv2.addWidget(QPushButton("Edit Action", clicked=self.edit_action))
        mv2.addWidget(QPushButton("Delete Action", clicked=self.del_action))
        mv2.addWidget(QPushButton("Move Up", clicked=self.move_action_up))
        mv2.addWidget(QPushButton("Move Down", clicked=self.move_action_down))

        # RIGHT (Control + Log)
        right = QWidget()
        rv = QVBoxLayout(right)

        self.status = QLabel("NO DEVICE SELECTED")
        rv.addWidget(self.status)
        rv.addWidget(QPushButton("Start / Stop (per-device)", clicked=self.toggle))
        rv.addWidget(QPushButton("Save Profile", clicked=self.save_profile))
        rv.addWidget(QPushButton("Load Profile", clicked=self.load_profile))
        rv.addWidget(QPushButton("Set ADB Path", clicked=self.set_adb_path))
        rv.addWidget(QPushButton("Capture", clicked=self.capture_screen))
        # ------ Test 버튼 추가 ------
        rv.addWidget(QPushButton("Test (Find Best Match)", clicked=self.test_match))

        rv.addWidget(QLabel("Log"))
        self.log = QTextEdit()
        self.log.setReadOnly(True)
        rv.addWidget(self.log, 1)

        splitter.addWidget(left)
        splitter.addWidget(mid)
        splitter.addWidget(mid2)
        splitter.addWidget(right)
        splitter.setSizes([220, 350, 350, 480])

        self.target_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.target_list.customContextMenuRequested.connect(self.image_menu)
        self.target_list.currentItemChanged.connect(self.on_target_selected)
    def on_target_selected(self, it):
        self.action_list.clear()

        if not it or not self.current_device:
            self.current_target = None
            return

        path = it.data(Qt.ItemDataRole.UserRole)
        if not path:
            self.current_target = None
            return

        self.current_target = path
        self.refresh_actions()



    def refresh_device_list(self):
        # show currently connected devices (no auto-add)
        devs = list_adb_devices()
        QMessageBox.information(self, "Devices", f"Connected devices:\n{os.linesep.join(devs) if devs else '(none)'}")

    def add_device(self):
        devs = list_adb_devices()
        if not devs:
            QMessageBox.critical(self, "ADB Error", "No ADB devices connected")
            return
        d, ok = QInputDialog.getItem(self, "Add Device", "Select device:", devs, 0, False)
        if not ok:
            return
        if d in self.devices:
            QMessageBox.information(self, "Already Added", f"Device {d} is already added.")
            return

        self.devices[d] = {
            "img": {},
            "targets": {},
            "running": False,
            "last_center": (0, 0),
            "log_q": queue.Queue(),
            "thread": None
        }
        self.device_list.addItem(d)
        self.log_msg_device(d, f"Device added: {d}")

    def on_device_selected(self, it):
        if not it:
            self.current_device = None
            self.status.setText("NO DEVICE SELECTED")
            self.target_list.clear()
            self.action_list.clear()
            return

        d = it.text()
        self.current_device = d

        st = "RUNNING" if self.devices[d]["running"] else "STOPPED"
        self.status.setText(f"Device: {d} — {st}")

        # 🔥 핵심: 먼저 UI 리셋
        self.target_list.clear()
        self.action_list.clear()
        self.current_target = None

        self.refresh_target_list()


    def image_menu(self, pos):
        it = self.target_list.itemAt(pos)
        if not it:
            return

        path = it.data(Qt.ItemDataRole.UserRole)
        t = self.devices[self.current_device]["targets"].get(path)
        if not t:
            return

        menu = QMenu(self)

        if t.get("enabled", True):
            act_toggle = QAction("Disable Image", self)
            act_toggle.triggered.connect(lambda: self.set_image_enabled(path, False))
        else:
            act_toggle = QAction("Enable Image", self)
            act_toggle.triggered.connect(lambda: self.set_image_enabled(path, True))

        menu.addAction(act_toggle)
        menu.addSeparator()

        act_cd = QAction("Set Cooldown", self)
        act_roi = QAction("Set Detection Area", self)
        act_clear = QAction("Clear Detection Area", self)
        act_kill = QAction("Set Kill App Package", self)
        act_kill_clear = QAction("Clear Kill App", self)
        act_launch = QAction("Set Launch App Package", self)
        act_launch_clear = QAction("Clear Launch App", self)
        act_stop_after = QAction("Stop Macro After Works", self)
        act_stop_clear = QAction("Clear Stop Flag", self)
        act_thresh = QAction("Set Match Threshold", self)

        act_cd.triggered.connect(lambda: self.set_cooldown(t))
        act_roi.triggered.connect(lambda: self.set_roi(t))
        act_clear.triggered.connect(lambda: self.clear_roi(t))
        act_kill.triggered.connect(lambda: self.set_kill_package(t))
        act_kill_clear.triggered.connect(lambda: self.clear_kill_package(t))
        act_launch.triggered.connect(lambda: self.set_launch_package(t))
        act_launch_clear.triggered.connect(lambda: self.clear_launch_package(t))
        act_stop_after.triggered.connect(lambda: self.set_stop_after(t))
        act_stop_clear.triggered.connect(lambda: self.clear_stop_after(t))
        act_thresh.triggered.connect(lambda: self.set_threshold(t))

        menu.addAction(act_cd)
        menu.addAction(act_roi)
        menu.addAction(act_clear)
        menu.addSeparator()
        menu.addAction(act_kill)
        menu.addAction(act_kill_clear)
        menu.addSeparator()
        menu.addAction(act_launch)
        menu.addAction(act_launch_clear)
        menu.addSeparator()
        menu.addAction(act_stop_after)
        menu.addAction(act_stop_clear)
        menu.addSeparator()
        menu.addAction(act_thresh)

        menu.exec(self.target_list.mapToGlobal(pos))

    def set_image_enabled(self, path, enabled):
        if not self.current_device:
            return

        t = self.devices[self.current_device]["targets"].get(path)
        if not t:
            return

        t["enabled"] = enabled

        state = "ENABLED" if enabled else "DISABLED"
        self.log_msg_device(self.current_device, f"Image {state}: {os.path.basename(path)}")

        self.refresh_target_list()

    def refresh_target_list(self):
        self.target_list.clear()

        if not self.current_device:
            return

        for path, t in self.devices[self.current_device]["targets"].items():
            label = path
            if not t.get("enabled", True):
                label = f"[OFF] {path}"

            item = QListWidgetItem(label)
            item.setData(Qt.ItemDataRole.UserRole, path)

            if not t.get("enabled", True):
                item.setForeground(Qt.GlobalColor.gray)

            self.target_list.addItem(item)



    def set_cooldown(self, t):
        v, ok = QInputDialog.getDouble(self, "Set Cooldown", "Cooldown (seconds)", t.get("cooldown", 0.0), 0.0, 60.0, 1)
        if ok:
            t["cooldown"] = v
            self.log_msg_device(self.current_device, f"Cooldown set to {v}s")

    def set_roi(self, t):
        if not self.current_device:
            return
        RoiPicker(self, self.current_device, lambda r: self._apply_roi(t, r)).exec()

    def _apply_roi(self, t, r):
        t["roi"] = r
        self.log_msg_device(self.current_device, f"ROI set to {r}")

    def clear_roi(self, t):
        t["roi"] = None
        self.log_msg_device(self.current_device, "ROI cleared")

    def set_kill_package(self, t):
        pkg, ok = QInputDialog.getText(self, "Kill App Package", "Enter package name (e.g. com.example.app):", text=t.get("kill_package", ""))
        if ok and pkg.strip():
            t["kill_package"] = pkg.strip()
            self.log_msg_device(self.current_device, f"Kill app set: {pkg.strip()}")

    def clear_kill_package(self, t):
        t["kill_package"] = None
        self.log_msg_device(self.current_device, "Kill app cleared")

    def set_launch_package(self, t):
        pkg, ok = QInputDialog.getText(self, "Launch App Package", "Enter package name to launch (e.g. com.example.app):", text=t.get("launch_package", ""))
        if ok and pkg.strip():
            t["launch_package"] = pkg.strip()
            self.log_msg_device(self.current_device, f"Launch app set: {pkg.strip()}")

    def clear_launch_package(self, t):
        t["launch_package"] = None
        self.log_msg_device(self.current_device, "Launch app cleared")

    def set_stop_after(self, t):
        t["stop_after"] = True
        self.log_msg_device(self.current_device, "Stop-after flag set")

    def clear_stop_after(self, t):
        t["stop_after"] = False
        self.log_msg_device(self.current_device, "Stop-after flag cleared")

    def set_threshold(self, t):
        current = float(t.get("threshold", DEFAULT_THRESHOLD))
        v, ok = QInputDialog.getDouble(self, "Set Match Threshold", "Matching threshold (0.0 - 1.0)", current, 0.0, 1.0, 2)
        if ok:
            t["threshold"] = float(v)
            self.log_msg_device(self.current_device, f"Threshold set to {v:.2f}")

    # ---------------- Logic ----------------
    def add_target(self):
        if not self.current_device:
            QMessageBox.warning(self, "No Device", "Select a device first")
            return
        f, _ = QFileDialog.getOpenFileName(self, "", "", "Images (*.png *.jpg)")
        img = cv2_imread_safe(f)
        if img is None:
            return
        d = self.devices[self.current_device]
        d["targets"][f] = {
            "img": img,
            "actions": [],
            "cooldown": 0.0,
            "last_hit": 0.0,
            "roi": None,
            "kill_package": None,
            "launch_package": None,
            "stop_after": False,
            "threshold": DEFAULT_THRESHOLD,
            "enabled": True
        }
        preprocess_template_hs(d["targets"][f])
        item = QListWidgetItem(f)
        item.setData(Qt.ItemDataRole.UserRole, f)
        self.target_list.addItem(item)

        self.log_msg_device(self.current_device, f"Image added: {os.path.basename(f)}")

    def del_target(self):
        tpath = self.current_target
        if not tpath:
            return
        del self.devices[self.current_device]["targets"][tpath]
        self.target_list.takeItem(self.target_list.currentRow())
        self.action_list.clear()
        self.current_target = None
        self.log_msg_device(self.current_device, f"Image removed: {os.path.basename(tpath)}")



    def refresh_actions(self):
        self.action_list.clear()
        t = self.current_target
        if not t or not self.current_device:
            return
        for a in self.devices[self.current_device]["targets"][t]["actions"]:
            tag = "[ABS]" if a.get("absolute") else "[REL]"
            self.action_list.addItem(f"{tag} {a}")

    def add_action(self):
        tpath = self.current_target
        if not tpath or not self.current_device:
            return
        dlg = ActionEditor(self, device=self.current_device)
        if dlg.exec() and dlg.result:
            self.devices[self.current_device]["targets"][tpath]["actions"].append(dlg.result)
            self.refresh_actions()
            self.log_msg_device(self.current_device, f"Action added ({'ABS' if dlg.result.get('absolute') else 'REL'})")

    def edit_action(self):
        tpath = self.current_target
        i = self.action_list.currentRow()
        if not tpath or i < 0 or not self.current_device:
            return
        actions = self.devices[self.current_device]["targets"][tpath]["actions"]
        dlg = ActionEditor(self, actions[i], device=self.current_device)
        if dlg.exec() and dlg.result:
            actions[i] = dlg.result
            self.refresh_actions()

    def del_action(self):
        tpath = self.current_target
        i = self.action_list.currentRow()
        if tpath and i >= 0:
            self.devices[self.current_device]["targets"][tpath]["actions"].pop(i)
            self.refresh_actions()

    def move_action_up(self):
        if not self.current_device or not self.current_target:
            return

        row = self.action_list.currentRow()
        if row <= 0:
            return

        t = self.current_target
        acts = self.devices[self.current_device]["targets"][t]["actions"]

        acts[row - 1], acts[row] = acts[row], acts[row - 1]

        self.refresh_actions()
        self.action_list.setCurrentRow(row - 1)



    def move_action_down(self):
        if not self.current_device or not self.current_target:
            return

        row = self.action_list.currentRow()
        if row < 0:
            return

        t = self.current_target
        acts = self.devices[self.current_device]["targets"][t]["actions"]

        if row >= len(acts) - 1:
            return

        acts[row + 1], acts[row] = acts[row], acts[row + 1]

        self.refresh_actions()
        self.action_list.setCurrentRow(row + 1)


    def save_profile(self):
        if not self.current_device:
            QMessageBox.warning(self, "No Device", "Select a device first")
            return

        f, _ = QFileDialog.getSaveFileName(self, "", "profile.json", "JSON (*.json)")
        if not f:
            return

        dv = self.devices[self.current_device]

        out = {}
        for path, t in dv["targets"].items():
            out[path] = {
                "actions": t.get("actions", []),
                "cooldown": t.get("cooldown", 0.0),
                "roi": t.get("roi"),
                "kill_package": t.get("kill_package"),
                "launch_package": t.get("launch_package"),
                "stop_after": t.get("stop_after", False),
                "threshold": t.get("threshold", DEFAULT_THRESHOLD),
                "enabled": t.get("enabled", True)
            }

        with open(f, "w", encoding="utf-8") as fp:
            json.dump(out, fp, indent=2, ensure_ascii=False)

        self.log.append(f"[PROFILE] Profile saved for device: {self.current_device}")


    def load_profile(self):
        if not self.current_device:
            QMessageBox.warning(self, "No Device", "Select a device first")
            return

        f, _ = QFileDialog.getOpenFileName(self, "Load Profile", "", "JSON (*.json)")
        if not f:
            return

        with open(f, encoding="utf-8") as fp:
            data = json.load(fp)

        dev = self.current_device

        self.devices[dev]["targets"].clear()

        for path, v in data.items():
            img = cv2_imread_safe(path)
            if img is None:
                self.log.append(f"[PROFILE] Image not found: {path}")
                continue

            self.devices[dev]["targets"][path] = {
                "img": img,
                "actions": v.get("actions", []),
                "cooldown": float(v.get("cooldown", 0.0)),
                "last_hit": 0.0,
                "roi": v.get("roi"),
                "kill_package": v.get("kill_package"),
                "launch_package": v.get("launch_package"),
                "stop_after": v.get("stop_after", False),
                "threshold": float(v.get("threshold", DEFAULT_THRESHOLD)),
                "enabled": v.get("enabled", True)
            }
            preprocess_template_hs(self.devices[dev]["targets"][path])

        self.target_list.clear()
        for path, t in self.devices[dev]["targets"].items():
            label = path
            if not t.get("enabled", True):
                label = f"[OFF] {path}"

            item = QListWidgetItem(label)
            item.setData(Qt.ItemDataRole.UserRole, path)

            if not t.get("enabled", True):
                item.setForeground(Qt.GlobalColor.gray)

            self.target_list.addItem(item)


        self.log.append(f"[PROFILE] Loaded profile into device: {dev}")


    def toggle(self):
        if not self.current_device:
            QMessageBox.warning(self, "No Device", "Select a device first")
            return
        d = self.devices[self.current_device]
        d["running"] = not d["running"]
        self.status.setText(f"Device: {self.current_device} — {'RUNNING' if d['running'] else 'STOPPED'}")
        self.log_msg_device(self.current_device, "Macro started" if d["running"] else "Macro stopped")
        if d["running"] and (not d.get("thread") or not d["thread"].is_alive()):
            d["thread"] = threading.Thread(target=self.worker, args=(self.current_device,), daemon=True)
            d["thread"].start()

    def worker(self, device):
        while self.devices.get(device, {}).get("running"):
            scr = adb_screencap(device)
            if scr is None:
                time.sleep(0.3)
                continue

            now = time.time()

            targets = self.devices[device]["targets"]

            # ROI 그룹화 (같은 ROI는 한번만 스캔)
            roi_groups = {}
            for path, t in targets.items():
                if not t.get("enabled", True):    # 🔥 비활성 이미지 무시
                    continue
                roi = tuple(t.get("roi")) if t.get("roi") else None
                roi_groups.setdefault(roi, []).append(path)

            for roi, paths in roi_groups.items():
                if roi:
                    rx, ry, rw, rh = roi
                    search = scr[ry:ry+rh, rx:rx+rw]
                    if search.size == 0:
                        continue
                else:
                    search = scr

                # 🔥 병렬 HSV 매칭
                subset = {p: targets[p] for p in paths}
                matches = match_all_templates_hs(search, subset, DEFAULT_THRESHOLD)

                for m in matches:
                    p = m["path"]
                    t = targets[p]

                    cd = t.get("cooldown", 0)
                    last = t.get("last_hit", 0)
                    if cd > 0 and now - last < cd:
                        continue

                    cx, cy = m["center"]
                    if roi:
                        cx += rx
                        cy += ry

                    t["last_hit"] = now
                    self.devices[device]["last_center"] = (cx, cy)

                    self.log_msg_device(device, f"Matched: {os.path.basename(p)} @ {(cx, cy)} ({m['score']:.2f})")

                    pkg = t.get("kill_package")
                    if pkg:
                        adb_force_stop(device, pkg)
                        time.sleep(0.3)

                    launch_pkg = t.get("launch_package")
                    if launch_pkg:
                        adb_launch_app(device, launch_pkg)
                        time.sleep(0.5)

                    for a in t["actions"]:
                        typ = a["type"]

                        if typ == "random_wait":
                            mn = a.get("min", 0)
                            mx = a.get("max", 0)
                            if mx >= mn and mx > 0:
                                time.sleep(random.uniform(mn, mx))
                            time.sleep(a.get("delay", 0))
                            continue

                        if typ == "type_text":
                            adb_input_text(device, a.get("text", ""))
                            if a.get("enter"):
                                adb_keyevent(device, 66)
                            time.sleep(a.get("delay", 0))
                            continue
                        if typ == "disable_image":
                            tgt = a.get("target")
                            if tgt in targets:
                                targets[tgt]["enabled"] = False
                                self.log_msg_device(device, f"Image disabled: {os.path.basename(tgt)}")
                                if device == self.current_device:
                                    self.refresh_target_list()
                                continue
                        if typ == "enable_image":
                            tgt = a.get("target")
                            if tgt in targets:
                                targets[tgt]["enabled"] = True
                                self.log_msg_device(device, f"Image enabled: {os.path.basename(tgt)}")
                                if device == self.current_device:
                                    self.refresh_target_list()
                                continue

                        if a.get("absolute"):
                            sx, sy = a["x"], a["y"]
                        else:
                            sx = cx + a["x"]
                            sy = cy + a["y"]

                        if typ == "tap":
                            adb_tap(device, sx, sy)

                        elif typ == "long":
                            adb_long(device, sx, sy, a.get("duration", 500))

                        elif typ == "hold_until_gone":
                            self.log_msg_device(device, f"Hold-until-gone at {(sx, sy)}")
                            while True:
                                adb_long(device, sx, sy, a.get("duration", 500))
                                time.sleep(a.get("delay", 0.4))

                                scr_check = adb_screencap(device)
                                if scr_check is None:
                                    break

                                if roi:
                                    chk = scr_check[ry:ry+rh, rx:rx+rw]
                                else:
                                    chk = scr_check

                                # HSV(H+S)로 다시 검사
                                hits = match_all_templates_hs(chk, {p: t}, DEFAULT_THRESHOLD)
                                if not hits:
                                    break

                        elif typ == "drag":
                            if a.get("absolute"):
                                ex, ey = a["x2"], a["y2"]
                            else:
                                ex = cx + a["x2"]
                                ey = cy + a["y2"]

                            adb_swipe(device, sx, sy, ex, ey, a.get("duration", 500))

                        time.sleep(a.get("delay", 0))

                    if t.get("stop_after"):
                        self.log_msg_device(device, "Stop-after image triggered. Macro stopped.")
                        self.devices[device]["running"] = False
                        if device == self.current_device:
                            self.status.setText(f"Device: {device} — STOPPED")
                        return

            time.sleep(0.2)



    def start_log_timer(self):
        t = QTimer(self)
        t.timeout.connect(self.flush_log)
        t.start(150)

    def flush_log(self):
        # collect logs from all devices
        for dev, dv in self.devices.items():
            q = dv.get("log_q")
            while not q.empty():
                self.log.append(f"[{dev}] {q.get()}")

    def log_msg_device(self, device, msg):
        ts = time.strftime("%H:%M:%S")
        q = self.devices.get(device, {}).get("log_q")
        if q:
            q.put(f"[{ts}] {msg}")
        else:
            self.log.append(f"[{device}] [{ts}] {msg}")

    def set_adb_path(self):
        global ADB_PATH

        path, _ = QFileDialog.getOpenFileName(self, "Select adb.exe", "", "adb.exe (adb.exe)")

        if not path:
            return

        if not os.path.basename(path).lower().startswith("adb"):
            QMessageBox.warning(self, "Invalid File", "Please select adb.exe")
            return

        if not check_adb(path):
            QMessageBox.critical(self, "ADB Error", "Selected adb.exe is not valid.")
            return

        ADB_PATH = path
        CONFIG["adb_path"] = path
        save_config(CONFIG)

        self.log.append(f"ADB path set to: {path}")

    # ------------------------------
    # Test 버튼 핸들러: 현재 스크린샷 찍고 가장 높은 매칭을 보여줌
    # ------------------------------
    def test_match(self):
        if not self.current_device:
            QMessageBox.warning(self, "No Device", "Select a device first")
            return

        scr = adb_screencap(self.current_device)
        if scr is None:
            QMessageBox.critical(self, "ADB Error", "Failed to capture screen")
            return

        targets = self.devices[self.current_device]["targets"]
        if not targets:
            QMessageBox.information(self, "No Targets", "No target images added for this device.")
            return

        # ensure templates preprocessed
        for p, t in targets.items():
            if t.get("img_hs") is None:
                preprocess_template_hs(t)

        matches = match_all_templates_hs(scr, targets, DEFAULT_THRESHOLD)
        if not matches:
            QMessageBox.information(self, "No Match", "No matches found (score too low).")
            return

        best = matches[0]
        # Show dialog with annotated screenshot
        tpl_img = None
        try:
            tpl_img = targets[best["path"]].get("img")
        except:
            tpl_img = cv2_imread_safe(best["path"])

        dlg = TestResultDialog(self, scr, best, tpl_img=tpl_img)
        dlg.exec()

# =====================================================
# RUN
# =====================================================
if __name__ == "__main__":
    app = QApplication(sys.argv)

    if not os.path.exists(ADB_PATH) or not check_adb(ADB_PATH):
        QMessageBox.critical(None, "ADB Error", "ADB not found.\nPlease set adb.exe path.")
        # show a main window anyway to allow user to set path

    w = MacroWindow()
    w.show()
    sys.exit(app.exec())
