# vim:fileencoding=utf-8:noet

from powerline.lint.markedjson import load
from powerline import find_config_file, Powerline
from powerline.lib.config import load_json_config
from powerline.lint.markedjson.error import echoerr, MarkedError
from powerline.segments.vim import vim_modes
from powerline.lint.inspect import getconfigargspec
from powerline.lib.threaded import ThreadedSegment
import itertools
import sys
import os
import re
from collections import defaultdict
from copy import copy
import logging


try:
	from __builtin__ import unicode
except ImportError:
	unicode = str


def open_file(path):
	return open(path, 'rb')


EMPTYTUPLE = tuple()


class JStr(unicode):
	def join(self, iterable):
		return super(JStr, self).join((unicode(item) for item in iterable))


key_sep = JStr('/')
list_sep = JStr(', ')


def context_key(context):
	return key_sep.join((c[0] for c in context))


class EchoErr(object):
	__slots__ = ('echoerr', 'logger',)

	def __init__(self, echoerr, logger):
		self.echoerr = echoerr
		self.logger = logger

	def __call__(self, *args, **kwargs):
		self.echoerr(*args, **kwargs)


class DelayedEchoErr(EchoErr):
	__slots__ = ('echoerr', 'logger', 'errs')

	def __init__(self, echoerr):
		super(DelayedEchoErr, self).__init__(echoerr, echoerr.logger)
		self.errs = []

	def __call__(self, *args, **kwargs):
		self.errs.append((args, kwargs))

	def echo_all(self):
		for args, kwargs in self.errs:
			self.echoerr(*args, **kwargs)

	def __nonzero__(self):
		return not not self.errs


