#! /usr/bin/python

banner = """
#    zophEdit - edit photo metadata in a zoph database
#
#    Copyright (C) 2003 Nils Decker <ndecker@gmx.de>
"""
#    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 2 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

# $Id: zophEdit,v 1.1 2004/10/26 06:14:12 jasongeiger Exp $

# modifications to the zoph database:
# - alter table albums add column done default 0

import MySQLdb, os
from string import join, split, strip
import readline
import re

# TODO: doku

# *************************************************************** 
# configuration

conffile = os.getenv('HOME') + '/.zophEdit'

connargs = {
	'unix_socket' : '/var/run/mysqld/mysqld.sock',
	'db' : 'zoph',
	'user' : 'zoph_rw',
	'passwd' : 'pass' }

db_prefix = 'zoph_'

photo_cmd = "gnome-moz-remote http://localhost/zoph-dev/photo.php?photo_id=%i > /dev/null"
album_cmd = "gnome-moz-remote http://localhost/zoph-dev/photos.php?album_id=%i > /dev/null"


# *************************************************************** 

# {{{ global constants
photo_props = ['title', 'view', 'rating', 'description', 'date', 'time', 'comment']

default_dict = {
		'.' : 'next:1',
		'q' : 'quit:1',
		'next' : 'next:1',
		'all'  : 'apply:1000',
		'quit' : 'quit:1',
		'list' : 'list:1',
		'help' : 'help:1',
		'done' : 'done:1',
	}

commands = [
		('help',         'help'),
		('setalbum',     'work on this album'),
		('firstalbum',   'work on the first pending album'),
		('done',         'mark this album as done'),
		('next',         'jump to nth image'),
		('apply',        'apply the rest of the line to the next n photos'),
		('list',         'list shortcuts'),
		('del',          'delete shortcut'),
		('album',        'add photo to album'),
		('category',     'add photo to category'),
		('person',       'add person to photo'),
		('place',        'set the place of the photo'),
		('photographer', 'set photographer of the photo'),
		(join(photo_props, ','), 'set attribute of the photo') ]

# }}}

# {{{ Exceptions

class NotFoundError(Exception):
	def __init__(self, type, key):
		self.type = type
		self.key = key

	def printMsg(self):
		print "Cannot find %s for %s" % ( self.type, self.key )

class ManyFoundError(Exception):
	def __init__(self, type, key, vals):
		self.type = type
		self.key = key
		self.vals = vals

	def printMsg(self):
		print "More than one %s for %s found: " % (self.type, self.key)
		for l in self.vals:
			print "  %8i : %s" % l

class CommandError(Exception):
	def __init__(self, cmd):
		self.cmd = cmd

	def printMsg(self):
		print "Unknown command: %s" % (self.cmd,)
# }}}

