#https://Makry.net
import os
import json
import datetime
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog

try:
    import win32print
    WIN32_AVAILABLE = True
except ImportError:
    WIN32_AVAILABLE = False

APP_DIR = os.path.abspath(os.path.dirname(__file__))
OUT_DIR = os.path.join(APP_DIR, "prints")
os.makedirs(OUT_DIR, exist_ok=True)
SETTINGS_PATH = os.path.join(APP_DIR, "settings.json")

DEFAULT_SETTINGS = {
    "printer_name": None,
    "width_mm": 80,
    "design": "Simple"
}

BOTTOM_FEED_LINES = 6  # Extra feed after each task

# -------------------------
# Settings Load/Save
# -------------------------
def load_settings():
    settings = DEFAULT_SETTINGS.copy()
    try:
        if os.path.exists(SETTINGS_PATH):
            with open(SETTINGS_PATH, "r", encoding="utf-8") as f:
                data = json.load(f)
                settings.update(data)
    except Exception:
        pass
    return settings

def save_settings(settings):
    try:
        with open(SETTINGS_PATH, "w", encoding="utf-8") as f:
            json.dump(settings, f, indent=4)
    except Exception as e:
        messagebox.showwarning("Save settings", f"Could not save settings:\n{e}")

# -------------------------
# Printer helpers
# -------------------------
def list_printers():
    if not WIN32_AVAILABLE:
        return []
    printers = []
    try:
        flags = win32print.PRINTER_ENUM_LOCAL | win32print.PRINTER_ENUM_CONNECTIONS
        for p in win32print.EnumPrinters(flags):
            if len(p) >= 3:
                printers.append(p[2])
    except Exception:
        try:
            printers = [win32print.GetDefaultPrinter()]
        except Exception:
            printers = []
    return printers

def send_bytes_to_printer(printer_name: str, data: bytes):
    if not WIN32_AVAILABLE:
        raise RuntimeError("pywin32 is not available")
    hPrinter = None
    try:
        if not printer_name:
            printer_name = win32print.GetDefaultPrinter()
        hPrinter = win32print.OpenPrinter(printer_name)
        win32print.StartDocPrinter(hPrinter, 1, ("PythonRawPrintJob", None, "RAW"))
        win32print.StartPagePrinter(hPrinter)
        win32print.WritePrinter(hPrinter, data)
        win32print.EndPagePrinter(hPrinter)
        win32print.EndDocPrinter(hPrinter)
    finally:
        if hPrinter:
            win32print.ClosePrinter(hPrinter)

# -------------------------
# ESC/POS Task Builder
# -------------------------
def build_design_lines(title: str, contents: str, design: str):
    contents = contents.replace("\r\n", "\n").replace("\r", "\n")
    lines = []
    if design == "Boxed":
        width = 30
        lines.append("=" * width)
        lines.append(title.center(width))
        lines.append("=" * width)
    elif design == "Header":
        lines.append(f"--- {title.upper()} ---")
    else:  # Simple
        lines.append(f"TASK: {title}")
        lines.append("-" * max(10, len(title)))
    if contents.strip() == "":
        lines.append("(no contents)")
    else:
        lines.extend(contents.splitlines())
    return lines

def build_raw_for_task(title: str, contents: str, design: str):
    ESC = b'\x1b'
    GS = b'\x1d'
    bold_on = ESC + b'\x45' + b'\x01'
    bold_off = ESC + b'\x45' + b'\x00'
    cut_full = GS + b'\x56' + b'\x00'
    lf = b'\n'
    def enc(s):
        return s.encode("cp437", errors="replace")
    lines = build_design_lines(title, contents, design)
    data = b''
    # Bold first line
    data += bold_on + enc(lines[0]) + lf + bold_off
    # Rest normal
    for ln in lines[1:]:
        data += enc(ln) + lf
    # Bottom feed
    data += lf * BOTTOM_FEED_LINES + cut_full
    return data

