# 
# Copyright 2003, 2004 S. Kevin Hester, kevinh@geeksville.com
#
# Airplane build log keeper - automatically scans the Pictures folder and 
# creates thumbnails/web pages/build totals.  
# 
# History:
#  V1.0 - Original version
#  V1.1 - Update for windows/Python2.3

import os
import time
import string
import Image
import EXIF
from xml.sax.saxutils import *

# Fix day of week FIXME
HUMAN_DATE_FORMAT = "%A, %B %d, %Y"
# HUMAN_DATE_FORMAT = "%B %d, %Y"
FILE_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
ISO_FORMAT = "%Y-%m-%d"
PICT_DIR = "pictures"
PICT_PREFIX = "bld-"
PAGE_DIR = "pages"
THUMB_DIR = "thumbs"

# task codes:
# vs, hs, elev, rudder, shop, elect, wing, aileron, flap

TASK_MAP = {
		"vs" : "Vertical Stabilizer",
		"hs" : "Horizontal Stabilizer",
		"elev" : "Elevator",
		"elect" : "Electrical/Instruments",
		"fuse" : "Fuselage",
		"ail" : "Ailerons",
		"flap" : "Flaps",
		"engine" : "Engine/Firewall forward",
		"finish" : "Finish kit (Canopy/gear/trim)",
		"shop" : "Shop",
		"rudder" : "Rudder", 
		"cowl" : "Cowl and Baffles (older entries under finish Kit)",
		"canopy" : "Canopy (older entries under finish kit)",
		"" : "Not yet categorized"
	}

TASK_SEQUENCE = [
		"vs",
		"hs",
		"elev",
		"rudder",
		"Wing",
		"ail",
		"flap",
		"elect",
		"fuse",
		"finish",
		"cowl",
		"canopy",
		"engine",
		"shop",
		""
	]
		

def fixWebPath(path):
	"""Convert a path that may have windows style seperators
	   into something civilized - so that web browsers don't freak."""

	return path.replace(os.sep, '/')
		
		
		
def	taskTypeToHuman(task):
    """Return human version of a particular task type"""
    try:
            return TASK_MAP[task]
    except Exception:
            return task				# Perhaps not a short string
		

def writeElement(file, element, value):
    """Write a simple XML element to a file"""
    file.write("<%s>" % element)
    file.write(escape(value));
    file.write("</%s>\n" % element)


def genStdHeaders(out):
    """Writes web page headers"""

    out.write('''
		<script type="text/javascript"><!--
		google_ad_client = "pub-4929052395633828";
		google_ad_width = 728;
		google_ad_height = 90;
		google_ad_format = "728x90_as";
		google_ad_channel ="";
		google_ad_type = "text_image";
		google_color_border = "CCCCCC";
		google_color_bg = "FFFFFF";
		google_color_link = "000000";
		google_color_url = "666666";
		google_color_text = "333333";
		//--></script>
		<script type="text/javascript"
		  src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
		</script>
        ''')
		
            
def dateToHuman(date):
		"""Return human readable version of this date"""
		year = date / (31 * 15)
		date = date % (31 * 15)
		mon = date / 31
		day = date % 31

		sectime = time.mktime( ( year, mon, day, 0, 0, 0, 0, 0, -1 ) )
		localtime = time.localtime( sectime )
		
		return time.strftime(HUMAN_DATE_FORMAT, localtime )
				
		
def getThumbnail(pictpath):
		"""Return the name of our thumbnail"""
		
		(dir, filename) = os.path.split(pictpath[0 : pictpath.rindex('.')])
		
		if not os.path.exists(THUMB_DIR):
			os.mkdir(THUMB_DIR)
			
		return os.path.join(THUMB_DIR, filename  + "-thumb.jpg")	
		

