import sys,os,string,math,getopt
import re,time,datetime,json
import xml.dom.minidom
from bisect import bisect

ZERO = datetime.timedelta(0)
IST_OFFSET = datetime.timedelta(seconds = 5.5 * 3600)

class Zulu(datetime.tzinfo):
    """Zulu timezone"""
    def utcoffset(self, dt):return ZERO
    def tzname(self, dt):   return "UTC"
    def dst(self, dt):      return ZERO
class IST(datetime.tzinfo):
    """Indian timezone"""
    def utcoffset(self, dt):return IST_OFFSET
    def tzname(self, dt):   return "IST"
    def dst(self, dt):      return ZERO

# excessive use of lambda, but I loves it ;)
concatelem = lambda node,elem : reduce(lambda x,y: x+y, 
				[v.nodeValue for v in ( 
					reduce(lambda x,y: x+y, 
					[v.childNodes for v in node.getElementsByTagName(elem)]))])

# date recognition pattern (stolen from PyFeed atom 1.0 code)

_pat_rfc3339 = re.compile(r"""
(\d\d\d\d)\D+(\d\d)\D+(\d\d)  # year month day, separated by non-digit
\D+  # non-digit
(\d\d?)\D+(\d\d)\D+(\d\d)  # hour minute sec, separated by non-digit
([.,]\d+)?  # optional fractional seconds (American decimal or Euro ",")
\s*  # optional whitespace
(\w+|[-+]\d\d?\D*\d\d)?  # time offset: letter(s), or +/- hours:minutes
""", re.X)

def parse_rfc3399(ts):
	m = _pat_rfc3339.search(ts)
	groups = [int(m.group(i)) for i in range(1,7)]
	(year, mon, mday, hour, mins, sec, wday, yday, isdst) = groups + [-1, -1, 0]
	tt = (year, mon, mday, hour, mins, sec, wday, yday, isdst)
	return datetime.datetime(year, mon, mday, hour, mins, sec, 0, Zulu())

def format_rfc3999(dt):
	return time.strftime("%Y-%m-%dT%H:%M:%SZ", dt.utctimetuple())

# for <cmt>24-JUN-07 12:49:13</cmt>
_pat_wptcmt = re.compile("(\d\d?)-([A-Z]{3})-(\d\d) (\d\d?):(\d\d):(\d\d)")

monmap = {"JAN" : 1, "FEB" : 2, "MAR" : 3, "APR" : 4, "MAY" : 5, 
			"JUN": 6, "JUL": 7, "AUG": 8, "SEP": 9, "OCT": 10,
			"NOV": 11, "DEC": 12};

def parse_wptcmt(ts, tz):
	m = _pat_wptcmt.match(ts)
	mday = int(m.group(1))
	mon = monmap[m.group(2)]
	year = int(m.group(3)) + 2000
	hour = int(m.group(4))
	mins = int(m.group(5))
	sec  = int(m.group(6))
	return datetime.datetime(year, mon, mday, hour, mins, sec, 0, tz)

class GPXTrackPoint:
	def __init__(self, node):
		self.lat = float(node.getAttribute("lat"))
		self.lon = float(node.getAttribute("lon"))
		self.ele = float(concatelem(node, "ele"))
		self.time = parse_rfc3399(concatelem(node, "time"))
	
	def __repr__(self):
		return """<trkpt lat="%s" lon="%s" ele="%s" time="%s">""" % (self.lat, self.lon, self.ele, self.time)
	
	def __cmp__(self, other):
		if(isinstance(other, GPXTrackPoint)):
			return cmp(self.time, other.time)
		elif(isinstance(other, GPXWayPoint)):
			return cmp(self.time, other.time)
		else:
			return -1
	
	def flatten(self):
		return {"lat" : self.lat, "lon" : self.lon, 
			"ele" : self.ele, "time": time.mktime(self.time.timetuple())}

	def toGPX(self):
		return """
		<trkpt lat="%f" lon="%f">
			<ele>%f</ele>
			<time>%s</time>
		</trkpt>
		""" % (self.lat, self.lon, self.ele, format_rfc3999(self.time))

