# vim:fileencoding=utf-8:noet
_log = []
vars = {}
vvars = {'version': 703}
_tabpage = 0
_mode = 'n'
_buf_purge_events = set()
options = {
	'paste': 0,
	'ambiwidth': 'single',
	'columns': 80,
	'encoding': 'utf-8',
}
_last_bufnr = 0
_highlights = {}
from collections import defaultdict as _defaultdict
_environ = _defaultdict(lambda: '')
del _defaultdict


_thread_id = None


def _set_thread_id():
	global _thread_id
	from threading import current_thread
	_thread_id = current_thread().ident


# Assuming import is done from the main thread
_set_thread_id()


def _print_log():
	for item in _log:
		print (item)
	_log[:] = ()


def _vim(func):
	from functools import wraps
	from threading import current_thread

	@wraps(func)
	def f(*args, **kwargs):
		global _thread_id
		if _thread_id != current_thread().ident:
			raise RuntimeError('Accessing vim from separate threads is not allowed')
		_log.append((func.__name__, args))
		return func(*args, **kwargs)

	return f


def _unicode(func):
	from functools import wraps
	import sys

	if sys.version_info < (3,):
		return func

	@wraps(func)
	def f(*args, **kwargs):
		from powerline.lib.unicode import u
		ret = func(*args, **kwargs)
		if isinstance(ret, bytes):
			ret = u(ret)
		return ret

	return f


class _Buffers(object):
	@_vim
	def __init__(self):
		self.d = {}

	@_vim
	def __len__(self):
		return len(self.d)

	@_vim
	def __getitem__(self, item):
		return self.d[item]

	@_vim
	def __setitem__(self, item, value):
		self.d[item] = value

	@_vim
	def __iter__(self):
		return iter(self.d.values())

	@_vim
	def __contains__(self, item):
		return item in self.d

	@_vim
	def _keys(self):
		return self.d.keys()

	@_vim
	def _pop(self, *args, **kwargs):
		return self.d.pop(*args, **kwargs)


buffers = _Buffers()


class _ObjList(object):
	@_vim
	def __init__(self, objtype):
		self.l = []
		self.objtype = objtype

	@_vim
	def __getitem__(self, item):
		return self.l[item - int(item > 0)]

	@_vim
	def __len__(self):
		return len(self.l)

	@_vim
	def __iter__(self):
		return iter(self.l)

	@_vim
	def _pop(self, idx):
		obj = self.l.pop(idx - 1)
		for moved_obj in self.l[idx - 1:]:
			moved_obj.number -= 1
		return obj

	@_vim
	def _append(self, *args, **kwargs):
		return self.l.append(*args, **kwargs)

	@_vim
	def _new(self, *args, **kwargs):
		number = len(self) + 1
		new_obj = self.objtype(number, *args, **kwargs)
		self._append(new_obj)
		return new_obj


def _construct_result(r):
	import sys
	if sys.version_info < (3,):
		return r
	else:
		if isinstance(r, str):
			return r.encode('utf-8')
		elif isinstance(r, list):
			return [_construct_result(i) for i in r]
		elif isinstance(r, dict):
			return dict((
				(_construct_result(k), _construct_result(v))
				for k, v in r.items()
			))
		return r


def _str_func(func):
	from functools import wraps

	@wraps(func)
	def f(*args, **kwargs):
		return _construct_result(func(*args, **kwargs))
	return f


def _log_print():
	import sys
	for entry in _log:
		sys.stdout.write(repr(entry) + '\n')


_current_group = None
_on_wipeout = []


@_vim
def command(cmd):
	global _current_group
	cmd = cmd.lstrip()
	if cmd.startswith('let g:'):
		import re
		varname, value = re.compile(r'^let g:(\w+)\s*=\s*(.*)').match(cmd).groups()
		vars[varname] = value
	elif cmd.startswith('hi '):
		sp = cmd.split()
		_highlights[sp[1]] = sp[2:]
	elif cmd.startswith('augroup'):
		augroup = cmd.partition(' ')[2]
		if augroup.upper() == 'END':
			_current_group = None
		else:
			_current_group = augroup
	elif cmd.startswith('autocmd'):
		rest = cmd.partition(' ')[2]
		auevent, rest = rest.partition(' ')[::2]
		pattern, aucmd = rest.partition(' ')[::2]
		if auevent != 'BufWipeout' or pattern != '*':
			raise NotImplementedError
		import sys
		if sys.version_info < (3,):
			if not aucmd.startswith(':python '):
				raise NotImplementedError
		else:
			if not aucmd.startswith(':python3 '):
				raise NotImplementedError
		_on_wipeout.append(aucmd.partition(' ')[2])
	elif cmd.startswith('set '):
		if cmd.startswith('set statusline='):
			options['statusline'] = cmd[len('set statusline='):]
		elif cmd.startswith('set tabline='):
			options['tabline'] = cmd[len('set tabline='):]
		else:
			raise NotImplementedError(cmd)
	else:
		raise NotImplementedError(cmd)