# {{{ Db
class Db:
	db = None
	cur = None

	def __init__(self):
		self.db = apply(MySQLdb.Connect, (), connargs)
		self.cur = self.db.cursor()

	def get_album(self, id):
		self.cur.execute("SELECT album_id, album FROM " + db_prefix + "albums WHERE album_id = %s", (id,))

		res = self.cur.fetchone()

		if res:
			(album_id, album) = res

			self.cur.execute("SELECT photo_id from " + db_prefix + "photo_albums where album_id = %s", (album_id,))
			album_photos = map( (lambda x: x[0]), self.cur.fetchall())

			return (album_id, album, album_photos)
		else:
			return (0, None, [])

	def get_first_album(self):
		self.cur.execute("SELECT album_id from " + db_prefix + "albums WHERE done = 0 ORDER BY album_id LIMIT 1")
		(id,) = self.cur.fetchone()
		return self.get_album(id)

	def mark_album_done(self, id):
		self.cur.execute("UPDATE " + db_prefix + "albums SET done = 1 WHERE album_id = '%s'", (id,))

	def set_prop(self, p, prop, val):
		if not prop in photo_props:
			raise Exception("invalid property %s" % (prop,))

		self.cur.execute("UPDATE " + db_prefix + "photos SET %s = '%s' WHERE photo_id = %s" % (prop, val, p))

	def add_album(self, p, name):
		album_id, title = self.find_album(name)
		self.cur.execute("INSERT INGNORE INTO " + db_prefix + "photo_albums(photo_id, album_id) " +
						 "VALUES( %s, %s )", (p, album_id))
		return title


	def find_album(self, name):
		sql = "SELECT album_id, album FROM " + db_prefix + "albums WHERE "
		if name.isdigit():
			sql = sql + "album_id = %s"
		else:
			sql = sql + "album like '%s%%'"

		self.cur.execute(sql % (name,))
		data = self.cur.fetchall()

		if len(data) == 0:
			raise NotFoundError("album", name)
	 	elif len(data) == 1:
			return data[0][0], data[0][1]
		else:
			raise ManyFoundError("album", name, data)

	def add_category(self, p, name):
		category_id, title = self.find_category(name)
		self.cur.execute("INSERT IGNORE INTO " + db_prefix + "photo_categories(photo_id, category_id) " +
						 "VALUES( %s, %s )", (p, category_id))
		return title


	def find_category(self, name):
		sql = "SELECT category_id, category FROM " + db_prefix + "categories WHERE "
		if name.isdigit():
			sql = sql + "category_id = %s"
		else:
			sql = sql + "category like '%s%%'"

		self.cur.execute(sql % (name,))
		data = self.cur.fetchall()

		if len(data) == 0:
			raise NotFoundError("category", name)
	 	elif len(data) == 1:
			return data[0][0], data[0][1]
		else:
			raise ManyFoundError("category", name, data)

	def add_person(self, p, name):
		person_id, fullname = self.find_person(name)
		self.cur.execute(("INSERT IGNORE INTO " + db_prefix + "photo_people (photo_id, person_id ) " +
						  "VALUES(%s, %s)") % (p, person_id))
		return fullname
		

	def find_person(self, name):
		names = split(name, ',', 1)
		names.append("")

		self.cur.execute(("SELECT person_id, first_name, last_name FROM " + db_prefix + "people " +
						  "WHERE first_name LIKE '%s%%' AND last_name LIKE '%s%%'" )
						 % (names[1], names[0]))

		data = self.cur.fetchall()
		data = map( (lambda x: (x[0], "%s, %s" % (x[2], x[1]))), data)

		if len(data) == 0:
			raise NotFoundError("person", name)
	 	elif len(data) == 1:
			return data[0][0], data[0][1]
		else:
			raise ManyFoundError("person", name, data)

	def set_photographer(self, p, name):
		person_id, fullname = self.find_person(name)
		self.cur.execute("UPDATE " + db_prefix + "photos SET photographer_id = %s WHERE photo_id = %s"
						 %( person_id, p))
		return fullname


	def set_place(self, p, name):
		place_id, title = self.find_place(name)
		self.cur.execute(("UPDATE " + db_prefix + "photos SET location_id = %s " +
						  "WHERE photo_id = %s") % (place_id, p))
		return title

	def find_place(self, name):
		sql = "SELECT place_id, title FROM " + db_prefix + "places WHERE "
		if name.isdigit():
			sql = sql + "place_id = %s"
		else:
			sql = sql + "title like '%s%%'"

		self.cur.execute(sql % (name,))
		data = self.cur.fetchall()

		if len(data) == 0:
			raise NotFoundError("place", name)
	 	elif len(data) == 1:
			return data[0][0], data[0][1]
		else:
			raise ManyFoundError("place", name, data)



# }}}

def help(dict):
	print "Commands: "
	for c in commands:
		print "  %-15s : %s" % c
	print
	print "Predefined shortcuts:"
	for k in default_dict.keys():
		if dict[k] == default_dict[k]:
			print "  %-5s : %s" % (k, default_dict[k])
	print

