# 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

#############################################
##                                         ##
##       Spindle Time By Hour v0.95        ##
##          www.tormachtips.com            ##
##                                         ##
#############################################

# 0.95 - public beta - 4/2/26

# SHOWS SPINDLE RUN TIME, BY HOUR, FOR A SELECTED DAY
# CAN BE RUN FROM TERMINAL OR "ADMIN HOBBSH" FROM MDI LINE
# WILL ALSO RUN WHEN CLICKING INDIVIDUAL DAYS FROM THE HOBBS MONTHLY SCRIPT

import Tkinter as tk
import tkFileDialog
import tkMessageBox
import tkFont
import os
import re
import sys
import datetime

CURRENT_VER   = "0.95"
SCRIPT_NAME   = "Hobbs Hourly Usage Viewer"
DESCRIPTION   = "An hourly display of spindle time."
HOBBS_DIR     = "/home/operator/gcode/python/hobbs"
BAR_WIDTH     = 60
SEC_PER_BLOCK = 60
BLOCK         = u"\u2588"
HOBBS_FILE_RE = re.compile(r"^hobbs_(\d{4}-\d{2}-\d{2})\.txt$", re.IGNORECASE)

def extract_data(line):
    match = re.search(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) .* total = (\d+) seconds', line)
    if match:
        timestamp_str = match.group(1)
        total_sec = int(match.group(2))
        try:
            dt = datetime.datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S")
            return dt, total_sec
        except ValueError:
            return None, None
    return None, None

def analyze_hourly(filename):
    if not os.path.exists(filename):
        return {}
    hourly = dict((h, 0) for h in range(24))
    prev_total = None
    try:
        with open(filename, 'r') as f:
            for raw_line in f:
                line = raw_line.strip()
                dt, total = extract_data(line)
                if dt is None or total is None:
                    continue
                hour = dt.hour
                if prev_total is not None:
                    seconds_this_line = total - prev_total
                    if 0 < seconds_this_line <= 2:
                        hourly[hour] += seconds_this_line
                prev_total = total
    except Exception as e:
        print("Error reading file: %s" % e)
    return hourly

def make_bar(seconds):
    num_blocks = int(round(float(seconds) / SEC_PER_BLOCK))
    if num_blocks > BAR_WIDTH:
        num_blocks = BAR_WIDTH
    if num_blocks < 0:
        num_blocks = 0
    bar = BLOCK * num_blocks + u" " * (BAR_WIDTH - num_blocks)
    return u"[%s]" % bar

def build_scale(prefix_width):
    left_pad = u" " * prefix_width
    scale_line_1 = left_pad + u"0" + u" " * 14 + u"30" + u" " * 13 + u"min"
    scale_line_2 = left_pad + u"|" + BLOCK * BAR_WIDTH + u"|   (each %s = 2 min)" % BLOCK
    return scale_line_1 + u"\n" + scale_line_2

def format_display(file_path, hourly_seconds):
    total_seconds = sum(hourly_seconds.values())
    def fmt_xm_xs(seconds):
        minutes = seconds // 60
        secs = seconds % 60
        return u"%dm %ds" % (minutes, secs)
    hour_w = 5
    time_w = 8
    prefix_width = hour_w + 2 + time_w + 2 + 1
    lines = []
    lines.append(u"Hourly Spindle Runtime for %s" % os.path.basename(file_path))
    lines.append(u"")
    lines.append(u"=" * 87)
    lines.append(u"%-*s  %*s  %s" % (hour_w, u"Hour", time_w, u"Runtime", u"Hour Bar"))
    lines.append(u"-" * 87)
    for h in range(24):
        secs = hourly_seconds.get(h, 0)
        bar = make_bar(secs)
        hour_text = u"%02d:00" % h
        if secs > 0:
            time_text = u"%*s" % (time_w, fmt_xm_xs(secs))
        else:
            time_text = u"%*s" % (time_w, u"-")
        line = u"%-*s  %s  %s" % (hour_w, hour_text, time_text, bar)
        lines.append(line)
    lines.append(u"-" * 87)
    lines.append(u"TOTAL   %s" % fmt_xm_xs(total_seconds))
    lines.append(u"")
    lines.append(u"Use \u25c0 Left and Right \u25b6 arrow keys to move between days.")
    lines.append(u"www.tormachtips.com")
    return u"\n".join(lines)