@_vim
@_unicode
def eval(expr):
	if expr.startswith('g:'):
		return vars[expr[2:]]
	elif expr.startswith('v:'):
		return vvars[expr[2:]]
	elif expr.startswith('&'):
		return options[expr[1:]]
	elif expr.startswith('$'):
		return _environ[expr[1:]]
	elif expr.startswith('PowerlineRegisterCachePurgerEvent'):
		_buf_purge_events.add(expr[expr.find('"') + 1:expr.rfind('"') - 1])
		return '0'
	elif expr.startswith('exists('):
		return '0'
	elif expr.startswith('getwinvar('):
		import re
		match = re.match(r'^getwinvar\((\d+), "(\w+)"\)$', expr)
		if not match:
			raise NotImplementedError(expr)
		winnr = int(match.group(1))
		varname = match.group(2)
		return _emul_getwinvar(winnr, varname)
	elif expr.startswith('has_key('):
		import re
		match = re.match(r'^has_key\(getwinvar\((\d+), ""\), "(\w+)"\)$', expr)
		if match:
			winnr = int(match.group(1))
			varname = match.group(2)
			return 0 + (varname in current.tabpage.windows[winnr].vars)
		else:
			match = re.match(r'^has_key\(gettabwinvar\((\d+), (\d+), ""\), "(\w+)"\)$', expr)
			if not match:
				raise NotImplementedError(expr)
			tabnr = int(match.group(1))
			winnr = int(match.group(2))
			varname = match.group(3)
			return 0 + (varname in tabpages[tabnr].windows[winnr].vars)
	elif expr == 'getbufvar("%", "NERDTreeRoot").path.str()':
		import os
		assert os.path.basename(current.buffer.name).startswith('NERD_tree_')
		return '/usr/include'
	elif expr.startswith('getbufvar('):
		import re
		match = re.match(r'^getbufvar\((\d+), ["\'](.+)["\']\)$', expr)
		if not match:
			raise NotImplementedError(expr)
		bufnr = int(match.group(1))
		varname = match.group(2)
		return _emul_getbufvar(bufnr, varname)
	elif expr == 'tabpagenr()':
		return current.tabpage.number
	elif expr == 'tabpagenr("$")':
		return len(tabpages)
	elif expr.startswith('tabpagewinnr('):
		tabnr = int(expr[len('tabpagewinnr('):-1])
		return tabpages[tabnr].window.number
	elif expr.startswith('tabpagebuflist('):
		import re
		match = re.match(r'tabpagebuflist\((\d+)\)\[(\d+)\]', expr)
		tabnr = int(match.group(1))
		winnr = int(match.group(2)) + 1
		return tabpages[tabnr].windows[winnr].buffer.number
	elif expr.startswith('gettabwinvar('):
		import re
		match = re.match(r'gettabwinvar\((\d+), (\d+), "(\w+)"\)', expr)
		tabnr = int(match.group(1))
		winnr = int(match.group(2))
		varname = match.group(3)
		return tabpages[tabnr].windows[winnr].vars[varname]
	elif expr.startswith('type(function('):
		import re
		match = re.match(r'^type\(function\("([^"]+)"\)\) == 2$', expr)
		if not match:
			raise NotImplementedError(expr)
		return 0
	raise NotImplementedError(expr)


@_vim
def bindeval(expr):
	if expr == 'g:':
		return vars
	elif expr == '{}':
		return {}
	elif expr == '[]':
		return []
	import re
	match = re.compile(r'^function\("([^"\\]+)"\)$').match(expr)
	if match:
		return globals()['_emul_' + match.group(1)]
	else:
		raise NotImplementedError


@_vim
@_str_func
def _emul_mode(*args):
	if args and args[0]:
		return _mode
	else:
		return _mode[0]


