#!/usr/bin/env python

import getopt
import os
import socket
import sys
import shutil
import tempfile

if sys.version_info >= (3,0):
	import configparser
	import io as ioio
else:
	import ConfigParser as configparser
	import StringIO as ioio

DATE           = '2016-11-03'
VERSION        = '1.0.3'
VERSION_STRING = 'rmate-python version %s (%s)' % (DATE, VERSION)

def log(msg):
	if settings.verbose:
		sys.stderr.write(msg.strip() + '\n')

def enc(string):
	if sys.version_info >= (3,0):
		return bytes(string, 'utf-8')
	else:
		return string

def dec(byte_string):
	if sys.version_info >= (3,0):
		return str(byte_string, 'utf-8')
	else:
		return byte_string

def handle_save(textmate, variables, data):
	path = variables['token']

	if path == '|':
		if settings.tmpFile is not None:
			settings.tmpFile.seek(0)
			settings.tmpFile.truncate()
			settings.tmpFile.write(data)
		else:
			log("Save failed!")
		return

	try:
		log('Saving %s' % path)
		backup_path = '%s~' % path
		while os.path.isfile(backup_path): backup_path = '%s~' % backup_path
		if os.path.isfile(path): shutil.copy2(path, backup_path)
		file = open(path, 'wb')
		file.write(data)
		if os.path.isfile(backup_path): os.remove(backup_path)
	except (IOError, OSError):
		log("Save failed!")

def handle_close(textmate, variables, data):
	if settings.tmpFile is not None:
		settings.tmpFile.seek(0)
		print(dec(settings.tmpFile.read()))
		settings.tmpFile.close()
		settings.tmpFile = None

	if 'token' in variables:
		path = variables['token']
		log("Closed %s" % path)
	else:
		log("Unknown file closed")

def handle_cmd(textmate, cmd):
	variables = {}
	data = enc('')
	for line in textmate:
		line = dec(line)
		if line.strip() == "": break
		s = line.split(': ', 2)
		name = s[0].strip()
		value = s[1].strip()
		variables[name] = value
		if name == 'data':
			data += textmate.read(int(value))

	if 'data' in variables: del variables['data']
	if   cmd == 'save':  handle_save(textmate, variables, data)
	elif cmd == 'close': handle_close(textmate, variables, data)

def connect_to_tm():
	socket.setdefaulttimeout(5)

	error = 0
	for option in socket.getaddrinfo(settings.host, settings.port, 0, socket.SOCK_STREAM):
		family, socktype, proto, canonname, sockaddr = option

		tm_sock = socket.socket(family, socktype, proto)
		error = tm_sock.connect_ex(sockaddr)
		if error == 0:
			break

	if error:
		raise Exception('Could not connect to textmate: %s' % (os.strerror(error)))

	tm_sock.setblocking(True)
	textmate = tm_sock.makefile('rwb')

	server_info = dec(textmate.readline())

	if server_info.strip() == "":
		sys.stderr.write("Couldn't connect to TextMate!\n")
		exit(1)

	log('Connect: %s' % server_info)

	return textmate

def handle_cmds(textmate, host, port, cmds):
	if len(cmds) == 0: return

	for cmd in cmds: cmd.send(textmate)
	textmate.write(enc('.\n'))

	textmate.flush()
	for command in textmate:
		handle_cmd(textmate, dec(command).strip())
	textmate.close()
	log('Done')

