#! /usr/bin/env python2
# -*- coding: UTF-8 -*-

"""
 Mass Storage Cloner
 writes an image file to many mass storage devices with a gui
 (C) 2014 Martin Süfke
 License: GPLv3 or newer: http://www.gnu.org/licenses/gpl.html
"""

"""
 History: 

2014-10-14 started
2015-03-26 added write.sh quick hack for LIP SS2015, released on redmine.fsmpi.rwth-aachen.de
"""

# Proper logging
from pprint import pprint, pformat
import logging
# SPE compatible logging format
logging.basicConfig(level=logging.DEBUG,format='%(levelno)2d:%(funcName)s:%(message)s:File "%(filename)s", line %(lineno)d')
logger = logging.getLogger(__name__)

#import stacktrace
import inspect

#import  Tkinter #as Tk
try:
  import  ttk as Tk
#  Tk.Tk = Tkinter.Tk # need a Tk.Tk to make the root Window. Faking one.
except:
  logger.info("Tiling Toolkit (ttk) not found. Falling back to classic Tk")
  import  Tkinter as Tk

#from    Tkinter import BooleanVar, IntVar, DoubleVar, StringVar
#import  tkMessageBox as dialog
import  tkFileDialog as filedialog
from    Tkconstants import N,E,S,W,HORIZONTAL,TOP,BOTTOM,BOTH, NORMAL, DISABLED
from    vtk import VCheckbutton, VSLabel, VIScale, Vset, VGridFrame#, Vget
import  dbus

import dbusdetect
import gobject

import StoreObjFrames 

import os.path
import sys

import threading
import Queue

LastSavedFile=None

def DoLoadFile():
  filename = filedialog.askopenfilename(
    filetypes=[ ("Image files", "*.img"),
# TODO: zipped/bzipped/xzipped images ?               
#               ("All files", "*.*"),
                ("All files", "*"),
              ],
    title="Select image file to write"
  )
  logger.debug( "Open dialog returns >>>%s<<< ",str(filename))
  if (len(filename)==0) or (str(filename).strip("\0\t\r\n ")==""):
    return
  
def Implement(msg=""):
  logger.warn('Implement File "%s", line %s, %s'+msg,*inspect.stack()[1][1:4])

class ThreadEventedQueue(Queue.Queue):
  def __init__(self, EventName, TkRoot=None, maxsize=0):
    # Cannot get super() to work ?!?
    #super(ThreadEventedQueue, self ).__init__(maxsize)   
    Queue.Queue.__init__(self, maxsize)
    self.EventName = '<<{0:s}>>'.format(EventName)
    self.TkRoot = TkRoot

  def AddEvent(self, EventObj):
    self.put(EventObj)
    if self.TkRoot is not None:
      self.TkRoot.event_generate(self.EventName, when='tail')
  
  def SetRoot(self, TkRoot, Callback):
    self.TkRoot = TkRoot
    self.TkRoot.bind(self.EventName, Callback)
  
