#!/usr/bin/env python

import os, sys, re, cgi, cPickle #, string, string can be replaced by str
from cgitools import isOnline, cgi_token, exitWithInfo, decodeStr, script_path_url, unescape, color_th, color_td, style_tb, js_dir

#print   'Content-type:   text/plain   name="test_name"\n'
#print   'Content-Disposition:   inline; filename="obj.Rdata"\n\n'  

#print "Content-Type:application/octet-stream\n"
#print "Content-Disposition:attachment;filename='test_name'\n\n"


#print "Content-Disposition: attachment;filename=test_file\n"
#print "Content-Transfer-Encoding: binary\n"
#print "Accept-Ranges: bytes\n"
#print "Content-Length: %ld\n" % os.stat(fn).st_size
#print "Connection: close\n"
#print "Content-type: application/octet-stream\n"
#print "\n");  

# need a comma after "print" to prevent extra line char - '\n'
#print "Content-Type:application/x-download\n",
#print "Content-Disposition:attachment;filename=%s\n\n" % fn,
#print open(fn, 'rb').read(), # this will append a line char anyway

if not isOnline(): exitWithInfo('Logon first please.')


pih_vars = {'filename':'', 'cur_pg':0, 'rec_num':100, 'order_by':-1, 'asc_desc':'ASC', 'save_file':False, 'save_range':['all_page','cur_page'][0], 'request_page':''}
form = cgi.FieldStorage() # this should override the request_page
for k in form.keys():
	try: pih_vars[k] = eval(form.getvalue(k)) # for number value
	except: pih_vars[k] = form.getvalue(k) # for string
if pih_vars['cur_pg'] > 0: pih_vars['cur_pg'] = pih_vars['cur_pg'] - 1

request_page = os.getenv('QUERY_STRING')
if not request_page: #is None: 
	request_page = os.getenv('PATH_INFO')
	if request_page and len(request_page)>1: request_page = request_page[1:] # remove the leading slash '/'
	elif pih_vars.get('request_page', None): request_page = pih_vars['request_page']
	else: exitWithInfo('No file name offered!')

# parse pih_vars
# request_page could be request_page[?|/]:{...a_python_dict_...}  
#pih_vars = {}
if request_page:
	m = re.compile(r'^\s*(.+?)\s*:\s*(\{.+\})\s*$').match(request_page)
	if m:
		request_page = m.groups()[0]
		try:
			#pih_vars = eval(unescape(m.groups()[1]))
			pih_vars.update(eval(unescape(m.groups()[1])))
		except:
			print cgi_token
			print m.groups()[1]
			sys.exit(0)
	filename = decodeStr(request_page)
else: filename = pih_vars['filename']

if not filename or not os.path.exists(filename): exitWithInfo('The file is not found!')

dec_str = re.compile(r"[\+ \-]?\s*\d+") 
float_str = re.compile(r"[\+\-]?\s*(\d+(\.\d*)?|\d*\.\d+)([Ee][\+\-]?\d+)?") # 33.4 or 34.44e+4
def IsIt(value, what): # Check if the str is a integer
	m = what.match(value)
	if m and m.group() == value: return 1
	return 0

def stripRows(lines):
	# remove blank lines at the begining
	i = 0
	for line in lines:
		if line.strip(): break
		i = i+1
	if i>0: lines = lines[i:]
	# remove blank lines at the end
	for i in xrange(len(lines)-1, -1, -1): #range(len(lines))[::-1]
		if lines[i].strip(): break
		lines.pop()
	return lines

