import json
import time
import csv

class Table:

	def __init__(self, name, desc=''):
		self.name = name
		self.desc = desc
		self.colnames = []
		self.rows = []
		self.stats = {}
		self.rgb = ''
		self.r1 = 10000
		self.r2 = 100000
		self.r3 = 1000000
		self.r4 = 10000000
		self.r5 = 100000000
		self.display_colnames = {}

		self.stats ['column-count'] = 0
		self.stats ['row-count'] = 0
		self.stats ['table-load-time-ms'] = 0

	def clone (self, colnames=False):
		ret_table = Table (self.name, self.desc)
		ret_table.rgb = self.rgb
		ret_table.r1 = self.r1
		ret_table.r2 = self.r2
		ret_table.r4 = self.r3
		ret_table.r4 = self.r4
		ret_table.r5 = self.r5
		if colnames:
			for cn in self.colnames:
				ret_table.add_column (cn)
			for dcn in self.display_colnames.keys():
				ret_table.display_colnames[dcn] = self.display_colnames[dcn]
		return ret_table

	def get_row (self, index=0):
		return_row = {}
		if len(self.rows) > index:
			row = self.rows[index]
			c_count = 0
			for c in self.colnames:
				return_row[c] = row[c_count]
				c_count += 1
		return return_row

	def set_amount_color_scheme (self, rgb, r1, r2, r3, r4, r5):
		return ret_table
		
	def add_column_with_name (self, colname):
		if colname in self.colnames:
			raise Exception ('Column [' + colname + '] already exists or was autogenerated')
		self.colnames.append (colname)
		self.stats ['column-count'] += 1
	
	def add_column (self, colname=''):
		if colname=='':
			num_existing_columns = len(self.colnames)
			colname = 'Column_' + str(num_existing_columns + 1)
		self.add_column_with_name (colname)
	
	def add_row (self, values=[]):
		while len(self.colnames) < len(values):
			self.add_column ()
		self.rows.append (values)
		self.stats ['row-count'] += 1
	
	def filter_greater_than (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if r[found_idx] > value:
				ret_tab.add_row (r)
		return ret_tab

	def filter_greater_than_or_equals (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if r[found_idx] >= value:
				ret_tab.add_row (r)
		return ret_tab

	def filter_less_than (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if r[found_idx] < value:
				ret_tab.add_row (r)
		return ret_tab

	def filter_less_than_or_equals (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if r[found_idx] <= value:
				ret_tab.add_row (r)
		return ret_tab

	def filter_equals (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if r[found_idx] == value:
				ret_tab.add_row (r)
		return ret_tab

	def filter_not_equals (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if r[found_idx] != value:
				ret_tab.add_row (r)
		return ret_tab

	def filter_in (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if r[found_idx] in value:
				ret_tab.add_row (r)
		return ret_tab

	def filter_not_in (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if r[found_idx] not in value:
				ret_tab.add_row (r)
		return ret_tab

	def filter_starts_with (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if str(r[found_idx]).startswith(value):
				ret_tab.add_row (r)
		return ret_tab

	def filter_ends_with (self, col, value):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (col)
		if found_idx == -1:
			return self
		for r in self.rows:
			if str(r[found_idx]).endswith(value):
				ret_tab.add_row (r)
		return ret_tab

	def sort_asc (self, colname):
		ret_tab = self.clone (True)
		found_idx = self.get_column_index (colname)
		if found_idx == -1:
			return self
		for r in self.rows:
			ret_tab.add_row (r)
		ret_tab.rows.sort(key=lambda x:x[found_idx])
		return ret_tab

	def sort_desc (self, colname):
		ret_tab = self.sort_asc (colname)
		ret_tab.rows.reverse ()
		return ret_tab

	def load_from_delimited_file (self, fname, d=',', includesHeader=True):
		startms = int(round(time.time() * 1000))
		with open (fname, 'r') as csvfile:
			csvreader = csv.reader (csvfile, delimiter=d)
			if includesHeader:
				for cn in next (csvreader):
					self.add_column (cn)
			for row in csvreader:
				if (len(row) == len(self.colnames)):
					self.add_row (row)
		endms = int(round(time.time() * 1000))
		self.stats ['table-load-time-ms'] += (endms - startms)

	def toJSON (self):
		return json.dumps (self, default=lambda o: o.__dict__, sort_keys=True, indent=4)

	def __str__ (self):
		return self.toJSON ()

	def get_column_index (self, colname=''):
		col_index = 0
		for c in self.colnames:
			if c == colname:
				return col_index
			col_index += 1
		return -1

	def get_grouping_method_and_column (self, s_inp):
		# Example input = '  MAX ( Column 4  ) '
		s = s_inp.replace (')', '(')
		sparts = s.split ('(')
		if (len(sparts) != 3):
			raise Exception ('Found unexpected tokens in grouped column [' + s_inp + ']' + str(len(sparts)))
		return sparts[0].strip(), sparts[1].strip()

	def do_operation (self, op, vector):
		startms = int(round(time.time() * 1000))
		rval = None
		if (len(vector) == 0):
			return None
		elif (op.lower() == 'count'):
			rval = len(vector)
		elif (op.lower() == 'min'):
			rval = vector[0]
			for v in vector:
				if v is None:
					return v
				if v < rval:
					rval = v
		elif (op.lower() == 'max'):
			rval = vector[0]
			for v in vector:
				if v is not None and v > rval:
					rval = v
		elif (op.lower() == 'sum'):
			rval = 0
			for v in vector:
				if v is not None:
					rval += v
		else:
			raise Exception ('Unknown grouping function [' + op + ']')
		endms = int(round(time.time() * 1000))
		stacode = 'table-op-' + op.lower () + '-time-ms'
		if not stacode in self.stats.keys ():
			self.stats [stacode] = 0
		self.stats [stacode] += (endms - startms)
		return rval
	
	def group_by (self, group_by, select_columns):

		exception_rows = []
		result_key     = {}
		result_val     = {}
		group_by_cols  = []
		select_cols    = []

		for c in group_by.split (','):
			group_by_cols.append (c.strip())
		for c in group_by_cols:
			if c not in self.colnames:
				raise Exception ('Group by column [' + c + '] not found')

		for c in select_columns.split (','):
			select_cols.append (c.strip())
		for s in select_cols:
			m,c = self.get_grouping_method_and_column (s)
			if c not in self.colnames:
				raise Exception ('Select column [' + c + '] not found')
			if c in group_by_cols:
				raise Exception ('Column [' + c + '] part of Select as well as Group By column list. It can be in either but not both.')

		for r in self.rows:
			keys = []
			key = ''
			for g in group_by_cols:
				gindex = self.get_column_index (g)
				val_at_gindex = None
				if gindex < len(r):
					val_at_gindex = r[gindex]
				keys.append (val_at_gindex)
				key += str(val_at_gindex).strip()
			if key in result_val.keys():
				val = result_val.get (key)
			else:
				val = []
				for s in select_cols:
					newarr = []
					val.append (newarr)

			sindex = 0
			for s in select_cols:
				m,c = self.get_grouping_method_and_column (s)
				gindex = self.get_column_index (c)
				val[sindex].append (r[gindex])
				sindex += 1
				
			result_key [key] = keys
			result_val [key] = val

		for key in result_val.keys():
			vectors = result_val.get (key)
			vectorindex = 0
			for s in select_cols:
				m,c = self.get_grouping_method_and_column (s)
				op_result = self.do_operation (m, vectors[vectorindex])
				vectors[vectorindex] = op_result
				vectorindex += 1
				
		return result_key, result_val, exception_rows

	def shape_shift (self, cols):
		colindices = []
		for c in cols:
			colindices.append (self.get_column_index(c))
		ret_table = self.clone ()
		for cidx in colindices:
			ret_table.add_column (self.colnames[cidx])
			if (len(self.display_colnames) > 0):
				ret_table.display_colnames[self.colnames[cidx]] = self.display_colnames[self.colnames[cidx]]
		for r in self.rows:
			rearranged = []
			for cidx in colindices:
				rearranged.append (r[cidx])
			ret_table.add_row (rearranged)
		return ret_table

	def maxlen_col (self, c):
		c_idx = self.get_column_index(c)
		vlen = 0
		for r in self.rows:
			if len(str(r[c_idx])) > vlen:
				vlen = en(str(r[c_idx]))
		return vlen

	def maxlen_cols (self, cols):
		vlens = []
		for c in cols:
			c_idx = self.get_column_index(c)
			vlen = len (self.colnames[c_idx])
			for r in self.rows:
				if len(str(r[c_idx])) > vlen:
					vlen = len(str(r[c_idx]))
			vlens.append (vlen)
		return vlens

	def set_amount_color_scheme (self, rgb, r1, r2, r3, r4, r5):
		self.rgb = rgb
		self.r1 = r1
		self.r2 = r2
		self.r3 = r3
		self.r4 = r4
		self.r5 = r5

	def get_stylecolor (self, t_amt):
		stylestring=''
		fontpart=''
		if self.rgb =='R':
			if t_amt > self.r1:
				stylestring='bgcolor="#ffeef6"'
			if t_amt > self.r2:
				stylestring='bgcolor="#ffdde6"'
			if t_amt > self.r3:
				stylestring='bgcolor="#ffbbc6"'
			if t_amt > self.r4:
				stylestring='bgcolor="#ff7786"'
			if t_amt > self.r5:
				stylestring='bgcolor="#550022"'
				fontpart='color="#fff"'
		if self.rgb =='G':
			if t_amt > self.r1:
				stylestring='bgcolor="#eeffee"'
			if t_amt > self.r2:
				stylestring='bgcolor="#aaffaa"'
			if t_amt > self.r3:
				stylestring='bgcolor="#33aa33"'
			if t_amt > self.r4:
				stylestring='bgcolor="#229922"'
			if t_amt > self.r5:
				stylestring='bgcolor="#005500"'
				fontpart='color="#fff"'
		if self.rgb =='B':
			if t_amt > self.r1:
				stylestring='bgcolor="#eeeeff"'
			if t_amt > self.r2:
				stylestring='bgcolor="#ddddff"'
			if t_amt > self.r3:
				stylestring='bgcolor="#bbbbff"'
			if t_amt > self.r4:
				stylestring='bgcolor="#7777ff"'
			if t_amt > self.r5:
				stylestring='bgcolor="#000066"'
				fontpart='color="#fff"'
		return stylestring, fontpart
		
	def to_string (self, delim_char, hidecols, urlmap, htmlprefixmap, htmldeco, showrows=0):
		plines_txt  = []
		plines_html = []
		tableclass  = 'sheetcalc_table' if 'table' not in htmldeco.keys() else htmldeco['table']
		trclass     = 'sheetcalc_tr'    if 'tr'    not in htmldeco.keys() else htmldeco['tr']
		thclass     = 'sheetcalc_th'    if 'th'    not in htmldeco.keys() else htmldeco['th']
		tdclass     = 'sheetcalc_td'    if 'td'    not in htmldeco.keys() else htmldeco['td']
		plines_html.append('<table class="' + tableclass + '">')
		col_lens = self.maxlen_cols (self.colnames)
		sarr = []
		plines_html.append(' <tr class="' + trclass + '">')
		for c_idx in range (0, len(col_lens)):
			if self.colnames[c_idx] not in hidecols:
				sarr.append (self.colnames[c_idx].ljust(col_lens[c_idx]))
				thc = '  <th class="' + thclass + '">'
				if self.colnames[c_idx] in self.display_colnames.keys():
					plines_html.append (thc + self.display_colnames[self.colnames[c_idx]] + '</th>')
				else:
					plines_html.append (thc + (self.colnames[c_idx]).replace('__c','').replace('.',' ').replace('_',' ') + '</th>')
		plines_html.append(' </tr>')
		s = delim_char.join (sarr)
		hr = '-' * (len(s) + 2)
		plines_txt.append (hr)
		plines_txt.append (delim_char + s + delim_char)
		plines_txt.append (hr)
		if (len(self.rows) == 0):
			return [], []
		numrows_added = 0
		for r in self.rows:
			if showrows == 0 or numrows_added < showrows:
				sarr = []
				plines_html.append(' <tr class="' + trclass + '">')
				for c_idx in range (0, len(col_lens)):
					part_txt = str(r[c_idx]).ljust(col_lens[c_idx])
					fmt = ''
					fontpart = ''
					if isinstance(r[c_idx], float) and 'amount' in self.colnames[c_idx].lower():
						part_txt = '${:,.0f}'.format(r[c_idx]).rjust(col_lens[c_idx])
						fmt, fontpart = self.get_stylecolor (r[c_idx])
					if self.colnames[c_idx] not in hidecols:
						sarr.append (part_txt)
					if self.colnames[c_idx] in urlmap.keys():
						part_txt1 = '<a href="'
						part_txt1 += htmlprefixmap[self.colnames[c_idx]]
						val_idx = self.get_column_index(urlmap[self.colnames[c_idx]])
						part_txt1 += str(r[val_idx]) + '">'
						part_txt1 += part_txt.strip()
						part_txt1 += '</a>'
						part_txt = part_txt1
					if self.colnames[c_idx] not in hidecols:
						fline = '  <td ' + fmt + ' class="' + tdclass + '">'
						fline += '<font ' + fontpart + '>' + part_txt.strip() + ('</font></td>')
						plines_html.append ('  ' + fline)
				s = delim_char.join (sarr)
				plines_txt.append (delim_char + s + delim_char)
				plines_html.append(' </tr>')
				numrows_added += 1
		plines_txt.append (hr)
		plines_html.append('</table>')
		return plines_txt, plines_html


	def get_total (self, c, limit_rows=0):
		c_idx = self.get_column_index(c)
		s_included = 0
		s_total = 0
		if c_idx != -1:
			for r in self.rows:
				if limit_rows > 0:
					limit_rows -= 1
					s_included += r[c_idx]
				s_total += r[c_idx]
		return s_included, s_total