class Spec(object):
	def __init__(self, **keys):
		new_keys = {}
		self.specs = list(keys.values())
		for k, v in keys.items():
			new_keys[k] = len(self.specs)
			self.specs.append(v)
		self.keys = new_keys
		self.checks = []
		self.cmsg = ''
		self.isoptional = False
		self.uspecs = []
		self.ufailmsg = lambda key: 'found unknown key: {0}'.format(key)
		if keys:
			self.type(dict)

	def copy(self):
		return self.__class__().update(self.__dict__)

	def update(self, d):
		self.__dict__.update(d)
		self.checks = copy(self.checks)
		self.uspecs = copy(self.uspecs)
		self.specs = [spec.copy() for spec in self.specs]
		return self

	def unknown_spec(self, keyfunc, spec):
		if isinstance(keyfunc, Spec):
			self.specs.append(keyfunc)
			keyfunc = len(self.specs) - 1
		self.specs.append(spec)
		self.uspecs.append((keyfunc, len(self.specs) - 1))
		return self

	def unknown_msg(self, msgfunc):
		self.ufailmsg = msgfunc
		return self

	def context_message(self, msg):
		self.cmsg = msg
		for spec in self.specs:
			if not spec.cmsg:
				spec.context_message(msg)
		return self

	def check_type(self, value, context_mark, data, context, echoerr, types):
		if type(value.value) not in types:
			echoerr(context=self.cmsg.format(key=context_key(context)),
					context_mark=context_mark,
					problem='{0!r} must be a {1} instance, not {2}'.format(
						value,
						list_sep.join((t.__name__ for t in types)),
						type(value.value).__name__
					),
					problem_mark=value.mark)
			return False, True
		return True, False

	def check_func(self, value, context_mark, data, context, echoerr, func, msg_func):
		proceed, echo, hadproblem = func(value, data, context, echoerr)
		if echo and hadproblem:
			echoerr(context=self.cmsg.format(key=context_key(context)),
					context_mark=context_mark,
					problem=msg_func(value),
					problem_mark=value.mark)
		return proceed, hadproblem

	def check_list(self, value, context_mark, data, context, echoerr, item_func, msg_func):
		i = 0
		hadproblem = False
		for item in value:
			if isinstance(item_func, int):
				spec = self.specs[item_func]
				proceed, fhadproblem = spec.match(item, value.mark, data, context + (('list item ' + unicode(i), item),), echoerr)
			else:
				proceed, echo, fhadproblem = item_func(item, data, context, echoerr)
				if echo and fhadproblem:
					echoerr(context=self.cmsg.format(key=context_key(context) + '/list item ' + unicode(i)),
							context_mark=value.mark,
							problem=msg_func(item),
							problem_mark=item.mark)
			if fhadproblem:
				hadproblem = True
			if not proceed:
				return proceed, hadproblem
			i += 1
		return True, hadproblem

	def check_either(self, value, context_mark, data, context, echoerr, start, end):
		new_echoerr = DelayedEchoErr(echoerr)

		hadproblem = False
		for spec in self.specs[start:end]:
			proceed, hadproblem = spec.match(value, value.mark, data, context, new_echoerr)
			if not proceed:
				break
			if not hadproblem:
				return True, False

		new_echoerr.echo_all()

		return False, hadproblem

	def check_tuple(self, value, context_mark, data, context, echoerr, start, end):
		hadproblem = False
		for (i, item, spec) in zip(itertools.count(), value, self.specs[start:end]):
			proceed, ihadproblem = spec.match(item, value.mark, data, context + (('tuple item ' + unicode(i), item),), echoerr)
			if ihadproblem:
				hadproblem = True
			if not proceed:
				return False, hadproblem
		return True, hadproblem

	def type(self, *args):
		self.checks.append(('check_type', args))
		return self

	cmp_funcs = {
		'le': lambda x, y: x <= y,
		'lt': lambda x, y: x < y,
		'ge': lambda x, y: x >= y,
		'gt': lambda x, y: x > y,
		'eq': lambda x, y: x == y,
	}

	cmp_msgs = {
		'le': 'lesser or equal to',
		'lt': 'lesser then',
		'ge': 'greater or equal to',
		'gt': 'greater then',
		'eq': 'equal to',
	}

	def len(self, comparison, cint, msg_func=None):
		cmp_func = self.cmp_funcs[comparison]
		msg_func = msg_func or (lambda value: 'length of {0!r} is not {1} {2}'.format(value, self.cmp_msgs[comparison], cint))
		self.checks.append(('check_func',
					(lambda value, *args: (True, True, not cmp_func(len(value), cint))),
					msg_func))
		return self

	def cmp(self, comparison, cint, msg_func=None):
		if type(cint) is str:
			self.type(unicode)
		elif type(cint) is float:
			self.type(int, float)
		else:
			self.type(type(cint))
		cmp_func = self.cmp_funcs[comparison]
		msg_func = msg_func or (lambda value: '{0} is not {1} {2}'.format(value, self.cmp_msgs[comparison], cint))
		self.checks.append(('check_func',
					(lambda value, *args: (True, True, not cmp_func(value.value, cint))),
					msg_func))
		return self

	def unsigned(self, msg_func=None):
		self.type(int)
		self.checks.append(('check_func',
					(lambda value, *args: (True, True, value < 0)),
					lambda value: '{0} must be greater then zero'.format(value)))
		return self

	def list(self, item_func, msg_func=None):
		self.type(list)
		if isinstance(item_func, Spec):
			self.specs.append(item_func)
			item_func = len(self.specs) - 1
		self.checks.append(('check_list', item_func, msg_func or (lambda item: 'failed check')))
		return self

	def tuple(self, *specs):
		self.type(list)

		max_len = len(specs)
		min_len = max_len
		for spec in reversed(specs):
			if spec.isoptional:
				min_len -= 1
			else:
				break
		if max_len == min_len:
			self.len('eq', len(specs))
		else:
			self.len('ge', min_len)
			self.len('le', max_len)

		start = len(self.specs)
		for i, spec in zip(itertools.count(), specs):
			self.specs.append(spec)
		self.checks.append(('check_tuple', start, len(self.specs)))
		return self

	def func(self, func, msg_func=None):
		self.checks.append(('check_func', func, msg_func or (lambda value: 'failed check')))
		return self

	def re(self, regex, msg_func=None):
		self.type(unicode)
		compiled = re.compile(regex)
		msg_func = msg_func or (lambda value: 'String "{0}" does not match "{1}"'.format(value, regex))
		self.checks.append(('check_func',
					(lambda value, *args: (True, True, not compiled.match(value.value))),
					msg_func))
		return self

	def ident(self, msg_func=None):
		msg_func = msg_func or (lambda value: 'String "{0}" is not an alphanumeric/underscore identifier'.format(value))
		return self.re('^\w+$', msg_func)

	def oneof(self, collection, msg_func=None):
		msg_func = msg_func or (lambda value: '"{0}" must be one of {1!r}'.format(value, list(collection)))
		self.checks.append(('check_func',
						lambda value, *args: (True, True, value not in collection),
						msg_func))
		return self

	def error(self, msg):
		self.checks.append(('check_func', lambda *args: (True, True, True),
							lambda value: msg.format(value)))
		return self

	def either(self, *specs):
		start = len(self.specs)
		self.specs.extend(specs)
		self.checks.append(('check_either', start, len(self.specs)))
		return self

	def optional(self):
		self.isoptional = True
		return self

	def match_checks(self, *args):
		hadproblem = False
		for check in self.checks:
			proceed, chadproblem = getattr(self, check[0])(*(args + check[1:]))
			if chadproblem:
				hadproblem = True
			if not proceed:
				return False, hadproblem
		return True, hadproblem

	def match(self, value, context_mark=None, data=None, context=EMPTYTUPLE, echoerr=echoerr):
		proceed, hadproblem = self.match_checks(value, context_mark, data, context, echoerr)
		if proceed:
			if self.keys or self.uspecs:
				for key, vali in self.keys.items():
					valspec = self.specs[vali]
					if key in value:
						proceed, mhadproblem = valspec.match(value[key], value.mark, data, context + ((key, value[key]),), echoerr)
						if mhadproblem:
							hadproblem = True
						if not proceed:
							return False, hadproblem
					else:
						if not valspec.isoptional:
							hadproblem = True
							echoerr(context=self.cmsg.format(key=context_key(context)),
									context_mark=None,
									problem='required key is missing: {0}'.format(key),
									problem_mark=value.mark)
				for key in value.keys():
					if key not in self.keys:
						for keyfunc, vali in self.uspecs:
							valspec = self.specs[vali]
							if isinstance(keyfunc, int):
								spec = self.specs[keyfunc]
								proceed, khadproblem = spec.match(key, context_mark, data, context, echoerr)
							else:
								proceed, khadproblem = keyfunc(key, data, context, echoerr)
							if khadproblem:
								hadproblem = True
							if proceed:
								proceed, vhadproblem = valspec.match(value[key], value.mark, data, context + ((key, value[key]),), echoerr)
								if vhadproblem:
									hadproblem = True
								break
						else:
							hadproblem = True
							if self.ufailmsg:
								echoerr(context=self.cmsg.format(key=context_key(context)),
										context_mark=None,
										problem=self.ufailmsg(key),
										problem_mark=key.mark)
		return True, hadproblem