def readFile(fname, sep='\t', delim='"', na='NA', order_by=-1, asc_desc='ASC', output_str=False, return_str=True, use_pickle=False, use_txt=True):
	# read file and keep them in string
	# file (fname) should be a table with header line and seperated by '\t'
	# output_str will convert all numbers to string.
	# cPickle has trouble with huge files, don't use it.
	fn_head = os.path.splitext(fname)[0]
	fn_pickle = fn_head + '.pik'
	fn_cat = fn_head + '.cat'
	fn_inf = fn_head + '.inf'
	#fn_srt = fn_head + '.srt'
	fn_srt = '%s_%d_%s.srt' % (fn_head, order_by, asc_desc)
	def outStr(lines, tps):
		idx_tp = filter(lambda a:a[1]!='string', zip(range(len(tps)), tps))
		#fun_dic = {'int':str, 'float':lambda a:type(a) is float and ('%.4f' % a) or str(a)}
		def mapfun(s, res = re.compile(r'([+-]?\b\d+\.\d{4})\d+([Ee][+-]?\d+)?\b')):
			if type(s) is not float: return str(s)
			s = str(s)
			m = res.match(s)
			if not m: return s
			if m.group(2): return m.group(1)+m.group(2)
			return m.group(1)
		#fun_dic = {'int':str, 'float':mapfun}
		fun_dic = {'int':str, 'float':str}
		idx_fun = map(lambda a:(a[0], fun_dic[a[1]]), idx_tp)
		for line in lines: map(lambda a:line.__setitem__(a[0], a[1](line[a[0]])), idx_fun)
	# cPickle data can be returned directly
	if use_pickle: #os.path.exists(fn_pickle): 
		try:
			tps, head, lines, ordered_by, direction = cPickle.load(open(fn_pickle, 'rb'))
			if ordered_by != order_by or asc_desc != direction: # sort lines
				if order_by == -1: order_by = 0
				lines.sort(key=lambda a:a[order_by], reverse=asc_desc=='DESC')
				cPickle.dump((tps, head, lines, order_by, asc_desc), open(fn_pickle, 'wb'))
			if output_str: outStr(lines, tps)
			return (tps, head, lines)
		except: pass
	# check sorted file first
	if False and use_txt and os.path.exists(fn_inf) and os.path.exists(fn_srt):
		ordered_by, direction = eval(open(fn_inf).read().strip())
		if ordered_by == order_by and direction == asc_desc:
			fp = open(fn_srt)
			head = fp.readline()
			lines = fp.readlines()
			fp.close()
			return None, head, lines
	if order_by >= 0 and use_txt and os.path.exists(fn_srt):
		idx = map(int, open(fn_srt))
		lines = open(fname).readlines()
		head = lines.pop(0)
		lines = map(lambda a:lines[a], idx)
		return None, head, lines

	lines = open(fname, 'rt').readlines()
	lines = stripRows(lines)
	if len(lines)<1: return None, None, None
	head = lines.pop(0)
	if not lines: return None, head, None

	# return a list of str
	if return_str: # only convert the column for order_by to numeric if needed, should be faster and use less memory
		dif = len(lines[0].split(sep)) - len(head.split(sep))
		if dif == 1: head = 'RowName\t' + head
		elif dif != 0: return None, None, None
		if order_by < 0: return None, head, lines
		# get order_by column
		def getOrdCol(s, i=order_by, sep=sep, delim=delim):
			s = s.split(sep)[i]
			if delim and len(s)>1 and delim==s[0]==s[-1]: s = s[1:-1]
			return s
		ord_col = map(getOrdCol, lines)
		# find type
		if os.path.exists(fn_cat):
			tp_dic = {'integer':'int', 'double':'float', 'int':'int', 'float':'float'}
			tp = map(lambda a:tp_dic.get(a.strip(), 'string'), open(fn_cat))[order_by]
		else:
			tpstrs = [dec_str, float_str]
			tps = ['int', 'float']
			tpstr = tpstrs.pop(0)
			tp = tps.pop(0)
			for val in ord_col:
				if IsIt(val, tpstr): continue
				if not tps: 
					tp = 'string'
					break
				tp = tps.pop(0)
				tpstr = tpstrs.pop(0)
				if IsIt(val, tpstr): continue
				tp = 'string'
				break
		if tp != 'string':
			fun = eval(tp)
			ord_col = map(lambda a:a==na and a or fun(a), ord_col)
		# sort it
		ord_col = zip(ord_col, xrange(len(ord_col))) #map(None, enumerate(ord_col))
		ord_col.sort(reverse=asc_desc=='DESC') #, key=lambda a:a[1])
		ord_col = map(lambda a:a[1], ord_col) # map(lambda a:a[0], ord_col)
		open(fn_srt, 'wt').write('\n'.join(map(str, ord_col)))
		lines = map(lambda a:lines[a], ord_col)
		return None, head, lines

	# return a list of lists
	lines = map(lambda a:a.split(sep), lines)
	head = map(str.strip, head.split(sep)) #normNames(lines[0])
	ln_h = len(head)
	tps = [None] * ln_h # --[None, int(long), float(double), string]-->
	idx = range(ln_h)
	# remove delim for head
	for i in idx: 
		value = head[i]
		if delim and len(value)>1 and delim==value[0]==value[-1]:
			head[i] = value[1:-1]
	#lines = lines[1:]
	# add a name for the first column (row.name) for some table saved by R
	if lines: 
		ln_d = len(lines[0])
		dif = ln_d - ln_h
		if dif == 1: 
			head.insert(0, 'RowName')
			tps.insert(0, None)
			idx = range(ln_d)
		elif dif != 0: # file is not in correct format
			return (None, None, None)
	# determine types of the columns, and convert data
	if os.path.exists(fn_cat): # read type from '.cat' file
		tp_dic = {'integer':'int', 'double':'float', 'int':'int', 'float':'float'}
		# types are 'int', 'float', 'string'
		tps = map(lambda a:tp_dic.get(a.strip(), 'string'), open(fn_cat)) 
		# find positions and types of columns where the types is not 'string'
		idx_tp = filter(lambda a:a[1]!='string', zip(range(len(tps)), tps))
		idx_fun = map(lambda a:(a[0], eval(a[1])), idx_tp)
		for values in lines:
			values[-1] = values[-1].strip() # remove '\n'
			for i, fun in idx_fun: 
				value = values[i].strip()
				if value and value != na: values[i] = fun(value)
	else: # check type from data file
		for values in lines:
			for i in idx:
				value = values[i] = values[i].strip()
				if not value: continue
				if delim and len(value)>1 and delim==value[0]==value[-1]:
					value = values[i] = value[1:-1]
				if value == na:
					#values[i] = '\\N'
					continue
				tp = tps[i]
				#tp_idx = tporder.index(tp)
				#if tp_idx == idx_string: continue
				#if tp_idx == idx_none:
				#else: start_idx = tp_idx
				if tp == 'string': continue
				if tp == None or tp == 'int':
					if IsIt(value, dec_str): 
						values[i] = int(value)
						if not tp: tps[i] = 'int'
					elif IsIt(value, float_str):
						values[i] = float(value)
						tps[i] = 'float'
					else: tps[i] = 'string'
				else: # must be tp == 'float':
					if IsIt(value, float_str): 
						#pass 
						values[i] = float(value)
					else: tps[i] = 'string'
		open(fn_cat, 'w').write('\n'.join(tps))
	if order_by != -1: 
		if use_txt:
			map(lambda a:a[1].append(a[0]), enumerate(lines))
			lines.sort(key=lambda a:a[order_by], reverse=asc_desc=='DESC')
			idx = map(lambda a:str(a.pop()), lines)
			open(fn_srt, 'wt').write('\n'.join(idx))
		else:
			lines.sort(key=lambda a:a[order_by], reverse=asc_desc=='DESC')
	if output_str: outStr(lines, tps)
	if use_pickle: cPickle.dump((tps, head, lines, order_by, asc_desc), open(fn_pickle, 'wb'))
	if False and use_txt:
		#open(fn_inf, 'wt').write(repr((order_by, asc_desc)))
		fp = open(fn_srt, 'wt')
		fp.write('\t'.join(head)+'\n')
		if output_str: 
			fp.write('\n'.join(map(lambda a:'\t'.join(a), lines)))
		else:
			fp.write('\n'.join(map(lambda a:'\t'.join(map(str, a)), lines)))
			#for line in lines: fp.write('\n' + '\t'.join(map(str, line)))
		fp.close()
	return (tps, head, lines)
	