def main(): # {{{ main
	dict = readConfig()
	db = Db()
	try:
		res = {}

		album_id, album = None, None
		album_photos = []
		photos_pos = -1

		while not res.has_key('quit'):
			if photos_pos != -1:
				photos = [album_photos[photos_pos]]
			else:
				photos = []

			prompt = ''
			if album_id:
				prompt = "%s" %(album,)
				if photos_pos != -1:
					prompt = prompt + "[%i]:%i" % (photos_pos, photos[0])
			prompt = prompt + '> '


			line = raw_input(prompt)

			try:
				(cmds, dict) = parse_line(line, dict)

				# {{{ commands
				for (cmd,val) in cmds:
					if cmd == 'help':
						help(dict)
					elif cmd == 'setalbum':
						album_id, album, album_photos = db.get_album(int(val))
						photos_pos = -1

						if album_id:
							os.system(album_cmd % (album_id,))
					elif cmd == 'firstalbum':
						album_id, album, album_photos = db.get_first_album()
						photos_pos = -1

						if album_id:
							os.system(album_cmd % (album_id,))
					elif cmd == 'done':
						db.mark_album_done(album_id)

					elif cmd == 'next':
						skip = int(val)
						photos_pos = photos_pos + skip

						if photos_pos < 0 or photos_pos >= len(album_photos):
							photos_pos = -1
							photos = []
						else:
							photos = [album_photos[photos_pos]]
							os.system(photo_cmd % (photos[0],))
					elif cmd == 'apply':
						pos = 0
						if photos_pos != -1:
							pos = photos_pos
						photos = album_photos[pos: pos + int(val)]
						print "working on photos: %s" % (str(photos),)
					elif cmd == 'list':
						print "Aliases:"
						for k in dict.keys():
							if not default_dict.has_key(k):
								print "%-8s = %s" % (k, dict[k])
					elif cmd == 'album':
						for p in photos:
							x = db.add_album(p, val)
							print "Added photo %i to album %s" % (p, x)
					elif cmd == 'category':
						for p in photos:
							x = db.add_category(p, val)
							print "Added photo %i to category %s" % (p, x)
					elif cmd == 'person':
						for p in photos:
							x = db.add_person(p, val)
							print "Added %s to photo %i" % (x, p)
					elif cmd == 'place':
						for p in photos:
							x = db.set_place(p, val)
							print "Photo %i is at place %s" % (p, x)
					elif cmd == 'photographer':
						for p in photos:
							x = db.set_photographer(p, val)
							print "Photographer for photo %i is %s" % (p, x)
					elif cmd in photo_props:
						for p in photos:
							db.set_prop(p, cmd, val)
							print "Photo %i %s set to %s" %(p, cmd, val)
					elif cmd == 'del':
						if dict.has_key(val):
							del dict[val]
					else:
						raise CommandError(cmd)
				# }}}

			except ManyFoundError, e:
				e.printMsg()
			except NotFoundError, e:
				e.printMsg()
			except CommandError, e:
				e.printMsg()

	except EOFError:
		print

	writeConfig(dict)

	# }}} main

# {{{ line
parse_eq = re.compile('^([A-Za-z0-9.]+)=(.*)$')
parse_col = re.compile('^([A-Za-z0-9.]+):(.*)$')
def parse_line(line, dict):
	res = []

	eq = parse_eq.match(line)
	if eq:
		dict[eq.group(1)] = eq.group(2)
	else:
		def split_line(line): # {{{ split_line
			res = []
			cur = ''
			in_quote = None

			while line:
				if line[0] == ' ' and not in_quote:
					if len(cur) > 0:
						res.append(cur)
					cur = ''
				elif line[0] == '\"':
					in_quote = not in_quote
				else:
					cur = cur + line[0]
				line = line[1:]

			if len(cur) > 0:
				res.append(cur)

			return res
			#}}}

		tokens = split_line(line)

		while len(tokens) > 0:
			token, tokens = tokens[0], tokens[1:]
			col = parse_col.match(token)
			if col:
				key = col.group(1)
				if dict.has_key(key) and dict[key].find(':') == -1:
					key = dict[key]
				res.append((key, col.group(2)))
			else:
				if dict.has_key(token):
					for t in split_line(dict[token]):
						tokens = [t] + tokens
				else:
					raise CommandError(token)

	return (res, dict)

#}}}

# {{{ config

def readConfig():
	dict = default_dict.copy()

	try:
		f = open(conffile, 'r')
		for l in f.readlines():
			if l[0] == '#':
				continue
			dict = parse_line(l, dict)[1]
		f.close()
	except Exception, e:
		pass

	return dict

def writeConfig(dict):
	f = open(conffile, 'w')
	for k in dict.keys():
		if not default_dict.has_key(k) or default_dict[k] != dict[k]:
			f.write("%s=%s\n"%(k, dict[k]))
	f.close()
		

#}}}

if __name__ == '__main__':
	print join(filter(None, map((lambda x: strip(x[1:])), split(banner,'\n'))), '\n')
	main()

