# coding: utf-8
# python 2 only

# Copyright (c) 2026 TormachTips.com. All rights reserved.
# Licensed under the TormachTips Personal Use License.
# Permission is granted only for private personal use and private personal modification.
# No sharing, publication, distribution, resale, sublicensing, screenshots, code excerpts,
# benchmarks, or videos are permitted without prior written permission.
# Requests:         tormach.1100m@gmail.com
# Information page: https://tormachtips.com/plugins.htm

#################################################
##                                             ##
##     Custom Pocket Retract Distance 0.97     ##
##             www.tormachtips.com             ##
##                                             ##
#################################################

# 0.97 - Updated UI, settings and set up proper versioning. - 4/18/2026
# 0.96 adds disabled / enabled flag. 4/12/2026

import os
import time
import gtk
import glib
import admincmd
import constants
from ui_hooks import plugin, version_list

CURRENT_VER         = "0.97"
SCRIPT_NAME         = "Custom Probe Pocket Retract"
DESCRIPTION         = "Allows you to adjust the lateral retract distance for your probe. Useful for tight holes (heh)."
ENABLED             = 1
DEV_MACHINE         = 0
DEV_MACHINE_FLAG    = "/home/operator/gcode/python/dev_machine.txt"
ANCHOR_LINE         = "#<abs_retract> = ABS[#<tip_diameter> / 2.0]"
BEAGLE_TAG          = "; beagle"
RESET_DELAY_SECONDS = 120

class UserPlugin(plugin):
    def __init__(self):
        plugin.__init__(self, 'Custom Retract Plugin')
        dev_machine_found = os.path.exists(DEV_MACHINE_FLAG)
        if dev_machine_found:
            plugin_enabled = DEV_MACHINE
        else:
            plugin_enabled = ENABLED
        if plugin_enabled:
            glib.timeout_add(1000, self._announce_loaded)
            return
        else:
            try:
                if dev_machine_found:
                    self.error_handler.write("[Custom Retract Plugin] Dev machine found. Plugin loaded, but disabled by DEV_MACHINE.", constants.ALARM_LEVEL_QUIET)
                else:
                    self.error_handler.write("[Custom Retract Plugin] Plugin loaded, but disabled.", constants.ALARM_LEVEL_QUIET)
                    self.error_handler.write("[Custom Retract Plugin] To enable, open script, find ENABLED = 0 and change to ENABLED = 1", constants.ALARM_LEVEL_QUIET)
            except Exception:
                pass
            return

    def _announce_loaded(self):
        try:
            self.error_handler.write("[Custom Retract Plugin] Loaded.",constants.ALARM_LEVEL_QUIET)
        except Exception:
            pass
        return False

