# 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

#############################################
##                                         ##
##     G53 Machine Coords Overlay 0.95     ##
##           www.tormachtips.com           ##
##                                         ##
#############################################

# 0.95 - public beta    - 6/19/2026

import os
import gtk
import glib
import pango
import linuxcnc
import constants
import singletons
from ui_hooks import plugin

CURRENT_VER             = "0.95"
SCRIPT_NAME             = "G53 Machine Coord Overlay"
DESCRIPTION             = "Shows current G53 machine coordinates in a compact right-aligned overlay."
ENABLED                 = 1
DEV_MACHINE             = 1
DEV_MACHINE_FLAG        = "/home/operator/gcode/python/dev_machine.txt"
LOCATION_RIGHT          = 1010
LOCATION_Y              = 385
SHOW_A_AXIS             = 0
BOX_MIN_WIDTH           = 260
BOX_TEXT_PADDING        = 14
ROW_HEIGHT              = 22
START_DELAY_MS          = 1500
POLL_MS                 = 100
OVERLAY_BOOTSTRAP_MS    = 250
OVERLAY_BOOTSTRAP_LIMIT = 40
OVERLAY_RESTACK_MS      = 1000
OVERLAY_RESTACK_LIMIT   = 30
DEBUG_TO_STATUS         = 0
BG_COLOR                = "#111111"
FG_COLOR                = "#ffffff"
OVERLAY_FONT_NAME       = "monospace 9"