class WithPath(object):
	def __init__(self, import_paths):
		self.import_paths = import_paths

	def __enter__(self):
		self.oldpath = sys.path
		sys.path = self.import_paths + sys.path

	def __exit__(self, *args):
		sys.path = self.oldpath


def check_matcher_func(ext, match_name, data, context, echoerr):
	import_paths = [os.path.expanduser(path) for path in context[0][1].get('common', {}).get('paths', [])]

	match_module, separator, match_function = match_name.rpartition('.')
	if not separator:
		match_module = 'powerline.matchers.{0}'.format(ext)
		match_function = match_name
	with WithPath(import_paths):
		try:
			func = getattr(__import__(match_module, fromlist=[match_function]), unicode(match_function))
		except ImportError:
			echoerr(context='Error while loading matcher functions',
					problem='failed to load module {0}'.format(match_module),
					problem_mark=match_name.mark)
			return True, True
		except AttributeError:
			echoerr(context='Error while loading matcher functions',
					problem='failed to load matcher function {0}'.format(match_function),
					problem_mark=match_name.mark)
			return True, True

	if not callable(func):
		echoerr(context='Error while loading matcher functions',
				problem='loaded "function" {0} is not callable'.format(match_function),
				problem_mark=match_name.mark)
		return True, True

	if hasattr(func, 'func_code') and hasattr(func.func_code, 'co_argcount'):
		if func.func_code.co_argcount != 1:
			echoerr(context='Error while loading matcher functions',
					problem='function {0} accepts {1} arguments instead of 1. Are you sure it is the proper function?'.format(match_function, func.func_code.co_argcount),
					problem_mark=match_name.mark)

	return True, False