@_vim
@_str_func
def _emul_getbufvar(bufnr, varname):
	import re
	if varname[0] == '&':
		if bufnr == '%':
			bufnr = current.buffer.number
		if bufnr not in buffers:
			return ''
		try:
			return buffers[bufnr].options[varname[1:]]
		except KeyError:
			try:
				return options[varname[1:]]
			except KeyError:
				return ''
	elif re.match('^[a-zA-Z_]+$', varname):
		if bufnr == '%':
			bufnr = current.buffer.number
		if bufnr not in buffers:
			return ''
		return buffers[bufnr].vars[varname]
	raise NotImplementedError


@_vim
@_str_func
def _emul_getwinvar(winnr, varname):
	return current.tabpage.windows[winnr].vars.get(varname, '')


@_vim
def _emul_setwinvar(winnr, varname, value):
	current.tabpage.windows[winnr].vars[varname] = value


@_vim
def _emul_virtcol(expr):
	if expr == '.':
		return current.window.cursor[1] + 1
	if isinstance(expr, list) and len(expr) == 3:
		return expr[-2] + expr[-1]
	raise NotImplementedError


_v_pos = None


@_vim
def _emul_getpos(expr):
	if expr == '.':
		return [0, current.window.cursor[0] + 1, current.window.cursor[1] + 1, 0]
	if expr == 'v':
		return _v_pos or [0, current.window.cursor[0] + 1, current.window.cursor[1] + 1, 0]
	raise NotImplementedError


@_vim
@_str_func
def _emul_fnamemodify(path, modstring):
	import os
	_modifiers = {
		'~': lambda path: path.replace(os.environ['HOME'].encode('utf-8'), b'~') if path.startswith(os.environ['HOME'].encode('utf-8')) else path,
		'.': lambda path: (lambda tpath: path if tpath[:3] == b'..' + os.sep.encode() else tpath)(os.path.relpath(path)),
		't': lambda path: os.path.basename(path),
		'h': lambda path: os.path.dirname(path),
	}

	for mods in modstring.split(':')[1:]:
		path = _modifiers[mods](path)
	return path


@_vim
@_str_func
def _emul_expand(expr):
	global _abuf
	if expr == '<abuf>':
		return _abuf or current.buffer.number
	raise NotImplementedError


@_vim
def _emul_bufnr(expr):
	if expr == '$':
		return _last_bufnr
	raise NotImplementedError


@_vim
def _emul_exists(ident):
	if ident.startswith('g:'):
		return ident[2:] in vars
	elif ident.startswith(':'):
		return 0
	raise NotImplementedError


@_vim
def _emul_line2byte(line):
	buflines = current.buffer._buf_lines
	if line == len(buflines) + 1:
		return sum((len(s) for s in buflines)) + 1
	raise NotImplementedError


@_vim
def _emul_line(expr):
	cursorline = current.window.cursor[0] + 1
	numlines = len(current.buffer._buf_lines)
	if expr == 'w0':
		return max(cursorline - 5, 1)
	if expr == 'w$':
		return min(cursorline + 5, numlines)
	raise NotImplementedError


@_vim
@_str_func
def _emul_strtrans(s):
	# FIXME Do more replaces
	return s.replace(b'\xFF', b'<ff>')


@_vim
@_str_func
def _emul_bufname(bufnr):
	try:
		return buffers[bufnr]._name or b''
	except KeyError:
		return b''


_window_id = 0


class _Window(object):
	def __init__(self, number, buffer=None, cursor=(1, 0), width=80):
		global _window_id
		self.cursor = cursor
		self.width = width
		self.number = number
		if buffer:
			if type(buffer) is _Buffer:
				self.buffer = buffer
			else:
				self.buffer = _Buffer(**buffer)
		else:
			self.buffer = _Buffer()
		_window_id += 1
		self._window_id = _window_id
		self.options = {}
		self.vars = {
			'powerline_window_id': self._window_id,
		}

	def __repr__(self):
		return '<window ' + str(self.number - 1) + '>'


class _Tabpage(object):
	def __init__(self, number):
		self.windows = _ObjList(_Window)
		self.number = number

	def _new_window(self, **kwargs):
		self.window = self.windows._new(**kwargs)
		return self.window

	def _close_window(self, winnr, open_window=True):
		curwinnr = self.window.number
		win = self.windows._pop(winnr)
		if self.windows and winnr == curwinnr:
			self.window = self.windows[-1]
		elif open_window:
			current.tabpage._new_window()
		return win

	def _close(self):
		global _tabpage
		while self.windows:
			self._close_window(1, False)
		tabpages._pop(self.number)
		_tabpage = len(tabpages)


tabpages = _ObjList(_Tabpage)


_abuf = None


