class CellularAutomataMain(Tk):
    GRID_WIDTH_PX = DisplayConfiguration.CA_CANVAS_MAX_PIX_X
    GRID_HEIGHT_PX = DisplayConfiguration.CA_CANVAS_MAX_PIX_Y

    def __init__(self, width_cells=DisplayConfiguration.DEFAULT_WIDTH, *args, **kwargs):
        Tk.__init__(self, *args, **kwargs)
        self.resizable(width=False, height=False)
        self.state = {'rules': None, 'rules_file': None, 'grid': None}

        self.random_start = BooleanVar(self)
        self.random_start.trace('w', self.random_start_callback)

        #Load blank grid
        self.width_cells = width_cells
        self.height_cells = width_cells  # Default setting should probably have the grid be square.
        canvas_width = self.GRID_WIDTH_PX-80
        canvas_height = self.GRID_HEIGHT_PX-80
        self.state['grid'] = build_blank_grid(width_cells, width_cells)
        self.grid_display = GridDisplay(self, self.state['grid'],
                                        self.GRID_WIDTH_PX, self.GRID_HEIGHT_PX,
                                        width=canvas_width, height=canvas_height, relief=GROOVE, bd=4)
        self.grid_display.grid(row=0, column=0, padx=20, pady=20)

        #Build top menus
        self.menubar = Menu(self)
        try:
            self.config(menu=self.Menu)
        except AttributeError:
            self.tk.call(self, 'config', '-menu', self.menubar)
        self.file_menu = Menu(self, tearoff=False)
        self.file_menu.add_command(label="Load Ruleset", command=self.load_dialogue)
        self.file_menu.add_separator()
        self.file_menu.add_command(label="Save Automata (Image)", command=self.save_image_dialogue)
        self.file_menu.add_separator()
        self.file_menu.add_command(label="Quit", command=self.quit)
        self.menubar.add_cascade(menu=self.file_menu, label='File')

        self.config_menu = Menu(self, tearoff=False)
        self.config_menu.add_command(label='Set Dimensions', command=self.config_dimensions)
        self.config_menu.add_checkbutton(label='Set Grid Wrap')
        self.config_menu.add_checkbutton(label='Random Start Row', variable=self.random_start)
        self.bg_color_menu = self.build_selector_menu(GridDisplay.ALLOWED_COLORS, self.grid_display.set_bg_color,
                                                      lambda: self.grid_display.draw_grid(self.state['grid']), 'white')
        self.fill_color_menu = self.build_selector_menu(GridDisplay.ALLOWED_COLORS, self.grid_display.set_fill_color,
                                                        lambda: self.grid_display.draw_grid(self.state['grid']), 'black')
        self.config_menu.add_cascade(menu=self.bg_color_menu, label="Set Background Color...")
        self.config_menu.add_cascade(menu=self.fill_color_menu, label="Set Fill Color...")
        self.config_menu.add_separator()
        self.config_menu.add_command(label='Configure Plug-ins')
        self.plugin_menu = Menu(self)
        self.config_menu.add_cascade(menu=self.plugin_menu, label="Plugins...")
        self.menubar.add_cascade(menu=self.config_menu, label='Configure')
        self.menubar.add_command(label="About", command=self.about)

        #Load status bar
        self.status_bar_var = StringVar(self)
        self.status_bar_var.set('Initialized.')
        self.status_bar = Label(self, textvar=self.status_bar_var, relief=SUNKEN, justify=LEFT, anchor=W)
        self.status_bar.grid(row=1, column=0, sticky="EW", padx=2, pady=2)


        #Build plugin manager
        disp_logger.info('Loading plug-in manager')
        self.status_bar_var.set('Loading plug-in manager...')
        self.plugin_manager = PluginManager()
        self.plugin_manager.setPluginPlaces(['../plugins/'])
        self.plugin_manager.collectPlugins()
        disp_logger.info('Plug-in manager loaded. {} plug-ins were loaded.'
                         ''.format(len(self.plugin_manager.getAllPlugins())))

    def load_dialogue(self):
        new_rule_filename = tkFileDialog.askopenfilename(parent=self, title='Open Rule File', defaultextension=".txt",
                                                         filetypes=[('Rules Files', '*.txt'), ('All Files', '*')])
        if not new_rule_filename:
            return
        try:
            self.load(new_rule_filename)
        except:
            disp_logger.exception('Faulted loading rules file!')

    def save_image_dialogue(self):
        automata_image_filename = tkFileDialog.asksaveasfilename(parent=self, title='Save Automata Image',
                                                                 defaultextension='.eps',
                                                                 filetypes=[('Postscript', '.eps'),
                                                                            ('PNG', '.png'),
                                                                            ('JPEG', '.jpg')])
        if not automata_image_filename:
            return
        try:

            #TODO: Should we be converting, or should I have a local image representation instead?
            if not automata_image_filename.endswith('.eps'):
                self.grid_display.postscript(file='tmp.eps')
                img = Image.open('tmp.eps')
                img.save(automata_image_filename)
                os.remove('tmp.eps')
            else:
                self.grid_display.postscript(file=automata_image_filename)
            self.status_bar_var.set('Saved automata image file as: {}'.format(automata_image_filename))
        except:
            disp_logger.exception('Faulted saving automata image!')

    def load(self, rule_file):
        disp_logger.info('Attempting to load new rules file: {}'.format(rule_file))
        rules = load_rules(rule_file)
        self.state['rules'] = rules
        self.state['rules_file'] = rule_file
        self._build_grid(rules)
        self.status_bar_var.set('Loaded rules file: {}'.format(rule_file))

    def _build_grid(self, rules):
        grid = []
        if self.random_start.get():
            start_row = build_random_start_row(self.width_cells)
        else:
            start_row = build_default_start_row(self.width_cells)
        grid.append(start_row)
        grid.extend(evolve_system_multi(start_row, rules, self.height_cells))
        self.state['grid'] = grid
        self.grid_display.draw_grid(grid)

    def config_dimensions(self):
        new_width, new_height = DimensionsDialog(self, title='Set Automata Dimensions',
                                                 prompt='Set Automata Dimensions', x_name='Width', y_name='Height',
                                                 x_default=self.width_cells, y_default=self.height_cells)
        if not new_width or not new_height:
            return
        disp_logger.info('Resizing automata to new width: {}, height: {}'.format(new_width, new_height))
        self.width_cells = new_width
        self.height_cells = new_height
        self._build_grid(self.state['rules'])


    def random_start_callback(self, *args):
        self._build_grid(self.state['rules'])

    @staticmethod
    def about():
        tkMessageBox.showinfo('About', '{app} ({ver})\nby {auth}, 2014'
                                       ''.format(ver=__version__, auth=__author__, app=__application_name__))

    def build_selector_menu(self, choices, set_fxn, update_fxn=None, default=None):
        new_menu = Menu(self)
        choice_map = {}

        def build_selector_trace(var):
            def callback(*args):
                set_fxn(choice_map[var.get()])
                if update_fxn:
                    update_fxn()
            return callback

        if not default:
            default = choices[0]
        new_var = StringVar(new_menu)
        new_var.set(str(default))
        new_var.trace('w', build_selector_trace(new_var))
        for choice in choices:
            choice_map[str(choice)] = choice
            new_menu.add_radiobutton(label=str(choice).capitalize(), variable=new_var, value=str(choice))
        return new_menu