def check_ext(ext, data, context, echoerr):
	hadsomedirs = False
	hadproblem = False
	for subdir in ('themes', 'colorschemes'):
		if ext not in data['configs'][subdir]:
			hadproblem = True
			echoerr(context='Error while loading {0} extension configuration'.format(ext),
					context_mark=ext.mark,
					problem='{0} configuration does not exist'.format(subdir))
		else:
			hadsomedirs = True
	return hadsomedirs, hadproblem


def check_config(d, theme, data, context, echoerr):
	if len(context) == 4:
		ext = context[-2][0]
	else:
		# local_themes
		ext = context[-3][0]
	if ext not in data['configs'][d] or theme not in data['configs'][d][ext]:
		echoerr(context='Error while loading {0} from {1} extension configuration'.format(d[:-1], ext),
				problem='failed to find configuration file {0}/{1}/{2}.json'.format(d, ext, theme),
				problem_mark=theme.mark)
		return True, False, True
	return True, False, False


divider_spec = Spec().type(unicode).len('le', 3,
					lambda value: 'Divider {0!r} is too large!'.format(value)).copy
divside_spec = Spec(
	hard=divider_spec(),
	soft=divider_spec(),
).copy
colorscheme_spec = Spec().type(unicode).func(lambda *args: check_config('colorschemes', *args)).copy
theme_spec = Spec().type(unicode).func(lambda *args: check_config('themes', *args)).copy
main_spec = (Spec(
	common=Spec(
		dividers=Spec(
			left=divside_spec(),
			right=divside_spec(),
		),
		spaces=Spec().unsigned().cmp('le', 2,
							lambda value: 'Are you sure you need such a big ({0}) number of spaces?'.format(value)),
		term_truecolor=Spec().type(bool).optional(),
		# Python is capable of loading from zip archives. Thus checking path 
		# only for existence of the path, not for it being a directory
		paths=Spec().list((lambda value, *args: (True, True, not os.path.exists(os.path.expanduser(value.value)))),
					lambda value: 'path does not exist: {0}'.format(value)).optional(),
		log_file=Spec().type(str).func(lambda value, *args: (True, True, not os.path.isdir(os.path.dirname(os.path.expanduser(value)))),
						lambda value: 'directory does not exist: {0}'.format(os.path.dirname(value))).optional(),
		log_level=Spec().re('^[A-Z]+$').func(lambda value, *args: (True, True, not hasattr(logging, value)),
										lambda value: 'unknown debugging level {0}'.format(value)).optional(),
		log_format=Spec().type(str).optional(),
		interval=Spec().either(Spec().cmp('gt', 0.0), Spec().type(type(None))).optional(),
		reload_config=Spec().type(bool).optional(),
	).context_message('Error while loading common configuration (key {key})'),
	ext=Spec(
		vim=Spec(
			colorscheme=colorscheme_spec(),
			theme=theme_spec(),
			local_themes=Spec()
				.unknown_spec(lambda *args: check_matcher_func('vim', *args), theme_spec())
		).optional(),
		ipython=Spec(
			colorscheme=colorscheme_spec(),
			theme=theme_spec(),
			local_themes=Spec(
				in2=theme_spec(),
				out=theme_spec(),
				rewrite=theme_spec(),
			),
		).optional(),
		shell=Spec(
			colorscheme=colorscheme_spec(),
			theme=theme_spec(),
			local_themes=Spec(
				continuation=theme_spec(),
				select=theme_spec(),
			),
		).optional(),
	).unknown_spec(check_ext,
				Spec(
					colorscheme=colorscheme_spec(),
					theme=theme_spec(),
				))
	.context_message('Error while loading extensions configuration (key {key})'),
).context_message('Error while loading main configuration'))

term_color_spec = Spec().unsigned().cmp('le', 255).copy
true_color_spec = Spec().re('^[0-9a-fA-F]{6}$',
						lambda value: '"{0}" is not a six-digit hexadecimal unsigned integer written as a string'.format(value)).copy
colors_spec = (Spec(
	colors=Spec().unknown_spec(
		Spec().ident(),
		Spec().either(
			Spec().tuple(term_color_spec(), true_color_spec()),
			term_color_spec()))
	.context_message('Error while checking colors (key {key})'),
	gradients=Spec().unknown_spec(
		Spec().ident(),
		Spec().tuple(
			Spec().len('gt', 1).list(term_color_spec()),
			Spec().len('gt', 1).list(true_color_spec()).optional(),
		)
	).context_message('Error while checking gradients (key {key})'),
).context_message('Error while loading colors configuration'))


