#!/usr/bin/env python2.5
# -*- coding: utf-8 -*-
import gtk,hildon,gobject,pickle,gzip
from Crypto.Cipher import Blowfish
from Crypto.Hash import MD5
import gettext


"""
Esta classe mantém as informações sobre os itens (grupos e seus itens internos)
Também salva e carrega do arquivo
"""
class Dados:

  DATABASE_OK = 0
  ARQUIVO_NAO_LOCALIZADO = 1
  SENHA_INVALIDA = 2
  DADOS_CORROMPIDOS = 3

  def __init__(self, p, db = "/home/user/MyDocs/pysafe.db"):
    self.password = p
    self.database_file = db
    self.changed = False
    self.dados = {}
    return

  def load(self):
    crypt = Blowfish.new(self.password)
    md5 = MD5.new()

    # le o arquivo compactado
    try:
      arch = gzip.GzipFile(self.database_file, 'rb')
    except IOError:
      return self.ARQUIVO_NAO_LOCALIZADO
    buffer = ""
    while True:
      data = arch.read()
      if data == "":
        break
      buffer += data
    arch.close()

    # teoricamente, descriptografa ele
    dados_tmp = crypt.decrypt(buffer)
    # valida a senha...segurança para saber se a senha é realmente válida
    # e as informações podem ser descriptografadas corretamente
    pos = dados_tmp.find("\n")
    if pos == -1:
      return self.SENHA_INVALIDA
    senha = dados_tmp[:pos]
    if senha != self.password:
      return self.SENHA_INVALIDA

    # chegou aqui, a senha confere...pode apagar!
    dados_tmp = dados_tmp[pos + 1:]

    # pega o MD5...
    pos = dados_tmp.find("\n")
    if pos == -1:
      return self.DADOS_CORROMPIDOS
    checksum = dados_tmp[:pos]

    # remove o checksum para criar efetivamente os dados
    dados_tmp = dados_tmp[pos + 1:].strip()
    md5.update(dados_tmp)

    if checksum != md5.hexdigest():
      return self.DADOS_CORROMPIDOS

    self.dados = pickle.loads(dados_tmp)

    return self.DATABASE_OK

  def save(self, force = False, new_pass = None):
    if force == False and self.changed == False:
      return True

    if new_pass != None:
      self.password = new_pass

    crypt = Blowfish.new(self.password)
    md5 = MD5.new()

    # serializa os dados
    dados_tmp = pickle.dumps(self.dados, 1)
    # cria o MD5 deles
    md5.update(dados_tmp)
    # acrescenta a senha e o checksum
    dados_tmp = "%s\n%s\n%s" % (self.password, md5.hexdigest(), dados_tmp)
    # criptografa!
    dados_tmp = crypt.encrypt(self.fillWithSpace(dados_tmp))

    # salva o arquivo compactado
    try:
      arch = gzip.GzipFile(self.database_file, "wb")
    except IOError:
      return False
    arch.write(dados_tmp)
    arch.close()
    return True

  def fillWithSpace(self, s):
    while len(s) % 8 != 0:
      s = "%s " % (s)
    return s

  def getGroups(self):
    return self.dados.keys()

  def addGroup(self, name):
    if name in self.dados:
      return False
    self.changed = True
    self.dados[name] = {}
    return True

  def delGroup(self, name):
    self.changed = True
    del self.dados[name]

  def addItem(self, group, name):
    if name in self.dados[group]:
      return False
    self.changed = True
    self.dados[group][name] = {}
    return True

  def delItem(self, group, item):
    self.changed = True
    g = self.dados[group]
    del g[item]
    self.dados[group] = g

  def getItens(self, group):
    return self.dados[group]

  def getItem(self, group, item):
    return self.dados[group][item]

  def addDetail(self, group, item, detail):
    if detail in self.dados[group][item]:
      return False
    self.changed = True
    self.dados[group][item][detail] = ""
    return True

  def delDetail(self, group, item, detail):
    self.changed = True
    g = self.dados[group][item]
    del g[detail]
    self.dados[group][item] = g

  def setDetail(self, group, item, detail, value):
    self.changed = True
    self.dados[group][item][detail] = value


