#!/usr/bin/python


# GVB - a GTK+/GNOME vibrations simulator
#
# Copyright (C) 2008 Pietro Battiston
#
# 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; either version 3 of the License, or
# (at your option) any later version.
#
# 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA


import gtk, gobject, gnome
from glib import GError
import os

from scipy import array, sin, pi, zeros, shape
from time import time

from gvbmod import dispositions, calculators, points, drawers
from gvbmod.advancededitor import AdvancedEditor


from gvbmod.gvbi18n import _


import locale
import gettext


########################## CONFIGURATION #######################################

APP = 'gvb'

import sys

not_installed_dir = os.path.dirname(os.path.realpath(__file__))
if os.path.exists(not_installed_dir + '/stuff/gvb.svg'):
	STUFF_DIR = not_installed_dir + '/stuff'
	HELP_DIR = not_installed_dir + '/help'
	LOCALE_DIR = not_installed_dir + '/locale'
	INSTALLED = False
else:
	for directory in [sys.prefix, sys.prefix + '/local']:
		installed_root_dir = directory + '/share'
		if os.path.exists(installed_root_dir + '/%s/stuff' % APP):
			STUFF_DIR = installed_root_dir + '/%s/stuff' % APP
			LOCALE_DIR = installed_root_dir + '/locale'
			break
	INSTALLED = True


########################## END OF CONFIGURATION ################################


gettext.install(APP, localedir=LOCALE_DIR, unicode=True)
locale.bindtextdomain(APP, LOCALE_DIR)

DIMS = [1,2] #(Still) no fluid dinamics...
WAIT_FRAMES = True		#If computer is too slow and we loose frames, shall we fake them (False) or wait for them at the cost of (willingly) altering speed (True)?
						#Notice that probably speed gets altered anyway. I imagine that if gobject.timeout_add calls can't get run, they just get queued, not lost.

class MainGbv():
	def __init__(self):
		self.STEPS = 127 #Number of points (per dimension). Suggestion: should be of the form 3^a*4^b*5^c...-1 or simply, 2^n-1.
		self.filename = None
		self.dumping = False
		self.inhibit_dumping = False
		self.occupied = False
		self.file_chooser_state = None

		xml=gtk.Builder()
		
		xml.set_translation_domain('gvb')

		xml.add_from_file(STUFF_DIR + '/gvb.glade')

		self.xml=xml
		self.main=xml.get_object('main')
		self.drawing=xml.get_object('drawing')
		self.start_button=xml.get_object('start button')
		self.frame_number=xml.get_object('frame number')
		self.frame_ms=xml.get_object('frame ms')
		self.draw_ms_label=xml.get_object('draw ms')
		self.calculator_entry=xml.get_object('calculator entry')
		self.ready=True		#This is used to block "draw" and "initialize" dummy calls
		self.inhibit_reconfigure=False
		self.inhibit_change_dim=False

		self.cr=None		#DrawingArea's Drawable is not created until it's not show()n

		self.editor = None
		self.file_chooser = None

		#Startup situation
		self.shape = (self.STEPS,)
		self.dim = len(self.shape)
		self.disposition='sin'
		self.startpos = None
		self.points = None
		self.MPF = int(1000/self.xml.get_object('mpf').get_value())		#Milliseconds per (drawn) frame (25 ==> 40 frames per second) are then taken from GUI
		self.actual_drawers = [None for dim in DIMS]				#One per dimension

		#Two counters representing drawn frames' ("real") time and calc ("anticipately calculated") time: both in ms.
		self.drawn_time=0
		self.calc_time=0
		self.set_text_counter=0
		self.speed=self.xml.get_object('speed').get_value()

		self.runner=None

		self.dispositions_menu()
		self.calculators_menu()


		handlers={
			'change granularity': self.change_granularity,
			'change speed': self.change_speed,
			'change steps': self.change_steps,
			'change calculator': self.change_calculator,
			'change dim': self.change_dim,
			'change mpf': self.change_mpf,
			'quit': self.quit,
			'start': self.start,
			'show about': self.show_about,
			'show help': self.show_help,
			'save': self.save,
			'save as': self.save,
			'load': self.load,
			'png dump': self.png_dump,
			'file chooser catcher': self.file_chooser_catcher,
			'steps_catcher': self.steps_catcher,
			'file activated': self.file_activated}

		xml.connect_signals(handlers)
		self.initialize()

	def initialize(self, *args):
