#!/usr/bin/env jython

# ===========================================
# hotkey-c.py
# Hotkey 配置工具（Job: Start / Step / Stop）
# - 基于 MainFrame.getHotkeyActionMap() 做自定义绑定
# - 将配置持久化到 script-state.xml 的 key：
#     hotkey.job.start
#     hotkey.job.step
#     hotkey.job.stop
# - 启动时会自动加载并应用上次保存的按键
# - 应用新热键时会移除旧的 Start/Step/Stop 绑定
# - 空输入表示禁用该动作热键
# - 检查重复键并防止与其他已存在绑定冲突
# ===========================================

from javax.swing import JDialog, JPanel, JLabel, JTextField, JButton, BoxLayout, JOptionPane, Box
from java.awt import BorderLayout
from java.awt.event import KeyAdapter, KeyEvent
from javax.swing import SwingUtilities, KeyStroke
import java.lang
import time

try:
    from org.openpnp.model import Configuration
    from org.openpnp.gui import MainFrame
except Exception:
    Configuration = None
    MainFrame = None

# script-state keys
_STATE_KEYS = {
    "start": "hotkey.job.start",
    "step": "hotkey.job.step",
    "stop": "hotkey.job.stop",
}


def _get_mainframe():
    try:
        return MainFrame.get()
    except Exception:
        return None


def _get_job_panel(frame):
    if frame is None:
        return None
    # Try several access patterns for compatibility
    try:
        return frame.getJobPanel()
    except Exception:
        pass
    try:
        return frame.getJobTab()
    except Exception:
        pass
    try:
        # try field access
        return frame.jobPanel
    except Exception:
        pass
    return None


def _get_hotkey_map(frame):
    try:
        return frame.getHotkeyActionMap()
    except Exception:
        return None


def _get_action_map(frame):
    job_panel = _get_job_panel(frame)
    if job_panel is None:
        return None
    try:
        return {
            "start": job_panel.startPauseResumeJobAction,
            "step": job_panel.stepJobAction,
            "stop": job_panel.stopJobAction,
        }
    except Exception:
        return None


def _find_keystroke_for_action(hotkey_map, action):
    if hotkey_map is None or action is None:
        return None
    try:
        entries = list(hotkey_map.entrySet())
    except Exception:
        return None
    for entry in entries:
        try:
            if entry.getValue() == action:
                return entry.getKey()
        except Exception:
            pass
    return None


def _normalize_keystroke(text):
    if text is None:
        return None
    s = text.strip()
    if not s:
        return None
    try:
        ks = KeyStroke.getKeyStroke(s)
    except Exception:
        ks = None
    if ks is None:
        return None
    try:
        return ks.toString()
    except Exception:
        return None


def _keystroke_from_event(evt):
    try:
        return KeyStroke.getKeyStrokeForEvent(evt)
    except Exception:
        return None


def _keystroke_to_text(ks):
    if ks is None:
        return None
    try:
        return ks.toString()
    except Exception:
        return None


def _load_saved_hotkeys(cfg):
    saved = {}
    try:
        state = cfg.scriptState
    except Exception:
        state = None
    for k, state_key in _STATE_KEYS.items():
        v = None
        try:
            if state is not None and state.containsKey(state_key):
                v = state.get(state_key)
        except Exception:
            v = None
        saved[k] = v
    return saved


def _save_hotkeys(cfg, mapping):
    try:
        state = cfg.scriptState
    except Exception:
        state = None
    if state is None:
        return False
    for k, state_key in _STATE_KEYS.items():
        val = mapping.get(k)
        try:
            if val:
                state.put(state_key, val)
            else:
                if state.containsKey(state_key):
                    state.remove(state_key)
        except Exception:
            pass
    try:
        cfg.save()
    except Exception:
        try:
            frame = _get_mainframe()
            if frame is not None:
                frame.saveConfig()
        except Exception:
            pass
    return True