"""
Classe responsável pela exibição da tela principal do aplicativo
"""
class MainWindow():

  def __init__(self):
    self.ignore_list_clicked = False
    self.atual_group = None
    self.database = None

    self.window = hildon.StackableWindow()
    self.window.set_title("pySafe")

    # adiciona o menu
    menu = hildon.AppMenu()
    self.add_group_button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
                          hildon.BUTTON_ARRANGEMENT_VERTICAL)
    self.add_group_button.set_text(_("Add group"), "")
    self.add_group_button.connect("clicked", self.add_group_clicked)

    self.del_group_button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
                          hildon.BUTTON_ARRANGEMENT_VERTICAL)
    self.del_group_button.set_text(_("Remove group"), "")
    self.del_group_button.connect("clicked", self.del_group_clicked)

    self.add_item_button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
                          hildon.BUTTON_ARRANGEMENT_VERTICAL)
    self.add_item_button.set_text(_("Add item"), "")
    self.add_item_button.connect("clicked", self.add_item_clicked)

    self.del_item_button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
                          hildon.BUTTON_ARRANGEMENT_VERTICAL)
    self.del_item_button.set_text(_("Remove item"), "")
    self.del_item_button.connect("clicked", self.del_item_clicked)

    self.change_pass_button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
                          hildon.BUTTON_ARRANGEMENT_VERTICAL)
    self.change_pass_button.set_text(_("Change password"), "")
    self.change_pass_button.connect("clicked", self.change_password_clicked)

    self.about_button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
                          hildon.BUTTON_ARRANGEMENT_VERTICAL)
    self.about_button.set_text(_("About"), "")
    self.about_button.connect("clicked", self.about_clicked)

    self.add_detail_button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
                          hildon.BUTTON_ARRANGEMENT_VERTICAL)
    self.add_detail_button.set_text(_("Add detail"), "")
    self.add_detail_button.connect("clicked", self.add_detail_clicked)

    self.del_detail_button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
                          hildon.BUTTON_ARRANGEMENT_VERTICAL)
    self.del_detail_button.set_text(_("Remove detail"), "")
    self.del_detail_button.connect("clicked", self.del_detail_clicked)

    self.del_group_button.set_sensitive(False)
    self.add_item_button.set_sensitive(False)
    self.del_item_button.set_sensitive(False)
    self.add_detail_button.set_sensitive(False)
    self.del_detail_button.set_sensitive(False)

    menu.append(self.add_group_button)
    menu.append(self.del_group_button)
    menu.append(self.add_item_button)
    menu.append(self.del_item_button)
    menu.append(self.change_pass_button)
    menu.append(self.about_button)

    menu.show_all()
    self.window.set_app_menu(menu)

    self.window.connect("destroy", self.quit, None)

    panel = gtk.HPaned()
    panel.set_position(300)
    self.window.add(panel)

    self.groupList = hildon.TouchSelector(text=True)
    self.groupList.connect("changed", self.list_clicked);
    panel.add1(self.groupList)
    """ este bloco seria para acrescentar um menu de contexto (popup) ao
        pressionar o botão do grupo, para excluir mais facilmente...mas usando
        o GtkTreeView não consigo eventos de seleção, e usando o TouchSelector
        o método tap_and_hold_setup dá um segmentation fault
    area = hildon.PannableArea()
    self.groupList = hildon.GtkTreeView(gtk.HILDON_UI_MODE_EDIT)
    renderer = gtk.CellRendererText()
    col = gtk.TreeViewColumn("Title", renderer, text=0)
    self.groupList.append_column(col)
    # Set multiple selection mode
    selection = self.groupList.get_selection()
    #selection.set_mode(gtk.SELECTION_MULTIPLE)
    store = gtk.ListStore(gobject.TYPE_STRING)
    self.groupList.set_model(store)
    area.add(self.groupList)
    panel.add1(area)

    urlmenu = gtk.Menu()
    urlmenu.set_title("hildon-context-sensitive-menu")
    urlmenu.append(gtk.MenuItem("URL actions"))
    urlmenu.show_all()
    self.groupList.tap_and_hold_setup(urlmenu, callback=gtk.tap_and_hold_menu_position_top)
    """

    panel1 = gtk.VBox()
    panel.add2(panel1)

    self.item_detail = hildon.PannableArea()
    self.item_detail_area = gtk.VBox()
    self.item_detail.add_with_viewport(self.item_detail_area)
    panel1.pack_start(self.item_detail, True, True, 0)

    box = gtk.HBox()
    box.add(self.add_detail_button)
    box.add(self.del_detail_button)
    panel1.pack_end(box, False, False, 0)

    # This call show the window and also add the window to the stack
    self.window.show_all()

  def set_database(self, d):
    self.database = d


  def quit(self, v1, v2):
    if self.database.save() != True:
      gtk.Dialog.run(hildon.Note("information", self.window, _("The database could not be updated. Check file permissions or disk space.")))

    gtk.main_quit()


  def show(self):
    self.showGroups()


  def add_group_clicked(self, src):
    entry = hildon.Entry(0)
    dialog = gtk.Dialog(title=_("New Group"), parent=self.window, buttons=(_("Done"), gtk.RESPONSE_OK))
    dialog.vbox.add(hildon.Caption(None, _("Name"), entry, None, True))
    dialog.show_all()
    response = dialog.run()
    dialog.destroy()
    if response == gtk.RESPONSE_OK:
      if self.database.addGroup(entry.get_text()):
        self.showGroups()
      else:
        gtk.Dialog.run(hildon.Note("information", self.window, _("The group \"%s\" already exists!") % (entry.get_text())))


  def del_group_clicked(self, src):
    note = hildon.Note("confirmation", self.window, _("Are you sure you want to remove the group \"%s\" and all their itens?") % (self.atual_group))
    retcode = gtk.Dialog.run(note)
    if retcode == gtk.RESPONSE_OK:
      self.database.delGroup(self.atual_group)
      self.showGroups()
    gtk.Dialog.destroy(note)


  def add_item_clicked(self, src):
    entry = hildon.Entry(0)
    dialog = gtk.Dialog(title=_("New Item for Group \"%s\"") % (self.atual_group), parent=self.window, buttons=("Done", gtk.RESPONSE_OK))
    dialog.vbox.add(hildon.Caption(None, _("Name"), entry, None, True))
    dialog.show_all()
    response = dialog.run()
    dialog.destroy()
    if response == gtk.RESPONSE_OK:
      if self.database.addItem(self.atual_group, entry.get_text()):
        self.showItens()
      else:
        gtk.Dialog.run(hildon.Note("information", self.window, _("The item \"%s\" for the group \"%s\" already exists!") % (entry.get_text(), self.atual_group)))


  def del_item_clicked(self, src):
    group = self.atual_group
    item = self.groupList.get_current_text()
    note = hildon.Note("confirmation", self.window, _("Are you sure you want to remove the item \"%s\" from the group \"%s\"?") % (item, group))
    retcode = gtk.Dialog.run(note)
    if retcode == gtk.RESPONSE_OK:
      self.database.delItem(group, item)
      self.showItens()
      self.remove_itens_from_list()
    gtk.Dialog.destroy(note)


  def add_detail_clicked(self, src):
    entry = hildon.Entry(0)
    dialog = gtk.Dialog(title=_("New detail for item \"%s\" in group \"%s\"") % (self.groupList.get_current_text(), self.atual_group), parent=self.window, buttons=(_("Done"), gtk.RESPONSE_OK))
    dialog.vbox.add(hildon.Caption(None, _("Name"), entry, None, True))
    dialog.show_all()
    response = dialog.run()
    dialog.destroy()
    if response == gtk.RESPONSE_OK:
      if self.database.addDetail(self.atual_group, self.groupList.get_current_text(), entry.get_text()):
        self.show_details_for_item()
      else:
        gtk.Dialog.run(hildon.Note("information", self.window, _("The detail \"%s\" for item \"%s\" in group \"%s\" already exists!") % (entry.get_text(), self.groupList.get_current_text(), self.atual_group)))


  def del_detail_clicked(self, src):
    RemoveDetailWindow(self.database, self.atual_group, self.groupList.get_current_text(), self)


  def about_clicked(self, src):
    dialog = gtk.AboutDialog()
    dialog.set_name("pySafe")
    dialog.set_version("0.1.0")
    dialog.set_authors(["Jorge Aguilar"])
    #dialog.set_logo(gtk.gdk.pixbuf_new_from_file_at_size("pysafe_128x128.png", 64, 64))
    dialog.run()


  def list_clicked(self, widget, column, user_data = None):
    if self.ignore_list_clicked:
      return

    self.ignore_list_clicked = True

    # estes botões só devem ser ativados caso seja exibido um item
    self.del_item_button.set_sensitive(False)
    self.add_detail_button.set_sensitive(False)
    self.del_detail_button.set_sensitive(False)

    self.remove_itens_from_list()

    if self.atual_group == None:
      self.atual_group = self.groupList.get_current_text()
      self.ignore_list_clicked = False
      self.showItens()
    elif self.groupList.get_current_text() == _("<< back to groups"):
      self.ignore_list_clicked = False
      self.showGroups()
    else:
      self.show_details_for_item()

      # habilita os botões
      self.del_item_button.set_sensitive(True)
      self.add_detail_button.set_sensitive(True)

    # habilita/desabilita botões dependendo do contexto
    self.del_group_button.set_sensitive(self.atual_group != None)
    self.add_item_button.set_sensitive(self.atual_group != None)

    self.ignore_list_clicked = False


  def remove_itens_from_list(self):
    # remove os itens atuais
    # o "get_children" devolve um array...por isso o índice 0
    # dentro do item_detail tem um viewport, que tem dentro um vbox, que tem dentro os elementos
    for i in self.item_detail.get_children()[0].get_children()[0].get_children():
      self.item_detail_area.remove(i)

    self.item_detail.jump_to(0, 0)


  def show_details_for_item(self):
    # pega os itens que deve inserir
    itens = self.database.getItem(self.atual_group, self.groupList.get_current_text())

    self.remove_itens_from_list()

    # insere os itens na tela
    keys = itens.keys()
    keys.sort(key=str.lower)
    for i in keys:
      view = hildon.Entry(0)
      view.set_text(itens[i])
      view.connect("changed", self.textview_changed, i)
      label = gtk.Label(i)
      label.set_alignment(0,0)
      self.item_detail_area.pack_start(label, False, True, 0)
      self.item_detail_area.pack_start(view, False, True, 0)

    # exibe os itens
    self.item_detail_area.show_all()

    self.del_detail_button.set_sensitive(len(keys) != 0)


  def showGroups(self):
    if self.ignore_list_clicked:
      return
  
    self.ignore_list_clicked = True

    self.atual_group = None

    # pega os valores e ordena
    keys = self.database.getGroups()
    #keys.sort(key=str.lower, reverse=True)
    keys.sort(key=str.lower)

    treemodel = self.groupList.get_model(0)
    treemodel.clear()
    for i in keys:
      self.groupList.append_text(i)
    """ se usar GtkTreeView este bloco deve ser ativo...usando
        TouchSelector o de cima
    store = self.groupList.get_model()
    for i in keys:
      store.insert(0, [i])
    self.groupList.set_model(store)
    """

    self.ignore_list_clicked = False


  def showItens(self):
    if self.ignore_list_clicked:
      return
  
    self.ignore_list_clicked = True

    treemodel = self.groupList.get_model(0)
    treemodel.clear()

    """
    button = hildon.Button(gtk.HILDON_SIZE_AUTO_WIDTH | gtk.HILDON_SIZE_FINGER_HEIGHT,
                           hildon.BUTTON_ARRANGEMENT_VERTICAL)
    button.set_text("Some title", "some value")
    image = gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON)
    button.set_image(image)
    button.set_image_position(gtk.POS_RIGHT)
    #a = gtk.icon_factory_lookup_default(gtk.STOCK_ADD)
    #self.groupList.add(a.render_icon(gtk.Style(), gtk.TEXT_DIR_LTR, gtk.STATE_NORMAL, gtk.ICON_SIZE_BUTTON, None, None))
    self.groupList.add(button)
    """

    self.groupList.append_text(_("<< back to groups"))
    keys = self.database.getItens(self.atual_group).keys()
    keys.sort(key=str.lower)
    for i in keys:
      self.groupList.append_text(i)

    self.ignore_list_clicked = False


  def textview_changed(self, src, detail):
    self.database.setDetail(self.atual_group, self.groupList.get_current_text(), detail, src.get_text())


  def change_password_clicked(self, src):
    while True:
      # cria a janela para pedir a senha
      passwordDialog = hildon.GetPasswordDialog(self.window, True)
      passwordDialog.set_title("")
      passwordDialog.set_caption(_("Type your new password"))
      passwordDialog.set_property("password", "")
      response = passwordDialog.run()
      pass1 = passwordDialog.get_password()
      passwordDialog.destroy() 
      if response != gtk.RESPONSE_OK:
        return

      if len(pass1) == 0:
        note = hildon.Note("information", self.window, _("The password can not be empty!"))
        gtk.Dialog.run(note)
        note.destroy()
        continue

      # solicita para redigitar
      passwordDialog = hildon.GetPasswordDialog(self.window, True)
      passwordDialog.set_title("")
      passwordDialog.set_caption(_("Re-type your new password"))
      passwordDialog.set_property("password", "")
      response = passwordDialog.run()
      passwordDialog.hide() 
      if response != gtk.RESPONSE_OK:
        return

      if passwordDialog.get_password() != pass1:
        note = hildon.Note("information", self.window, _("The passwords are not equals!"))
        gtk.Dialog.run(note)
        note.destroy()
      else:
        self.database.save(force = True, new_pass = pass1)
        note = hildon.Note("information", self.window, _("Password changed!"))
        gtk.Dialog.run(note)
        note.destroy()
        break