class MSClonerGui(threading.Thread):
  """
  Separate Thread according to 
  http://stackoverflow.com/questions/459083/how-do-you-run-your-own-code-alongside-tkinters-event-loop
  """
  def __init__(self, autostart=True, NotifierThread = None):
    threading.Thread.__init__(self)
    self.NotifierThread = NotifierThread
    self.ThreadNotifyQueue = ThreadEventedQueue('MyUDisk2Notify', None)
    #self.DoQuit = False
    if autostart:
      self.start()
    
  def BuildGui(self):
    try:  
      self.root = Tk.Tk()
    except AttributeError:
      self.root = Tk.Tkinter.Tk()
  
    #build the GUI on 'root'
    self.root.wm_title("MassStorageCloner - GPLv3 or newer (C) 2014 Martin Süfke")
    FrameHead = VGridFrame(self.root, relief='raised')
    self.CbRemovableOnly = VCheckbutton(master=FrameHead, text="Removable only", command=self.OnCbRemovableOnly)
    FrameContent = Tk.Frame(self.root)
    FrameContDevice = Tk.Frame(FrameContent)
      
    self.root.columnconfigure(0,weight=1)
    self.root.rowconfigure(0,weight=1)
    FrameHead.grid(column=0, row=0, sticky=(N,E,W))
    FrameHead.grid_next([self.CbRemovableOnly,
                         Tk.Button(master=FrameHead, text="Unhide ignored", command=self.OnUnhideIgnored),
                         ])    
    FrameContent.grid(column=0, row=1, sticky=(N,S,E,W))
    FrameContent.columnconfigure(0,weight=1)
    FrameContent.rowconfigure(0,weight=1)
    FrameContDevice.grid(sticky=(N,E,S,W))
  
    self.TKStorageFrame = StoreObjFrames.TkFStorage(FrameContDevice)
    dbusdetect.ScanDevices(self.TKStorageFrame)

  def run(self):
    logger.debug("GUI thread run()")
      #self.root.update()
    self.BuildGui()
    logger.debug("GUI thread built GUI")
    # Activate Event system
    self.ThreadNotifyQueue.SetRoot(self.root, self.ThreadNotifyCallback)
    #if not self.DoQuit:
    self.root.mainloop()
    logger.debug("Tk mainloop() ended")
    if self.NotifierThread is not None:
      if self.NotifierThread.is_alive():
        self.NotifierThread.quit()
      else:
        logger.debug('NotifierThread already dead')
        
  def quit(self):
    #self.DoQuit = False
    #try:
    self.root.quit()
    #except:
    #  pass

  def OnCbRemovableOnly(self):
    """ CbRemovableOnly has been checked or unchecked """
    #logger.debug("CbRemovableOnly(): %s", self.CbRemovableOnly.value )
    if self.CbRemovableOnly.value:
      self.TKStorageFrame.FilterViewDriveAdd({'Removable':False})
    else:
      self.TKStorageFrame.FilterViewDriveRemove(['Removable', ])

  def OnUnhideIgnored(self):
    """ Unhide Ignore has been clicked"""
    self.TKStorageFrame.FilterViewDriveRemove(['Id', ])

  def SignalDBusEvent(self, EventObj):
    """ This can be called by the DBus notifier thread """
    assert self.ThreadNotifyQueue is not None
    self.ThreadNotifyQueue.AddEvent(EventObj)
    
  def ThreadNotifyCallback(self, event):
    try:
      while True:
        ev = self.ThreadNotifyQueue.get_nowait()
        logger.debug("GUI Thread %s %s", ev, event)
        self.ProcessNotifyEvent(ev)
        self.ThreadNotifyQueue.task_done()
    except Queue.Empty:
      pass

  def ProcessNotifyEvent(self, event):
    """ event is a tuple containing at least an Identifier in event[0] 
      called by ThreadNotifyCallback()
    """
    assert len(event)>0, "Event tuple length >0 required, got {0!s}".format(event)
    if event[0] == 'MountPointsChanged':
      #assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
      assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
      self.TKStorageFrame.UpdateMountPoints(event[1], event[2], dbusdetect.UnmountByUDisks)
    elif event[0] == 'DriveAdded':
      assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
      self.TKStorageFrame.AddDrive(event[1], event[2], dbusdetect.EjectByUDisks, dbusdetect.PowerOffByUDisks)
    elif event[0] == 'BlockDeviceAdded':
      assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
      self.TKStorageFrame.AddBlockDevice(event[1], event[2])
    elif event[0] == 'FileSystemAdded':
      assert len(event)>2, "Event tuple length >2 required, got {0!s}".format(event)
      self.TKStorageFrame.AddFileSystem(event[1],event[2], dbusdetect.MountByUDisks, dbusdetect.UnmountByUDisks)
    elif event[0] == 'DriveRemoved':
      #self.TKStorageFrame.AddDrive(event[1], event[2], dbusdetect.EjectByUDisks, dbusdetect.PowerOffByUDisks)
      assert len(event)>1, "Event tuple length >1 required, got {0!s}".format(event)
      self.TKStorageFrame.RemoveDrive(event[1])
    elif event[0] == 'BlockDeviceRemoved':
      #self.TKStorageFrame.AddBlockDevice(event[1], event[2])
      assert len(event)>1, "Event tuple length >1 required, got {0!s}".format(event)
      self.TKStorageFrame.RemoveBlockDevice(event[1])
    elif event[0] == 'FileSystemRemoved':
      assert len(event)>1, "Event tuple length >1 required, got {0!s}".format(event)
      self.TKStorageFrame.RemoveFileSystem(event[1])
      #self.TKStorageFrame.AddFileSystem(event[1],event[2], dbusdetect.MountByUDisks, dbusdetect.UnmountByUDisks)
    else:
      logger.warn("Unknown Event %s",event[0])