#import t0=time.time(); 
#tps, title, lines = readFile('rlt.txt', order_by=17, asc_desc='ASC', output_str=True); t1=time.time(); print t1-t0

cur_pg, rec_num = pih_vars.get('cur_pg',0), pih_vars.get('rec_num', 1000)
order_by, asc_desc = pih_vars.get('order_by', -1), pih_vars.get('asc_desc', 'ASC')
save_file, save_range = pih_vars.get('save_file', False), pih_vars.get('save_range', 'all_page')

if save_file:
	re_fn = re.compile(r'\s')
	raw_name = re_fn.sub('_', os.path.basename(filename))
	print "Content-Type:application/x-download\n",
	if order_by == -1: # use original file
		if save_range == 'all_page':
			print "Content-Disposition:attachment;filename=%s\n\n" % raw_name,
			sys.stdout.write(open(filename, 'rb').read())
		else: # should be save_range: current page (cur_pg)
			print "Content-Disposition:attachment;filename=%s\n\n" % ('_Page'+str(cur_pg+1)).join(os.path.splitext(raw_name)),
			lines = open(filename).readlines()
			lines = stripRows(lines)
			sys.stdout.write(lines[0])
			lines =  lines[(cur_pg*rec_num+1):((cur_pg+1)*rec_num+1)]
			sys.stdout.write(''.join(lines))
	else: # use ordered file
		tps, head, lines = readFile(filename, order_by=order_by, asc_desc=asc_desc, output_str=True)
		if tps is None: order_col =  head.split('\t')[order_by]
		else: order_col = head[order_by]
		order_col = re_fn.sub('_', order_col)
		raw_name = ('_orderBy_%s_%s' % (order_col, asc_desc)).join(os.path.splitext(raw_name))
		if save_range == 'all_page':
			print "Content-Disposition:attachment;filename=%s\n\n" % raw_name,
		else:
			lines =  lines[(cur_pg*rec_num):((cur_pg+1)*rec_num)]
			raw_name = ('_Page'+str(cur_pg+1)).join(os.path.splitext(raw_name))
			print "Content-Disposition:attachment;filename=%s\n\n" % raw_name,
		if tps is not None:
			sys.stdout.write('\t'.join(head))
			#for line in lines: sys.stdout.write('\n' + '\t'.join(map(str, line)))
			for line in lines: sys.stdout.write('\n' + '\t'.join(line))
		else: 
			sys.stdout.write(head)
			for line in lines: sys.stdout.write(line)
	sys.exit(0)