"""
Esta classe exibe a janela de exclusão de detalhes do item
"""
class RemoveDetailWindow:

  def __init__(self, d, g, i, c):
    self.database = d
    self.grupo = g
    self.item = i
    self.callback_function_on_close = c

    window = hildon.StackableWindow()
    window.set_border_width(6)

    # Create a new edit toolbar
    toolbar = hildon.EditToolbar(_("Choose details to delete"), _("Delete"))

    area = hildon.PannableArea()
    tree_view = self.create_treeview(gtk.HILDON_UI_MODE_EDIT)

    # Add toolbar to the window
    window.set_edit_toolbar(toolbar)

    area.add(tree_view)
    window.add(area)

    toolbar.connect("button-clicked", self.delete_button_clicked, tree_view)
    toolbar.connect_object("arrow-clicked", self.close_window, window)

    window.show_all()
    # Set window to fullscreen
    window.fullscreen()


  def close_window(self, window):
    MainWindow.show_details_for_item(self.callback_function_on_close)
    window.destroy()


  def delete_button_clicked(self, button, treeview):
      selection = treeview.get_selection()

      (model, selected_rows) = selection.get_selected_rows()

      row_references = []
      for path in selected_rows:
          ref = gtk.TreeRowReference(model, path)
          row_references.append(ref)

      for ref in row_references:
          path = ref.get_path()
          iter = model.get_iter(path)
          self.database.delDetail(self.grupo, self.item, model.get_value(iter, 0))
          model.remove(iter)


  def create_treeview(self, tvmode):
      tv = hildon.GtkTreeView(tvmode)
      renderer = gtk.CellRendererText()
      col = gtk.TreeViewColumn("Title", renderer, text=0)

      tv.append_column(col)

      # Set multiple selection mode
      selection = tv.get_selection()
      selection.set_mode(gtk.SELECTION_MULTIPLE)

      store = gtk.ListStore(gobject.TYPE_STRING)
      itens = self.database.getItem(self.grupo, self.item)
      keys = itens.keys()
      keys.sort(key=str.lower, reverse=True)
      # insere os itens na tela
      for i in keys:
        store.insert(0, [i])

      tv.set_model(store)

      return tv