class Cap(Frame):
	def __init__(self):
		' set defaults, create widgets, bind callbacks, start live view '
		self.root = Tk()
		# menu:
		self.menu = Menu(self.root)
		self.root.config(menu=self.menu)
		# bind global keypresses:
		self.root.bind('q', lambda e: self.root.quit())
		self.root.bind('x', lambda e: self.root.quit())
		self.root.bind('<Destroy>', self.do_stop_video)
		self.root.bind('<space>', self.do_single_shot)
		self.root.bind('<Return>', self.do_single_shot)
		self.root.bind('<Button-3>', self.do_single_shot)
		self.root.bind('<Left>', lambda e: self.degree.set(-90))
		self.root.bind('<Right>', lambda e: self.degree.set(90))
		self.root.bind('<Up>', lambda e: self.degree.set(0))
		self.root.bind('a', lambda e: self.autocontrast.set(not self.autocontrast.get()))
		self.root.bind('e', lambda e: self.equalize.set(not self.equalize.get()))
		self.root.bind('g', lambda e: self.grayscale.set(not self.grayscale.get()))
		self.root.bind('i', lambda e: self.invert.set(not self.invert.get()))
		self.root.bind('s', lambda e: self.solarize.set(not self.solarize.get()))
		# config:
		self.config = RawConfigParser()
		self.config.read('filmroller.conf')
		if not self.config.has_section('global'):
			self.config.add_section('global')
		self.video = None
		self.invert = BooleanVar(name='invert')
		self.invert.set(self.config_get('invert', True))
		self.invert.trace('w', self.do_configure)
		self.grayscale = BooleanVar(name='grayscale')
		self.grayscale.set(self.config_get('grayscale', False))
		self.grayscale.trace('w', self.do_configure)
		self.autocontrast = BooleanVar(name='autocontrast')
		self.autocontrast.set(self.config_get('autocontrast', True))
		self.autocontrast.trace('w', self.do_configure)
		self.equalize = BooleanVar(name='equalize')
		self.equalize.set(self.config_get('equalize', False))
		self.equalize.trace('w', self.do_configure)
		self.solarize = BooleanVar(name='solarize')
		self.solarize.set(self.config_get('solarize', False))
		self.solarize.trace('w', self.do_configure)
		self.degree = IntVar(name='degree')
		self.degree.set(0)
		self.filename = StringVar(name='filename')
		self.videodevice = StringVar(name='videodevice')
		dev_names = sorted(['/dev/{}'.format(x) for x in listdir('/dev') if x.startswith('video')])
		d = self.config_get('videodevice', dev_names[-1])
		if not d in dev_names:
			d = dev_names[-1]
		self.videodevice.set(d)
		self.videodevice.trace('w', self.do_configure)
		#
		self.path = 'filmroller'
		if not exists(self.path):
			makedirs(self.path)
		# create gui:
		Frame.__init__(self, self.root)
		self.grid()
		self.x_canvas = Canvas(self, width=640, height=640, )
		self.x_canvas.pack(side='top')
		self.x_canvas.bind('<Button-1>', self.do_change_rotation)
		Checkbutton(self, text='Invert', variable=self.invert).pack(side='left')
		Checkbutton(self, text='Gray', variable=self.grayscale).pack(side='left')
		Checkbutton(self, text='Auto', variable=self.autocontrast).pack(side='left')
		OptionMenu(self, self.videodevice, *dev_names, command=self.restart_video).pack(side='left')
		Button(self, text='First role', command=self.do_first_role).pack(side='left')
		Label(self, textvariable=self.filename).pack(side='left')
		Button(self, text='Next role', command=self.do_inc_role).pack(side='left')
		Button(self, text='Take!', command=self.do_single_shot).pack(side='right')
		#filemenu = Menu(self.menu)
		#self.menu.add_cascade(label=self.videodevice.get(), menu=filemenu, )
		#for n in dev_names:
		#	filemenu.add_command(label=n, )
		#filemenu.add_separator()
		# start operation:
		self.do_first_role()
		self.do_start_video()

	def do_change_rotation(self, event):
		' determine where the image was clicked and turn that to the top '
		if event.x < 200:
			self.degree.set(-90)
		elif event.x > 640 - 200:
			self.degree.set(90)
		else:
			self.degree.set(0)

	def config_get(self, name, default):
		' read a configuration entry, fallback to default if not already stored '
		if not self.config.has_option('global', name):
			return default
		if isinstance(default, bool):
			return self.config.getboolean('global', name)
		else:
			return self.config.get('global', name)

	def do_configure(self, name, mode, cbname):
		' change a configuration entry '
		if cbname == 'w':
			value = getattr(self, name).get()
			self.config.set('global', name, str(value))
			self.config.write(open('filmroller.conf', 'w'))

	def do_first_role(self, *args):
		' jump back to first role '
		self.role = 'aa'
		self.serial = 0
		self.inc_picture()

	def inc_picture(self):
		' increment the picture number, jump over existing files '
		self.filename.set('{}/scanned.{}-{:04}.jpg'.format(self.path, self.role, self.serial, ))
		while exists(self.filename.get()):
			self.serial += 1
			self.filename.set('{}/scanned.{}-{:04}.jpg'.format(self.path, self.role, self.serial, ))
		self.root.title('filmroller - ' + self.filename.get())

	def do_inc_role(self, *args):
		' increment to next role '
		self.serial = 0
		self.role = ascii_increment(self.role)
		self.inc_picture()

	def set_pauseimage(self):
		' show pause image (during shot) '
		self.image = fromfile('filmroller.pause.png')
		self.image.thumbnail((self.previewsize['size_x'], self.previewsize['size_y'], ), )
		self.photo = PhotoImage(self.image)
		self.x_canvas.create_image(640/2, 640/2, image=self.photo)

	def do_stop_video(self, *args):
		' stop video and release device '
		if self.video is not None:
			self.video.stop()
			self.video.close()
			self.video = None

	def restart_video(self, *args):
		' restart video (if device changes or hangs) '
		self.do_stop_video()
		self.root.after(1, self.do_start_video)

	def do_start_video(self, *args):
		' init video and start live view '
		if self.video is None:
			self.video = Video_device(self.videodevice.get())
			_, _, self.fourcc = self.video.get_format()
			caps = sorted(self.video.get_framesizes(self.fourcc), cmp=lambda a, b: cmp(a['size_x']*a['size_y'], b['size_x']*b['size_y']))
			self.previewsize, self.highressize = caps[0], caps[-1]
			self.previewsize['size_x'], self.previewsize['size_y'] = self.video.set_format(
				self.previewsize['size_x'], self.previewsize['size_y'], 0, 'MJPEG')
			try: self.video.set_auto_white_balance(True)
			except: pass
			try: self.video.set_exposure_auto(True)
			except: pass
			try: self.video.set_focus_auto(True)
			except: pass
			self.video.create_buffers(30)
			self.video.queue_all_buffers()
			self.video.start()
			self.root.after(1, self.do_live_view)
			#self.x_canvas.config(width=640, height=480)
			#self.x_canvas.pack(side='top')
			self.degree.set(0)

	def do_live_view(self, *args):
		' show single pic live view and ask tk to call us again later '
		if self.video is not None:
			select((self.video, ), (), ())
			data = self.video.read_and_queue()
			self.image = frombytes('RGB', (self.previewsize['size_x'], self.previewsize['size_y']), data)
			if self.invert.get():
				self.image = invert(self.image)
			if self.grayscale.get():
				self.image = grayscale(self.image)
			if self.autocontrast.get():
				self.image = autocontrast(self.image)
			if self.equalize.get():
				self.image = equalize(self.image)
			if self.solarize.get():
				self.image = solarize(self.image)
			if self.degree.get():
				self.image = self.image.rotate(self.degree.get())
			self.photo = PhotoImage(self.image)
			self.x_canvas.create_image(640/2, 640/2, image=self.photo)
			self.root.after(3, self.do_live_view)

	def do_single_shot(self, *args):
		' do a high res single shot and store it '
		def _go():
			self.video = Video_device(self.videodevice.get())
			try:
				self.highressize['size_x'], self.highressize['size_y'] = self.video.set_format(
					self.highressize['size_x'], self.highressize['size_y'], 0, 'MJPEG')
				try: self.video.set_auto_white_balance(True)
				except: pass
				try: self.video.set_exposure_auto(True)
				except: pass
				try: self.video.set_focus_auto(True)
				except: pass
				self.video.create_buffers(7)
				self.video.queue_all_buffers()
				self.video.start()
				stop_time = time() + 3.0
				# wait for auto
				while stop_time >= time():
					select((self.video, ), (), ())
					self.update_idletasks()
					data = self.video.read_and_queue()
				image = frombytes('RGB', (self.highressize['size_x'], self.highressize['size_y'], ), data)
				if self.invert.get():
					image = invert(image)
				if self.grayscale.get():
					image = grayscale(image)
				if self.autocontrast.get():
					image = autocontrast(image)
				if self.equalize.get():
					self.image = equalize(self.image)
				if self.solarize.get():
					self.image = solarize(self.image)
				if self.degree.get():
					image = image.rotate(self.degree.get())
				image.save(self.filename.get())
				self.inc_picture()
				self.root.bell()
				self.video.stop()
			finally:
				self.video.close()
				self.video = None
			self.root.after(1, self.do_start_video)
		self.do_stop_video()
		self.set_pauseimage()
		self.update_idletasks()
		self.root.after(1, _go)