class UserPlugin(plugin):
    def __init__(self):
        plugin.__init__(self, "%s %s" % (SCRIPT_NAME, CURRENT_VER))
        dev_machine_found = os.path.exists(DEV_MACHINE_FLAG)
        if dev_machine_found:
            plugin_enabled = DEV_MACHINE
        else:
            plugin_enabled = ENABLED
        if not plugin_enabled:
            self.ShowMsg("Plugin loaded, but disabled.", constants.ALARM_LEVEL_QUIET)
            return
        self.ui = None
        self.overlay_box = None
        self.overlay_content_box = None
        self.overlay_parent = None
        self.coord_event_box = None
        self.coord_label = None
        self.coord_text = ""
        self._overlay_bootstrap_count = 0
        self._overlay_restack_id = None
        self._overlay_restack_count = 0
        glib.timeout_add(OVERLAY_BOOTSTRAP_MS, self.bootstrap_overlay_first)
        glib.timeout_add(START_DELAY_MS, self.start_process)

    def _debug(self, message):
        try:
            if DEBUG_TO_STATUS:
                self.error_handler.write("[%s] %s" % (SCRIPT_NAME, message), constants.ALARM_LEVEL_LOW)
        except:
            pass

    def start_process(self):
        try:
            self.ui = singletons.g_Machine
            if not self.ui:
                return True
            self.ensure_overlay()
            if self._overlay_restack_id is None:
                self._overlay_restack_id = glib.timeout_add(OVERLAY_RESTACK_MS, self.restack_overlay)
            glib.timeout_add(POLL_MS, self.update_overlay)
            self.error_handler.write("[%s] %s %s started." % (SCRIPT_NAME, SCRIPT_NAME, CURRENT_VER), constants.ALARM_LEVEL_QUIET)
        except Exception as e:
            self.error_handler.write("[%s] Startup error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
        return False

    def get_overlay_parent(self):
        try:
            ui = singletons.g_Machine
            if ui:
                pass
            else:
                self._debug("UI singleton not ready")
                return None
            if hasattr(ui, "get_normal_fixed_object"):
                parent = ui.get_normal_fixed_object("notebook_main_fixed")
                if parent:
                    return parent
            if hasattr(ui, "notebook_main_fixed") and ui.notebook_main_fixed:
                return ui.notebook_main_fixed
            if hasattr(ui, "builder"):
                parent = ui.builder.get_object("notebook_main_fixed")
                if parent:
                    return parent
        except Exception as e:
            self.error_handler.write("[%s] Parent lookup error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
        return None

    def bootstrap_overlay_first(self):
        try:
            self._overlay_bootstrap_count += 1
            if self.ensure_overlay():
                self._debug("Overlay ready after %d attempt(s)" % self._overlay_bootstrap_count)
                if self._overlay_restack_id is None:
                    self._overlay_restack_id = glib.timeout_add(OVERLAY_RESTACK_MS, self.restack_overlay)
                return False
            if self._overlay_bootstrap_count >= OVERLAY_BOOTSTRAP_LIMIT:
                self.error_handler.write("[%s] Overlay bootstrap gave up after %d attempts" % (SCRIPT_NAME, self._overlay_bootstrap_count), constants.ALARM_LEVEL_LOW)
                return False
            return True
        except Exception as e:
            self.error_handler.write("[%s] Overlay bootstrap error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
            return True

    def _destroy_existing_named_overlays(self, parent):
        # Prevent duplicate overlays after plugin reloads.
        try:
            for child in parent.get_children():
                try:
                    if child.get_name() == "g53_machine_coord_overlay":
                        parent.remove(child)
                        child.destroy()
                except:
                    pass
        except:
            pass

    def ensure_overlay(self):
        try:
            parent = self.get_overlay_parent()
            if parent:
                pass
            else:
                return False
            if self.overlay_box:
                current_parent = None
                try:
                    current_parent = self.overlay_box.get_parent()
                except:
                    current_parent = None
                if current_parent is parent:
                    parent.move(self.overlay_box, self._get_overlay_container_x(), LOCATION_Y)
                    self.overlay_box.set_size_request(self._get_overlay_container_width(), ROW_HEIGHT)
                    self._sync_overlay_geometry()
                    self.overlay_box.show_all()
                    self.overlay_box.queue_resize()
                    self.overlay_box.queue_draw()
                    parent.queue_draw()
                    return True
                if current_parent:
                    try:
                        self.overlay_box.destroy()
                    except:
                        pass
                    self.overlay_box = None
                    self.overlay_content_box = None
                    self.overlay_parent = None
                    self.coord_event_box = None
                    self.coord_label = None
            return self.create_overlay(parent)
        except Exception as e:
            self.error_handler.write("[%s] Overlay watchdog error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
        return False

    def create_overlay(self, parent):
        try:
            if parent:
                pass
            else:
                return False
            if self.overlay_box:
                return True
            self._destroy_existing_named_overlays(parent)
            # Full-width invisible alignment container. It locks the right edge.
            outer = gtk.Alignment(1.0, 0.0, 0.0, 0.0)
            outer.set_size_request(self._get_overlay_container_width(), ROW_HEIGHT)
            outer.set_name("g53_machine_coord_overlay")
            # Visible right-aligned backing box.
            content_box = gtk.EventBox()
            content_box.set_size_request(self._get_overlay_width(), ROW_HEIGHT)
            content_box.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse(BG_COLOR))
            label = gtk.Label()
            label.set_use_markup(True)
            label.set_alignment(0.0, 0.5)
            label.set_justify(gtk.JUSTIFY_LEFT)
            label.set_size_request(self._get_overlay_width(), ROW_HEIGHT)
            label.set_padding(4, 0)
            try:
                label.modify_font(pango.FontDescription(OVERLAY_FONT_NAME))
            except:
                pass
            content_box.add(label)
            outer.add(content_box)
            parent.put(outer, self._get_overlay_container_x(), LOCATION_Y)
            outer.show_all()
            outer.queue_draw()
            self.overlay_box = outer
            self.overlay_content_box = content_box
            self.overlay_parent = parent
            self.coord_event_box = content_box
            self.coord_label = label
            self.update_coord_text()
            self._sync_overlay_geometry()
            self._debug("Created overlay parent=%s x=%d y=%d w=%d h=%d" % (str(parent), self._get_overlay_container_x(), LOCATION_Y, self._get_overlay_width(), ROW_HEIGHT))
            return True
        except Exception as e:
            self.overlay_box = None
            self.overlay_content_box = None
            self.overlay_parent = None
            self.coord_event_box = None
            self.coord_label = None
            self.error_handler.write("[%s] Create overlay error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
            return False

    def restack_overlay(self):
        try:
            self._overlay_restack_count += 1
            parent = self.get_overlay_parent()
            if parent:
                pass
            else:
                return self._overlay_restack_count < OVERLAY_RESTACK_LIMIT
            if self.overlay_box:
                pass
            else:
                self.ensure_overlay()
                return self._overlay_restack_count < OVERLAY_RESTACK_LIMIT
            current_parent = None
            try:
                current_parent = self.overlay_box.get_parent()
            except:
                current_parent = None
            if current_parent is parent:
                try:
                    parent.remove(self.overlay_box)
                except:
                    pass
                parent.put(self.overlay_box, self._get_overlay_container_x(), LOCATION_Y)
                self._sync_overlay_geometry()
                self.overlay_box.show_all()
                self.overlay_box.queue_resize()
                self.overlay_box.queue_draw()
                parent.queue_draw()
            else:
                self.ensure_overlay()
            if self._overlay_restack_count >= OVERLAY_RESTACK_LIMIT:
                self._overlay_restack_id = None
                return False
        except Exception as e:
            self.error_handler.write("[%s] Overlay restack error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
        if self._overlay_restack_count >= OVERLAY_RESTACK_LIMIT:
            self._overlay_restack_id = None
            return False
        return True

    def _get_overlay_right_edge(self):
        # Hard right anchor in the same fixed coordinate space used by parent.put()/move().
        return LOCATION_RIGHT

    def _get_overlay_container_width(self):
        # Invisible alignment container runs from x=0 to LOCATION_RIGHT.
        # Actual visible row right-aligns inside it, so growth always goes left.
        return self._get_overlay_right_edge()

    def _get_overlay_container_x(self):
        return 0

    def _get_text_pixel_width(self, text):
        try:
            if self.overlay_box:
                layout = self.overlay_box.create_pango_layout(str(text))
            else:
                label = gtk.Label()
                layout = label.create_pango_layout(str(text))
            layout.set_font_description(pango.FontDescription(OVERLAY_FONT_NAME))
            width, height = layout.get_pixel_size()
            return int(width)
        except:
            return len(str(text)) * 8

    def _get_overlay_width(self):
        calculated_width = self._get_text_pixel_width(self.coord_text) + BOX_TEXT_PADDING
        if calculated_width < BOX_MIN_WIDTH:
            calculated_width = BOX_MIN_WIDTH
        right_edge = self._get_overlay_right_edge()
        if calculated_width > right_edge:
            calculated_width = right_edge
        return calculated_width

    def _sync_overlay_geometry(self):
        # Keep the invisible container fixed from x=0 to LOCATION_RIGHT.
        # The visible row resizes inside it, right-aligned by gtk.Alignment.
        try:
            box_width = self._get_overlay_width()
            if self.overlay_box:
                self.overlay_box.set_size_request(self._get_overlay_container_width(), ROW_HEIGHT)
            if self.overlay_content_box:
                self.overlay_content_box.set_size_request(box_width, ROW_HEIGHT)
            if self.coord_label:
                self.coord_label.set_size_request(box_width, ROW_HEIGHT)
            if self.overlay_parent and self.overlay_box:
                self.overlay_parent.move(self.overlay_box, self._get_overlay_container_x(), LOCATION_Y)
                self.overlay_box.queue_resize()
                self.overlay_box.queue_draw()
                self.overlay_parent.queue_draw()
        except:
            pass

    def clean_coord_value(self, value):
        try:
            value = float(value)
            if abs(value) < 0.00005:
                return 0.0
            return value
        except:
            return 0.0

    def get_machine_coord_text(self):
        # Uses the same G53 source as the WCS Matrix tab: linuxcnc status.position.
        try:
            ui = singletons.g_Machine
            if not ui or not hasattr(ui, "status"):
                return "G53 | X0.0000 | Y0.0000 | Z0.0000"
            ui.status.poll()
            pos = ui.status.position[:4]
            x_val = self.clean_coord_value(pos[0])
            y_val = self.clean_coord_value(pos[1])
            z_val = self.clean_coord_value(pos[2])
            if SHOW_A_AXIS:
                a_val = self.clean_coord_value(pos[3])
                return "G53 | X%.4f | Y%.4f | Z%.4f | A%.4f" % (x_val, y_val, z_val, a_val)
            return "G53 | X%.4f | Y%.4f | Z%.4f" % (x_val, y_val, z_val)
        except Exception as e:
            self.error_handler.write("[%s] Coordinate read error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
            return self.coord_text

    def set_coord_label_text(self, text):
        try:
            if not self.coord_label:
                return
            self.coord_label.set_markup('<span foreground="%s">%s</span>' % (FG_COLOR, text))
        except:
            pass

    def update_coord_text(self):
        new_text = self.get_machine_coord_text()
        if new_text != self.coord_text:
            self.coord_text = new_text
            self.set_coord_label_text(new_text)
            self._sync_overlay_geometry()

    def update_overlay(self):
        try:
            if not self.overlay_box:
                self.ensure_overlay()
            self.update_coord_text()
            self._sync_overlay_geometry()
            if self.overlay_box:
                self.overlay_box.show_all()
        except Exception as e:
            self.error_handler.write("[%s] Update error: %s" % (SCRIPT_NAME, str(e)), constants.ALARM_LEVEL_LOW)
        return True