def _check_conflicts(hotkey_map, new_mapping, own_actions):
    # new_mapping: dict name->normalized string or None
    # own_actions: set of Action instances that belong to Start/Step/Stop
    # Check duplicates in new_mapping
    values = [v for v in [new_mapping.get('start'), new_mapping.get('step'), new_mapping.get('stop')] if v]
    if len(values) != len(set(values)):
        return (False, "Duplicate hotkeys among Start/Step/Stop")
    # Check conflict with existing bindings (keys used by other actions)
    try:
        entries = list(hotkey_map.entrySet())
    except Exception:
        entries = []
    for name, val in new_mapping.items():
        if not val:
            continue
        try:
            ks = KeyStroke.getKeyStroke(val)
        except Exception:
            return (False, "Invalid keystroke: %s" % val)
        for entry in entries:
            try:
                existingKs = entry.getKey()
                existingAction = entry.getValue()
                if existingKs is None:
                    continue
                # If same keystroke and existingAction is not one of our job actions, it's a conflict
                if existingKs.equals(ks) and (existingAction not in own_actions):
                    return (False, "Conflict: key %s already bound to another action" % val)
            except Exception:
                pass
    return (True, None)


def _apply_hotkeys(frame, action_map, mapping):
    hotkey_map = _get_hotkey_map(frame)
    if hotkey_map is None:
        return False
    # gather our actions
    own_actions = set()
    for k in ('start', 'step', 'stop'):
        try:
            a = action_map.get(k)
            if a is not None:
                own_actions.add(a)
        except Exception:
            pass
    # remove existing entries that map to our actions
    try:
        entries = list(hotkey_map.entrySet())
    except Exception:
        entries = []
    for entry in entries:
        try:
            if entry.getValue() in own_actions:
                try:
                    hotkey_map.remove(entry.getKey())
                except Exception:
                    pass
        except Exception:
            pass
    # put new ones
    for key_name, action in action_map.items():
        txt = mapping.get(key_name)
        if txt:
            try:
                ks = KeyStroke.getKeyStroke(txt)
            except Exception:
                ks = None
            if ks is not None:
                try:
                    hotkey_map.put(ks, action)
                except Exception:
                    pass
    return True