# -------------------------
# GUI Application
# -------------------------
class ThermalTasksApp(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("Thermal Tasks Printer")
        self.geometry("900x600")
        self.settings = load_settings()
        self.tasks = []
        self.create_menu()
        self.create_widgets()
        self.refresh_task_list()
        self.status_write("Ready.\n")

    def create_menu(self):
        menubar = tk.Menu(self)
        file_menu = tk.Menu(menubar, tearoff=0)
        file_menu.add_command(label="Settings", command=self.open_settings)
        file_menu.add_separator()
        file_menu.add_command(label="Exit", command=self.quit)
        menubar.add_cascade(label="File", menu=file_menu)
        self.config(menu=menubar)

    def create_widgets(self):
        left = ttk.Frame(self)
        left.pack(side="left", fill="y", padx=8, pady=8)
        ttk.Label(left, text="Tasks").pack(anchor="w")
        self.listbox = tk.Listbox(left, width=44, height=28)
        self.listbox.pack(padx=4, pady=4)
        self.listbox.bind("<Double-Button-1>", lambda e: self.edit_task())
        btn_frame = ttk.Frame(left)
        btn_frame.pack(fill="x", pady=6)
        ttk.Button(btn_frame, text="Add Task", command=self.add_task).pack(side="left", padx=3)
        ttk.Button(btn_frame, text="Edit Selected", command=self.edit_task).pack(side="left", padx=3)
        ttk.Button(btn_frame, text="Delete Selected", command=self.delete_task).pack(side="left", padx=3)
        action_frame = ttk.Frame(left)
        action_frame.pack(fill="x", pady=6)
        ttk.Button(action_frame, text="Print Selected", command=self.print_selected).pack(side="left", padx=3)
        ttk.Button(action_frame, text="Print All", command=self.print_all).pack(side="left", padx=3)

        right = ttk.Frame(self)
        right.pack(side="left", fill="both", expand=True, padx=8, pady=8)
        ttk.Label(right, text="Status / Log").pack(anchor="w")
        self.text_status = tk.Text(right, wrap="word", state="disabled")
        self.text_status.pack(fill="both", expand=True, padx=4, pady=4)

        bottom = ttk.Frame(self)
        bottom.pack(side="bottom", fill="x", padx=8, pady=6)
        self.printer_label_var = tk.StringVar(value=f"Printer: {self.settings.get('printer_name')}")
        ttk.Label(bottom, textvariable=self.printer_label_var).pack(side="left")
        self.design_label_var = tk.StringVar(value=f"Design: {self.settings.get('design')}")
        ttk.Label(bottom, textvariable=self.design_label_var).pack(side="left", padx=(12,0))

    def status_write(self, s: str):
        self.text_status.configure(state="normal")
        self.text_status.insert("end", s)
        self.text_status.see("end")
        self.text_status.configure(state="disabled")

    # -------------------------
    # Task operations
    # -------------------------
    def add_task(self):
        dlg = TaskDialog(self, "Add Task")
        self.wait_window(dlg.top)
        if dlg.result:
            title, contents = dlg.result
            self.tasks.append((title, contents))
            self.refresh_task_list()
            self.status_write(f"Added task: {title}\n")

    def edit_task(self):
        sel = self.listbox.curselection()
        if not sel:
            messagebox.showinfo("Edit", "Select a task to edit.")
            return
        idx = sel[0]
        title, contents = self.tasks[idx]
        dlg = TaskDialog(self, "Edit Task", init_title=title, init_contents=contents)
        self.wait_window(dlg.top)
        if dlg.result:
            self.tasks[idx] = dlg.result
            self.refresh_task_list()
            self.status_write(f"Edited task #{idx+1}\n")

    def delete_task(self):
        sel = self.listbox.curselection()
        if not sel:
            messagebox.showinfo("Delete", "Select a task to delete.")
            return
        idx = sel[0]
        t, _ = self.tasks.pop(idx)
        self.refresh_task_list()
        self.status_write(f"Deleted task: {t}\n")

    def refresh_task_list(self):
        self.listbox.delete(0, "end")
        for i, (t, _) in enumerate(self.tasks):
            display = t if len(t) <= 36 else t[:33] + "..."
            self.listbox.insert("end", f"{i+1}. {display}")

    # -------------------------
    # Printing
    # -------------------------
    def print_selected(self):
        sel = self.listbox.curselection()
        if not sel:
            messagebox.showinfo("Print", "Select a task to print.")
            return
        idx = sel[0]
        self._print_tasks([self.tasks[idx]])

    def print_all(self):
        if not self.tasks:
            messagebox.showinfo("Print", "No tasks to print.")
            return
        self._print_tasks(self.tasks)

    def _print_tasks(self, task_list):
        printer_name = self.settings.get("printer_name")
        design = self.settings.get("design", "Simple")
        if not printer_name:
            messagebox.showerror("No Printer", "Please select a printer in Settings.")
            return
        self.status_write(f"Printing to {printer_name}...\n")
        for title, contents in task_list:
            timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S_%f")
            try:
                raw_bytes = build_raw_for_task(title, contents, design=design)
                if WIN32_AVAILABLE:
                    try:
                        send_bytes_to_printer(printer_name, raw_bytes)
                        self.status_write(f"Printed: {title}\n")
                    except Exception as e:
                        self.status_write(f"Print failed ({e}), saved to file.\n")
                        path = os.path.join(OUT_DIR, f"task_raw_{timestamp}.bin")
                        with open(path, "wb") as f:
                            f.write(raw_bytes)
                        self.status_write(f"Saved: {path}\n")
                else:
                    path = os.path.join(OUT_DIR, f"task_raw_{timestamp}.bin")
                    with open(path, "wb") as f:
                        f.write(raw_bytes)
                    self.status_write(f"Saved raw print file: {path}\n")
            except Exception as e:
                self.status_write(f"Error printing '{title}': {e}\n")
        messagebox.showinfo("Print", "Sent to printer.")

    # -------------------------
    # Settings
    # -------------------------
    def open_settings(self):
        dlg = SettingsDialog(self, self.settings)
        self.wait_window(dlg.top)
        if getattr(dlg, "saved", False):
            self.settings = load_settings()
            self.printer_label_var.set(f"Printer: {self.settings.get('printer_name')}")
            self.design_label_var.set(f"Design: {self.settings.get('design')}")
            self.status_write("Settings updated.\n")

# -------------------------
# Dialogs
# -------------------------
class TaskDialog:
    def __init__(self, parent, title, init_title="", init_contents=""):
        self.top = tk.Toplevel(parent)
        self.top.title(title)
        self.result = None
        frm = ttk.Frame(self.top, padding=8)
        frm.pack(fill="both", expand=True)
        ttk.Label(frm, text="Task (bold)").pack(anchor="w")
        self.entry_title = ttk.Entry(frm, width=80)
        self.entry_title.pack(fill="x", pady=(0,6))
        self.entry_title.insert(0, init_title)
        ttk.Label(frm, text="Contents").pack(anchor="w")
        self.text_contents = tk.Text(frm, width=80, height=12)
        self.text_contents.pack(fill="both", expand=True, pady=(0,6))
        self.text_contents.insert("1.0", init_contents)
        btns = ttk.Frame(frm)
        btns.pack(fill="x")
        ttk.Button(btns, text="OK", command=self.on_ok).pack(side="right", padx=4)
        ttk.Button(btns, text="Cancel", command=self.top.destroy).pack(side="right")
    def on_ok(self):
        title = self.entry_title.get().strip()
        contents = self.text_contents.get("1.0", "end").rstrip()
        if not title:
            messagebox.showwarning("Validation", "Task title cannot be empty.")
            return
        self.result = (title, contents)
        self.top.destroy()

class SettingsDialog:
    def __init__(self, parent, settings):
        self.top = tk.Toplevel(parent)
        self.top.title("Settings")
        self.saved = False
        self.settings = settings.copy()
        frm = ttk.Frame(self.top, padding=8)
        frm.pack(fill="both", expand=True)
        ttk.Label(frm, text="Printer (Windows):").grid(row=0, column=0, sticky="w", pady=4)
        self.printers = list_printers()
        if not self.printers:
            self.printers = ["(no printers detected)"]
        self.printer_var = tk.StringVar(value=self.settings.get("printer_name") or "")
        self.printer_combo = ttk.Combobox(frm, values=self.printers, textvariable=self.printer_var, width=60)
        self.printer_combo.grid(row=0, column=1, sticky="w", padx=6, pady=4)
        ttk.Label(frm, text="Paper width (mm):").grid(row=1, column=0, sticky="w", pady=4)
        self.width_var = tk.IntVar(value=self.settings.get("width_mm", 80))
        ttk.Spinbox(frm, from_=40, to=120, textvariable=self.width_var, width=8).grid(row=1, column=1, sticky="w", padx=6, pady=4)
        ttk.Label(frm, text="Design:").grid(row=2, column=0, sticky="w", pady=4)
        self.design_var = tk.StringVar(value=self.settings.get("design", "Simple"))
        ttk.Combobox(frm, values=["Simple", "Boxed", "Header"], textvariable=self.design_var, width=20).grid(row=2, column=1, sticky="w", padx=6, pady=4)
        btn_frame = ttk.Frame(frm)
        btn_frame.grid(row=10, column=0, columnspan=2, pady=(12,0), sticky="e")
        ttk.Button(btn_frame, text="Save", command=self.save).pack(side="right", padx=6)
        ttk.Button(btn_frame, text="Cancel", command=self.top.destroy).pack(side="right")
        ttk.Button(frm, text="Test Print", command=self.test_print).grid(row=11, column=0, columnspan=2, pady=(8,0))

    def save(self):
        new_settings = {
            "printer_name": self.printer_var.get() or None,
            "width_mm": int(self.width_var.get()),
            "design": self.design_var.get()
        }
        save_settings(new_settings)
        self.saved = True
        self.top.destroy()

    def test_print(self):
        sample_title = "SAMPLE TASK"
        sample_contents = "This is a test print."
        raw = build_raw_for_task(sample_title, sample_contents, design=self.design_var.get())
        printer_name = self.printer_var.get() or None
        ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
        if WIN32_AVAILABLE and printer_name:
            try:
                send_bytes_to_printer(printer_name, raw)
                messagebox.showinfo("Test Print", "Test print sent to printer.")
            except Exception as e:
                path = os.path.join(OUT_DIR, f"test_raw_{ts}.bin")
                with open(path, "wb") as f:
                    f.write(raw)
                messagebox.showwarning("Test Print Failed", f"Could not send to printer: {e}\nSaved raw bytes to {path}")
        else:
            path = os.path.join(OUT_DIR, f"test_raw_{ts}.bin")
            with open(path, "wb") as f:
                f.write(raw)
            messagebox.showinfo("Test Print", f"No printer available. Test file saved:\n{path}")

# -------------------------
# Run Application
# -------------------------
if __name__ == "__main__":
    if not os.path.exists(SETTINGS_PATH):
        save_settings(DEFAULT_SETTINGS)
    app = ThermalTasksApp()
    app.mainloop()