#		print args
		self.stop()

		if not self.ready:
			return


		if self.points:
			self.actual_drawers[len(self.points.shape)-1] = self.points.drawer.dr_type

#		print self.actual_drawers, self.shape
		self.points=points.Points(	shape = self.shape,
									gr = self.xml.get_object('granularity').get_value(),
									disp = self.disposition,
									calc = self.xml.get_object('combo calculator').get_active_text(),
									drawer = drawers.Drawer(self.drawing, self.xml.get_object('combo drawer'), dr_type = self.actual_drawers[len(self.shape)-1]),
									pos = self.startpos)


		self.xml.get_object('granularity').set_sensitive(self.points.calculator.discrete)



		self.drawn_time=0

		self.frame_number.set_text('1')
		self.frame_ms.set_text('')
		self.draw_ms_label.set_text('')

	def change_speed(self, spin, *args):
		self.speed=float(spin.get_value())
#		print self.speed

	def change_granularity(self, spin, *args):
#		print "changed"
		self.points.reconfigure(gr=spin.get_value())

	def change_dim(self, widget=None, data=None, dim=None):
#		print widget, data, dim
		if self.inhibit_change_dim:
#			print "inhibited"
			return
		self.inhibit_change_dim = True
#		print "Widget", widget
		if widget:
			for dim in [1,2]:
				if self.xml.get_object('dim '+str(dim)).get_active():
					self.dim = dim
					
#					print "dim", dim
					break
		else:
			self.dim = dim
			self.xml.get_object('dim '+str(dim)).set_active(True)

		self.shape = self.recalculate_shape()
		self.startpos = None
		self.inhibit_reconfigure = True		#"change_calculator" would call a useless "reconfigure"
		self.calculators_menu()
		self.inhibit_reconfigure = False
		self.initialize()
		self.inhibit_change_dim = False

	def recalculate_shape(self, steps=None, dim=None):
		if not steps:
			steps = self.STEPS
		if not dim:
			dim = self.dim
		
		shape = [int(steps**(1/float(dim)))]*dim

		# Not ready for 3 dimensions.
		# Also: this cycle will run at most 2 times, so I don't bother about
		# overhead of testing dim also the second:
		while dim > 1 and shape[0] * (shape[1] + 1) <= self.STEPS:
			shape[1] += 1
		
		return tuple(shape)

	def change_mpf(self, spin, *args):
		self.MPF = int(1000/spin.get_value())
#		print "changed"
		if self.runner:
			gobject.source_remove(self.runner)
			self.runner=gobject.timeout_add(self.MPF, self.update)

	def change_disposition(self, menu_item=None, pos=None):

		if menu_item:
			new_dim = int(menu_item.name.partition(' ')[0])
#			print new_dim
			self.disposition = menu_item.name.partition(' ')[2]
			self.startpos = None

		else:
			self.startpos = pos
			new_dim=len(self.startpos.shape)

		if new_dim is not self.dim:
			self.change_dim(dim=new_dim)
		elif menu_item:
			self.points.reconfigure(disp=self.disposition)
		else:
			self.points.reconfigure(pos=pos)

		self.stop()