def pick_mono_font(root):
    families = set(tkFont.families(root))
    candidates = ["DejaVu Sans Mono", "Liberation Mono", "FreeMono", "Courier New", "Courier"]
    for name in candidates:
        if name in families:
            return (name, 10)
    return ("Courier", 10)

def list_dated_hobbs_files():
    dated_files = []
    try:
        names = os.listdir(HOBBS_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(HOBBS_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()
    for file_date, path in reversed(dated_files):
        if file_date <= today:
            return path
    return dated_files[-1][1]

class HourlyWindow(object):
    def __init__(self, root, initial_file):
        self.root = root
        self.file_list = [path for _, path in list_dated_hobbs_files()]
        self.file_path = None
        self.root.title("Hobbs Hourly Spindle Runtime")
        w = root.winfo_screenwidth()
        h = root.winfo_screenheight()
        root.geometry("%dx%d+0+0" % (w, h))
        self.frame = tk.Frame(root)
        self.frame.pack(fill="both", expand=True, padx=12, pady=12)
        self.yscroll = tk.Scrollbar(self.frame, orient="vertical")
        self.yscroll.pack(side="right", fill="y")
        self.xscroll = tk.Scrollbar(self.frame, orient="horizontal")
        self.xscroll.pack(side="bottom", fill="x")
        self.text = tk.Text(
            self.frame,
            wrap="none",
            font=pick_mono_font(root),
            bg="#f8f8f8",
            width=110,
            yscrollcommand=self.yscroll.set,
            xscrollcommand=self.xscroll.set        )
        self.text.pack(fill="both", expand=True)
        self.yscroll.config(command=self.text.yview)
        self.xscroll.config(command=self.text.xview)
        self.root.bind("<Left>", self.on_prev_day)
        self.root.bind("<Right>", self.on_next_day)
        self.load_file(initial_file)
    
    def load_file(self, file_path):
        hourly_seconds = analyze_hourly(file_path)
        display_text = format_display(file_path, hourly_seconds)
        self.file_path = file_path
        self.root.title("Hobbs Hourly Spindle Runtime - %s" % os.path.basename(file_path))
        self.text.config(state="normal")
        self.text.delete("1.0", "end")
        self.text.insert("1.0", display_text)
        self.text.config(state="disabled")
    
    def get_current_index(self):
        try:
            return self.file_list.index(self.file_path)
        except ValueError:
            return -1
    
    def navigate_days(self, step):
        if not self.file_list:
            return "break"
        idx = self.get_current_index()
        if idx < 0:
            return "break"
        new_idx = idx + step
        if new_idx < 0 or new_idx >= len(self.file_list):
            return "break"
        new_path = self.file_list[new_idx]
        try:
            self.load_file(new_path)
        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 main():
    root = tk.Tk()
    initial_file = None
    if len(sys.argv) > 1:
        file_path = sys.argv[1]
        if not os.path.exists(file_path):
            tkMessageBox.showerror("Missing File", "File not found:\n%s" % file_path)
            root.destroy()
            return
        initial_file = file_path
    else:
        initial_file = get_default_hobbs_file()
        if not initial_file:
            initial_file = tkFileDialog.askopenfilename(
                title="Select hobbs log file",
                initialdir=HOBBS_DIR,
                filetypes=[("Hobbs files", "hobbs_*.txt"), ("All files", "*.*")]            )
            if not initial_file:
                tkMessageBox.showinfo("Cancelled", "No file selected.")
                root.destroy()
                return
    try:
        analyze_hourly(initial_file)
    except Exception as e:
        tkMessageBox.showerror("Error", "Failed to process file:\n%s" % str(e))
        root.destroy()
        return
    HourlyWindow(root, initial_file)
    root.mainloop()

if __name__ == "__main__":
    main()