class GPXTrackSegment:
	def __init__(self, node):
		self.points = [GPXTrackPoint(v) for v in node.getElementsByTagName("trkpt")]
		self.first =  reduce(lambda x,y: x.time < y.time and x or y, self.points)
		self.last =   reduce(lambda x,y: x.time > y.time and x or y, self.points)

		self.left =   reduce(lambda x,y: x.lon < y.lon and x or y, self.points)
		self.top =    reduce(lambda x,y: x.lat < y.lat and x or y, self.points)
		self.right =  reduce(lambda x,y: x.lon > y.lon and x or y, self.points)
		self.bottom = reduce(lambda x,y: x.lat > y.lat and x or y, self.points)
		self.wpts = []

	def merge(self, waypoints):
		for each in waypoints:
			if(each.time < self.first.time or each.time > self.last.time): continue
			self.checkmerge(each)
	
	def checkmerge(self, wpt):
		i = bisect(self.points, wpt)
		if(i < len(self.points)):
			near = self.points[i-1]
			deviation = (near.lat - wpt.lat) + (near.lon - wpt.lon)
			(sig, exponent) = math.frexp(deviation)
			if(exponent > -8): # 1/1024
				print "** Possible error ?"
			self.wpts.append(wpt)
		else:
			print "**ERROR !"

	def flatten(self):
		return { "points" : [v.flatten() for v in self.points]}
	
	def toGPX(self):
		return """<trkseg>%s</trkseg>""" % \
			(reduce(lambda x,y: x+y, [v.toGPX() for v in self.points], ""))
		
class GPXTrack:
	def __init__(self, node):
		self.segments = [GPXTrackSegment(v) for v in node.getElementsByTagName("trkseg")]
		self.name = concatelem(node, "name")
		self.first = reduce(lambda x,y: x.first.time < y.first.time and x or y, self.segments).first
		self.last = reduce(lambda x,y: x.last.time > y.last.time and x or y, self.segments).last
		# HELP ! I'm in love with lambdas !
		self.left =   reduce(lambda x,y: x.left.lon    < y.left.lon and x or y, self.segments).left
		self.top =    reduce(lambda x,y: x.top.lat     < y.top.lat and x or y, self.segments).top
		self.right =  reduce(lambda x,y: x.right.lon   > y.right.lon and x or y, self.segments).right
		self.bottom = reduce(lambda x,y: x.bottom.lat  > y.bottom.lat and x or y, self.segments).bottom
		self.wpts = []
	
	def merge(self, waypoints):
		likely = filter(lambda x: x.time >= self.first.time and x.time <= self.last.time, waypoints)
		for each in self.segments:
			each.merge(likely)
		self.wpts = reduce(lambda x,y: x+y, [v.wpts for v in self.segments], [])
	
	def flatten(self):
		return {
			"name" : self.name,
			"segments" : [v.flatten() for v in self.segments]
		}
	
	def toGPX(self):
		return """<trk>
		<name>%s</name>
		%s</trk>""" % (self.name, reduce(lambda x,y: x+y, [v.toGPX() for v in self.segments], ""))
	
	def __repr__(self):
		return """<track %s, %d segments >""" % (self.name, len(self.segments))

