# 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

#############################################
##                                         ##
##        Hobbs Daily Viewer v0.95         ##
##          www.tormachtips.com            ##
##                                         ##
#############################################

# 0.95 - public beta - 4/2/26

import os
import re
import datetime
from collections import defaultdict
import Tkinter as tk
import tkFileDialog
import tkMessageBox

DEFAULT_DIR       = "/home/operator/gcode/python/hobbs"
LINE_RE           = re.compile(r"""\(\+(\d+)s\).*?tool\s+(\d+)""", re.IGNORECASE)
HOBBS_FILE_RE     = re.compile(r"^hobbs_(\d{4}-\d{2}-\d{2})\.txt$", re.IGNORECASE)
CURRENT_VER       = "0.95"
SCRIPT_NAME       = "Hobbs Daily Usage Viewer"
DESCRIPTION       = "A daily view of tool life and spindle time."
WINDOW_WIDTH      = 500
WINDOW_HEIGHT     = 700
TITLE_FONT        = ("Helvetica", 14, "bold")
FILE_FONT         = ("Helvetica", 11)
INFO_FONT         = ("Helvetica", 11)
TABLE_HEADER_FONT = ("Helvetica", 11, "bold")
TABLE_CELL_FONT   = ("Helvetica", 11)
TABLE_FOOTER_FONT = ("Helvetica", 11, "bold")

def parse_hobbs_file(path):
    tool_seconds = defaultdict(int)
    with open(path, "r") as f:
        for line in f:
            m = LINE_RE.search(line)
            if not m:
                continue
            delta_seconds = int(m.group(1))
            tool_num = int(m.group(2))
            tool_seconds[tool_num] += delta_seconds
    return dict(tool_seconds)

def format_hms(seconds):
    """Format seconds as a sensible Xh Xm Xs string (omitting zero parts for cleanliness)."""
    hours = seconds // 3600
    minutes = (seconds % 3600) // 60
    secs = seconds % 60

    if hours > 0:
        return "%dh %dm %ds" % (hours, minutes, secs)
    elif minutes > 0:
        return "%dm %ds" % (minutes, secs)
    else:
        return "%ds" % secs

def pick_file(root):
    return tkFileDialog.askopenfilename(
        parent=root,
        title="Select Hobbs Log File",
        initialdir=DEFAULT_DIR,
        initialfile="hobbs*.txt",
        filetypes=[
            ("Hobbs text files", "hobbs*.txt"),
            ("Text files", "*.txt"),
            ("All files", "*")        ]    )

def list_dated_hobbs_files():
    dated_files = []
    try:
        names = os.listdir(DEFAULT_DIR)
    except:
        return dated_files
    for name in names:
        m = HOBBS_FILE_RE.match(name)
        if not m:
            continue
        date_text = m.group(1)
        try:
            file_date = datetime.datetime.strptime(date_text, "%Y-%m-%d").date()
        except:
            continue
        dated_files.append((file_date, os.path.join(DEFAULT_DIR, name)))
    dated_files.sort(key=lambda item: item[0])
    return dated_files

def get_default_hobbs_file():
    dated_files = list_dated_hobbs_files()
    if not dated_files:
        return None
    today = datetime.date.today()
    best_path = None
    for file_date, path in reversed(dated_files):
        if file_date <= today:
            best_path = path
            break
    if best_path is not None:
        return best_path
    return dated_files[-1][1]

def center_window(root, width, height):
    root.update_idletasks()
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    x = (screen_width - width) / 2
    y = (screen_height - height) / 2
    root.geometry("%dx%d+%d+%d" % (width, height, x, y))