def main():

  # cria a janela principal
  win = MainWindow()

  while True:
    # cria a janela para pedir a senha
    passwordDialog = hildon.GetPasswordDialog(win.window, True)
    passwordDialog.set_title("")
    passwordDialog.set_caption(_("Type your password"))
    passwordDialog.set_property("password", "")
    response = passwordDialog.run()
    pass1 = passwordDialog.get_password()
    passwordDialog.destroy() 
    if response != gtk.RESPONSE_OK:
      return

    if len(pass1) == 0:
      note = hildon.Note("information", win.window, _("The password can not be empty!"))
      gtk.Dialog.run(note)
      note.destroy()
      continue

    database = Dados(pass1)
    ret = database.load()
    if ret == database.DATABASE_OK:
      # tenta salvar o banco (para garantir que será atualizavel)
      if database.save(force = True) == False:
        gtk.Dialog.run(hildon.Note("information", win.window, _("The database are not updataple. Check file permissions or disk space.")))
      break
    elif ret == database.ARQUIVO_NAO_LOCALIZADO:
      note = hildon.Note("confirmation", win.window, _("Database file not found. Confirm the creation of a new one?"))
      if gtk.Dialog.run(note) != gtk.RESPONSE_OK:
        return
      note.destroy()

      # solicita para redigitar
      passwordDialog = hildon.GetPasswordDialog(win.window, True)
      passwordDialog.set_title("")
      passwordDialog.set_caption(_("Re-type your password"))
      passwordDialog.set_property("password", "")
      response = passwordDialog.run()
      passwordDialog.hide() 
      if response != gtk.RESPONSE_OK:
        return

      if passwordDialog.get_password() != pass1:
        note = hildon.Note("information", win.window, _("The passwords are not equals!"))
        gtk.Dialog.run(note)
        note.destroy()
      else:
        database.save(force = True)
        break
    else:
      texto = _("An undefined error has ocurred!")
      if ret == database.SENHA_INVALIDA:
        texto = _("Invalid password!")
      elif ret == database.DADOS_CORROMPIDOS:
        texto = _("Database corrupted or invalid password!")
      note = hildon.Note("information", win.window, texto)
      gtk.Dialog.run(note)
      note.destroy()

  passwordDialog.destroy() 

  win.set_database(database)
  win.show()
  gtk.main()


gettext.install('pysafe')
if __name__ == "__main__":
  main()