def renameUniqueFile(filename):
		"""Rename the given filepath using our naming convention and ensure all filenames are unique
		Return the new filename"""
		
		# For now I've decided not to rename files
		return filename
		
		# How many filenames have we tried
		attempt = 0
		
		oldname = filename
		(dir, srcfile) = os.path.split(filename)
		if not srcfile.startswith(PICT_PREFIX):		# Already been processed?
			filename = None
			while not filename:
				newname = PICT_PREFIX 
			
				if attempt != 0:
					newname = newname + str(attempt) + "-"
				
				newname = newname + srcfile
			
				if os.path.exists(newname):
					attempt = attempt + 1
				else:
					filename = os.path.join(dir, newname)
					os.rename(oldname, filename)
					
		return filename
			
def	dateToPath(date):
		"""Convert a path into the URL for that page"""
		
		# yuck - nasty hack cause 2.2 python don't have Date objects
		year = date / (31 * 15)
		date = date % (31 * 15)
		mon = date / 31
		day = date % 31
				
		return os.path.join(PAGE_DIR, "day-%04u-%02u-%02u.html" % (year, mon, day))
	
	
# Ugly hack to swap endianness
endianchar = '>'
# TIFF file format
TIFF_FORMAT = lambda : "%c2HL" % endianchar

# each IFD is [ # fields, interop fields, offset to next ifd ]	
IFD_FORMAT = lambda : "%cH" % endianchar

# tag, type, count, valoffset
IFD_INTEROP = lambda : "%cHHLL" % endianchar

# offset to next id
IFD_NEXT_INTEROP = lambda : "%cL" % endianchar


					
					
							
		
EXIM_DATE_FORMAT = "%Y:%m:%d %H:%M:%S"

def getEximDate(eximdata):
		"""Extract the creation date as a struct_time object"""
		
		s = eximdata['EXIF DateTimeOriginal']

		return time.strptime(str(s), EXIM_DATE_FORMAT)
	
		
class LogRecord:
	def __init__(self):
		self.date = None
		self.pictPath = None
		self.numHours = None
		self.description = ""
		self.subsystem = ""
	

	def __cmp__(self, other):
		"""Order by date"""
		mydate = time.mktime(self.date)
		otherdate = time.mktime(other.date)
		
		return mydate - otherdate
		
	def makeRow(self):
		"""Return a CSV row to write this item"""
		return [ time.strftime(FILE_DATE_FORMAT, self.date), self.pictPath, self.numHours, self.subsystem, self.description ]
		
	def writeRecord(self, file):
		"""Return a CSV row to write this item"""
		file.write(time.strftime(FILE_DATE_FORMAT, self.date) + "~")
		file.write(self.pictPath + "~")
		file.write(str(self.numHours) + "~")
		file.write(self.subsystem + "~")
		file.write(self.description + "\n")

	def writeRecordXML(self, file):
		"""Write an XML representation of this item"""
		file.write("<Entry date=%s>\n" % quoteattr(time.strftime(FILE_DATE_FORMAT, self.date)))
                writeElement(file, "Picture", self.pictPath)
                writeElement(file, "Hours", str(self.numHours))
                writeElement(file, "Subsystem", self.subsystem)
                writeElement(file, "Description", self.description)
		file.write("</Entry>\n")
		

	def setFromXML(self, elementname, value):
                pass
            
	def readRow(self, row):
		"""Init this item based on a CSV row"""
		
		try:
			[ self.date, self.pictPath, self.numHours, self.subsystem, self.description ] = row
		except ValueError:
			raise Exception("Can't parse row: %s" % ( row ) )
		
		# Convert the file path to have the appropriate seperator
		# for whatever platform we are on
		self.pictPath = self.pictPath.replace('/', os.sep)
		self.pictPath = self.pictPath.replace('\\', os.sep)

		# fix the date (kinda yucky)
		self.date = time.strptime(self.date, FILE_DATE_FORMAT)
		self.numHours = float(self.numHours)
	
	
	def readRecord(self, line):
		"""Init this item based on a CSV row"""
		
		line = string.strip(line)
		row = string.split(line, "~")
		self.readRow(row)
		
		
		
	def importPict(self, pictpath):
		"""Import as much info as possible from a JPG picture"""
		
		self.pictPath = pictpath
		self.numHours = 0			
		self.description = ""
		self.date = time.gmtime(os.path.getmtime(pictpath))

		# Get creation date
		f = file(pictpath, "rb")
		tifftags = EXIF.process_file(f)
		f.close()
		self.date = getEximDate(tifftags)		
		
		# Create a thumbnail
		im = Image.open(pictpath)	
		im.load()						# for debugging
		im.thumbnail((256, 256))
		im.save(getThumbnail(pictpath), "JPEG")
	
	
	def getSubsystem(self):
		"""Return human readable string for the subsystem"""
		return taskTypeToHuman(self.subsystem)
			
		
	def getDescription(self):
		"""Return a human readable description"""
		
		if self.description != "":
			return self.description
		else:
			return "(No description yet)"
			
	def getUniqueKey(self):
		"""Return a unique key for this item"""
		return ( self.date, self.pictpath )
		
	def getDate(self):
		"""Return a date object for this date - i.e. drop the time info"""
		
		obj = self.date
		
		date = obj.tm_year * 31 * 15 + obj.tm_mon * 31 + obj.tm_mday
		return date
		