class SummaryWindow(object):
    def __init__(self, root, filepath):
        self.root = root
        self.filepath = None
        self.filename = ""
        self.tool_seconds = {}
        self.available_files = [path for _, path in list_dated_hobbs_files()]
        self._mouse_in_table = False
        self.root.title("Tool Usage Summary")
        self.root.configure(bg="#d9d9d9")
        self.root.resizable(True, True)
        self._build_ui()
        self.load_file(filepath)
        self.root.bind("<Left>", self.on_prev_day)
        self.root.bind("<Right>", self.on_next_day)        
        self.root.bind("<Escape>", self.on_close)
        self.root.bind("<Return>", self.on_close)
        self.root.bind("<KP_Enter>", self.on_close)
    
    def on_close(self, event=None):
        self.root.destroy()
        return "break"
    
    def _refresh_table(self):
        for widget in self.table_frame.winfo_children():
            widget.destroy()
        self._build_table(self.table_frame)
        self._sync_table_scrollregion()
    
    def load_file(self, filepath):
        tool_seconds = parse_hobbs_file(filepath)
        self.filepath = filepath
        self.filename = os.path.basename(filepath)
        self.tool_seconds = tool_seconds
        self.file_lbl.config(text=self.filename)
        self._refresh_table()
    
    def _get_current_file_index(self):
        try:
            return self.available_files.index(self.filepath)
        except ValueError:
            return -1
    
    def _navigate_days(self, step):
        if not self.available_files:
            return "break"
        idx = self._get_current_file_index()
        if idx < 0:
            return "break"
        new_idx = idx + step
        if new_idx < 0 or new_idx >= len(self.available_files):
            return "break"
        try:
            self.load_file(self.available_files[new_idx])
        except Exception as e:
            tkMessageBox.showerror(
                "Error",
                "Failed to process file:\n%s" % str(e),
                parent=self.root            )
        return "break"
    
    def on_prev_day(self, event=None):
        return self._navigate_days(-1)
    
    def on_next_day(self, event=None):
        return self._navigate_days(1)
    
    def _build_ui(self):
        outer = tk.Frame(self.root, bg="#d9d9d9", padx=12, pady=12)
        outer.pack(fill="both", expand=True)
        header = tk.Frame(outer, bg="#ffffff", bd=1, relief="solid")
        header.pack(fill="x", expand=False)
        self.file_lbl = tk.Label(
            header,
            text="",
            font=FILE_FONT,
            bg="#ffffff",
            fg="#333333",
            anchor="w",
            padx=12,
            pady=0        )
        self.file_lbl.pack(fill="x")
        info_lbl = tk.Label(
            header,
            text="On the selected date, you used the following\ntools for the listed amount of time.\n\nUse ◀ Left & Right ▶ arrows to move between days.",
            justify="left",
            font=INFO_FONT,
            bg="#ffffff",
            fg="#333333",
            anchor="w",
            padx=12,
            pady=10        )
        info_lbl.pack(fill="x")
        self.table_outer = tk.Frame(outer, bg="#ffffff", bd=1, relief="solid")
        self.table_outer.pack(fill="both", expand=True, pady=(10, 0))
        self.table_canvas = tk.Canvas(
            self.table_outer,
            bg="#ffffff",
            highlightthickness=0,
            bd=0        )
        self.table_canvas.pack(side="left", fill="both", expand=True)
        self.table_scrollbar = tk.Scrollbar(
            self.table_outer,
            orient="vertical",
            command=self.table_canvas.yview        )
        self.table_canvas.configure(yscrollcommand=self.table_scrollbar.set)
        self.table_frame = tk.Frame(self.table_canvas, bg="#ffffff")
        self.table_window = self.table_canvas.create_window((0, 0),window=self.table_frame,anchor="nw")
        self.table_frame.bind("<Configure>", self._sync_table_scrollregion)
        self.table_canvas.bind("<Configure>", self._sync_table_width)
        self.table_canvas.bind("<Home>", self._scroll_home)
        self.table_canvas.bind("<End>", self._scroll_end)
        self.table_canvas.bind("<Prior>", self._scroll_page_up)   # Page Up
        self.table_canvas.bind("<Next>", self._scroll_page_down)  # Page Down
        self.table_canvas.bind("<Enter>", self._bind_table_mousewheel)
        self.table_canvas.bind("<Leave>", self._unbind_table_mousewheel)
        self.table_frame.bind("<Enter>", self._bind_table_mousewheel)
        self.table_frame.bind("<Leave>", self._unbind_table_mousewheel)
        self.table_canvas.bind_all("<Button-4>", self._on_mousewheel_linux)
        self.table_canvas.bind_all("<Button-5>", self._on_mousewheel_linux)
        self.table_canvas.bind("<Up>", self._scroll_up)
        self.table_canvas.bind("<Down>", self._scroll_down)        
        self.table_canvas.configure(takefocus=1)        
        self.table_canvas.bind("<1>", lambda event: self.table_canvas.focus_set())
        button_frame = tk.Frame(outer, bg="#d9d9d9")
        button_frame.pack(fill="x", pady=(10, 0))
        close_btn = tk.Button(button_frame,text="Close",width=10,command=self.on_close)
        close_btn.pack(side="right")
        center_window(self.root, WINDOW_WIDTH, WINDOW_HEIGHT)
    
    def _scroll_up(self, event=None):
        self.table_canvas.yview_scroll(-1, "units")
        return "break"
    
    def _scroll_down(self, event=None):
        self.table_canvas.yview_scroll(1, "units")
        return "break"    
    
    def _bind_table_mousewheel(self, event=None):
        self.table_canvas.focus_set()
        self._mouse_in_table = True
        self.table_canvas.bind_all("<MouseWheel>", self._on_mousewheel_windows)
    
    def _unbind_table_mousewheel(self, event=None):
        self._mouse_in_table = False
        self.table_canvas.unbind_all("<MouseWheel>")    
    
    def _on_mousewheel_windows(self, event):
        if not self._mouse_in_table:
            return
        self.table_canvas.yview_scroll(-1 * (event.delta / 120), "units")
        return "break"
    
    def _on_mousewheel_linux(self, event):
        if not self._mouse_in_table:
            return
        if event.num == 4:
            self.table_canvas.yview_scroll(-1, "units")
        elif event.num == 5:
            self.table_canvas.yview_scroll(1, "units")
        return "break"
    
    def _scroll_home(self, event=None):
        self.table_canvas.yview_moveto(0)
        return "break"
    
    def _scroll_end(self, event=None):
        self.table_canvas.yview_moveto(1)
        return "break"
    
    def _scroll_page_up(self, event=None):
        self.table_canvas.yview_scroll(-1, "pages")
        return "break"
    
    def _scroll_page_down(self, event=None):
        self.table_canvas.yview_scroll(1, "pages")
        return "break"
    
    def _sync_table_scrollregion(self, event=None):
        self.table_canvas.configure(scrollregion=self.table_canvas.bbox("all"))
        frame_req_height = self.table_frame.winfo_reqheight()
        canvas_height = self.table_canvas.winfo_height()
        if frame_req_height > canvas_height:
            if not self.table_scrollbar.winfo_ismapped():
                self.table_scrollbar.pack(side="right", fill="y")
        else:
            if self.table_scrollbar.winfo_ismapped():
                self.table_scrollbar.pack_forget()
            self.table_canvas.yview_moveto(0)
    
    def _sync_table_width(self, event):
        self.table_canvas.itemconfigure(self.table_window, width=event.width)
        self._sync_table_scrollregion()
    
    def _build_table(self, parent):
        header_bg = "#cfd6de"
        row_a = "#ffffff"
        row_b = "#f3f6f9"
        headings = [("Tool", 10, "w"), ("Time", 18, "e")]
        for col, (text, width, anchor) in enumerate(headings):
            lbl = tk.Label(
                parent,
                text=text,
                width=width,
                font=TABLE_HEADER_FONT,
                bg=header_bg,
                fg="#000000",
                anchor=anchor,
                padx=12,
                pady=8            )
            lbl.grid(row=0, column=col, sticky="ew")
        if not self.tool_seconds:
            empty = tk.Label(
                parent,
                text="No matching tool runtime entries were found.",
                font=TABLE_CELL_FONT,
                bg="#ffffff",
                anchor="w",
                padx=12,
                pady=12            )
            empty.grid(row=1, column=0, columnspan=2, sticky="nsew")
            return
        sorted_tools = sorted(self.tool_seconds.items())
        total_seconds = 0
        for row_idx, (tool_num, seconds) in enumerate(sorted_tools, start=1):
            bg = row_a if (row_idx % 2) else row_b
            total_seconds += seconds
            values = [
                ("T%d" % tool_num, 10, "w"),
                (format_hms(seconds), 18, "e"),
            ]
            for col, (value, width, anchor) in enumerate(values):
                lbl = tk.Label(
                    parent,
                    text=value,
                    width=width,
                    font=TABLE_CELL_FONT,
                    bg=bg,
                    fg="#222222",
                    anchor=anchor,
                    padx=12,
                    pady=7                )
                lbl.grid(row=row_idx, column=col, sticky="ew")
        footer_row = len(sorted_tools) + 1
        footer_bg = "#e4e9ee"
        footer_vals = [("Total", 10, "w"), (format_hms(total_seconds), 18, "e")]
        for col, (value, width, anchor) in enumerate(footer_vals):
            lbl = tk.Label(
                parent,
                text=value,
                width=width,
                font=TABLE_FOOTER_FONT,
                bg=footer_bg,
                fg="#000000",
                anchor=anchor,
                padx=12,
                pady=8            )
            lbl.grid(row=footer_row, column=col, sticky="ew")
        parent.grid_columnconfigure(0, weight=1, minsize=80)
        parent.grid_columnconfigure(1, weight=1, minsize=180)

def main():
    try:
        import update_checker
        update_checker.tormachtips(__file__, CURRENT_VER)
    except Exception:
        pass
    root = tk.Tk()
    root.withdraw()
    path = get_default_hobbs_file()
    if not path:
        path = pick_file(root)
    if not path:
        root.destroy()
        return
    try:
        parse_hobbs_file(path)
    except Exception as e:
        tkMessageBox.showerror("Error","Failed to process file:\n%s" % str(e),parent=root)
        root.destroy()
        return
    root.deiconify()
    SummaryWindow(root, path)
    root.minsize(WINDOW_WIDTH, WINDOW_HEIGHT)
    root.mainloop()

if __name__ == "__main__":
    main()
