#!/usr/bin/env python3
# File: fprintd_tk.py
# Location: https://bgstack15.cgit/fprintd-tk
# Author: bgstack15
# Startdate: 2024-09-22-1 14:26
# SPDX-License-Identifier: GPL-3.0-only
# Title: Gui for fprintd
# Purpose: tkinter desktop gui app for management fingerprint enrollments
# Project: fprintd-tk
# History:
# Usage:
#    Run from window manager application menu
# Reference:
#    stackrpms_tk.py
# Improve:
# Dependencies:
#    dep-devuan: python3:any, python3-tkstackrpms, python3-tk, python3-pil
#    fprintd_tk_lib.py
# Documentation:
#    README.md

import tkinter as tk, os, tkinter.simpledialog, sys, threading, time
import tkstackrpms as stk
import fprintd_tk_lib as lib
from PIL import Image, ImageTk

ABOUT_TEXT = """
fprintd_tk \"Gui for fprintd\"
(C) 2024 bgstack15
SPDX-License-Identifier: GPL-3.0-only
Icons adapted from Numix-Icon-Theme-Circle (GPL 3)
"""

# These will be used a lot in the program.
str_hands = ["left","right"]
str_fingers = ["thumb","index-finger","middle-finger","ring-finger","little-finger"]

class App(tk.Frame):
   def __init__(self, master):
      super().__init__(master)
      # variables
      self.statustext = tk.StringVar()
      self.advanced = tk.BooleanVar(value=False)
      self.advanced.trace_add("write",self.load_data_into_form)
      self.current_username = os.getenv("USER")
      self.username = tk.StringVar(value=self.current_username)
      self.verbose = tk.BooleanVar(value=False)

      # configurable by admin or installation
      img_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"images")

      self.master.title("Gui for fprintd")
      imgicon = stk.get_scaled_icon("fingerprint-gui",24,"default", "","apps")
      self.master.tk.call("wm","iconphoto",self.master._w,imgicon)
      menu = tk.Menu(self.master)
      menu_file = tk.Menu(menu,tearoff=0)
      menu_file.add_checkbutton(label="Advanced features", variable=self.advanced, underline=0, state="disabled")
      menu_file.add_checkbutton(label="Verbose", variable=self.verbose, underline=0)
      menu_file.add_command(label="Delete...", command=self.func_delete, underline=0)
      menu_file.add_separator()
      menu_file.add_command(label="Exit", command=self.func_exit, underline=1)
      menu.add_cascade(label="File",menu=menu_file,underline=0)
      menu_help = tk.Menu(menu,tearoff=0)
      menu_help.add_command(label="About", command=self.func_about, underline=0)
      menu.add_cascade(label="Help",menu=menu_help,underline=0)
      self.master.config(menu=menu)
      self.grid() # use this instead of pack()
      self.background_color = self.master.cget("bg")

      # prepare finger images
      try:
         img_path + ""
      except:
         img_path = os.path.join(os.path.dirname(os.path.realpath(__file__)),"images")
      self.img_notenrolled = ImageTk.PhotoImage(stk.image_from_svg(os.path.join(img_path,"fingerprint-gui.svg"),32))
      self.img_enrolled = ImageTk.PhotoImage(stk.image_from_svg(os.path.join(img_path,"fingerprint-enrolled.svg"),32))

      # advanced pane, top
      self.frm_advanced = tk.Frame(self.master)
      # start hidden
      #self.frm_advanced.grid(row=0,column=0,columnspan=100)
      tk.Label(self.frm_advanced,text="User").grid(row=0,column=0)
      self.ent_username = stk.Entry(self.frm_advanced,textvariable=self.username,func=self.load_data_into_form)
      self.ent_username.grid(row=0,column=1)

      # action pane, left side
      self.frm_actions = tk.Frame(self.master)
      self.frm_actions.grid(row=1,column=0)
      self.action = tk.IntVar(value=1)
      self.last_fingerbutton_used = tk.StringVar()
      stk.Radiobutton(self.frm_actions,value=1,text="enroll",variable=self.action,underline=0).grid(row=0,column=0)
      stk.Radiobutton(self.frm_actions,value=2,text="verify",variable=self.action,underline=0).grid(row=1,column=0)
      tk.Button(self.frm_actions,text="Refresh",underline=0,command=self.load_data_into_form).grid(row=2,column=0)

      # array of fingers
      # we want 2 rows, 5 columns
      self.frm_fingers = tk.Frame(self.master)
      self.frm_fingers.grid(row=1,column=1)
      self.fingers = []
      hand = 0
      while hand < 2:
         finger = 0
         while finger < 5:
            fnum = finger if hand==1 else len(str_fingers)-finger-1
            #print(f"While preparing hand {hand}, evaluating finger {finger}, which might need to be number {fnum}")
            tf = str_hands[hand] + "-" + str_fingers[fnum]
            self.fingers.append(tk.Button(self.frm_fingers,text=tf,padx=0,pady=0,command=lambda f=tf: self.func_finger_button(f),image=self.img_notenrolled,compound="top"))
            self.fingers[-1].grid(row=hand,column=finger)
            finger = finger + 1
         hand = hand + 1
      # because the underline business does not work on the radio button itself
      # somehow the keypress takes over the lambda first var, so just set a dummy value, and pass our useful value as second parameter.
      self.master.bind("<Alt-e>",lambda a=1,b=1: self.set_action(a,b))
      self.master.bind("<Alt-v>",lambda a=1,b=2: self.set_action(a,b))
      self.master.bind("<Alt-r>",self.load_data_into_form)

      # status bar
      stk.StatusBar(self.master,var=self.statustext)
      # check if user has setusername polkit-1 permission, which lets him control fingerprint enrollments for other users.
      temp1 = lib.check_setusername_permission(self.func_update_status)
      if self.verbose.get():
         print(f"DEBUG (init): has set_username: {temp1}")
      if temp1:
         #self.chk_advanced.configure(state="enabled")
         #menu_file.child[0].child[0].configure(state="enabled")
         menu_file.entryconfigure(0,state="normal")
         #print(menu_file.children)
      # and now, load data into form for the first time
      self.load_data_into_form()

   def set_action(self, keypress, a):
      #print(f"DEBUG: got keypress {keypress}, a={a}")
      # this is used by Alt+E, Alt+V to select the radio buttons for enroll, verify, etc.
      self.action.set(a)

   def get_used_user(self):
      advanced = self.advanced.get()
      if advanced:
         used_user = self.username.get()
      else:
         used_user = self.current_username
      return used_user

   def load_data_into_form(self, a = None, b = None, c = None):
      time.sleep(0.05)
      advanced = self.advanced.get()
      # show advanced toolbar
      used_user = self.get_used_user()
      if advanced:
         if self.verbose.get():
            print(f"DEBUG: showing advanced toolbar")
         self.frm_advanced.grid(row=0,column=0,columnspan=100)
      else:
         if self.verbose.get():
            print(f"DEBUG: hiding advanced toolbar")
         self.frm_advanced.grid_forget()
      # update enrolled fingers icons
      try:
         self.old_enrolled_fingers = self.enrolled_fingers
      except:
         # will happen if self.enrolled_fingers is not present
         self.old_enrolled_fingers = []
      temp1 = lib.get_enrolled_fingers(used_user)
      if temp1:
         self.enrolled_fingers = temp1
      else:
         if self.verbose.get():
            print(f"DEBUG (load_data_into_form): having to skip empty response from get_enrolled_fingers")
         self.func_update_status(f"Unable to read enrolled fingers for user {used_user}.", reload = False)
      if self.verbose.get():
         print(f"DEBUG (load_data_into_form): got user {used_user} enrolled fingers {self.enrolled_fingers}")
      for i in self.fingers:
         if str(i.cget('text')) in self.enrolled_fingers:
            i.config(image=self.img_enrolled)
         else:
            i.config(image=self.img_notenrolled)

   # functions
   def func_about(self):
      """ Display about dialog. """
      tk.messagebox.Message(title="About",message=ABOUT_TEXT,icon="info").show()

   def func_exit(self):
      # in case we need to manually do stuff
      # otherwise command=self.client_exit would have sufficed.
      self.master.quit()

   def func_finger_button(self, finger):
      if self.verbose.get():
         print(f"DEBUG: func_finger_button finger {finger}, action {self.action.get()}")
      action = self.action.get()
      used_user = self.get_used_user()
      # position in array is same as the value coming from radio button for actions.
      available_actions = ["none","enroll","verify"]
      try:
         action_str = available_actions[action]
      except ValueError:
         action_str = "OFF"
      if action_str in available_actions:
         self.last_fingerbutton_used.set(finger)
         try:
            t1 = threading.Thread(target=lib.fprintd_action, args=(action_str, finger, self.func_update_status, used_user))
            t1.start()
         except Exception as e:
            self.statustext.set(e)
      else:
         self.statustext.set(f"Invalid action {action}, string {action_str}.")
      # This blocks everything! Do not use this.
      #t1.join()
      # unfortunately useless here, because of the threading.
      #self.load_data_into_form()

   def func_update_status(self, msg, reload = True):
      msg = msg.strip()
      if self.verbose.get():
         print(f"DEBUG (func_update_status): msg {msg}",file=sys.stderr)
      self.statustext.set(msg)
      try:
         this_finger = [i for i in self.fingers if i.cget("text") == self.last_fingerbutton_used.get()][0]
      except Exception as e:
         this_finger = None
         if self.verbose.get():
            print(f"DEBUG (func_update_status): while evaluating last-used finger, got {e}")
      # flash these red
      failure_messages = [
         "enroll-duplicate",
         "verify-no-match"
      ]
      # flash these green
      success_messages = [
         "verify-match",
         "enroll-completed"
      ]
      flashed = False
      for i in failure_messages:
         if i in msg:
            flashed = True
            if this_finger:
               stk.flash_entry(this_finger,["red",self.background_color]*3,300)
            break
      if not flashed:
         for i in success_messages:
            if i in msg:
               if this_finger:
                  stk.flash_entry(this_finger,["green",self.background_color]*3,300)
               break
      if reload:
         self.load_data_into_form()

   def func_delete(self):
      used_user = self.get_used_user()
      sure = tkinter.messagebox.askokcancel(title="Delete all enrolled fingerprints",message=f"Are you sure you want to delete all enrolled fingerprints for user {used_user}?")
      if sure:
         t1 = threading.Thread(target=lib.fprintd_action, args=("delete", "none", self.func_update_status, used_user))
         t1.start()
      else:
         self.statustext.set("Cancelled the delete action.")

# main
root = tk.Tk()
fprintd_tk = App(root)
fprintd_tk.mainloop()