#		print "draw!"
		self.drawn_time=0

	def change_calculator(self, combobox=None, arg2=None):
		if self.points and not self.inhibit_reconfigure:
			self.points.reconfigure(calc=combobox.get_active_text())
			self.xml.get_object('granularity').set_sensitive(self.points.calculator.discrete)
			self.drawn_time = 0

	def start(self, *args):
		if self.runner:
			self.stop()
		else:
			self.runner=gobject.timeout_add(self.MPF, self.update)
			self.labeler=gobject.timeout_add_seconds(2, self.set_labels)
			self.start_button.set_label(_('Stop'))

	def stop(self, *args):
		if self.runner:
			gobject.source_remove(self.runner)
			gobject.source_remove(self.labeler)
			self.runner = 0
			self.start_button.set_label(_('Start'))


	def update(self, *args):
		if self.occupied and (self.points.drawer.dumpdir or SKIP_FRAMES):	#If we are dumping, we can wait, user won't notice it.
			return True

		self.occupied = True
		elapsed_ms, draw_ms = self.points.update(self.drawn_time)
		if elapsed_ms:
			self.elapsed_ms=elapsed_ms
		if draw_ms:
			self.draw_ms = draw_ms

		self.drawn_time=self.drawn_time+self.MPF*(float(self.speed)*min(self.shape)/100)

		self.occupied = False


		return True

	def set_labels(self, *args):
		self.frame_number.set_text(str(self.points.number))
		self.frame_ms.set_text(str(round(self.elapsed_ms, 3)))
		self.draw_ms_label.set_text(str(round(self.draw_ms, 3)))

		return True

	def dispositions_menu(self):
		main_menu = self.xml.get_object('dispositions menu')
		for dim in DIMS:
			if dim == 1:
				submenu_title = _('%d dimension: precooked')
				advanced_title = _('%d dimension: advanced')
			else:
				submenu_title = _('%d dimensions: precooked')
				advanced_title = _('%d dimensions: advanced')
			submenu_header=gtk.MenuItem(submenu_title % dim)
			submenu=gtk.Menu()
			for disposition in dispositions.dispositions[dim]:
				disposition_item = gtk.MenuItem(_(disposition))
				disposition_item.set_name(str(dim)+' '+disposition)
				disposition_item.connect('activate', self.change_disposition)
				submenu.append(disposition_item)
			submenu_header.set_submenu(submenu)
			submenu_header.show()
			submenu.show_all()
			main_menu.append(submenu_header)
			advanced_item = gtk.MenuItem(advanced_title % dim)
			advanced_item.connect('activate', self.editor_start, dim)
			advanced_item.show()
			main_menu.append(advanced_item)

	def calculators_menu(self):
		self.build_menu('calculator', calculators.calculators)

#	def drawers_menu(self):
#		self.ready = True
#		self.build_menu('drawer', drawers.drawers)

	def build_menu(self, name, items):
		combo = self.xml.get_object('combo '+name)
		combo_model=gtk.ListStore(str)
		for item in items[self.dim]:
			combo_model.append([item])
		combo.set_model(combo_model)
		combo.set_active(0)


	def editor_start(self, *args):
		self.stop()

		new_shape = self.recalculate_shape(self.STEPS, args[1])
		if len(new_shape) != len(self.shape):
		    self.change_dim(dim=len(new_shape))

#		print "passing shape:", self.points.pos.shape


		if self.editor:
			self.editor.go(self.points, new_shape)
		else:
			self.editor=AdvancedEditor(self, self.xml, self.points, new_shape)
		

	def editor_save(self, points):
		self.change_disposition(pos=points.pos)
#		self.editor_esc()

	def editor_esc(self):
#		print "self.editor", self.editor
		self.editor.window.hide()
		del self.editor

	def show_about(self, *args):
		self.about_window=self.xml.get_object('about')
		self.about_window.show()
		self.about_window.connect('response', self.about_catcher)

	def show_help(self, *args):
		if INSTALLED:
			gnome.help_display_uri('ghelp:gvb')
		else:
			import locale
			loc = locale.getdefaultlocale()[0]
			if os.path.exists('%s/%s/gvb.xml' % (HELP_DIR, loc)):
				gnome.help_display_uri('ghelp:%s/%s/gvb.xml' % (HELP_DIR, loc))
			elif os.path.exists('%s/%s/gvb.xml' % (HELP_DIR, loc.split('_')[0])):
				gnome.help_display_uri('ghelp:%s/%s/gvb.xml' % (HELP_DIR, loc.split('_')[0]))
			else:
				gnome.help_display_uri('ghelp:%s/C/gvb.xml' % HELP_DIR)

	def change_steps(self, *args):
		self.steps_window = self.xml.get_object('steps')
		self.xml.get_object('steps spin').set_adjustment(gtk.Adjustment(self.STEPS, 1, 10000, 1, 10))
		self.steps_window.run()

	def steps_catcher(self, dialog, response):
		self.steps_window.hide()

		if response == 1:
			new_steps = int(self.xml.get_object('steps spin').get_value())
			if new_steps != self.STEPS:
				self.STEPS = new_steps
				self.shape = self.recalculate_shape()