class MSGObjectNotifier(threading.Thread):
  """ Another thread to contain the GObject mainloop """
  def __init__(self, GuiThread = None):
    threading.Thread.__init__(self)
    self.GuiThread = GuiThread

  def run(self):
    logger.debug("GObject thread started")
    try:
      gobject.threads_init() # the key to make gobject threads run in python
      self.loop = gobject.MainLoop()
      self.context = self.loop.get_context()
      self.InstallDBusListener()
      logger.debug("running GObject mainloop()")
      self.loop.run()
    finally:
      logger.debug("GObject mainloop() ended")
      if self.GuiThread is not None:
        if self.GuiThread.is_alive():
          self.GuiThread.quit()
        else:
          logger.debug('GuiThread already dead')
    
  def quit(self):
    self.loop.quit()

  def InstallDBusListener(self):
    dbusdetect.InstallChangeReceiver(
      PropertiesChangedHandler=self.UDisks2Signal_PropertiesChanged,
      InterfaceAddedHander=self.UDisks2Signal_InterfaceAdded,
      InterfaceRemovedHandler=self.UDisks2Signal_InterfaceRemoved,
    )

  def UDisks2Signal_PropertiesChanged(self, *args, **kwargs):
    """ Callback for any DBus.Properties -> PropertiesChanged """
  #  print "UDisks2 Properties Changed: {0!s} {1!s}\n".format(args,pformat(kwargs))
    if (len(args) >= 3) and (len(kwargs) >= 1):
      # May have args: subject, message, signature   (TODO: How are these defined in DBus ?)
      if 'org.freedesktop.UDisks2.Filesystem' == args[0]:
        if isinstance(args[1],dict) and ('MountPoints' in args[1]):
          # don't care about args[2] / Signature
          U2MountPoints=args[1].get('MountPoints',[])
          #logger.debug('U2Path: %s %s',type(kwargs), pformat(kwargs))
          U2Path=kwargs.get('path','')
          #logger.debug('U2Path: %s',U2Path)
          if U2Path.startswith('/org/freedesktop/UDisks2/block_devices/'):
            # Have a change in Block Devices
            self.SignalGuiMountPointChanged(U2Path, U2MountPoints)
            return
            
    assert logger is not None
    logger.info("Unhandled Message %s %s\n", args, pformat(kwargs))
      
  def UDisks2Signal_InterfaceAdded(self, *args, **kwargs):
    """ Callback for any UDisks2.ObjectManager -> InterfaceAdded 
    Through this call go drives and blockdevices that are added """
    if (len(args) >= 2):
      U2Path = args[0]
      if isinstance(args[1], dict):
        U2Dict = args[1]
        #logger.debug("U2Dict: %s\n", pformat(U2Dict))
      else:
        U2Dict = {}
      if U2Path.startswith('/org/freedesktop/UDisks2/drives/'):
        #logger.debug("Drive added %s\n", args[0].rsplit('/')[-1])
        DriveDict = U2Dict.get('org.freedesktop.UDisks2.Drive')
        if DriveDict is not None:
          self.SignalGuiDriveAdded(U2Path, DriveDict)
          return
      elif U2Path.startswith('/org/freedesktop/UDisks2/block_devices/'):
        #logger.debug("Blockdevice added %s\n", args[0].rsplit('/')[-1])
        BlockDict = U2Dict.get('org.freedesktop.UDisks2.Block')
        if BlockDict is not None:
          self.SignalGuiBlockDeviceAdded(U2Path, BlockDict)
        FsDict = U2Dict.get('org.freedesktop.UDisks2.Filesystem')
        if FsDict is not None:
          self.SignalGuiFileSystemAdded(U2Path, FsDict)
        if (BlockDict is not None) or (FsDict is not None):
          return
        
    assert logger is not None
    logger.info("Unknown %s %s\n", args, pformat(kwargs))
  
  def UDisks2Signal_InterfaceRemoved(self, *args, **kwargs):
    """ Callback for any UDisks2.ObjectManager -> InterfaceRemoved """
    if (len(args) >= 2):
      U2Path = args[0]
      if isinstance(args[1], list):
        U2List = args[1]
      else:
        U2List = []
      Unknown = True
      if U2Path.startswith('/org/freedesktop/UDisks2/block_devices/'):
        if 'org.freedesktop.UDisks2.Filesystem' in U2List:
          self.SignalGuiFileSystemRemoved(U2Path)
          Unknown = False
        if 'org.freedesktop.UDisks2.Block' in U2List:
          self.SignalGuiBlockDeviceRemoved(U2Path)
          Unknown = False
      if U2Path.startswith('/org/freedesktop/UDisks2/drives/'):
        #logger.debug("Drive removed %s\n", args[0].rsplit('/')[-1])
        if 'org.freedesktop.UDisks2.Drive' in U2List:
          self.SignalGuiDriveRemoved(U2Path)
          Unknown = False

    if not Unknown:
      return

    assert logger is not None
    logger.info("Unknown %s %s\n", args, pformat(kwargs))

  def SignalGuiMountPointChanged(self, U2Path, U2MountPoints):
    logger.debug("GObj() path %s mp %s\n", U2Path, U2MountPoints)
    EvObj=('MountPointsChanged', U2Path, U2MountPoints)
    self.GuiThread.SignalDBusEvent(EvObj)

  def SignalGuiDriveAdded(self, U2Path, U2Dict):
    logger.debug("GObj() path %s u2d.k %s\n", U2Path, U2Dict.keys())
    EvObj=('DriveAdded', U2Path, U2Dict)
    self.GuiThread.SignalDBusEvent(EvObj)

  def SignalGuiBlockDeviceAdded(self, U2Path, U2Dict):
    logger.debug("GObj() path %s u2d.k %s\n", U2Path, U2Dict.keys())
    EvObj=('BlockDeviceAdded', U2Path, U2Dict)
    self.GuiThread.SignalDBusEvent(EvObj)

  def SignalGuiFileSystemAdded(self, U2Path, U2Dict):
    logger.debug("GObj() path %s u2d.k %s\n", U2Path, U2Dict.keys())
    EvObj=('FileSystemAdded', U2Path, U2Dict)
    self.GuiThread.SignalDBusEvent(EvObj)

  def SignalGuiEventRemoved(self, name, param1):
    EvObj=(name, param1)
    self.GuiThread.SignalDBusEvent(EvObj)

  def SignalGuiDriveRemoved(self, U2Path):
    self.SignalGuiEventRemoved('DriveRemoved', U2Path)

  def SignalGuiBlockDeviceRemoved(self, U2Path):
    self.SignalGuiEventRemoved('BlockDeviceRemoved', U2Path)

  def SignalGuiFileSystemRemoved(self, U2Path):
    self.SignalGuiEventRemoved('FileSystemRemoved', U2Path)
    
gnotifier =  MSGObjectNotifier()
gui = MSClonerGui(NotifierThread=gnotifier)
gnotifier.GuiThread = gui
gnotifier.start()

#dbusdetect.InstallChangeReceiver()

#logger.debug('Time to do more in the main thread')
# Do not know what to do here (sensibly) to wait ... trying join()
#import time
#while gui.is_alive() and gnotifier.is_alive():
#    time.sleep(1)
#    logger.debug('tick ')

# Waiting for a key press is not smart either
#logger.info("Waiting for key press on stdin")
#sys.stdin.read(1)
#gnotifier.quit()
#gui.quit()

logger.debug('Waiting for thread joins')
gnotifier.join()
gui.join()     # don't know: join() again or not ?
logger.debug('End, threads joined')

"""make use of figure.ginput() for mouse input or waitforbuttonpress() for keyboard-stuff
"""

def _quit(self):
  logger.debug("called python _quit()")
  if gui is not None:
    gui.quit()     # stops mainloop
    gui.root.destroy()  # this is necessary on Windows to prevent
                        # Fatal Python Error: PyEval_RestoreThread: NULL tstate

#end;