#!/usr/bin/python2.5


# 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
import gtk.glade
import os

#Require 2.8!

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

APP = 'gvb'
if os.path.exists('locale'):
	DIR = 'locale'
else:
	DIR = '/usr/share/locale'

libdir = '/usr/share/gvb'

locale.setlocale(locale.LC_ALL, '')
gtk.glade.bindtextdomain(APP, DIR)
gtk.glade.textdomain(APP)


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... or simply, 2^n-1.
		self.filename = None
		self.dumping = False
		self.inhibit_dumping = False
		self.occupied = False
		self.file_chooser_state = None

		try:
			xml=gtk.glade.XML(libdir+'/stuff/gvb.glade')
		except:
			xml=gtk.glade.XML('stuff/gvb.glade')

		self.xml=xml
		self.drawing=xml.get_widget('drawing')
		self.start_button=xml.get_widget('start button')
		self.frame_number=xml.get_widget('frame number')
		self.frame_ms=xml.get_widget('frame ms')
		self.draw_ms_label=xml.get_widget('draw ms')
		self.calculator_entry=xml.get_widget('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_widget('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_widget('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,
			'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.signal_autoconnect(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_widget('granularity').get_value(),
									disp = self.disposition,
									calc = self.xml.get_widget('combo calculator').get_active_text(),
									drawer = drawers.Drawer(self.drawing, self.xml.get_widget('combo drawer'), dr_type = self.actual_drawers[len(self.shape)-1]),
									pos = self.startpos)


		self.xml.get_widget('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_n(self, spin, *args):
		self.shape = (spin.get_value(),)*self.dim
		self.points.reconfigure(shape=self.shape)

	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_widget('dim '+str(dim)).get_active():
					self.dim = dim
					
#					print "dim", dim
					break
		else:
			self.dim = dim
			self.xml.get_widget('dim '+str(dim)).set_active(True)

		self.shape = (self.STEPS,)*self.dim
		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 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_widget('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*self.speed

		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_widget('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_widget('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.STEPS,)*args[1]

#		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_widget('about')
		self.about_window.show()
		self.about_window.connect('response', self.about_catcher)

	def change_steps(self, *args):
		self.steps_window = self.xml.get_widget('steps')
		self.xml.get_widget('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_widget('steps spin').get_value())
			if new_steps != self.STEPS:
				self.STEPS = new_steps
				self.shape=(self.STEPS,)*self.dim
#				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_widget('file chooser')
		self.xml.get_widget('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_widget('file chooser')
		self.xml.get_widget('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_widget('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_widget('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_widget('file not exists').hide()
				return
			self.filename = dialog.get_filename()
			if not os.path.exists(self.filename):
				self.xml.get_widget('file not exists').run()
				return
			self.points=points.Points(drawer = drawers.Drawer(self.drawing, self.xml.get_widget('combo drawer')),
										gr = self.xml.get_widget('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_widget('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_widget('file chooser')
		self.xml.get_widget('file chooser button').set_label('gtk-save')
		self.file_chooser.run()


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

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

