#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#       sammaty.py
#       This file is part of Sammaty Election 1.2.9-202307011142-ppa1~ubuntu22.04
#       
#       Copyright 2012, 2013, 2018, 2019, 2023 Nandakumar Edamana <contact@nandakumar.co.in>
#       
#       This program is free software; you can redistribute it and/or modify
#       it under the terms of the GNU General Public License as published by
#       the Free Software Foundation; version 3 of the License.
#       
#       This program is distributed in the hope that it will be useful,
#       but WITHOUT ANY WARRANTY; without even the implied warranty of
#       MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#       GNU General Public License for more details.
#       
#       You should have received a copy of the GNU General Public License
#       along with this program; if not, write to the Free Software
#       Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#       MA 02110-1301, USA.
#
#       This additional term has been added to the license under section 7:
#       You may not reuse the name Sammaty (or its variants like
#       "Sammaty Election") while making public releases of your derived work.
#       This is for consistency. However, you must refer to Sammaty as the
#       source package, and preserve all the copyright information and
#       authorship attribution.
#       

# TODO FIXME `make install` doesn't set appropriate read permission for all.
# https://bugs.launchpad.net/sammaty/+bug/1786158

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gdk, GdkPixbuf, GLib
import os, pwd, sys, subprocess, shutil, time
import hashlib
import webbrowser

VERSION = '1.2.9-202307011142-ppa1~ubuntu22.04'
SAMMATY_PRGNAME = 'sammaty'
SAMMATY_APP_TITLE = 'Sammaty Election'
SAMMATY_URL = 'https://nandakumar.co.in/software/sammaty/'
SAM_TIME_FMT = '%a, %d %b %Y %H:%M:%S %Z'
SAM_NAMEPLATE_WIDTH = 640
SAM_NAMEPLATE_HEIGHT = 80
SAM_SEL_NAMEPLATE_BORDER_WIDTH = 4
SAM_SW_BALLOT_WIDTH = SAM_NAMEPLATE_WIDTH + SAM_SEL_NAMEPLATE_BORDER_WIDTH * 2 + SAM_NAMEPLATE_HEIGHT # SAM_NAMEPLATE_HEIGHT for the indicator image
NAN_CSS_STR = '''#lbl_sammaty {
		font-weight: bold;
	}
	
	#lbl_elecname {
		font-size: 2.5em;
		font-weight: bold;
	}
	
	image.sel_nameplate { /* Selected Nameplate */
		border: ''' + str(SAM_SEL_NAMEPLATE_BORDER_WIDTH) + '''px solid green;
	}'''
NAN_CSS = NAN_CSS_STR.encode()

LOG_NONE = 0
LOG_ERR = 1
LOG_WARN = 2
LOG_ALL = 3
loglevel = LOG_WARN # 0 - none, 1 - errors, 2 - warnings, 3 - all

PLAYERS = ['ogg123', 'paplay', 'sndfile-play', 'mplayer'] # TODO include gst123?

# TODO FIXME use gettext
def _(txt):
	return txt

class NanVoterSession: # 2018
	def __init__(self):
		self.selindex = None