class _Buffer(object):
	def __init__(self, name=None):
		global _last_bufnr
		_last_bufnr += 1
		bufnr = _last_bufnr
		self.number = bufnr
		# FIXME Use unicode() for python-3
		self.name = name
		self.vars = {'changedtick': 1}
		self.options = {
			'modified': 0,
			'readonly': 0,
			'fileformat': 'unix',
			'filetype': '',
			'buftype': '',
			'fileencoding': 'utf-8',
			'textwidth': 80,
		}
		self._buf_lines = ['']
		self._undostate = [self._buf_lines[:]]
		self._undo_written = len(self._undostate)
		buffers[bufnr] = self

	@property
	def name(self):
		import sys
		if sys.version_info < (3,):
			return self._name
		else:
			return str(self._name, 'utf-8') if self._name else None

	@name.setter
	def name(self, name):
		if name is None:
			self._name = None
		else:
			import os
			if type(name) is not bytes:
				name = name.encode('utf-8')
			if b':/' in name:
				self._name = name
			else:
				self._name = os.path.abspath(name)

	def __getitem__(self, line):
		return self._buf_lines[line]

	def __setitem__(self, line, value):
		self.options['modified'] = 1
		self.vars['changedtick'] += 1
		self._buf_lines[line] = value
		from copy import copy
		self._undostate.append(copy(self._buf_lines))

	def __setslice__(self, *args):
		self.options['modified'] = 1
		self.vars['changedtick'] += 1
		self._buf_lines.__setslice__(*args)
		from copy import copy
		self._undostate.append(copy(self._buf_lines))

	def __getslice__(self, *args):
		return self._buf_lines.__getslice__(*args)

	def __len__(self):
		return len(self._buf_lines)

	def __repr__(self):
		return '<buffer ' + str(self.name) + '>'

	def __del__(self):
		global _abuf
		bufnr = self.number
		try:
			import __main__
		except ImportError:
			pass
		except RuntimeError:
			# Module may have already been garbage-collected
			pass
		else:
			if _on_wipeout:
				_abuf = bufnr
				try:
					for event in _on_wipeout:
						exec(event, __main__.__dict__)
				finally:
					_abuf = None


class _Current(object):
	@property
	def buffer(self):
		return self.window.buffer

	@property
	def window(self):
		return self.tabpage.window

	@property
	def tabpage(self):
		return tabpages[_tabpage - 1]


current = _Current()


_dict = None


@_vim
def _init():
	global _dict

	if _dict:
		return _dict

	_dict = {}
	for varname, value in globals().items():
		if varname[0] != '_':
			_dict[varname] = value
	_tabnew()
	return _dict


@_vim
def _get_segment_info():
	mode_translations = {
		chr(ord('V') - 0x40): '^V',
		chr(ord('S') - 0x40): '^S',
	}
	mode = _mode
	mode = mode_translations.get(mode, mode)
	window = current.window
	buffer = current.buffer
	tabpage = current.tabpage
	return {
		'window': window,
		'winnr': window.number,
		'buffer': buffer,
		'bufnr': buffer.number,
		'tabpage': tabpage,
		'tabnr': tabpage.number,
		'window_id': window._window_id,
		'mode': mode,
		'encoding': options['encoding'],
	}


@_vim
def _launch_event(event):
	pass


@_vim
def _start_mode(mode):
	global _mode
	if mode == 'i':
		_launch_event('InsertEnter')
	elif _mode == 'i':
		_launch_event('InsertLeave')
	_mode = mode


@_vim
def _undo():
	if len(current.buffer._undostate) == 1:
		return
	buffer = current.buffer
	buffer._undostate.pop(-1)
	buffer._buf_lines = buffer._undostate[-1]
	if buffer._undo_written == len(buffer._undostate):
		buffer.options['modified'] = 0


@_vim
def _edit(name=None):
	if current.buffer.name is None:
		buffer = current.buffer
		buffer.name = name
	else:
		buffer = _Buffer(name)
		current.window.buffer = buffer


@_vim
def _tabnew(name=None):
	global windows
	global _tabpage
	tabpage = tabpages._new()
	windows = tabpage.windows
	_tabpage = len(tabpages)
	_new(name)
	return tabpage


@_vim
def _new(name=None):
	current.tabpage._new_window(buffer={'name': name})


@_vim
def _split():
	current.tabpage._new_window(buffer=current.buffer)


@_vim
def _close(winnr, wipe=True):
	win = current.tabpage._close_window(winnr)
	if wipe:
		for w in current.tabpage.windows:
			if w.buffer.number == win.buffer.number:
				break
		else:
			_bw(win.buffer.number)