#				print "steps changed:", self.shape
				self.initialize()

	def about_catcher(self, *args):
		self.about_window.hide()

	def quit(self, *args):
		gtk.main_quit()

	def save(self, *args):
		if self.filename:
			if self.running:
				self.stop()
				self.points.dump(self.filename)
				self.start()
			else:
				self.points.dump(self.filename)
		else:
			self.save_as(*args)

	def save_as(self, *args):
		self.stop()
		self.file_chooser_state='saving'
		self.file_chooser = self.xml.get_object('file chooser')
		self.xml.get_object('file chooser button').set_label('gtk-save')
		self.file_chooser.run()

	def load(self, *args):
		self.stop()
		self.file_chooser_state='loading'
		self.file_chooser = self.xml.get_object('file chooser')
		self.xml.get_object('file chooser button').set_label('gtk-open')
		self.file_chooser.run()


	def file_chooser_catcher(self, dialog, response=None):
#		print self.file_chooser.get_filename()
#		print response
#		filename = args[0].get_filename()
		if int(response) in [-1,-4]:	#never mind...
			if self.file_chooser_state=='dumping':		#Toggle has been set, but user changed his mind
				self.inhibit_dumping = True				#So let's say to "png_dump" it's not the user
				self.xml.get_object('png dump').set_active(False)	#And unset it
			self.file_chooser_state = None
			self.file_chooser.hide()
			if self.file_chooser is not dialog:
				dialog.hide()
			return

		filename = self.file_chooser.get_filename()
		if not filename:
			return

		if self.file_chooser_state == 'saving':
			if response == 2 and os.path.exists(filename):
				self.xml.get_object('file exists').run()
				return
			self.filename = filename
			self.points.dump(self.filename)
			self.file_chooser.hide()
			if self.file_chooser is not dialog:
				dialog.hide()

		elif self.file_chooser_state == 'loading':
			if response == 5:
				self.xml.get_object('file not exists').hide()
				return
			self.filename = dialog.get_filename()
			if not os.path.exists(self.filename):
				self.xml.get_object('file not exists').run()
				return
			self.points=points.Points(drawer = drawers.Drawer(self.drawing, self.xml.get_object('combo drawer')),
										gr = self.xml.get_object('granularity').get_value(),
										from_file = self.filename)

			self.shape = self.points.pos.shape
			self.STEPS = self.shape[0]
			new_dim = len(self.shape)
			if new_dim is not self.dim:
				self.change_dim(dim=new_dim)
			self.startpos = self.points.pos

			self.initialize()

			dialog.hide()

		elif self.file_chooser_state == 'dumping':
			if os.path.exists(filename):
				if not os.path.isdir(filename):
					if response == 2:			#Let's ask the user if he is sure
						self.xml.get_object('file exists').run()
						return
					else:						#OK, he's sure
						os.remove(filename)
						os.mkdir(filename)
			else:
				os.mkdir(filename)
			self.dumping = True
			self.points.drawer.dump(filename)
			self.file_chooser.hide()
			if self.file_chooser is not dialog:
				dialog.hide()

				
		self.file_chooser_state = None


	def file_activated(self, dialog):

		self.file_chooser_catcher(dialog, 2)

	def png_dump(self, *args):
		if self.inhibit_dumping:
			self.inhibit_dumping = False
			return
		if self.dumping:
			self.points.drawer.dump(None)
			self.dumping = False
			return
		self.stop()
		self.file_chooser_state = 'dumping'
		self.file_chooser = self.xml.get_object('file chooser')
		self.xml.get_object('file chooser button').set_label('gtk-save')
		self.file_chooser.run()


def main():
	global app
	app=MainGbv()

if __name__=='__main__':
	main()
	gtk.main()