class NanAudibleNotifier: # 2018
	def __init__(self, sam_datadir):
		self.sam_datadir = sam_datadir
	
		if loglevel >= LOG_ALL:
			print('Trying to find an audio player command...')
		self.aplayercmd = None
		for p in PLAYERS:
			if loglevel >= LOG_ALL:
				print('  Checking ' + p + '...')
			try:
				proc = subprocess.Popen([p, '--version'], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
				print('  Found. Setting ' + p + ' as the audio player command.')
				self.aplayercmd = p
				break
			except:
				print('  Couldn\'t start ' + p)
		if self.aplayercmd == None and loglevel >= LOG_WARN:
			print('  No supported audio player found. There will be no audible notifications.')
				
	def play(self, id):
		if(self.aplayercmd):
			proc = subprocess.Popen([self.aplayercmd, self.sam_datadir + '/audio/' + id + '.ogg'], stdout = subprocess.PIPE, stderr = subprocess.PIPE)
			return True
			
		return False
		
class NanForm:  # 2018 # TODO
	def __init__(self, forminfo):
		self.dlg = Gtk.Dialog(title=forminfo['title'],
			parent=forminfo['parent'],
			modal=True, destroy_with_parent=True)
		self.dlg.add_buttons(_("Cancel"), Gtk.ResponseType.CANCEL,
			_("OK"), Gtk.ResponseType.OK)
		self.dlg.set_default_response(Gtk.ResponseType.OK) # TODO fixme make working
			
		self.inputs = {}
		
		grid = Gtk.Grid()
		self.dlg.get_content_area().add(grid)

		top = 0
		for field in forminfo['fields']:
			lbl  = Gtk.Label(label=_(field['label'])) # TODO consider field['localize'] = False
			
			if 'type' not in field:
				field['type'] = 'text'

			if field['type'] == 'text':
				self.inputs[field['name']] = Gtk.Entry()
				if 'value' in field:
					self.inputs[field['name']].set_text(field['value'])
			elif field['type'] == 'checkbox':
				self.inputs[field['name']] = Gtk.CheckButton()
				if 'value' in field:
					self.inputs[field['name']].set_active(field['value'])
			elif field['type'] == 'combobox-text':
				self.inputs[field['name']] = Gtk.ComboBoxText()
				if 'options' in field:
					for opt in field['options']:
						if 'localizeOptions' in field and field['localizeOptions'] == True:
							self.inputs[field['name']].append_text(_(opt))
						else:
							self.inputs[field['name']].append_text(opt)
				# TODO FIXME implement default value
				#if 'value' in field:
				#	self.inputs[field['name']].set_active(field['value'])

			if 'sensitive' in field and field['sensitive'] == False:
				self.inputs[field['name']].set_sensitive(False)
			
			lbl.set_halign(Gtk.Align.START)
			
			grid.attach(lbl, 0, top, 1, 1)
			grid.attach(self.inputs[field['name']], 1, top, 1, 1)
			
			top += 1
		
		grid.show_all()
	
	def destroy(self):
		self.dlg.destroy()
			
	def run(self):
		return self.dlg.run()
	
class main:
	def __init__(self):
		self.MAXELECS = 100 # Max. no. of elections that can be set up parallelly.
		self.datadir = '/usr/share/sammaty'
		if len(sys.argv) == 3 and sys.argv[1] == '--datadir':
			self.datadir = sys.argv[2]
			
		if os.path.exists(self.datadir) == False:
			print("Data directory '" + self.datadir + "' was not found. You can set it manually using the option --datadir. For example, if you trying to run Sammaty from the source tarball without installing it, you could try: ./sammaty.py --datadir ../data")
			sys.exit(-1)
			
		self.elecbasedir = pwd.getpwuid(os.getuid()).pw_dir + '/sammaty-election-' + VERSION
		
		self.notifier = NanAudibleNotifier(self.datadir)
		
		Gtk.init(sys.argv)
		GLib.set_application_name(_(SAMMATY_APP_TITLE))
		# for dogtail
		GLib.set_prgname(SAMMATY_PRGNAME)
		Gtk.IconTheme.get_default().append_search_path('icons') # TODO allow command line adjustment
		
		self.wnd = Gtk.Window()
		self.draw_main_window()
		
		self.load_election(None)
		
		self.wnd.connect('delete-event', self.on_wnd_delete)
		self.wnd.connect('destroy', self.on_wnd_destory)
		self.wnd.connect('key_press_event', self.wnd_key_press)
		self.wnd.add_events(Gdk.EventMask.KEY_PRESS_MASK)
		self.wnd.show_all()

	def load_election(self, elecid = None):
		self.elecdir = None
		self.candidates_count = 0
		self.candidates = []
		self.votes = []
		self.strips = []
		self.candicons = []
		self.tot_votes = 0
		self.voting_wnd_visible = False
		self.curvoter_session = None
		
		if(elecid):
			self.elecdir = self.elecbasedir + '/' + elecid
		else:
			self.elecdir = None

		# TODO election indicator
		self.btnsmain_update_sensitivity()

	def nan_dialog_admin_auth(self, prompt):
		pwd_in = self.nan_dialog_input(_(prompt), _('Authentication'), password = True)
		if(pwd_in == None): # User pressed Cancel
			return False
			
		if(self.admin_auth(pwd_in) == False):
			self.nan_dialog_error(_('Wrong password.'), _('Authentication Failure'))
			return False
		else: # Correct password
			return True

	def nan_dialog_confirm(self, msg, title = _('Confirmation')):
		dlg = Gtk.MessageDialog(title=self.wnd,
			modal=True, destroy_with_parent=True,
			message_type=Gtk.MessageType.ERROR,
			buttons=Gtk.ButtonsType.YES_NO)
		dlg.set_markup(msg)
		dlg.set_title(title)
		retval = dlg.run()
		dlg.destroy()
		return (True if retval == Gtk.ResponseType.YES else False)

	def nan_dialog_error(self, msg, title = 'Error'):
		dlg = Gtk.MessageDialog(title=self.wnd,
			modal=True, destroy_with_parent=True,
			message_type=Gtk.MessageType.ERROR,
			buttons=Gtk.ButtonsType.OK)
		dlg.set_markup(msg)
		dlg.set_title(title)
		dlg.run()
		dlg.destroy()
		
	def nan_dialog_info(self, msg, title = 'Information'):
		dlg = Gtk.MessageDialog(title=self.wnd,
			modal=True, destroy_with_parent=True,
			message_type=Gtk.MessageType.INFO,
			buttons=Gtk.ButtonsType.OK)
		dlg.set_title(title)
		dlg.set_markup(msg)
		dlg.run()
		dlg.destroy()
	
	def nan_dialog_input(self, prompt, title, password = False, default_text = ''):
		dlg = Gtk.Dialog(title=title,
			parent=self.wnd,
			modal=True, destroy_with_parent=True)
		dlg.add_buttons(_('Cancel'),
			Gtk.ResponseType.REJECT,
			_('OK'),
			Gtk.ResponseType.ACCEPT)
		dlg.set_default_response(Gtk.ResponseType.ACCEPT)
				
		lbl = Gtk.Label(prompt)
		txt = Gtk.Entry()
		txt.set_text(default_text)
		if(password):
			txt.set_input_purpose(Gtk.InputPurpose.PASSWORD)
			txt.set_visibility(False)
		txt.set_activates_default(True) # XXX Important
		txt.set_size_request(350, -1)
		vbox = Gtk.VBox()
		vbox.add(lbl)
		vbox.add(txt)
		dlg.get_content_area().add(vbox)
		
		vbox.show_all()
		resp = dlg.run()
		retval = txt.get_text() if (resp == Gtk.ResponseType.ACCEPT) else None
		dlg.destroy()
		return retval
	
	# Especially to prevent .. (parent dir) path attack
	def cand_codename_validate(self, codename):
		if len(codename) == False: # No need of trim since spaces will be rejected in the upcoming loop
			return False
			
		for ch in codename:
			if ch.isalpha() == False and ch.isdigit() == False and ch not in ['-', '_']:
				return False
				
			return True
	
	def create_candid_nameplate(self, codename, fullname, fname_img):
		if self.cand_codename_validate(codename) == False:
			return False
		
		svgdir = self.elecdir + '/candidates/svg-tmp'
		if os.path.exists(svgdir) == False:
			os.mkdir(svgdir) # TODO err
			
		svgfile = svgdir + '/' + codename + '.svg'
		pngfile = self.elecdir + '/candidates/' + codename + '.png'
		if(os.path.exists(pngfile)):
			return False
		
		fp = open(svgfile, 'w')
		if fp == None:
			return False
			
		fp.write('<svg width="' + str(SAM_NAMEPLATE_WIDTH) + 'px" height="' + str(SAM_NAMEPLATE_HEIGHT) + '''px">
		<rect x="0" y="0" width="100%" height="100%" style="fill: white; stroke: #444; stroke-width: 5px" />
		<text x="50%" y="50px" width="100%" height="100%" style="font-size: 40px; text-anchor: middle">''' + fullname + '''</text>
	</svg>''')
		fp.close()

		img = Gtk.Image.new_from_file(svgfile)
		img.get_pixbuf().savev(pngfile, 'png', '', '')
		
		if fname_img != None:
			imgdir = self.elecdir + '/candidates/.img'
			if os.path.exists(imgdir) == False:
				os.mkdir(imgdir) # TODO err
			
			shutil.copyfile(fname_img, imgdir + '/' + codename)
		
		return True
	
	def new_ballot(self):
		if self.curvoter_session:
			return
			
		self.curvoter_session = NanVoterSession()
		for item in self.eb:
			item.imgindic.set_from_file(self.datadir + '/pics/indicator-gray.svg')
			item.set_sensitive(True)
		
		self.notifier.play('pluck')
	
	def get_new_elecdir(self):
		# TODO better method
		for i in range(0, self.MAXELECS):
			newdir = self.elecbasedir + '/' + str(i)
			if not os.path.exists(newdir):
				return newdir
		
		return False
		
	def get_elections(self, addIdPrefix = True):
		elecs = []
		
		# TODO better method
		for i in range(0, self.MAXELECS):
			edir = self.elecbasedir + '/' + str(i)
			if os.path.isdir(edir) and os.path.isfile(edir + '/title'):
				title = open(edir + '/title', 'r').read()
				elecs.append(str(i) + ' - ' + title)
		
		return elecs

	def election_switch(self, widget = None, event = None):
		elecs = self.get_elections()
		
		fields = [
			{
				"name" : "elec",
				"type" : "combobox-text",
				"label": "Election: ",
				"options": elecs
				# "localize": False TODO implement for needed fields (if any)
			}
		]
	
		forminfo = {
			"parent": self.wnd,
			"title": "Select Election",
			"fields": fields
		}
	
		nanForm = NanForm(forminfo)

		resp = nanForm.run()
		if resp == Gtk.ResponseType.OK:
			elec = nanForm.inputs['elec'].get_active_text()
			if elec == '' or elec == None:
				self.nan_dialog_error(_("Sorry, you didn't select any election."), _('Error'))
			else:
				# TODO FIXME does this reload everything actually?
				elecid = elec.split('-')[0].strip()
				self.load_election(elecid)
			
		nanForm.destroy()
		
	def election_setup(self, widget = None, event = None):
		elecdir = self.get_new_elecdir()
		if elecdir == False:
			self.nan_dialog_error(_('Reached the maximum number of elections. Delete one before creating a new one.'), _('Error'))
			return
			
		fields = [
			{
				"name" : "title",
				"label": "Title: "
				# "localize": False TODO implement for needed fields (if any)
			},
			{
				"name" : "passwd",
				"label": "Password: "
			},
			{ # TODO make sensitive and implement
				"name" : "voteOnSecondClick",
				"label": "Accept vote on second click only: ",
				"type" : "checkbox",
				"value": True,
				"sensitive": False
			}
		]
	
		forminfo = {
			"parent": self.wnd,
			"title": "Election Setup",
			"fields": fields
		}
	
		nanForm = NanForm(forminfo)

		while True:
			resp = nanForm.run()
			if resp != Gtk.ResponseType.OK:
				nanForm.destroy()
				return
			
			title = nanForm.inputs['title'].get_text()
			if title == '' or title == None:
				self.nan_dialog_error(_('Please enter a title for the election.'), _('Error'))
				continue
				
			passwd = nanForm.inputs['passwd'].get_text()
			if passwd == '' or passwd == None:
				self.nan_dialog_error(_('Please enter a password for the election.'), _('Error'))
				continue
			
			break

		nanForm.destroy()
		
		try:
			# XXX Password confirmation removed since the first password doesn't use password chars.
			#passwdc = self.nan_dialog_input('Election Setup', 'Confirm the password: ')
			#if passwd != passwdc:
			#	self.nan_dialog_error('Password confirmation mismatch. Please try again.',
			#		'Error')
			#	return False
			# TODO validate passwd
			os.makedirs(elecdir) # TODO ERR
			os.mkdir(elecdir + '/candidates') # TODO ERR

			open(elecdir + '/passwd', 'wb').write(hashlib.sha512(passwd.encode()).digest())
			open(elecdir + '/title', 'w').write(title)
					
			self.btnsmain_update_sensitivity()
			self.nan_dialog_info(_("Election created successfully. You can edit the election details after selecting the election using the Select Election button."), _('Success'))
			return True
		except Exception as e:
			# TODO proper logging
			print(str(e))
			self.nan_dialog_error(_('Error creating election; please launch the application from a command terminal (if not already launched so), retry creating the election and check for error messages in the terminal.'), _('Error'))
			return False
	
	def admin_auth(self, passwd):
		try:
			pwhash_real = open(self.elecdir + '/passwd', 'rb').read()
			return ( pwhash_real == hashlib.sha512(passwd.encode()).digest() )
		except:
			return False
		return False
	
	# Updates the sensitivity of each among the main buttons
	def btnsmain_update_sensitivity(self):
		if self.elecdir != None and os.path.exists(self.elecdir): # TODO FIXME use better method
			candcount = len(self.get_candidates(False))
			for btnname in self.btnsmain:
				if btnname in ['cand_edit_nameplate', 'cand_remove', 'cand_list']:
					self.btnsmain[btnname].set_sensitive(candcount > 0)
				else:
					self.btnsmain[btnname].set_sensitive(True)
		else:
			for btnname in self.btnsmain:
				self.btnsmain[btnname].set_sensitive(False)
		
		self.btnsmain['elec_create'].set_sensitive(True)
		self.btnsmain['elec_switch'].set_sensitive(True)
		
		self.btnsmain['result'].set_sensitive(
			self.elecdir != None and
			os.path.exists(self.elecdir + '/result.html') )
	
	def delete_election(self, widget = None, event = None):
		if os.path.exists(self.elecdir): # TODO FIXME use better method
			if self.nan_dialog_confirm(_('Are you sure you want to delete the election? This will delete all the data related to the last poll and it cannot be undone.'), _('Confirmation')):
				if self.nan_dialog_admin_auth(_('Enter the password you set for this election to delete the election:')):
					shutil.rmtree(self.elecdir) # TODO err
					self.btnsmain_update_sensitivity()
					self.nan_dialog_info(_('Election deleted successfully.'), _('Success'))
		else:
			self.nan_dialog_error(_('No election to delete.'), _('Error'))

	def draw_main_window(self,widget = None, event = None):
		self.vbox0=Gtk.VBox()
		self.hbox_about_help=Gtk.HBox()
		
		self.btn_about = Gtk.Button(label=_('_About'))
		self.btn_about.set_use_underline(True)
		self.btn_about.connect('clicked', self.show_about_dlg)
		
		self.btn_help = Gtk.Button(label=_('_Help'))
		self.btn_help.set_use_underline(True)
		self.btn_help.connect("clicked",self.help)
		
		self.btn_settings = Gtk.Button(label=_('Se_ttings')) # TODO rem?
		
		self.btnsmaininfo = [
			{
				'name': 'elec_create',
				'icon-name': 'preferences-other',
				'label': 'Add Elec_tion',
				'handler': self.election_setup
			},
			{
				'name': 'elec_switch',
				'icon-name': 'document-open',
				'label': '_Select Election',
				'handler': self.election_switch
			},
			{
				'name': 'elec_delete',
				'icon-name': 'edit-delete',
				'label': '_Delete Election',
				'handler': self.delete_election
			},
			{
				'name': 'cand_add',
				'icon-name': 'list-add',
				'label': 'Add _Candidate',
				'handler': self.on_btn_cand_add_clicked
			},
			# TODO FIXME hide if not implemented
			{
				'name': 'cand_manage',
				'icon-name': 'input-tablet', # TODO FIXME
				'label': '_Edit/Remove Candidate',
				'handler': self.on_btn_cand_manage_clicked
			},
			{
				'name': 'cand_list',
				'icon-name': 'emblem-documents',
				'label': '_List Candidates',
				'handler': self.list_candidates
			},
			{
				'name': 'elect_start',
				'icon-name': 'media-playback-start',
				'label': 'Start _Poll',
				'handler': self.draw_election_window
			},
			{
				'name': 'result',
				'icon-name': 'emblem-favorite',
				'label': '_Result of the Last Poll',
				'handler': self.show_last_result
			}]
			
		self.btnsmain = {}
		
		for btninfo in self.btnsmaininfo:
			btn = Gtk.Button()
			img = Gtk.Image.new_from_icon_name(_(btninfo['icon-name']), Gtk.IconSize.LARGE_TOOLBAR)
			lbl = Gtk.Label(label=_(btninfo['label']))
			lbl.set_use_underline(True)
			hbox = Gtk.HBox()
			hbox.add(img)
			hbox.add(lbl)
			hbox.set_child_packing(img, False, False, 10, 0)
			hbox.set_child_packing(lbl, False, False, 0, 0)
			btn.add(hbox)
			btn.connect('clicked', btninfo['handler'])
			self.vbox0.add(btn)
			
			if 'name' in btninfo:
				self.btnsmain[btninfo['name']] = btn
		
		self.wnd.set_size_request(300, 250)
		self.wnd.set_position(1)
		self.wnd.set_resizable(False)
		self.wnd.set_border_width(5)
		self.wnd.set_title(_(SAMMATY_APP_TITLE))
		self.wnd.set_icon_name('sammaty') # TODO FIXME
		
		self.hbox_about_help.add(self.btn_about)
		self.hbox_about_help.add(self.btn_help)
		#self.hbox_about_help.add(self.btn_settings) TODO rem?
		
		self.vbox0.set_spacing(5)
		self.vbox0.add(Gtk.HSeparator())
		self.vbox0.add(self.hbox_about_help)
		try:
			self.wnd.remove(self.vbox1)
		except:
			buff=None
		self.wnd.add(self.vbox0)
	
	def on_btn_cand_add_clicked(self, widget = None, event = None):
		if self.nan_dialog_admin_auth(_('Enter the password you set for this election to add a new candidate:')) == False:
			return
			
		fullname = self.nan_dialog_input(_('Enter the full name of the new candidate:'), _('Add Candidate'))
		if fullname == None or fullname == '':
			self.nan_dialog_info(_('Candidate addition cancelled by the user.'), _('Abort by User'))
			return
		
		fname_img = None
		# TODO FIXME complete and uncomment
		'''if(self.nan_dialog_confirm(_('Would you like to add an image for the candidate?'), _('Add Candidate'))):
			while True:
				dlg = Gtk.FileChooserDialog( _('Choose Candidate Image'),
					self.wnd,
					Gtk.FileChooserAction.OPEN,
					buttons = (_('_Open'), Gtk.ResponseType.ACCEPT, _('_Cancel'), Gtk.ResponseType.CANCEL) )
				resp = dlg.run()
				if(resp != Gtk.ResponseType.ACCEPT):
					dlg.destroy()
					break;
				
				fname_img = dlg.get_filename()
				dlg.destroy()
				if(os.path.exists(fname_img)):
					break;
				else:
					self.nan_dialog_message(_('Invalid filename. Please try again or press Cancel.'), _('Error'))
					fname_img = None'''
			
		codename_suggest = ''
		for ch in fullname:
			if ch.isalpha() or ch.isdigit() or ch in ['-', '_']:
				codename_suggest += ch
		
		# Collision resolution
		codename_suggest_tmp = codename_suggest
		i = 1
		while(os.path.exists(self.elecdir + '/candidates/' + codename_suggest_tmp + '.png')):
			codename_suggest_tmp = codename_suggest + i
		codename_suggest = codename_suggest_tmp
				
		codename = self.nan_dialog_input(_("Enter a codename for the candidate\n(English alphabets, digits, hyphen and underscore only):"), _('Add Candidate'), False, codename_suggest)

		if self.create_candid_nameplate(codename, fullname, fname_img) == True:
			self.nan_dialog_info(_('Candidate added successfully. You can review the changes by clicking on the List Candidates button. You can also manually edit the nameplate by manipulating the following file:\n') + self.elecdir + '/candidates/' + codename + '.png', _('Success'))
		else:
			self.nan_dialog_info(_('Couldn\'t create a candidate with the given codename. Make sure that it consists of English letters, digits, hyphen and underscore only. Empty strings are also rejected.'), _('Error'))
		
		self.btnsmain_update_sensitivity()
		return True
	
	def on_btn_cand_manage_clicked(self, widget = None, event = None):
		if self.nan_dialog_admin_auth(_('Enter the password you set for this election to manage candidates:')) == False:
			return
		
		candidates = self.get_candidates(True)
		
		fields = [
			{
				"name" : "candidate",
				"label": "Candidate: ",
				"type" : "combobox-text",
				"options": candidates
			},
			{
				"name" : "action",
				"label": "Action: ",
				"type" : "combobox-text",
				"options": ["Edit", "Remove"],
				"localizeOptions": True
			}
		]
	
		forminfo = {
			"parent": self.wnd,
			"title": "Manage Candidates",
			"fields": fields
		}
	
		nanForm = NanForm(forminfo)

		while True:
			resp = nanForm.run()
			if resp != Gtk.ResponseType.OK:
				nanForm.destroy()
				return
			
			cand = nanForm.inputs['candidate'].get_active_text()
			if cand == '' or cand == None:
				self.nan_dialog_error(_("Sorry, you didn't select any candidate."), _('Error'))
				continue
				
			action = nanForm.inputs['action'].get_active_text()
			if action == '' or action == None:
				self.nan_dialog_error(_("Sorry, you didn't select any action."), _('Error'))
				continue
				
			break

		nanForm.destroy()
		
		if action == _('Edit'):
			nameplatefile = self.elecdir + '/candidates/' + cand + '.png'
			if self.cand_codename_validate(cand) and os.path.exists(nameplatefile) == False:
				self.nan_dialog_info(_('The following candidate couldn\'t be found. Make sure that you\'ve typed the name correctly:\n\n') + cand, _('Error'))
				return
			
			# TODO FIXME gettext str replacing %	
			self.nan_dialog_info(_('The nameplate of the candidate ' + cand + ' will be opened in GIMP now. After making edits, you\'ll have to replace the original file (by using <b>File > Save</b> in old versions and <b>File > Overwrite ' + cand + '.png</b> in newer ones.'), _('Edit Candidate Nameplate'))
			subprocess.Popen(['gimp', nameplatefile])
		
		elif action == _('Remove'):
			msg = _('Are you sure you want to delete the following candidates?\n\n')
			msg += str.join('\n', rmlist)
			if self.nan_dialog_confirm(msg, _('Confirmation')):
				if self.nan_dialog_admin_auth(_('Enter the password you set for this election to delete these candidate(s):')):
					for cand in rmlist:
						os.remove(self.elecdir + '/candidates/' + cand + '.png') # TODO err
						if os.path.exists(self.elecdir + '/candidates/' + cand + '.svg'):
							os.remove(self.elecdir + '/candidates/' + cand + '.svg') # TODO err
				self.nan_dialog_info(_('Candidate(s) deleted successfully.'), _('Success'))
			else:
				return
			
			self.btnsmain_update_sensitivity()		

		return True
				
	def draw_election_window(self, widget = None, event = None):
		if self.nan_dialog_admin_auth(_('Enter the password you set for this election to start the poll:')) == False:
			return
	
		if os.path.exists(self.elecdir + '/result.html'):
			i = 0
			while os.path.exists(self.elecdir + '/result_old-' + str(i) +'.html'):
				i += 1
			
			try:	
				os.rename(self.elecdir + '/result.html', self.elecdir + '/result_old-' + str(i) +'.html')
			except:
				self.nan_dialog_error(_("Couldn't rename the file '" + self.elecdir + "/result.html'. Make sure you have appropriate permissions."), _('Error'))
				return
	
		self.tot_votes=0
		self.candidates_count = 0
		self.candidates=[]
		self.votes=[]
		self.tbr=Gtk.Toolbar()
		self.tlb=[]
		self.eb=[]
		self.vbox1 = Gtk.VBox()
		self.vbox2 = Gtk.VBox()
		self.hbox1 = Gtk.HBox()
		self.lbl_elecname  = Gtk.Label()
		self.lbl_sammaty = Gtk.Label()
		self.lbl_votes_cast = Gtk.Label()
		self.lbl_voting_inst = Gtk.Label(label=_('Click twice on a nameplate to cast your vote. The presiding officer will have to press the Tab key to close the poll and launch the results.'))
		self.img_side1 = Gtk.Image() # TODO use
		self.img_side2 = Gtk.Image()
		self.sw1=Gtk.ScrolledWindow()
		
		# TODO make sure works
		self.csspro = Gtk.CssProvider()
		self.csspro.load_from_data(NAN_CSS);
		Gtk.StyleContext.add_provider_for_screen(Gdk.Screen.get_default(),
			self.csspro, Gtk.STYLE_PROVIDER_PRIORITY_USER)
		
		for item in self.get_candidates():
			self.candidates_count = self.candidates_count + 1
			self.candidates.append(item)
			self.votes.append(0)
			
			# Nameplate loading
			# TODO FIXME width and heght
			# TODO FIXME make sure that GdkPixbuf is neither depricated nor unsupported by old OS versions
			pxb = GdkPixbuf.Pixbuf.new_from_file_at_scale(self.elecdir + '/candidates/' + item + '.png', SAM_NAMEPLATE_WIDTH, SAM_NAMEPLATE_HEIGHT, True)
			self.strips.append(Gtk.Image.new_from_pixbuf(pxb))
			
			self.candicons.append(Gtk.Image())
			self.candicons[-1].set_from_file(self.elecdir + '/candidates/img/' + item)
			
			imgindic = Gtk.Image()
			hbox = Gtk.HBox()
			#hbox.add(self.candicons[-1]) # TODO FIXME
			# TODO FIXME
			hbox.add(self.strips[-1])
			hbox.add(imgindic)
			#hbox.add(Gtk.Label(item))
			
			self.eb.append(Gtk.EventBox())
			self.eb[-1].add(hbox)
			self.eb[-1].imgcandicon = self.candicons[-1]
			self.eb[-1].img = self.strips[-1]
			self.eb[-1].imgindic = imgindic
			self.vbox2.add(self.eb[-1])
			self.vbox2.set_child_packing(self.eb[-1], False, False, 0, 0)
			self.eb[-1].set_name('eb_nameplate_' + str(len(self.strips) - 1))
			self.eb[-1].connect('button_release_event', self.eb_released)
			self.eb[-1].add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)

		self.lbl_elecname.set_label(self.election_get_title())
		self.lbl_sammaty.set_name('lbl_sammaty')
		self.lbl_elecname.set_name('lbl_elecname')
		self.lbl_sammaty.set_label(_(SAMMATY_APP_TITLE))
		self.lbl_votes_cast.set_markup("Votes Cast: <b>0</b>")
		self.sw1.set_size_request(SAM_SW_BALLOT_WIDTH, 590) # TODO FIXME height
		self.sw1.set_policy(1,1)
		self.tbr.set_orientation(1)
		# TODO Remove images. Use better centre alignment methods
		self.img_side1.set_size_request(((self.wnd.get_screen().get_width())-SAM_SW_BALLOT_WIDTH - 10)/2, -1)
		self.img_side2.set_size_request(((self.wnd.get_screen().get_width())-SAM_SW_BALLOT_WIDTH - 10)/2, -1)
			
		self.sw1.add_with_viewport(self.vbox2)
		self.hbox1.add(self.img_side1)
		self.hbox1.add(self.sw1)
		self.hbox1.add(self.img_side2)
		self.vbox1.add(self.lbl_sammaty)
		self.vbox1.add(Gtk.HSeparator())
		self.vbox1.add(self.lbl_elecname)
		self.vbox1.add(self.hbox1)
		self.vbox1.add(self.lbl_voting_inst)
		self.vbox1.add(Gtk.HSeparator())
		self.vbox1.add(self.lbl_votes_cast)
		
		try:
			self.wnd.remove(self.vbox0)
		except:
			buff = None
		self.wnd.hide()
		self.wnd.set_resizable(True)
		self.wnd.add(self.vbox1)
		self.tbr.set_sensitive(False)
		self.wnd.show_all()
		self.wnd.fullscreen()
		self.voting_wnd_visible = True
		self.time_poll_start = time.localtime() # This returns a tuple which can be used directly with strftime()
		self.new_ballot()
	
	def get_candidates(self, cut_extension = True):
		nameplates = os.listdir(self.elecdir + '/candidates/')
		nameplates.sort()
		candids = []
		for item in nameplates:
			if item[-4:] == '.png':
				candids.append(item[:-4] if cut_extension else item)
			
		return candids
	
	def wnd_key_press(self, widget = None, event = None):
		if self.voting_wnd_visible == False:
			return 0

		if Gdk.keyval_name(event.keyval) == 'Return':
			self.new_ballot()
			
			return True
		
		if Gdk.keyval_name(event.keyval) == 'Tab':
			auth = self.nan_dialog_admin_auth(_('Enter the password you set for this election:'))
			if auth == False: # User pressed Cancel or got a Wrong Password message
				True # True to stop other handlers (GTK+)
			else: # Auth success
				self.tbr.set_sensitive(False) # TODO what is this?
				self.wnd.hide()
				#self.wnd.window.set_cursor(Gtk.gdk.Cursor(Gtk.gdk.WATCH)) TODO FIXME
				self.tbr.set_sensitive(True)
				
				self.time_poll_end = time.localtime()
				
				# Sort results
				candidates=[]
				votes=[]
				n=nn=place=buf=cnds=0
				for item in self.candidates:
					cnds += 1
				while nn<cnds:
					for item in self.votes:
						if item>buf:
							buf=item
							place=n
						n += 1
					votes.append(buf)
					candidates.append(self.candidates[place])
					self.votes.pop(place)
					self.candidates.pop(place)
					buf=n=place=0
					nn += 1
				n=0
				f=open(self.elecdir + '/result.html', 'w')
				f.write("<!DOCTYPE HTML><html><head><meta charset=\"utf-8\"/><title>Result: " + self.lbl_elecname.get_label() + " | " + _(SAMMATY_APP_TITLE) + "</title></head><body style=\"text-align: center\"><h1>Result: "+self.lbl_elecname.get_label()+"</h1><b>" + _(SAMMATY_APP_TITLE) + "</b><hr/><br/>Total Votes: "+str(self.tot_votes)+"<br/>Poll started at " + time.strftime(SAM_TIME_FMT, self.time_poll_start) + "<br/>Poll ended at " + time.strftime(SAM_TIME_FMT, self.time_poll_end) + '<br/><br/><table border=1 style="margin: auto"><tr><td>Candidate</td><td>Votes</td><td>Percentage</td>')
				for item in candidates:
					f.write("<tr><td><img src=\"candidates/" + item + ".png\"/></td><td>" + str(votes[n]) + "</td><td>" + ( (str((votes[n]*100)/self.tot_votes) + '%') if self.tot_votes > 0 else 'N/A' ) + "<br/>")
					n=n+1
				f.write("</table><br/><hr/>Generated by " + _(SAMMATY_APP_TITLE) + ' ' + VERSION + ", Free Software by<br/>Nandakumar Edamana &lt;contact@nandakumar.co.in&gt;<hr/><a href=\"" + SAMMATY_URL + "\">" + SAMMATY_URL + "</a></body></html>")
				f.close()
				self.show_last_result()
				sys.exit(0)
				
	def show_last_result(self, widget = None, event = None):
		if os.path.exists(self.elecdir + '/result.html'):
			webbrowser.open(self.elecdir + '/result.html')
		else:
			self.nan_dialog_error(_('Sorry, the results file could not be found. Check if you can find a file named sammaty-election/votes.txt under your Home directory.'), _('Error'))
			self.btnsmain_update_sensitivity()
	
	def election_get_title(self):
		try:
			title = open(self.elecdir + '/title', 'r').read().strip()
		except:
			title = _('Untitled Election')
			
		return title
					
	def list_candidates(self, widget = None, event = None):
		f = open(self.elecdir + '/.candidates.html', 'w')
		title = self.election_get_title()
		f.write('<!DOCTYPE HTML><html><head><meta charset="utf-8" /><title>Candidates: ' + title + ' | ' + _(SAMMATY_APP_TITLE) + '</title></head><body style="text-align: center"><h1>Candidates: ' + title + '</h1><b>' + _(SAMMATY_APP_TITLE) + '</b><hr/>')

		for item in self.get_candidates():
			cand_img = self.elecdir + '/candidates/.img/' + item
			cand_img_url = cand_img if os.path.exists(cand_img) else '/usr/share/pixmaps/nobody.png'
				
			f.write('<img src="file://' + cand_img_url + '" style="max-width: 100px"><img src="candidates/' + item + '.png"/><br/>')
			
		f.write('<hr/>Generated by ' + _(SAMMATY_APP_TITLE) + ' ' + VERSION + ', Free Software by Nandakumar Edamana &lt;contact@nandakumar.co.in&gt;<hr/><a href="' + SAMMATY_URL + '">' + SAMMATY_URL + '</a></body></html>')
		f.close()
		webbrowser.open(self.elecdir + '/.candidates.html')
			
	def eb_released(self, widget = None, event = None):
		index = int(widget.get_name()[len('eb_nameplate_'):])
		
		if self.curvoter_session.selindex == index: # Last selected item again selected
		# Record the vote
			for item in self.eb:
				#item.img.get_style_context().remove_class('sel_nameplate') # TODO FIXME sel_nameplate css doesn't work on IT@School Ubuntu 14.04
				item.imgindic.set_from_file(self.datadir + '/pics/indicator-gray.svg')
				item.set_sensitive(False)
				
			self.votes[index] = self.votes[index] + 1
			self.tot_votes = self.tot_votes + 1
			f=open(self.elecdir + '/votes.txt', 'w')
			n=0
			for item in self.candidates:
				f.write(self.candidates[n] + ' : ' + str(self.votes[n]) + '\n')
				n += 1
			f.close()
			self.lbl_votes_cast.set_markup('Votes Cast: <b>' + str(self.tot_votes) + '</b>')
			self.notifier.play('beep')
			self.curvoter_session = None
		else: # Selection changed
			self.curvoter_session.selindex = index
			
			selebname = 'eb_nameplate_' + str(index)
			for item in self.eb:
				if item.get_name() == selebname:
					#item.img.get_style_context().add_class('sel_nameplate') # TODO doens't work on IT@School Ubuntu 14.04
					item.imgindic.set_from_file(self.datadir + '/pics/indicator-green.svg')
				else:
					#item.img.get_style_context().remove_class('sel_nameplate') # TODO doens't work on IT@School Ubuntu 14.04
					item.imgindic.set_from_file(self.datadir + '/pics/indicator-gray.svg')
	
	def show_about_dlg(self, widget = None, event = None):
		ad = Gtk.AboutDialog(title=self.wnd)
		ad.set_transient_for(self.wnd)
		# TODO move values to global consts
		ad.set_title("About Sammaty")
		ad.set_program_name("Sammaty")
		ad.set_version(VERSION)
		ad.set_logo_icon_name('sammaty')
		ad.set_comments("Free Software to conduct computer-aided elections.")
		ad.set_website(SAMMATY_URL)
		ad.set_authors(["Nandakumar Edamana <contact@nandakumar.co.in>"])
		ad.set_artists(["Nandakumar Edamana <contact@nandakumar.co.in>"])
		
		try:
			license = open('/usr/share/doc/sammaty/copyright', 'r').read()
		except:
			try:
				license = open(self.datadir + '/../COPYING', 'r').read()
			except:
				license = "GNU GPLv3 with some additions.\nError: couldn't find the expanded license text. Please visit nandakumar.co.in\nor contact at contact@nandakumar.co.in"
		ad.set_license(license)
		
		ad.run()
		ad.destroy()
		
	def help(self, widget = None, event = None):
		url = self.datadir + '/help/Sammaty_0.1_Manual.html' # TODO FIXME update and move to docdir
		if os.path.exists(url) == False:
			url = '../doc/Sammaty_0.1_Manual.html'
			if os.path.exists(url) == False:
				self.nan_dialog_error(_('Sorry, the Help file could not be found. You will be now redirected to the Sammaty online portal.'), _('Error'))
				url = SAMMATY_URL
				
		webbrowser.open(url)

	def on_wnd_delete(self, widget = None, event = None):
		if self.voting_wnd_visible:
			if( self.nan_dialog_admin_auth(_('Enter the password to close the poll or press Cancel to continue the poll.')) ): # Correct password
				Gtk.main_quit()
			else: # Incorrect password or Cancel
				return True # Prevent GTK+ from executing other handlers
							
	def on_wnd_destory(self, widget = None, event = None):
		sys.exit(0)
					
if __name__ == '__main__':
	main()
	Gtk.main()