def check_color(color, data, context, echoerr):
	if color not in data['colors_config'].get('colors', {}) and color not in data['colors_config'].get('gradients', {}):
		echoerr(context='Error while checking highlight group in colorscheme (key {key})'.format(key=context_key(context)),
				problem='found unexistent color or gradient {0}'.format(color),
				problem_mark=color.mark)
		return True, False, True
	return True, False, False


def check_translated_group_name(group, data, context, echoerr):
	if group not in context[0][1].get('groups', {}):
		echoerr(context='Error while checking translated group in colorscheme (key {key})'.format(key=context_key(context)),
				problem='translated group {0} is not in main groups dictionary'.format(group),
				problem_mark=group.mark)
		return True, False, True
	return True, False, False


color_spec = Spec().type(unicode).func(check_color).copy
name_spec = Spec().type(unicode).len('gt', 0).optional().copy
group_spec = Spec(
	fg=color_spec(),
	bg=color_spec(),
	attr=Spec().list(Spec().type(unicode).oneof(set(('bold', 'italic', 'underline')))).optional(),
).copy
group_name_spec = Spec().re('^\w+(?::\w+)?$').copy
groups_spec = Spec().unknown_spec(
	group_name_spec(),
	group_spec(),
).context_message('Error while loading groups (key {key})').copy
colorscheme_spec = (Spec(
	name=name_spec(),
	groups=groups_spec(),
).context_message('Error while loading coloscheme'))
vim_mode_spec = Spec().oneof(set(list(vim_modes) + ['nc'])).copy
vim_colorscheme_spec = (Spec(
	name=name_spec(),
	groups=groups_spec(),
	mode_translations=Spec().unknown_spec(
		vim_mode_spec(),
		Spec(
			colors=Spec().unknown_spec(
				color_spec(),
				color_spec(),
			).optional(),
			groups=Spec().unknown_spec(
				group_name_spec().func(check_translated_group_name),
				group_spec(),
			).optional(),
		),
	).context_message('Error while loading mode translations (key {key})'),
).context_message('Error while loading vim colorscheme'))
shell_mode_spec = Spec().re('^(?:[\w\-]+|\.safe)$').copy
shell_colorscheme_spec = (Spec(
	name=name_spec(),
	groups=groups_spec(),
	mode_translations=Spec().unknown_spec(
		shell_mode_spec(),
		Spec(
			colors=Spec().unknown_spec(
				color_spec(),
				color_spec(),
			).optional(),
			groups=Spec().unknown_spec(
				group_name_spec().func(check_translated_group_name),
				group_spec(),
			).optional(),
		),
	).context_message('Error while loading mode translations (key {key})'),
).context_message('Error while loading shell colorscheme'))


generic_keys = set(('exclude_modes', 'include_modes', 'width', 'align', 'name', 'draw_soft_divider', 'draw_hard_divider', 'priority', 'after', 'before'))
type_keys = {
		'function': set(('args', 'module', 'draw_inner_divider')),
		'string': set(('contents', 'type', 'highlight_group', 'divider_highlight_group')),
		'filler': set(('type', 'highlight_group', 'divider_highlight_group')),
		}
required_keys = {
		'function': set(),
		'string': set(('contents',)),
		'filler': set(),
		}
function_keys = set(('args', 'module'))
highlight_keys = set(('highlight_group', 'name'))


def check_key_compatibility(segment, data, context, echoerr):
	segment_type = segment.get('type', 'function')

	if segment_type not in type_keys:
		echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
				problem='found segment with unknown type {0}'.format(segment_type),
				problem_mark=segment_type.mark)
		return False, False, True

	hadproblem = False

	keys = set(segment)
	if not ((keys - generic_keys) < type_keys[segment_type]):
		unknown_keys = keys - generic_keys - type_keys[segment_type]
		echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),
				context_mark=context[-1][1].mark,
				problem='found keys not used with the current segment type: {0}'.format(
					list_sep.join(unknown_keys)),
				problem_mark=list(unknown_keys)[0].mark)
		hadproblem = True

	if not (keys > required_keys[segment_type]):
		missing_keys = required_keys[segment_type] - keys
		echoerr(context='Error while checking segments (key {key})'.format(key=context_key(context)),