@_vim
def _bw(bufnr=None):
	bufnr = bufnr or current.buffer.number
	winnr = 1
	for win in current.tabpage.windows:
		if win.buffer.number == bufnr:
			_close(winnr, wipe=False)
		winnr += 1
	buffers._pop(bufnr)
	if not buffers:
		_Buffer()
	_b(max(buffers._keys()))


@_vim
def _b(bufnr):
	current.window.buffer = buffers[bufnr]


@_vim
def _set_cursor(line, col):
	current.window.cursor = (line, col)
	if _mode == 'n':
		_launch_event('CursorMoved')
	elif _mode == 'i':
		_launch_event('CursorMovedI')


@_vim
def _get_buffer():
	return current.buffer


@_vim
def _set_bufoption(option, value, bufnr=None):
	buffers[bufnr or current.buffer.number].options[option] = value
	if option == 'filetype':
		_launch_event('FileType')


class _WithNewBuffer(object):
	def __init__(self, func, *args, **kwargs):
		self.call = lambda: func(*args, **kwargs)

	def __enter__(self):
		self.call()
		self.bufnr = current.buffer.number
		return _get_segment_info()

	def __exit__(self, *args):
		_bw(self.bufnr)


@_vim
def _set_dict(d, new, setfunc=None):
	if not setfunc:
		def setfunc(k, v):
			d[k] = v

	old = {}
	na = []
	for k, v in new.items():
		try:
			old[k] = d[k]
		except KeyError:
			na.append(k)
		setfunc(k, v)
	return old, na


class _WithBufOption(object):
	def __init__(self, **new):
		self.new = new

	def __enter__(self):
		self.buffer = current.buffer
		self.old = _set_dict(self.buffer.options, self.new, _set_bufoption)[0]

	def __exit__(self, *args):
		self.buffer.options.update(self.old)


class _WithMode(object):
	def __init__(self, new):
		self.new = new

	def __enter__(self):
		self.old = _mode
		_start_mode(self.new)
		return _get_segment_info()

	def __exit__(self, *args):
		_start_mode(self.old)


class _WithDict(object):
	def __init__(self, d, **new):
		self.new = new
		self.d = d

	def __enter__(self):
		self.old, self.na = _set_dict(self.d, self.new)

	def __exit__(self, *args):
		self.d.update(self.old)
		for k in self.na:
			self.d.pop(k)


class _WithSplit(object):
	def __enter__(self):
		_split()

	def __exit__(self, *args):
		_close(2, wipe=False)


class _WithBufName(object):
	def __init__(self, new):
		self.new = new

	def __enter__(self):
		import os
		buffer = current.buffer
		self.buffer = buffer
		self.old = buffer.name
		buffer.name = self.new

	def __exit__(self, *args):
		self.buffer.name = self.old


class _WithNewTabPage(object):
	def __init__(self, *args, **kwargs):
		self.args = args
		self.kwargs = kwargs

	def __enter__(self):
		self.tab = _tabnew(*self.args, **self.kwargs)

	def __exit__(self, *args):
		self.tab._close()


class _WithGlobal(object):
	def __init__(self, **kwargs):
		self.kwargs = kwargs

	def __enter__(self):
		self.empty = object()
		self.old = dict(((key, globals().get(key, self.empty)) for key in self.kwargs))
		globals().update(self.kwargs)

	def __exit__(self, *args):
		for k, v in self.old.items():
			if v is self.empty:
				globals().pop(k, None)
			else:
				globals()[k] = v


@_vim
def _with(key, *args, **kwargs):
	if key == 'buffer':
		return _WithNewBuffer(_edit, *args, **kwargs)
	elif key == 'bufname':
		return _WithBufName(*args, **kwargs)
	elif key == 'mode':
		return _WithMode(*args, **kwargs)
	elif key == 'bufoptions':
		return _WithBufOption(**kwargs)
	elif key == 'options':
		return _WithDict(options, **kwargs)
	elif key == 'globals':
		return _WithDict(vars, **kwargs)
	elif key == 'wvars':
		return _WithDict(current.window.vars, **kwargs)
	elif key == 'environ':
		return _WithDict(_environ, **kwargs)
	elif key == 'split':
		return _WithSplit()
	elif key == 'tabpage':
		return _WithNewTabPage(*args, **kwargs)
	elif key == 'vpos':
		return _WithGlobal(_v_pos=[0, kwargs['line'], kwargs['col'], kwargs['off']])


class error(Exception):
	pass