# Now to present rows on a web page
if order_by == -1: # use original file
	lines = open(filename).readlines()
	lines = stripRows(lines)
	n = len(lines)
	if not n: exitWithInfo('Empty file!')
	n = n-1
	title = lines[0]
	title = title.replace('\r','').replace('\n','')
	lines.pop(0)
	tps = None
else: # use ordered file
	tps, title, lines = readFile(filename, order_by=order_by, asc_desc=asc_desc, output_str=True)
	if title is None: exitWithInfo('Empty file!')
	n = len(lines)

pg_num = n/rec_num + ( (n % rec_num) and 1 or 0 )
at_beginning = cur_pg<=0 and True or False
if at_beginning: cur_pg = 0
reach_end = ((cur_pg+1)*rec_num)>=n and True or False
if reach_end: cur_pg = pg_num - 1
lines =  lines[(cur_pg*rec_num):((cur_pg+1)*rec_num)]

def repls(obj, sdic={'\n':'', '\t':'</td><td>'}):
	#return (obj.group() not in sdic) and obj.group(4)+obj.group(5) or sdic[obj.group()]
	if obj.group() == '\n': return ''
	if obj.group() == '\t': return '</td><td>'
	if obj.group(5): return obj.group(4)+obj.group(5)
	return obj.group(4)
res = re.compile(r'(\n)|(\t)|(([+-]?\b\d+\.\d{4})\d+([Ee][+-]?\d+)?\b)')  # res.sub(repls, s)

def getOptions(opts, sel=''): # each opt is tuple: (value, display name)
	if type(sel) == type(0): sel = opts[sel][0]
	return ''.join(map(lambda i:'<option value="' + opts[i][0] + '" ' + ((opts[i][0]==sel or (i==0 and sel=='')) and ' selected>' or '>') + opts[i][1] + '</option>', range(len(opts))) )

direction_opts = [('ASC', 'ascending'), ('DESC', 'decending')]
if type(title) is str: title_list = title.split('\t')
else: title_list = title
n_cols = len(title_list)
order_opts = zip(map(str, range(len(title_list))), title_list)
order_opts.insert(0, ('-1', ''))
pgstr = []
if not (at_beginning and reach_end): 
	pgstr.append('''<font size=-1><input type='button' value='save this page' onClick="document.vals.save_file.value='True';document.vals.save_range.value='cur_page';document.vals.submit()">''')
	pgstr.append('''<input type='button' value='save all' onClick="document.vals.save_file.value='True';document.vals.save_range.value='all_page';document.vals.submit()">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;''')
	if not at_beginning: 
		pgstr.append("""<input type='button' value='|<' onClick="document.vals.save_file.value='False';goPage(0)">""" )
		pgstr.append("""<input type='button' value='<' onClick="document.vals.save_file.value='False';goPage(%d)">""" % (cur_pg-1+1) )
	if not reach_end:
		pgstr.append("""<input type='button' value='>' onClick="document.vals.save_file.value='False';goPage(%d)">""" % (cur_pg+1+1) )
		pgstr.append("""<input type='button' value='>|' onClick="document.vals.save_file.value='False';goPage(%d)">""" % (n/rec_num - ((not (n % rec_num)) and 1 or 0) + 1 ) )
	pgstr.append('''&nbsp;&nbsp;<input type='button' value='Go to' onClick="document.vals.save_file.value='False';document.vals.submit()"> <input type='text' name='text_cur_pg' size=3 value=%d onChange='javascript:syncTextTo(this, "cur_pg")'> of %d pages''' % (cur_pg+1, pg_num))
	pgstr.append('''<input type="button" value="Show" onClick="document.vals.save_file.value='False';goPage(0, this)"><input type="text" size=3 name="text_rec_num" value=%d onChange="javascript:syncTextTo(this, 'rec_num')">records by<select name="sel_asc_desc" onChange="javascript:syncSelTo(this, 'asc_desc')">%s</select><select name="sel_order_by" onChange="javascript:syncSelTo(this, 'order_by')">%s</select>&nbsp;&nbsp&nbsp;&nbsp;&nbsp;&nbsp;''' % (rec_num, getOptions(direction_opts, sel=asc_desc), getOptions(order_opts, sel=str(order_by))) )
	pgstr.append('</font>')
	pgstr = '&nbsp;&nbsp;'.join(pgstr)

