#!/usr/bin/python
import serial
import serial.tools.list_ports as list_ports
import argparse, datetime, os, re, socket, stat, sys, uuid
from textwrap import wrap
from subprocess import check_output, CalledProcessError
from time import sleep
from unidecode import unidecode
import string
import codecs
from binascii import hexlify

try:
	from nltk.corpus import wordnet
	wordnetAvailable = True
except:
	wordnetAvailable = False
try:
	from aspell import Speller
	spellerAvailable = True
except:
	spellerAvailable = False

try:
	import dropbox
	dropboxAvailable = True
except:
	dropboxAvailable = False

defaultPort = '/dev/ttyUSB0'
wifi = '/usr/local/bin/wifi'
batteryInfo = '/usr/local/bin/battery.sh'
elinks = '/usr/bin/elinks'
home = os.path.expanduser('~')
dropboxPath = os.path.join(home, 'dropbox/')
trashPath = os.path.join(home, 'trash/')
dropboxTokenFile = os.path.join(home, 'etc/dropboxToken')
dumpFile = os.path.join(home, 'logs/dump.txt')
backspace = '\b \b'
printable = string.digits + string.letters + string.punctuation + ' '

#settings for specific models
#viscosity is in thousandths of a second
configurations = {
	'100': {'height': 7, 'width': 40, 'viscosity': 16, 'eol': "\r\n"},
	'200': {'height': 15, 'width': 40, 'viscosity': 0, 'eol': "\r\n"},
	'replica': {'height': 11, 'width': 35, 'viscosity': 15, 'eol': "\r"},
}

default = {}
default['height'] = min(configurations[configuration]['height'] for configuration in configurations)
default['width'] = 40
default['viscosity'] = max(configurations[configuration]['viscosity'] for configuration in configurations)
default['eol'] = "\r\n"
configurations['default'] = default

#ntlk
positions = {'v': 'VERB', 'a': 'ADJ.', 's': 'ADJ.', 'n': 'NOUN', 'r': 'ADV.'}

#BASIC setup code
setupCodeBASIC = """10 MAXFILES=2
20 OPEN "COM:<COMMCODE>" FOR INPUT AS 1
30 OPEN "COM:<COMMCODE>" FOR OUTPUT AS 2
40 PRINT # 2, "SETUP time"
50 INPUT # 1, A$, T$, B$
60 TIME$ = T$
70 PRINT #2, "SETUP date"
80 INPUT #1, A$, D$, B$
90 DATE$ = D$
100 PRINT #2, "SETUP day"
110 INPUT #1, A$, W$, B$
120 DAY$ = W$
130 PRINT "TIME:", T$
140 PRINT "DATE:", D$
150 PRINT "DAY:", W$
160 CLOSE 1,2"""