class Settings:
	def __init__(self):
		self.host    = 'localhost'
		self.port    = 52698
		self.wait    = False
		self.force   = False
		self.verbose = False
		self.lines   = []
		self.names   = []
		self.types   = []
		self.files   = []
		self.tmpFile = None

		self.read_disk_settings()

		# Environment settings
		self.host = os.getenv('RMATE_HOST', self.host)
		self.port = int(os.getenv('RMATE_PORT', self.port))

		self.parse_cli_options()

		if self.host == 'auto' and os.environ.get('SSH_CONNECTION') != None:
			self.host = os.getenv('SSH_CONNECTION', 'localhost').split(' ')[0]

	def usage(self):
		print("usage: %s [OPTION]... FILE...\n\n"
		      "      --host HOST  Connect to HOST. Use 'auto' to detect the host from\n"
		      "                   SSH. Defaults to %s\n"
		      "  -p, --port PORT  Port number to use for connection. Defaults to %d\n"
		      "  -w, --[no-]wait  Wait for file to be closed by TextMate\n"
		      "  -l, --line LINE  Place carat on line LINE after loading the file.\n"
		      "                   TextMate selection strings can be used\n"
		      "  -m, --name NAME  The display name shown in TextMate\n"
		      "  -t, --type TYPE  Treat file as having TYPE\n"
		      "  -f, --force      Open even if the file is not wratable\n"
		      "  -v, --verbose    Verbose logging messages\n"
		      "  -h, --help       Show this help and exit\n"
		      "      --version    Show version and exit\n\n"
		      "When FILE is -, read standard input.\n"
		      % (sys.argv[0], self.host, self.port))

	def read_disk_settings(self):
		config = Parser()
		try:
			config.read(['/etc/rmate.rc', '/usr/local/etc/rmate.rc', os.path.expanduser('~/.rmate.rc')])
		except:
			sys.stderr.write('Could not read settings from disk.\n')
		if config.has_option('NOSECTION', 'host'): self.host = config.get('NOSECTION', 'host')
		if config.has_option('NOSECTION', 'port'): self.port = config.get('NOSECTION', 'port')

	def parse_cli_options(self):
		try:
			optlist, args = getopt.gnu_getopt(sys.argv[1:], 'hp:wl:m:t:fv', ['host=', 'port=', 'wait', 'no-wait', 'line=', 'name=', 'type=', 'force', 'verbose', 'help', 'version'])
		except getopt.GetoptError:
			self.usage()
			sys.exit(2)

		for name, value in optlist:
			if name == '--version':           print(VERSION_STRING); sys.exit()
			elif name in ('-h', '--help'):    self.usage(); sys.exit()
			elif name == '--host':            self.host = value
			elif name in ('-p', '--port'):    self.port = int(value)
			elif name in ('-w', '--wait'):    self.wait = True
			elif name == '--no-wait':         self.wait = False
			elif name in ('-l', '--line'):    self.lines.append(value)
			elif name in ('-m', '--name'):    self.names.append(value)
			elif name in ('-t', '--type'):    self.types.append(value)
			elif name in ('-f', '--force'):   self.force = True
			elif name in ('-v', '--verbose'): self.verbose = True

		if '-' in sys.argv[1:] and '-' not in args: args.append('-')
		self.files = args
		if not self.files: self.files = ['=']

class Parser(configparser.ConfigParser):
	def read(self, filenames):
		if isinstance(filenames, str):
			filenames = [filenames]
		read_ok = []
		for filename in filenames:
			try:
				file = open(filename)
				fp = ioio.StringIO("[NOSECTION]\n" + file.read())
				self._read(fp, filename)
				file.close()
			except:
				continue
			read_ok.append(filename)
		return read_ok

class Command:
	def __init__(self, command):
		self.command = command
		self.variables = {}
		self.data = None
		self.size = None

	def __setitem__(self, index, value):
		self.variables[index] = value

	def read_stdin(self):
		self.data = enc(sys.stdin.read())
		self.size = len(self.data)

	def read_file(self, path):
		file = open(path, 'rb')
		self.data = file.read()
		self.size = file.tell()
		file.close()

	def send(self, textmate):
		textmate.write(enc(self.command + '\n'))
		for name in self.variables.keys():
			value = self.variables[name]
			textmate.write(enc('%s: %s\n' % (name, value)))
		if self.data != None:
			textmate.write(enc('data: %d\n' % self.size))
			textmate.write(self.data)
		textmate.write(enc('\n'))

def main():
	cmds = []
	for idx, path in enumerate(settings.files):
		if path == '=':
			if sys.stdin.isatty(): continue
			else: path = '-'
		elif path == '-' and sys.stdin.isatty():
			sys.stderr.write('Reading from stdin, press ^D to stop\n')
		elif os.path.isdir(path):
			sys.stderr.write("'%s' is a directory!\n" % path)
			continue
		elif os.path.isfile(path) and not os.access(path, os.W_OK):
			if settings.force:
				log("File %s is not writable. Opening anyway." % path)
			else:
				sys.stderr.write("File %s is not writable! Use -f/--force to open anyway.\n" % path)
				continue

		cmd = Command("open")
		if   len(settings.names) > idx: cmd['display-name'] = settings.names[idx]
		elif path == '-':               cmd['display-name'] = '%s:untitled (stdin)' % socket.gethostname()
		else:                           cmd['display-name'] = '%s:%s' % (socket.gethostname(), path)
		if   len(settings.types) > idx: cmd['file-type']    = settings.types[idx]
		elif path == '-':               cmd['file-type']    = 'txt'
		if   path != '-':               cmd['real-path']    = os.path.abspath(path)
		if   len(settings.lines) > idx: cmd['selection']    = settings.lines[idx]

		cmd["data-on-save"] = 'yes'
		cmd["re-activate"] = 'yes'
		cmd["token"] = path
		if path == '-':
			cmd.read_stdin()
		elif os.path.isfile(path):
			cmd.read_file(path)
		else:
			cmd['data'] = "0"

		if path == '-' and not sys.stdout.isatty() and settings.tmpFile is None:
			settings.tmpFile = tempfile.TemporaryFile()
			settings.tmpFile.write(cmd.data)
			settings.tmpFile.seek(0)
			cmd["token"] = '|'

		cmds.append(cmd)

	s = connect_to_tm()

	if not settings.wait and os.fork():
		sys.exit()

	handle_cmds(s, settings.host, settings.port, cmds)

if __name__ == "__main__":
	settings = Settings()
	main()