print cgi_token

print '<html><head>%s\n' % style_tb

# print javascript
print '''
<script type="text/javascript" src=%s/dbs_show_tb.js> </script>
''' % js_dir


print '</head><body bgcolor="white"><form name=vals method="post" action="%s/gridFile" enctype="multipart/form-data">\n' % script_path_url

print '''
<input type='hidden' name=request_page value='"%s"'>
<input type='hidden' name=cur_pg id='cur_pg_id' value='%d'>
<input type='hidden' name=rec_num value='%d'>
<input type='hidden' name=order_by value='%d'>
<input type='hidden' name=asc_desc value='"%s"'>
<input type='hidden' name=save_file value='%s'>
<input type='hidden' name=save_range value='"%s"'>
''' % (request_page, cur_pg+1, rec_num, order_by, asc_desc, save_file, save_range)

print '<table class="mytb" cellspacing="0" cellpadding="5">\n'

if pgstr: print '<tr><td colspan=%d>%s</td></tr>' % (n_cols, pgstr)

if tps is None: #order_by == -1:
	print '<tr bgcolor="%s"><th>%s</th></tr>\n' % (color_th, title.replace('\n','').replace('\t', '</th><th>'))
	#print '<tr><th>%s</th></tr>\n' % title.replace('\n','').replace('\t', '</th><th>')
	
	#for line in file(filename):
	#	print '<tr><td>%s</td></tr>\n' % line.replace('\n','').replace('\t', '</td><td>')
	#map(lambda a:sys.stdout.write('<tr><td>%s</td></tr>\n' % a.replace('\n','').replace('\t', '</td><td>')), open(filename).readlines())
	#map(lambda a:sys.stdout.write('<tr><td>%s</td></tr>\n' % a.replace('\n','').replace('\t', '</td><td>')), lines)
	#map(lambda a:sys.stdout.write('<tr%s><td>%s</td></tr>\n' % ( (a % 2) and ' bgcolor="#F7F7F7"' or '', lines[a].replace('\n','').replace('\t', '</td><td>')) ), range(len(lines)))
	#map(lambda a:sys.stdout.write('<tr%s><td>%s</td></tr>\n' % ( (a % 2) and ' bgcolor="#d2dbe7"' or 'bgcolor="white"', res.sub(repls, lines[a]) ) ), range(len(lines)))
	map(lambda a:sys.stdout.write('<tr%s><td>%s</td></tr>\n' % ( (a % 2) and ' bgcolor="%s"' % color_td or ' bgcolor="white"', res.sub(repls, lines[a]) ) ), range(len(lines)))
else:
	print '<tr bgcolor="%s"><th>%s</th></tr>\n' % (color_th, '</th><th>'.join(title))
	#map(lambda a:sys.stdout.write('<tr%s><td>%s</td></tr>\n' % ( (a % 2) and ' bgcolor="%s"' % color_td or ' bgcolor="white"', '</td><td>'.join(map(str, lines[a])) ) ), range(len(lines)))
	#map(lambda a:sys.stdout.write('<tr%s><td>%s</td></tr>\n' % ( (a % 2) and ' bgcolor="%s"' % color_td or ' bgcolor="white"', '</td><td>'.join(lines[a]) ) ), range(len(lines)))
	map(lambda a:sys.stdout.write('<tr%s><td>%s</td></tr>\n' % ( (a % 2) and ' bgcolor="%s"' % color_td or ' bgcolor="white"', res.sub(repls, '</td><td>'.join(lines[a])) ) ), range(len(lines)))

if pgstr: print '<tr><td colspan=%d>%s</td></tr>' % (n_cols, pgstr)

print '</table>'

print '</form></body>'
print '</html>'