class HotkeyDialog(object):
    def __init__(self):
        self.frame = _get_mainframe()
        self.cfg = Configuration.get() if Configuration is not None else None
        self.action_map = _get_action_map(self.frame) if self.frame is not None else None
        self.saved = _load_saved_hotkeys(self.cfg) if self.cfg is not None else {}

        self.dialog = JDialog()
        self.dialog.setModal(False)
        self.dialog.setTitle("Job Hotkeys (hotkey-c)")
        self.dialog.setSize(520, 220)
        self.dialog.setLocationRelativeTo(None)
        self.dialog.setLayout(BorderLayout())

        panel = JPanel()
        panel.setLayout(BoxLayout(panel, BoxLayout.Y_AXIS))

        self.input_start = JTextField(26)
        self.input_step = JTextField(26)
        self.input_stop = JTextField(26)

        self._init_field("Start", "start", self.input_start, panel)
        self._init_field("Step", "step", self.input_step, panel)
        self._init_field("Stop", "stop", self.input_stop, panel)

        panel.add(Box.createVerticalStrut(10))
        hint = JLabel("Click field, press hotkey to record. Example: ctrl shift R. Empty to disable.")
        panel.add(hint)

        self.dialog.add(panel, BorderLayout.CENTER)

        btn_panel = JPanel()
        btn_panel.setLayout(BoxLayout(btn_panel, BoxLayout.X_AXIS))
        self.btn_apply = JButton("Apply")
        self.btn_cancel = JButton("Close")
        btn_panel.add(self.btn_apply)
        btn_panel.add(Box.createHorizontalStrut(8))
        btn_panel.add(self.btn_cancel)
        self.dialog.add(btn_panel, BorderLayout.SOUTH)

        self.btn_apply.addActionListener(self._on_apply)
        self.btn_cancel.addActionListener(self._on_close)

    def _refresh_context(self):
        if self.frame is None:
            self.frame = _get_mainframe()
        if self.cfg is None and Configuration is not None:
            try:
                if Configuration.isInstanceInitialized():
                    self.cfg = Configuration.get()
            except Exception:
                try:
                    self.cfg = Configuration.get()
                except Exception:
                    pass
        if self.frame is not None:
            self.action_map = _get_action_map(self.frame)
        return self.frame is not None and self.cfg is not None and self.action_map is not None

    def _init_field(self, label_text, key_name, field, panel):
        row = JPanel()
        row.setLayout(BoxLayout(row, BoxLayout.X_AXIS))
        row.add(JLabel(label_text + ": "))
        row.add(field)
        panel.add(row)
        panel.add(Box.createVerticalStrut(6))

        value = self.saved.get(key_name)
        if not value and self.frame is not None and self.action_map is not None:
            try:
                action = self.action_map.get(key_name)
                ks = _find_keystroke_for_action(_get_hotkey_map(self.frame), action)
                if ks is not None:
                    try:
                        value = ks.toString()
                    except Exception:
                        value = None
            except Exception:
                pass
        if value:
            try:
                field.setText(value)
            except Exception:
                pass
        self._install_capture(field)

    def _install_capture(self, field):
        class _Capture(KeyAdapter):
            def keyPressed(self, evt):
                try:
                    code = evt.getKeyCode()
                except Exception:
                    code = None
                if code in (KeyEvent.VK_BACK_SPACE, KeyEvent.VK_DELETE):
                    try:
                        field.setText("")
                    except Exception:
                        pass
                    try:
                        evt.consume()
                    except Exception:
                        pass
                    return
                ks = _keystroke_from_event(evt)
                text = _keystroke_to_text(ks)
                if text:
                    try:
                        field.setText(text)
                    except Exception:
                        pass
                    try:
                        evt.consume()
                    except Exception:
                        pass
        try:
            field.addKeyListener(_Capture())
        except Exception:
            pass

    def _on_apply(self, evt=None):
        if not self._refresh_context():
            JOptionPane.showMessageDialog(self.dialog, "OpenPnP not ready yet. Please try again.", "Error", JOptionPane.ERROR_MESSAGE)
            return

        start = _normalize_keystroke(self.input_start.getText())
        step = _normalize_keystroke(self.input_step.getText())
        stop = _normalize_keystroke(self.input_stop.getText())

        mapping = {"start": start, "step": step, "stop": stop}

        hotkey_map = _get_hotkey_map(self.frame)
        if hotkey_map is None:
            JOptionPane.showMessageDialog(self.dialog, "Cannot access hotkey map.", "Error", JOptionPane.ERROR_MESSAGE)
            return

        action_map = self.action_map
        own_actions = set()
        for k in ('start','step','stop'):
            try:
                a = action_map.get(k)
                if a is not None:
                    own_actions.add(a)
            except Exception:
                pass

        ok, reason = _check_conflicts(hotkey_map, mapping, own_actions)
        if not ok:
            JOptionPane.showMessageDialog(self.dialog, reason or "Conflict detected", "Error", JOptionPane.ERROR_MESSAGE)
            return

        # Apply
        _apply_hotkeys(self.frame, action_map, mapping)
        _save_hotkeys(self.cfg, mapping)
        JOptionPane.showMessageDialog(self.dialog, "Hotkeys saved.", "OK", JOptionPane.INFORMATION_MESSAGE)

    def _on_close(self, evt=None):
        try:
            self.dialog.dispose()
        except Exception:
            pass

    def show(self):
        self.dialog.setVisible(True)


def _apply_saved_on_startup():
    if Configuration is None or MainFrame is None:
        return False
    try:
        if not Configuration.isInstanceInitialized():
            return False
    except Exception:
        pass
    cfg = None
    try:
        cfg = Configuration.get()
    except Exception:
        return False
    frame = _get_mainframe()
    if frame is None:
        return False
    action_map = _get_action_map(frame)
    if action_map is None:
        return False
    saved = _load_saved_hotkeys(cfg)
    # mapping is already stored as normalized strings; apply directly
    try:
        _apply_hotkeys(frame, action_map, saved)
        return True
    except Exception:
        return False


# Entry points

def _run():
    # Show dialog
    try:
        dlg = HotkeyDialog()
        dlg.show()
    except Exception:
        pass


def _run_async():
    try:
        class _Runner(java.lang.Runnable):
            def run(self):
                # show dialog on EDT
                _run()
        SwingUtilities.invokeLater(_Runner())
    except Exception:
        _run()


# On import / startup try to auto-apply saved keys. Give small delay to allow MainFrame init.
try:
    # try immediate apply
    applied = _apply_saved_on_startup()
    if not applied:
        # schedule a delayed attempt via SwingUtilities after short wait
        try:
            class _Delayed(java.lang.Runnable):
                def run(self):
                    try:
                        _apply_saved_on_startup()
                    except Exception:
                        pass
            SwingUtilities.invokeLater(_Delayed())
        except Exception:
            pass
except Exception:
    pass

# Expose dialog when script is run manually (async)
try:
    _run_async()
except Exception:
    pass