class OldSchoolSerial(serial.Serial):
	def __init__(self, *args, **kwargs):
		kwargs['port'] = self.getPort()
		super(OldSchoolSerial, self).__init__(*args, **kwargs)
		self.configure('default')
		self.recentLinks = []
		self.online = True
		self.dropboxToken = None
		self.codeCount = 0

		self.commands = {
			'MODEL': (self.hello, ['introduce yourself', 'model number (' + self.humanReadableList(configurations.keys()) + ')']),
			'THROTTLE': (self.throttle, ['slow downloads', '[thousandths-of-seconds]']),
			'SETUP': (self.setup, ['show setup information', '[type: stat, time, date, day, code]']),
			'DUMP': (self.dump, ['dump all input to dump.txt', 'minutes']),

			'LIST': (self.list, ['list available files', '[details]']),
			'UPLOAD': (self.upload, ['transfer file to server', 'name-for-file']),
			'REPLACE': (self.replace, ['transfer file to server', 'name-of-file']),
			'APPEND': (self.append, ['append file to file on server', 'name-of-file']),
			'DOWNLOAD': (self.download, ['transfer file from server', 'name-of-file']),
			'KILL': (self.kill, ['delete file on server', 'name-of-file']),
			'RESTORE': (self.restore, ['restore deleted file', '[name-of-file]']),
			'VIEW': (self.view, ['view file on server', 'name-of-file']),

			'WEB': (self.web, ['display web page text', '[link-number or search-text]']),
			'WIKI': (self.wiki, ['look up term on Wikipedia', '[term-to-look-up]']),

			'HELP': (self.help, ['provide help', '[command name]'])
		}

		if dropboxAvailable:
			self.commands['PULL'] = (self.pull, ['list or get Dropbox files', '[name-of-file]'])
			self.commands['PUSH'] = (self.push, ['push file to Dropbox', 'name-of-file'])

		if spellerAvailable:
			self.commands['SPELL'] = (self.spell, ['correct spelling', '[word-to-spell]'])

		if os.path.exists(batteryInfo):
			self.commands['BATTERY'] = (self.battery, ['show battery status'])
		if os.path.exists(wifi):
			self.commands['WIFI'] = (self.wifi, ['list networks in range', '[connect|disconnect]'])

		# initialize dictionary
		if wordnetAvailable:
			wordnet.synsets('awesome', None)
			self.commands['DEFINE'] = (self.define, ['look up word', '[word-to-define]'])

	#determine which port to use
	def getPort(self):
		goodPort = None
		#prefer the first digikey port
		ports = list_ports.comports()
		ports.sort()
		for port in ports:
			if port.device.startswith('/dev/ttyUSB'):
				if port.manufacturer in portMakers:
					goodPort = unicode(port.device)
					break
			elif port.device == defaultPort:
				goodPort = defaultPort
		if goodPort:
			print 'Using port', goodPort, 'by', port.manufacturer
		else:
			print 'No port found'
			sys.exit()
		return goodPort

	# Model 100/200 stuff
	def commParameters(self):
		speeds = [75, 110, 300, 600, 1200, 2400, 4800, 9600, 19200]
		if self.baudrate in speeds:
			code = str(speeds.index(self.baudrate)+1)
		else:
			code = 'X'
			print "Unknown baudrate"

		if self.bytesize < 6 or self.bytesize > 8:
			print "Unknown word length"
			code += 'X'
		else:
			code += str(self.bytesize)

		if self.parity == serial.PARITY_NONE:
			code += 'N'
		elif self.parity == serial.PARITY_EVEN:
			code += 'E'
		elif self.parity == serial.PARITY_ODD:
			code += 'O'
		else:
			code += 'X'
			print "Unknown parity"

		if self.stopbits < 1 or self.stopbits > 2:
			print "Unknown stop bit"
			code += 'X'
		else:
			code += str(self.stopbits)

		if self.xonxoff:
			code += 'E'
		else:
			code += 'D'

		if self.model == '200':
			code += 'NN'

		return code

	def setupCodeBASIC(self):
		code = setupCodeBASIC
		commCode = self.commParameters()
		code = code.replace('<COMMCODE>', commCode)
		return code

	def configure(self, configuration):
		if configuration in configurations:
			self.model = configuration
		else:
			self.model = 'default'

		configuration = configurations[self.model]

		self.viscosity = configuration['viscosity']
		self.height = configuration['height']
		self.width = configuration['width']
		self.eol = configuration['eol']

	#loop for standard BBS-style connection
	def loop(self):
		while True:
			command, option = self.readCommand()
			if command is None:
				continue
			elif command in self.commands:
				(method, help) = self.commands[command]
				method(option)
			elif command:
				self.write(self.eol)
				print "Unknown command: '" + command + "'"
				if args.verbose:
					print len(command)
					print " ".join(hex(ord(n)) for n in command)
				self.writeLine('I do not understand ' + command)

	#loop for Replica 1-style, ie, tied in to all input/output BASIC only
	#Pretend that REM stands for REMote command.
	def remLoop(self):
		self.codeCount = 0
		while True:
			command, option = self.remoteCommand()
			if command in self.commands:
				(method, help) = self.commands[command]
				method(option)

	def remoteCommand(self):
		command = ""
		while True:
			character = self.readCharacter()
			if character in ["\r", "\n"]:
				if command.startswith(">REM "):
					(trigger, command) = command.split(" ", 1)
					if " " in command:
						(command, option) = command.split(" ", 1)
						option = option.lower()
					else:
						option = ""
					return command, option
				else:
					command = ""
			else:
				command += character

	def replicaLineSleep(self, line):
		lineDelay = .26
		if not line.startswith("REM:"):
			#BASIC programs need more time per line
			#the bigger and more complex they get
			#increase delay as memory fills up
			self.codeCount += len(line)
			memoryThrottle = 1.0 + self.codeCount/3000
			#increase delay for more complex lines
			lineThrottle = 1 + 1.12*line.count(':') + 1.12*line.count('(')
			lineDelay = lineDelay*memoryThrottle*lineThrottle
		sleep(lineDelay)

	# commands
	def append(self, filename):
		self.upload(filename, append=True)

	def battery(self, option):
		batteryCommand = [batteryInfo]
		status = check_output(batteryCommand)

		batteryStatus = {}
		for line in status.splitlines():
			line = line.replace(' = ', '=')
			(key, value) = line.split('=', 2)
			batteryStatus[key] = value

		if int(batteryStatus['BAT_EXIST']):
			self.writeLine('Temperature: ' + batteryStatus['Internal temperature'])
			self.writeLine('Charge: ' + batteryStatus['Battery gauge'])
			if int(batteryStatus['CHARG_IND']):
				self.writeLine('Charging')
			else:
				self.writeLine('Not charging')

		else:
			self.writeLine('There is no battery connected.')

	def define(self, word):
		if not word:
			word = self.readValue('Word to look up')

		if word:
			lines = []
			for i, synonym in enumerate(wordnet.synsets(word, None)):
				position = '%s ' % (positions[synonym.pos()],)
				definition = self.indentedWrap(synonym.definition(), position)
				definitionIndent = ' ' * len(position)
				lines.extend(definition)

				synonyms = [n.replace('_', ' ') for n in synonym.lemma_names()]
				if word in synonyms:
					synonyms.pop(synonyms.index(word))
				if synonyms:
					synonyms = 'synonyms: ' + ', '.join(synonyms)
					synonyms = self.indentedWrap(synonyms, definitionIndent)
					lines.extend(synonyms)

				antonyms = [antonym for n in synonym.lemmas() for antonym in n.antonyms()]
				if antonyms:
					antonyms = 'antonyms: ' + ', '.join(antonym.name() for antonym in antonyms)
					antonyms = self.indentedWrap(antonyms, definitionIndent)
					lines.extend(antonyms)

			if lines:
				self.pageList(lines)
			else:
				self.writeLine('No definition for ' + word)
				self.spell(word)

	def download(self, filename):
		if not filename:
			self.writeLine('Specify filename to download.')
		else:
			if self.model != "replica":
				self.writeLine('Press a key to start the download. Once')
				self.writeLine('the download finishes, press a key to')
				self.writeLine('return to the command line.')
				if self.isEscape(self.readCharacter()):
					self.writeLine('Download canceled')
					return
			self.transferToClient(filename)
			self.emptyBuffer()
			if self.model != "replica":
				self.pauseScroll()
				self.write(self.eol*2)
			self.writeLine('Download completed')

	def dump(self, minutes):
		try:
			duration = int(minutes)
		except ValueError:
			duration = None
		if minutes and not duration:
			self.writeLine('Specify number of minutes to dump.')
		if not minutes:
			duration = 2
		self.writeLine('Dumping for ' + str(duration) + ' minutes.')

		endTime = datetime.datetime.now() + datetime.timedelta(minutes=duration)
		dump = open(dumpFile, 'a')
		dump.write("\n\nDUMP START " + str(datetime.datetime.now()) + "\n")
		line = ''
		counter = 0
		lineLength = 16
		while (datetime.datetime.now() < endTime):
			character = self.read(1)
			if character:
				line += character
				counter += 1
				if len(line) == lineLength:
					self.dumpLine(dump, line, counter, lineLength)
					line = ''
		if line:
			self.dumpLine(dump, line, counter, lineLength)
		dump.write("DUMP END " + str(datetime.datetime.now()) + "\n")
		dump.close()
		self.writeLine('Dumped ' + str(counter) + ' bytes.')

	def help(self, commandName):
		help = None
		commandName = commandName.upper()
		if commandName:
			if commandName in self.commands:
				(method, help) = self.commands[commandName]
			else:
				self.writeLine('No command named ' + commandName)

		if help:
			self.writeLine(commandName)
			self.writeLine(help.pop(0))
			for option in help:
				self.writeLine("   " + option)
		else:
			commandNames = self.commands.keys()
			commandNames.sort()
			self.formattedColumns(commandNames)

	def hello(self, model):
		if model in configurations:
			self.configure(model)

			print 'Greeting Model', self.model
			self.writeLine('Hello, Model ' + self.model + '!')
		elif not model and self.model:
			self.writeLine('Current Model: ' + self.model)
		else:
			self.writeLine('Known models: ' + self.humanReadableList(configurations.keys()))

	def kill(self, filename):
		if not filename:
			self.writeLine('Specify filename to delete.')
			return

		filepath = self.makeUploadPath(filename)
		if os.path.isfile(filepath):
			trash = self.makeUploadPath(filename, directory=trashPath)
			os.rename(filepath, trash)
			self.writeLine('Deleted file ' + filename)
		else:
			self.writeLine('No such file ' + filename)

	def list(self, option):
		if option and option != 'details':
			self.writeLine('No such option ' + option)
			return

		self.listDirectory(dropboxPath, option)

	def pull(self, filename):
		if not filename:
			self.dropboxDirectory()
		else:
			self.fromDropbox(filename)

	def push(self, filename):
		if not filename:
			self.writeLine('Specify filename to push.')
		else:
			self.toDropbox(filename)

	def replace(self, filename):
		self.upload(filename, replace=True)

	def restore(self, filename):
		if not filename:
			self.listDirectory(trashPath)
		else:
			trash = self.makeUploadPath(filename, directory=trashPath)
			if os.path.isfile(trash):
				filepath = self.makeUploadPath(filename)
				if os.path.isfile(filepath):
					swap = self.makeUploadPath(str(uuid.uuid4()), directory=trashPath)
					os.rename(filepath, swap)
				else:
					swap = None
				os.rename(trash, filepath)
				if swap:
					os.rename(swap, trash)
				self.writeLine('Restored file ' + filename)
			else:
				self.writeLine('No trashed file ' + filename)

	def setup(self, option):
		if option == 'stat' or not option:
			self.writeLine(self.commParameters())
			return

		if option == 'code':
			self.writeLine('Press a key to start the download. Once')
			self.writeLine('the download finishes, press a key to')
			self.writeLine('return to the command line.')
			if self.isEscape(self.readCharacter()):
				self.writeLine('Download canceled')
				return
			self.writeLines(self.setupCodeBASIC())
			self.readCharacter()
			self.write(self.eol*2)
			self.writeLine('Transfer completed')
			return

		now = datetime.datetime.now()
		if option == 'time':
			self.writeLine(now.strftime('%H:%M:%S'))
		elif option == 'date':
			self.writeLine(now.strftime('%m/%d/%y'))
		elif option == 'day':
			self.writeLine(now.strftime('%a'))
		else:
			self.writeLine('Request stat, time, date, or day.')

	def spell(self, word):
		if not word:
			word = self.readValue('Best guess')

		if word:
			speller = Speller()
			suggestions = speller.suggest(word)
			if args.verbose:
				print suggestions

			if suggestions:
				suggestions = ', '.join(suggestions)
				lines = wrap(suggestions, self.width)
				self.pageList(lines)
			else:
				self.writeLine('No suggestions found.')

	def throttle(self, viscosity):
		if not viscosity:
			if self.viscosity:
				self.writeLine("Viscosity currently at " + str(self.viscosity))
			else:
				self.writeLine("Viscosity currently disabled.")
			return

		try:
			viscosity = int(viscosity)
		except:
			self.writeLine("Viscosity must be an integer.")
			return

		viscosity = float(viscosity)
		if viscosity:
			self.viscosity = viscosity
			self.writeLine("Viscosity set to " + str(self.viscosity))
		else:
			self.viscosity = 0
			self.writeLine("Viscosity turned off.")

	def upload(self, filename, replace=False, append=False):
		if not filename:
			self.writeLine('Specify filename to upload to.')
		elif not replace and not append and self.fileExists(filename):
			self.writeLine('File ' + filename + ' exists; consider REPLACE.')
		else:
			self.writeLine('Ready to accept ' + filename)
			self.writeLine('Use ESC or CTRL-C to end transfer.')
			if self.transferFromClient(filename, append=append):
				self.toDropbox(filename)

	def view(self, filename):
		if not filename:
			self.writeLine('Specify filename to view.')
		else:
			if self.fileExists(filename):
				filepath = self.makeUploadPath(filename)
				data = self.getServerFile(filepath)
				paragraphs = data.splitlines()
				lines = []
				for paragraph in paragraphs:
					if len(paragraph) > self.width-1:
						#wrapping to 40 means that sometimes a line will be less than 40 but
						#the next line will be 40, and the first line will scroll off the top
						#if this happens at the second-to-last line
						paragraphLines = wrap(paragraph, self.width-1)
						lines.extend(paragraphLines)
					else:
						lines.append(paragraph)
				self.pageList(lines)
			else:
				print "No such file."

	def web(self, link):
		if link:
			if '.' in link:
				return self.displayWeb(link)
			try:
				linkNumber = int(link)
			except ValueError:
				linkNumber = None
			if linkNumber and linkNumber <= len(self.recentLinks):
				linkNumber -= 1
				url = self.recentLinks[linkNumber]
			else:
				self.displayURLs(link)
				return

		else:
			url = self.readValue('URL')

		if url:
			self.displayWeb(url)

	def wifi(self, connect):
		if connect == 'connect':
			network = self.readValue('Network')
			if not network:
				return

			password = self.readValue('Password', secret=True)
			if password is False:
				return

			self.writeLine('Trying ' + network + '...')
			wifiCommand = [wifi, '--connect', network]
			if password:
				wifiCommand.extend(['--password', password])

			try:
				wifiResponse = check_output(wifiCommand)
				self.writeLines(wifiResponse)
			except CalledProcessError, error:
				self.writeLine('Error ' + str(error.returncode))
				self.writeLines(str(error))
		elif connect == 'disconnect':
			wifiCommand = [wifi, '--disconnect']
			wifiResponse = check_output(wifiCommand)
			self.writeLines(wifiResponse)
		else:
			wifiList = check_output(wifi)
			self.pageLines(wifiList)

	def wiki(self, option):
		if option:
			term = option
		else:
			term = self.readValue('Term')

		if term:
			url = 'http://en.wikipedia.org/wiki/' + term
			self.displayWeb(url)

	# read from user
	def isEscape(self, character):
		if character is None:
			return None
		elif character == "":
			return None
		#CTRL-C, CTRL-X, CTRL-Z, ESCAPE
		elif ord(character) in [3, 24, 26, 27]:
			return True
		else:
			return False

	def readCharacter(self):
		character = None
		while not character:
			character = self.read(1)
			sleep(.05)
		return character

	def readCommand(self):
		self.write(self.eol)
		self.write('> ')
		line = ''
		inCommand = True
		foundExtension = False
		while True:
			try:
				character = self.read(1)
			except Exception as e:
				character = None
				print 'Ignoring error', e.__class__.__name__
				print e.message
				print e.args

			if not character:
				sleep(.1)
				continue

			if args.verbose:
				self.display(character)

			if character == '\r':
				break
			elif self.isEscape(character):
				return None, None
			elif character == '\b':
				if line:
					if line[-1] == ' ':
						inCommand = True
					elif line[-1] == '.':
						foundExtension = False
					line = line[:-1]
					self.write(backspace)
				continue
			elif inCommand and character == ' ':
				inCommand = False
			elif not inCommand and character == '\t':
				completion = self.autocomplete(line)
				if completion == '':
					continue
				character = completion
				if '.' in completion:
					foundExtension = True
			elif not inCommand and not foundExtension and character == '.':
				foundExtension = True
			elif not inCommand:
				if ord(character) > 64 and ord(character) < 91:
					#convert uppercase to lowercase if in filename
					character = chr(ord(character)+32)
				if ord(character) < 97 or ord(character) > 122:
					if ord(character) < 48 or ord(character) > 57:
						continue
			elif inCommand:
				if ord(character) > 96 and ord(character) < 123:
					#convert lowercase to uppercase if in command
					character = chr(ord(character)-32)
				if ord(character) < 65 or ord(character) > 90:
					continue
			self.write(character)
			line += character

		self.write(self.eol)

		line = line.strip('\x13\x11 ')
		if ' ' in line:
			(command, option) = line.split(' ')
			option = option.strip()
			return command, option
		else:
			return line, ''

	def readValue(self, name, secret=False):
		self.write(name)
		self.write('? ')
		line = ''
		while True:
			character = self.read(1)
			if character:
				if args.verbose and not secret:
					self.display(character)

				if character == '\r':
					break
				elif self.isEscape(character):
					return False
				elif character == '\b':
					if line:
						line = line[:-1]
						if not secret:
							self.write(backspace)
					continue
				if not secret:
					self.write(character)
				line += character
			else:
				sleep(.1)

		self.write(self.eol)
		return line.strip()

	#display to user
	def dumpLine(self, dump, line, counter, lineLength):
		dots = ''
		hexes = ''
		for character in line:
			if character in printable:
				dots += character
			else:
				dots += '.'

			if hexes:
				hexes += ' '
			hexes += hexlify(character)

		counter -= len(line)
		dots = dots.ljust(lineLength, ' ')
		output = "%08d\t%s\t%s" % (counter, dots, hexes)
		dump.write(output)
		dump.write("\n")
		if args.verbose:
			print output

	def formattedColumns(self, theList):
		maxwidth = max(map(lambda x: len(x), theList))
		columns = int(self.width/(maxwidth+1))
		if columns > len(theList):
			columns = len(theList)
		spaces = (self.width - columns*maxwidth)/columns * ' '
		justifyList = map(lambda x: x.ljust(maxwidth), theList)
		lines = (spaces.join(justifyList[i:i+columns]) for i in xrange(0,len(justifyList),columns))
		self.pageList(lines)

	def indentedWrap(self, text, title):
		return wrap(text, self.width, initial_indent=title, subsequent_indent=' '*len(title))

	def listDirectory(self, path, option):
		print 'Listing directory', path
		files = [x for x in os.listdir(path) if self.fileIsVisible(x)]
		if files:
			files.sort()
			if not option:
				self.formattedColumns(files)
			else:
				self.detailedFileList(files)
		else:
			self.writeLine('No files available')

	def autocomplete(self, line):
		prefix = line.split(' ', 1)[1]
		files = [x for x in os.listdir(dropboxPath) if x.startswith(prefix)]
		common = os.path.commonprefix(files)
		if len(common) >= len(prefix):
			return common[len(prefix):]
		else:
			return ''

	def detailedFileList(self, files):
		lines = []
		for file in files:
			size = self.humanReadableFileSize(file)
			modified = self.humanReadableFileDate(file)

			filename = file.ljust(16)
			size = size.rjust(9)
			modified = modified.rjust(15)

			lines.append(filename + size + modified)
		self.pageList(lines)

	def pauseScroll(self):
		if self.model == 'replica':
			self.write(self.eol)
			response, option = self.remoteCommand()
			if response == 'STOP':
				self.writeLine('Canceled')
				return chr(27)
			else:
				return ' '
		else:
			return self.readCharacter()

	def pageLines(self, lines):
		lines = lines.splitlines()
		self.pageList(lines)

	def pageList(self, lines):
		lineCounter = 0
		for line in lines:
			lineCounter = self.pageLine(line, lineCounter)
			if lineCounter is False:
				return

		if lineCounter >= self.height-2:
			self.pauseScroll()

	def pageLine(self, line, lineCounter):
		line = str(line)
		subLine = line[0:self.width]
		self.writeLine(subLine, useEOL=False)
		while True:
			lineCounter += 1
			if (lineCounter >= self.height) or (len(subLine) >= self.width and lineCounter >= self.height-1):
				if args.verbose:
					print "----"
				pager = self.pauseScroll()
				if self.isEscape(pager):
					return False
				lineCounter = 0

			if len(line) <= self.width:
				if len(line) < self.width or self.model == 'replica':
					self.write(self.eol)
				break

			line = line[self.width:]
			subLine = line[0:self.width]
			self.writeLine(subLine, useEOL=False)

		return lineCounter

	def writeLine(self, line, useEOL=True):
		if self.online:
			if self.model == "replica":
				line = "REM:" + line.upper()
				self.replicaLineSleep(line)
			self.sendData(line)
			self.emptyBuffer()
			if useEOL:
				self.write(self.eol)
		if args.verbose or not self.online:
			print line

	def writeLines(self, lines):
		for line in lines.splitlines():
			self.writeLine(line)

	# data transfer routines
	# Close (without flush, it *will* hang)
	def closePort(self):
		self.flush()
		print "Flushed"
		self.close()
		print "Closed"

	def display(self, character):
		if ord(character) < 32 or ord(character) > 122:
			sys.stdout.write('[' + str(ord(character)) + ']')
		else:
			sys.stdout.write(character)
		if character == '\r':
			sys.stdout.write('\n')
		sys.stdout.flush()

	def emptyBuffer(self):
		if self.model == "replica":
			while self.read():
				pass

	def getServerFile(self, filepath):
		file = codecs.open(filepath, 'r', 'utf8')
		data = file.read()
		file.close()

		data = data.replace("\n", "\r")
		data = unidecode(data)

		return data

	def sendData(self, data):
		line = ""
		for character in data:
			if self.model == "replica":
				#replica requires carriage returns, not line feeds
				if character == "\n":
					character = "\r"
				if ord(character) > 96 and ord(character) < 123:
					character = chr(ord(character)-32)
				#ignore all non-ascii characters
				elif character != "\r" and (ord(character) < 32 or ord(character) > 96):
					print "Unwanted character:", ord(character)
					return
			self.write(character)
			if self.model == "replica" and character == "\r":
				self.replicaLineSleep(line)
				line = ""
			elif self.viscosity:
				sleep(self.viscosity/1000.0)
			if self.model == "replica":
				line += character

	def transferFromClient(self, filename, append=False):
		data = ''
		while True:
			character = self.read(1)
			if self.isEscape(character):
				break
			elif len(data) > 100000:
				print 'Too much data uploaded for', filename
				self.writeLine('Too big. Canceled')
				return False
			elif character:
				if args.verbose:
					self.display(character)
				data += character
				if not len(data) % 100:
					sys.stdout.write(unicode(len(data)))
					sys.stdout.write(' bytes for ')
					sys.stdout.write(filename)
					sys.stdout.write('\r')
					sys.stdout.flush()
		sys.stdout.write('\n')

		#these control characters seem to be at the start of the download
		data = data.strip(chr(17) + chr(19))
		if data:
			filepath = self.makeUploadPath(filename)
			if append:
				mode = 'a'
			else:
				mode = 'w'
			file = open(filepath, mode)
			file.write(data)
			file.close()
			self.writeLine('Upload accepted.')
			return True
		else:
			print 'No data uploaded for', filename
			self.writeLine('No data found.')
			return None

	def transferToClient(self, filename):
		if self.fileExists(filename):
			filepath = self.makeUploadPath(filename)
			print "Sending file", filepath
			data = self.getServerFile(filepath)
			self.sendData(data)
			print "Sent file", filepath
		else:
			self.writeLine("No file " + filename)
			print "No such file."

	# general utilities
	def fileExists(self, filename):
		filepath = self.makeUploadPath(filename)
		if os.path.exists(filepath):
			return True
		else:
			return False

	def makeUploadPath(self, filename, directory=dropboxPath):
		if not os.path.exists(directory):
			os.mkdir(directory)
		return os.path.join(directory, filename)

	def fileIsVisible(self, filename):
		if filename.startswith('.'):
			return False

		filepath = self.makeUploadPath(filename)
		#if this is running on a Mac, don't show hidden files
		try:
			stats = os.stat(filepath)
			if stats.st_flags & stat.UF_HIDDEN:
				return False
		except:
			pass

		return True

	def humanReadableFileSize(self, filename):
		filepath = self.makeUploadPath(filename)
		filesize = os.path.getsize(filepath)

		for bytesLevel in ['B', 'KB', 'MB', 'GB', 'TB']:
			if filesize < 1024.0:
				return "%3.1f%s" % (filesize, bytesLevel)
			filesize /= 1024.0

		return "%3.1f%s" % (filesize, 'PB')

	def humanReadableFileDate(self, filename):
		filepath = self.makeUploadPath(filename)
		filedate = os.path.getmtime(filepath)
		filedate = datetime.datetime.fromtimestamp(filedate)
		now = datetime.datetime.now()

		if filedate.date() == now.date():
			if filedate.hour == now.hour and filedate.minute == now.minute:
				formattedDate = 'NOW'
			else:
				formattedDate = filedate.strftime('%l:%M %p')
				formattedDate = formattedDate.lstrip('0')
		elif filedate > now - datetime.timedelta(days=7):
			formattedDate = filedate.strftime('%a %l:%M %p')
		elif filedate.date().year == now.date().year:
			formattedDate = filedate.strftime('%b %e')
		else:
			formattedDate = filedate.strftime('%b %e, %Y')
		formattedDate = formattedDate.replace('  ', ' ')

		return formattedDate

	def humanReadableList(self, items):
		if not items:
			return None

		if len(items) == 1:
			return items[0]

		finalItem = items.pop()
		itemList = ', '.join(items)
		itemList = itemList + ' or ' + finalItem

		return itemList

	#dropbox stuff
	def dropboxDirectory(self):
		if self.dropboxIsThere():
			client = dropbox.Dropbox(self.dropboxToken)
			folder = client.files_list_folder('')
			if folder.has_more:
				self.writeLine('THERE ARE MORE FILES IN THIS DIRECTORY')
			files = [str(item.name) for item in folder.entries]
			files.sort()
			self.formattedColumns(files)

	def setDropboxToken(self):
		if self.dropboxToken:
			return True

		if not os.path.exists(dropboxTokenFile):
			self.writeLine('NO DROPBOX TOKEN FILE FOUND')
			return False

		try:
			token = open(dropboxTokenFile).read().strip()
		except Exception as e:
			self.writeLine('UNABLE TO READ TOKEN FILE')
			self.writeLine(str(e))
			return False

		if not token:
			self.writeLines(('NO DROPBOX TOKEN FOUND IN', dropboxTokenFile))
			return False

		self.dropboxToken = token
		return True

	def dropboxIsThere(self):
		if not dropboxAvailable or not self.setDropboxToken():
			return False

		try:
			host = socket.gethostbyname('dropbox.com')
			s = socket.create_connection((host, 80), 2)
			return True
		except:
			self.writeLine("Dropbox.com is not currently available.")
			return False

	def fromDropbox(self, filename):
		if self.dropboxIsThere():
			filepath = self.makeUploadPath(filename)
			client = dropbox.Dropbox(self.dropboxToken)
			dropboxPath = '/' + filename
			try:
				response = client.files_download_to_file(filepath, dropboxPath)
			except dropbox.exceptions.ApiError as e:
				if e.error.is_path() and e.error.get_path().is_not_found():
					self.writeLine('File not found')
				else:
					self.writeLine('Unknown error')
					print e
				return False

			self.writeLine("Pulled " + filename + " from Dropbox.")

	def toDropbox(self, filename):
		if self.dropboxIsThere():
			filepath = self.makeUploadPath(filename)
			dropboxPath = '/' + filename
			client = dropbox.Dropbox(self.dropboxToken)
			upstream = open(filepath, 'r').read()
			upmode = dropbox.files.WriteMode('overwrite', None)
			dropboxResponse = client.files_upload(upstream, dropboxPath, mode=upmode)
			self.writeLine("Uploaded " + str(dropboxResponse.size) + " bytes to Dropbox")

	#web stuff
	def collectURLs(self, text):
		#keep the list no more than 500 items
		#but always keep the current page's links
		#and a hundred more
		if len(self.recentLinks) > 500:
			self.recentLinks = self.recentLinks[400:]

		# elinks URL list:
		#   . http://www.hoboes.com/Mimsy/
		search = re.compile('^   . (https?:.+)', flags=re.MULTILINE)
		links = re.findall(search, text)
		for link in links:
			if '#' in link:
				link = link.split('#', 1)[0]

			if link and link not in self.recentLinks:
				self.recentLinks.append(link)

	def displayWeb(self, url):
		browser = [elinks, '-dump', '-no-numbering', '-dump-width', str(self.width), '-dump-charset', 'ascii', '-eval', 'set document.html.link_display = 0', url]
		try:
			webpage = check_output(browser)
			self.collectURLs(webpage)
			self.pageLines(webpage)
		except CalledProcessError, error:
			self.writeLine('Error ' + str(error.returncode))
			self.writeLines(str(error))

	def displayURLs(self, query):
		linkIndex = 0
		query = query.lower()
		indexWidth = len(str(len(self.recentLinks)))
		printer = '%' + str(indexWidth) + 'i %s'

		links = []
		for link in self.recentLinks:
			linkIndex += 1
			linkText = link.lower()[7:]
			if linkText.find(query) >= 0:
				links.append(printer % (linkIndex, link))
				while len(links[-1]) > self.width:
					wrapped = ' '*(indexWidth+1) + links[-1][self.width-1:]
					links[-1] = links[-1][:self.width-1]
					links.append(wrapped)

		if links:
			self.pageList(links)
		else:
			self.writeLine('No links matching ' + query)