class AdminRetractCmd(admincmd.AdminCmd):
    def __init__(self, uibase):
        super(AdminRetractCmd, self).__init__('RETRACT', doc=True)
        self.uibase = uibase
        self._help = 'Change probe retract distance temporarily.'

    def add_completion_strings(self, store):
        store.append(['ADMIN RETRACT'])

    def activate(self, arglist):
        try:
            self.show_dialog()
        except Exception as e:
            self.uibase.error_handler.write("[ADMIN RETRACT] Failed: " + str(e),constants.ALARM_LEVEL_QUIET)

    def get_target_file(self):
        version_path = "v%d.%d.%d" % (version_list[0],version_list[1],version_list[2])
        return os.path.join("/home/operator",version_path,"subroutines","probe_xyz.ngc")

    def is_beagle_line(self, line):
        stripped = line.strip()
        return stripped.startswith("#<abs_retract> =") and BEAGLE_TAG in stripped

    def update_probe_file(self, retract_value):
        target_file = self.get_target_file()
        if not os.path.isfile(target_file):
            raise IOError("File not found: %s" % target_file)
        with open(target_file, "r") as f:
            lines = f.readlines()
        cleaned_lines = [line for line in lines if not self.is_beagle_line(line)]
        anchor_index = -1
        for i, line in enumerate(cleaned_lines):
            if line.strip() == ANCHOR_LINE:
                anchor_index = i
                break
        if anchor_index == -1:
            raise ValueError("Anchor line not found:\n%s" % ANCHOR_LINE)
        new_line = "#<abs_retract> = %s %s\n" % (retract_value, BEAGLE_TAG)
        cleaned_lines.insert(anchor_index + 1, new_line)
        with open(target_file, "w") as f:
            f.writelines(cleaned_lines)
        return target_file

    def announce_reset(self):
        try:
            target_file = self.get_target_file()
            self.uibase.error_handler.write(" ", constants.ALARM_LEVEL_QUIET)
            self.uibase.error_handler.write("[ADMIN RETRACT] Custom retract value reset to default.",constants.ALARM_LEVEL_QUIET)
            self.uibase.error_handler.write(" ", constants.ALARM_LEVEL_QUIET)
        except Exception:
            pass    

    def remove_beagle_lines(self):
        target_file = self.get_target_file()
        if not os.path.isfile(target_file):
            return
        with open(target_file, "r") as f:
            lines = f.readlines()
        new_lines = [line for line in lines if not self.is_beagle_line(line)]
        with open(target_file, "w") as f:
            f.writelines(new_lines)

    def start_reset_daemon(self):
        def do_reset():
            try:
                self.remove_beagle_lines()
                self.announce_reset()
            except Exception as e:
                try:
                    self.uibase.error_handler.write("[ADMIN RETRACT] Reset failed: " + str(e),constants.ALARM_LEVEL_QUIET)
                except Exception:
                    pass
            return False
        glib.timeout_add(RESET_DELAY_SECONDS * 1000, do_reset)

    def show_dialog(self):
        dialog = gtk.Dialog(
            "Set Retract Distance",
            self.uibase.window,
            gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
            (
                gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL,
                gtk.STOCK_OK, gtk.RESPONSE_OK            )        )
        dialog.set_default_size(480, 220)
        dialog.set_keep_above(True)
        info_text = (
            "Pocket probing in small holes can be difficult because the lateral retract move "
            "may travel too far after contact and strike the opposite wall.\n\n"
            "This tool lets you temporarily reduce that retract distance.\n\n"
            "Enter a value below, click OK, then probe your pocket within %s seconds. "
            "After that, the setting will automatically return to normal."
            % RESET_DELAY_SECONDS        )
        info_label = gtk.Label()
        info_label.set_alignment(0.0, 0.0)
        info_label.set_justify(gtk.JUSTIFY_LEFT)
        info_label.set_line_wrap(True)
        info_label.set_size_request(360, -1)
        info_label.set_text(info_text)
        field_label = gtk.Label("Retract Distance")
        field_label.set_alignment(0.0, 0.5)
        entry = gtk.Entry()
        entry.set_activates_default(True)
        dialog.vbox.pack_start(info_label, False, False, 8)
        dialog.vbox.pack_start(field_label, False, False, 8)
        dialog.vbox.pack_start(entry, False, False, 8)
        dialog.set_default_response(gtk.RESPONSE_OK)
        dialog.show_all()
        entry.grab_focus()
        response = dialog.run()
        value = entry.get_text().strip()
        dialog.destroy()
        if response != gtk.RESPONSE_OK:
            return
        if not value:
            self.uibase.error_handler.write("[ADMIN RETRACT] Retract Distance is required.",constants.ALARM_LEVEL_QUIET)
            return
        try:
            float(value)
        except ValueError:
            self.uibase.error_handler.write("[ADMIN RETRACT] Retract Distance must be numeric.",constants.ALARM_LEVEL_QUIET)
            return
        target_file = self.update_probe_file(value)
        self.start_reset_daemon()
        self.uibase.error_handler.write(" ", constants.ALARM_LEVEL_QUIET)
        self.uibase.error_handler.write(
            "[ADMIN RETRACT] Updated: %s" % target_file,
            constants.ALARM_LEVEL_QUIET
        )
        self.uibase.error_handler.write("[ADMIN RETRACT] Custom value will reset to default after %s seconds." % RESET_DELAY_SECONDS,constants.ALARM_LEVEL_QUIET)
        self.uibase.error_handler.write(" ", constants.ALARM_LEVEL_QUIET)
        self.show_nonblocking_dialog()

    def show_nonblocking_dialog(self):
        dialog = gtk.MessageDialog(
            self.uibase.window,
            gtk.DIALOG_DESTROY_WITH_PARENT,
            gtk.MESSAGE_INFO,
            gtk.BUTTONS_OK,
            "The custom retract distance is now active.\n\n"
            "Probe your pocket within %s seconds.\n"
            "After that, the setting will automatically return to normal."
            % RESET_DELAY_SECONDS        )
        dialog.set_title("Retract Distance")
        dialog.set_keep_above(True)
        dialog.set_modal(False)

        def on_response(widget, response_id):
            widget.destroy()
        dialog.connect("response", on_response)
        dialog.show_all()

def get_custom_admin_cmds(uibase):
    return [AdminRetractCmd(uibase)]
    
DESCRIPTION_LONG = """A small plugin that 
    temporarily reduces the probe’s lateral retract distance when pocket probing 
    tight holes or small bores. It helps prevent the probe from retracting too 
    far after contact and striking the opposite wall. The user runs ADMIN 
    RETRACT from MDI, enters a temporary retract value, performs the probing 
    operation within a short time window, and the script then automatically 
    restores the default retract behavior."""    