class GPXWayPoint:
	def __init__(self, node):
		self.lat = float(node.getAttribute("lat"))
		self.lon = float(node.getAttribute("lon"))
		self.ele = float(concatelem(node, "ele"))
		self.name = concatelem(node, "name")
		self.desc = concatelem(node, "desc")
		self.cmt = concatelem(node, "cmt")
		# somehow my garmin dump had time in the CMT 
		if(len(node.getElementsByTagName("time")) == 0):
			self.time =	parse_wptcmt(self.cmt, IST())
		else:
			self.time = parse_rfc3399(concatelem(node,"time"))

	def __repr__(self):
		return "<wpt %s>" % (self.name)

	def __cmp__(self, other):
		# .time is valid for GPXWayPoint & GPXTrackPoint
		if(isinstance(other, GPXWayPoint)):
			return cmp(self.time, other.time)
		elif(isinstance(other, GPXTrackPoint)):
			return cmp(self.time, other.time)
		else:
			return -1

	
	def toGPX(self):
		return """
		<wpt lat="%f" lon="%f">
		  <ele>%f</ele>
		  <name>%s</name>
		  <cmt>%s</cmt>
		  <desc>%s</desc>
		  <time>%s</time>
		</wpt>""" % (self.lat, self.lon, self.ele, self.name, self.cmt, self.desc, format_rfc3999(self.time))
	
	def flatten(self):
		return {"lat" : self.lat, "lon" : self.lon, 
			"ele" : self.ele, "name" : self.name,
			"name" : self.name, "desc": self.desc,
			"cmt" : self.cmt, "time": time.mktime(self.time.timetuple())}
		
class GPXFile:
	def __init__(self, file):
		self.parse(file)
	
	def parse(self, file):
		data = xml.dom.minidom.parse(file);
		self.tracks = [GPXTrack(v) for v in data.getElementsByTagName("trk")]
		self.wpts = [GPXWayPoint(v) for v in data.getElementsByTagName("wpt")]

		self.left = self.top = self.right = self.bottom = -180;
		if len(self.tracks) > 0:
			self.left =   reduce(lambda x,y: x.left.lon    < y.left.lon and x or y, self.tracks).left
			self.top =    reduce(lambda x,y: x.top.lat     < y.top.lat and x or y, self.tracks).top
			self.right =  reduce(lambda x,y: x.right.lon   > y.right.lon and x or y, self.tracks).right
			self.bottom = reduce(lambda x,y: x.bottom.lat  > y.bottom.lat and x or y, self.tracks).bottom
	
	def merge(self, wptsfile):
		for each in self.tracks:
			each.merge(wptsfile.wpts)
	
	def flatten(self):
		wpts = self.wpts +  reduce(lambda x,y: x+y, [v.wpts for v in self.tracks], [])
		data = { 
			"tracks": [v.flatten() for v in self.tracks],
			"waypoints" : [v.flatten() for v in wpts]
		}
		if(len(self.tracks) > 0):
			data["bounds"] = {"left" : self.left.lon, "top" : self.top.lat,
								"right": self.right.lon, "bottom" : self.bottom.lat};
		return data
	
	def toGPX(self):
		bounds = """<bounds minlat="%f" minlon="%f" maxlat="%f" maxlon="%f" />""" % \
				(self.top.lat, self.left.lon, self.bottom.lat, self.right.lon)

		tracks = reduce(lambda x,y: x+y, [v.toGPX() for v in self.tracks],"")
		cwpts = self.wpts +  reduce(lambda x,y: x+y, [v.wpts for v in self.tracks], [])
		wpts = reduce(lambda x,y: x+y, [v.toGPX() for v in cwpts],"")
		return """<?xml version="1.0"?>
		<gpx
		version="1.0"
		creator="gpx.py"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xmlns="http://www.topografix.com/GPX/1/0"
		xsi:schemaLocation="http://www.topografix.com/GPX/1/0 http://www.topografix.com/GPX/1/0/gpx.xsd">
		<time>%s</time>
		%s
		%s
		%s
		</gpx>""" % (format_rfc3999(datetime.datetime.now()), bounds, tracks, wpts)
		

def main(argv):
	if(len(argv) == 0): argv = ["data/cycle.gpx"]

	optlist, args = getopt.getopt(argv, 'w:o:')

	wptfile = reduce(lambda x,y: y[1], filter(lambda x: x[0] == "-w", optlist), "data/wpt.gpx")
	outfile = reduce(lambda x,y,: y[1], filter(lambda x: x[0] == "-o", optlist), "data/combined.gpx")
	tracks = GPXFile(args[0])
	waypoints = GPXFile(wptfile)

	tracks.merge(waypoints)

	fp = open(outfile, "w")
	fp.write(tracks.toGPX())
	fp.close()

if __name__ == "__main__":
	main(sys.argv[1:])