parser = argparse.ArgumentParser(description='Model 100 Data Manager')
parser.add_argument('--toDropbox', help='upload file to dropbox')
parser.add_argument('--fromDropbox', help='download file from dropbox')
parser.add_argument('--listDropbox', help='list files on dropbox', action='store_true')
parser.add_argument('--list', action='store_true', help='list available serial ports')
parser.add_argument('--model', choices=configurations.keys())
parser.add_argument('--verbose', action='store_true')
args = parser.parse_args()

if args.list:
	ports = list_ports.comports()
	for port in ports:
		print port.device, port.manufacturer
else:
	if args.model == 'replica':
		baudrate = 9600
		useXON = False
		portMakers = ['FTDI']
	else:
		baudrate = 19200
		useXON = True
		portMakers = ['Digi International', 'Prolific Technology Inc.']

	port = OldSchoolSerial(
		timeout = 1,
		baudrate = baudrate,
		bytesize = 8,
		parity = serial.PARITY_NONE,
		stopbits = serial.STOPBITS_ONE,
		xonxoff = useXON,
		rtscts = False,
		dsrdtr = False,
		#doesn't work well, but bears looking into:
		#screen /dev/ptmx
		#port = '/dev/pts/1'
	)

	if args.toDropbox or args.fromDropbox or args.listDropbox:
		if not dropboxAvailable:
			print 'Dropbox is not available.'
			sys.exit()

		port.online = False
		if args.toDropbox:
			if port.fileExists(args.toDropbox):
				print 'Attemping to upload', args.toDropbox, 'to dropbox.'
				port.toDropbox(args.toDropbox)
			else:
				print 'File', args.dropbox, 'not found.'
		elif args.fromDropbox:
			print 'Attempting to download', args.fromDropbox, 'from dropbox.'
			port.fromDropbox(args.fromDropbox)
		elif args.listDropbox:
			port.dropboxDirectory()
		sys.exit()

	parameters = port.commParameters()
	print "Use " + parameters + " for Model 100, " + parameters + "NN for Model 200."

	try:
		if args.model:
			port.hello(args.model)

		if args.model == 'replica':
			port.remLoop()
		else:
			port.loop()
	except KeyboardInterrupt:
		print 'Quitting.'
		port.closePort()