class LogDB:
	def __init__(self, filename):
		self.pictdict = {}			# A mapping based solely on picture path
		self.datedict = {}			# A mapping based solely by date
		self.records = []			# Our data records
		self.filename = filename
		
		try:
			f = file(filename, "r")
			self.readXML(f)
			f.close()
		except IOError, arg:
			print "Ignoring ", arg
	
	def save(self):
		"""Save to our existing filename"""

		tempname = self.filename + "~"
		f = file(tempname, "w")
		self.write(f)
		f.close()

                # If we don't already have a DB this will ignore the failure
		try:
                    tempname2 = self.filename + "~~"
                    try:
                        os.unlink(tempname2)
                    except OSError, arg:
                        pass
                    
                    os.rename(self.filename, tempname2)
                except OSError, arg:
                    print "Ignoring ", arg
                        
		os.rename(tempname, self.filename)
		
	
	def append(self, rec):
		"""Add a record"""

		self.records.append(rec)
		self.pictdict[rec.pictPath] = rec
		date = rec.getDate()
		if date in self.datedict:
			list = self.datedict[date]
			list.append(rec)
			list.sort()				# Yuck - very ineffiecent to repeatedly sort like this
		else:
			self.datedict[date] = [ rec ]
		
	def read(self, file):
		"""Read from a file"""
		
		for line in file:
			rec = LogRecord()
			rec.readRecord(line)
			self.append(rec)
			
	def readXML(self, file):
		"""Read from an XML file"""
		
		for line in file:
			rec = LogRecord()
			rec.readRecord(line)
			self.append(rec)

    	def write(self, file):
		"""Write to a file"""
		for rec in self.records:
			rec.writeRecord(file)
			
	def writeXML(self, file):
		"""Write to an XML file"""
		file.write("<!-- This file is auto generated by log-keeper.py -->\n");
		file.write("<BuildLog>\n");
		for rec in self.records:
			rec.writeRecordXML(file)
                file.write("</BuildLog>\n");
		
							
	def readCsv(self, file):
		"""Read from a CSV based file"""
		import csv
		
		reader = csv.reader(file)
		for row in reader:
			rec = LogRecord()
			rec.readRow(row)
			self.append(rec)
			
	def writeCsv(self, file):
		"""Write to a CSV based file"""
		import csv
		
		writer = csv.writer(file)
		for rec in self.records:
			writer.writerow(rec.makeRow())
			
	def addPictures(self, dirpath):
		"""Add all pictures in the indicated directory"""
		import mimetypes
		
		filenames = os.listdir(dirpath)
		for file in filenames:
			filepath = os.path.join(dirpath, file)

			if (mimetypes.guess_type(filepath)[0] == "image/jpeg") and (
							filepath not in self.pictdict) and (
							not filepath.endswith("-thumb.jpg")):

				filepath = renameUniqueFile(filepath)
				
				print "Adding record for " + filepath
				
				rec = LogRecord()
				rec.importPict(filepath)
				
				# Ugly hack - always assume at least 2 hours work if we find a picture
				if self.hoursByDate(rec.getDate()) == 0.0:
					rec.numHours = 2.0
					
				self.append(rec)
			
	def hoursByDate(self, date):
		"""Return the number of hours worked on this day"""
		
		try:
			tasks = self.datedict[date]
			
			return reduce((lambda val, task: val + task.numHours), tasks, 0)
		except KeyError:
			return 0.0				# Not found
	
		
	def getDatesByType(self, tasktype):
		"""Return a list of all dates that mention the specified subsystem"""
		matchdates = {}
		for rec in self.records:
			if rec.subsystem.lower() == tasktype.lower():
				matchdates[rec.getDate()] = True
				
		dates = matchdates.keys()
		dates.sort()
		
		return dates
		

	def genIndexPage(self):
		"""Generate the top level index page and all subpages"""
		
		if not os.path.exists(PAGE_DIR):
			os.mkdir(PAGE_DIR)
					
		# Make a filename
		filename = "index.html"
		
		# Make the page
		simpleTitle = """Kevin's build log"""
		title = '''<a href="/~kevinh">Kevin's</a> build log'''
		
		out = file(filename, "w")
		out.write("<html>\n")
				
		out.write("<head>\n")
		out.write("<title>")
		out.write(simpleTitle)
		out.write("</title>\n")
		out.write("</head>\n")
		
		out.write("<body>\n")

		genStdHeaders(out)

		out.write("<h1>")
		out.write(title)
		out.write("</h1>")
		
		# Generate the subpages
		dates = self.datedict.keys()
		dates.sort()	
					
		totalhours = 0.0
		
		# Map from date to pagepath
		dateToPage = {}
		prevPath = None
		nextPath = None
		for i in range(len(dates)):
			date = dates[i]
			if i < len(dates) - 1:
				nextPath= dateToPath(dates[i + 1])
			else:
				nextPath = None
				
			prevPath = dateToPage[date] = self.genPageForDate(date, prevPath, nextPath)
			hours = self.hoursByDate(date)
			totalhours = totalhours + hours
	
		latestdate = dates[-1]
		latestpage = dateToPage[latestdate]
		
		out.write('''<img src="thumbs/IMG_1721-thumb.jpg" alt="Picture" /><p>\n''')

		out.write('''Latest update: <a href="%s">%s</a><p>''' % 
			( fixWebPath(latestpage), dateToHuman(latestdate) ))

		out.write('''This is the actual FAA mandated build log for my RV-7A <a href="../index.html">project</a>.<p>''')
		
		out.write('''This HTML is currently rather ugly - but it is at least complete.
					I've written a little <a href="log-keeper.py">tool</a> to automatically pull
					pictures out of my camera, update a database, and generate this webpage.<p>''')

		out.write('''<b>New!</b> I now have a link for a <a href="printable.html">printable</a> version of this log.<p>''')
					
		out.write("Total hours (all subsystems): " + str(totalhours) + "<br>\n" +
			  "<i>This time estimate is of pretty poor quality, typically I work two hours on week nights and six hours on weekends.  However - I usually forget to tell my tool these details.</i>")

		# Order by date, but within each category
		for taskType in TASK_SEQUENCE:
			
			totalhours = 0.0
			
			out.write("<h2>" + taskTypeToHuman(taskType) + "</h2>")
			out.write("<ul>\n")
									
			for date in self.getDatesByType(taskType):
				pagename = dateToPage[date]
	
				out.write("<li>\n")
				out.write('<a href="%s">%s</a>' % 
					( fixWebPath(pagename), dateToHuman(date) ) )
				hours = self.hoursByDate(date)
				totalhours = totalhours + hours
					
				out.write(' ' + str(hours) + " hours ")
				out.write("</li>\n")
				
			out.write("</ul>\n")
		
			out.write("Total hours: " + str(totalhours) + "<p>\n")
		
		out.write("</body>\n")
		out.write("</html>\n")
		out.close()
		
		return filename
						
			
	def genPrintableBody(self, out, date):
		"""Generate the body for a day of work - formatted for a printer"""
		
		out.write("<hr>\n")			# page divider
		
		# Make the page
		title = "Build log date: " + dateToHuman(date)
		
		out.write("<h1>")
		out.write(title)
		out.write("</h1>")
		
		out.write("<i>" + str(self.hoursByDate(date)) + " hours</i>\n");
		
		tasks = self.datedict[date]
		out.write('<table border="1">\n')
		# Specify two columns, each equal sized
		out.write('<colgroup><col width="1*"><col width="1*">')
		colnum = 0
		for task in tasks:
			if colnum == 0:
				out.write("<tr>")

			out.write("<td>\n")
			out.write(task.getDescription())
			out.write("<p>\n")
			
			# width="80%"
			if len(task.pictPath) != 0:
				out.write('<img src="%s" alt="Picture" >' % fixWebPath(getThumbnail(task.pictPath)) )

			out.write("<br><i>" + task.getSubsystem() + " </i>\n");

			out.write("</td>\n")
			
			colnum = colnum + 1
			if colnum == 2:
				colnum = 0
				
		out.write("</table>\n")
	
		
		
					
	def genPrintablePage(self):
		"""Generate a large printable document"""
		
		if not os.path.exists(PAGE_DIR):
			os.mkdir(PAGE_DIR)
					
		# Make a filename
		filename = "printable.html"
		
		# Make the page
		title = "Kevin's build log"
		
		out = file(filename, "w")
		out.write("<html>\n")
				
		out.write("<head>\n")

		# Give hint about page breaks
		if None:
		    out.write('''<STYLE TYPE="text/css">
			     H1 {page-break-before: always}
			     </STYLE>''');

		out.write("<title>")
		out.write(title)
		out.write("</title>\n")
		out.write("</head>\n")
		
		out.write("<body>\n")
		out.write("<h1>")
		out.write(title)
		out.write("</h1>")
		
		# Generate the subpages
		dates = self.datedict.keys()
		dates.sort()	

		out.write('''This is the actual FAA mandated build log for my RV-7A <a href="../index.html">project</a>.<p>''')
									
		for date in dates:
			self.genPrintableBody(out, date)
		
		out.write("</body>\n")
		out.write("</html>\n")
		out.close()
		
		return filename
						
			
			
	def genPageForDate(self, date, prevPath, nextPath):
		"""Create the HTML file for the indicated date - return the name of the file"""
		
		# Make a filename
		filename = dateToPath(date)
		
		# Make the page
		title = "Build log date: " + dateToHuman(date)
		
		out = file(filename, "w")
		out.write("<html>\n")
		
		out.write("<head>\n")
		out.write("<title>")
		out.write(title)
		out.write("</title>\n")
		out.write("</head>\n")
		
		out.write("<body>\n")

		genStdHeaders(out)

		out.write("<h1>")
		out.write(title)
		out.write("</h1>")
		
		tasks = self.datedict[date]
		out.write("<ul>\n")
		for task in tasks:
			out.write("<li>\n")
			out.write(task.getDescription())
			out.write("<p>\n")
			
			if len(task.pictPath) != 0:
				out.write('<a href="../%s"><img src="../%s" alt="Picture" border=1 ></a>' % 
				( fixWebPath(task.pictPath), 
				  fixWebPath(getThumbnail(task.pictPath)) ))

			out.write("</li>\n")
		out.write("</ul>\n")
		
		if prevPath:
			out.write('<a href="../' + fixWebPath(prevPath) + '">Previous</a> ')

		out.write('<a href="..">Top</a> ');
		
		if nextPath:
			out.write('<a href="../' + fixWebPath(nextPath) + '">Next</a>')
		
		out.write("</body>\n")
		out.write("</html>\n")
		out.close()
		
		return filename
		
		

# Begin main

print "Buildlog update starting, v1.0"
# os.chdir("/Users/kevinh/development/log-keeper")
db = LogDB("buildlog.db")

if not os.path.exists(PICT_DIR):
	os.mkdir(PICT_DIR)
			
db.addPictures(PICT_DIR)
db.save()
db.genIndexPage()
db.genPrintablePage()

		
		
	
	


		
