# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)

import os
import logging

from collections import defaultdict
from itertools import chain
from functools import partial

from powerline import generate_config_finder, get_config_paths, load_config
from powerline.segments.vim import vim_modes
from powerline.lib.dict import mergedicts_copy
from powerline.lib.config import ConfigLoader
from powerline.lib.unicode import unicode
from powerline.lib.path import join
from powerline.lint.markedjson import load
from powerline.lint.markedjson.error import echoerr, EchoErr, MarkedError
from powerline.lint.checks import (check_matcher_func, check_ext, check_config, check_top_theme,
                                   check_color, check_translated_group_name, check_group,
                                   check_segment_module, check_exinclude_function, type_keys,
                                   check_segment_function, check_args, get_one_segment_function,
                                   check_highlight_groups, check_highlight_group, check_full_segment_data,
                                   get_all_possible_functions, check_segment_data_key, register_common_name,
                                   highlight_group_spec, check_log_file_level, check_logging_handler)
from powerline.lint.spec import Spec
from powerline.lint.context import Context


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


def generate_json_config_loader(lhadproblem):
	def load_json_config(config_file_path, load=load, open_file=open_file):
		with open_file(config_file_path) as config_file_fp:
			r, hadproblem = load(config_file_fp)
			if hadproblem:
				lhadproblem[0] = True
			return r
	return load_json_config


function_name_re = '^(\w+\.)*[a-zA-Z_]\w*$'


divider_spec = Spec().printable().len(
	'le', 3, (lambda value: 'Divider {0!r} is too large!'.format(value))).copy
ext_theme_spec = Spec().type(unicode).func(lambda *args: check_config('themes', *args)).copy
top_theme_spec = Spec().type(unicode).func(check_top_theme).copy
ext_spec = Spec(
	colorscheme=Spec().type(unicode).func(
		(lambda *args: check_config('colorschemes', *args))
	),
	theme=ext_theme_spec(),
	top_theme=top_theme_spec().optional(),
).copy
gen_components_spec = (lambda *components: Spec().list(Spec().type(unicode).oneof(set(components))))
log_level_spec = Spec().re('^[A-Z]+$').func(
	(lambda value, *args: (True, True, not hasattr(logging, value))),
	(lambda value: 'unknown debugging level {0}'.format(value))
).copy
log_format_spec = Spec().type(unicode).copy
main_spec = (Spec(
	common=Spec(
		default_top_theme=top_theme_spec().optional(),
		term_truecolor=Spec().type(bool).optional(),
		term_escape_style=Spec().type(unicode).oneof(set(('auto', 'xterm', 'fbterm'))).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().either(
			Spec().type(unicode).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)))
			),
			Spec().list(Spec().either(
				Spec().type(unicode, type(None)),
				Spec().tuple(
					Spec().re(function_name_re).func(check_logging_handler),
					Spec().tuple(
						Spec().type(list).optional(),
						Spec().type(dict).optional(),
					),
					log_level_spec().func(check_log_file_level).optional(),
					log_format_spec().optional(),
				),
			))
		).optional(),
		log_level=log_level_spec().optional(),
		log_format=log_format_spec().optional(),
		interval=Spec().either(Spec().cmp('gt', 0.0), Spec().type(type(None))).optional(),
		reload_config=Spec().type(bool).optional(),
		watcher=Spec().type(unicode).oneof(set(('auto', 'inotify', 'stat'))).optional(),
	).context_message('Error while loading common configuration (key {key})'),
	ext=Spec(
		vim=ext_spec().update(
			components=gen_components_spec('statusline', 'tabline').optional(),
			local_themes=Spec(
				__tabline__=ext_theme_spec(),
			).unknown_spec(
				Spec().re(function_name_re).func(partial(check_matcher_func, 'vim')),
				ext_theme_spec()
			),
		).optional(),
		ipython=ext_spec().update(
			local_themes=Spec(
				in2=ext_theme_spec(),
				out=ext_theme_spec(),
				rewrite=ext_theme_spec(),
			),
		).optional(),
		shell=ext_spec().update(
			components=gen_components_spec('tmux', 'prompt').optional(),
			local_themes=Spec(
				continuation=ext_theme_spec(),
				select=ext_theme_spec(),
			),
		).optional(),
		wm=ext_spec().update(
			local_themes=Spec().unknown_spec(
				Spec().re('^[0-9A-Za-z-]+$'),
				ext_theme_spec()
			).optional(),
			update_interval=Spec().cmp('gt', 0.0).optional(),
		).optional(),
	).unknown_spec(
		check_ext,
		ext_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'))


color_spec = Spec().type(unicode).func(check_color).copy
name_spec = Spec().type(unicode).len('gt', 0).optional().copy
group_name_spec = Spec().ident().copy
group_spec = Spec().either(Spec(
	fg=color_spec(),
	bg=color_spec(),
	attrs=Spec().list(Spec().type(unicode).oneof(set(('bold', 'italic', 'underline')))),
), group_name_spec().func(check_group)).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'))
mode_translations_value_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(),
).copy
top_colorscheme_spec = (Spec(
	name=name_spec(),
	groups=groups_spec(),
	mode_translations=Spec().unknown_spec(
		Spec().type(unicode),
		mode_translations_value_spec(),
	).optional().context_message('Error while loading mode translations (key {key})').optional(),
).context_message('Error while loading top-level coloscheme'))
vim_mode_spec = Spec().oneof(set(list(vim_modes) + ['nc', 'tab_nc', 'buf_nc'])).copy
vim_colorscheme_spec = (Spec(
	name=name_spec(),
	groups=groups_spec(),
	mode_translations=Spec().unknown_spec(
		vim_mode_spec(),
		mode_translations_value_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(),
		mode_translations_value_spec(),
	).optional().context_message('Error while loading mode translations (key {key})'),
).context_message('Error while loading shell colorscheme'))


args_spec = Spec(
	pl=Spec().error('pl object must be set by powerline').optional(),
	segment_info=Spec().error('Segment info dictionary must be set by powerline').optional(),
).unknown_spec(Spec(), Spec()).optional().copy
segment_module_spec = Spec().type(unicode).func(check_segment_module).optional().copy
exinclude_spec = Spec().re(function_name_re).func(check_exinclude_function).copy
segment_spec_base = Spec(
	name=Spec().re('^[a-zA-Z_]\w*$').optional(),
	function=Spec().re(function_name_re).func(check_segment_function).optional(),
	exclude_modes=Spec().list(vim_mode_spec()).optional(),
	include_modes=Spec().list(vim_mode_spec()).optional(),
	exclude_function=exinclude_spec().optional(),
	include_function=exinclude_spec().optional(),
	draw_hard_divider=Spec().type(bool).optional(),
	draw_soft_divider=Spec().type(bool).optional(),
	draw_inner_divider=Spec().type(bool).optional(),
	display=Spec().type(bool).optional(),
	module=segment_module_spec(),
	priority=Spec().type(int, float, type(None)).optional(),
	after=Spec().printable().optional(),
	before=Spec().printable().optional(),
	width=Spec().either(Spec().unsigned(), Spec().cmp('eq', 'auto')).optional(),
	align=Spec().oneof(set('lr')).optional(),
	args=args_spec().func(lambda *args, **kwargs: check_args(get_one_segment_function, *args, **kwargs)),
	contents=Spec().printable().optional(),
	highlight_groups=Spec().list(
		highlight_group_spec().re(
			'^(?:(?!:divider$).)+$',
			(lambda value: 'it is recommended that only divider highlight group names end with ":divider"')
		)
	).func(check_highlight_groups).optional(),
	divider_highlight_group=highlight_group_spec().func(check_highlight_group).re(
		':divider$',
		(lambda value: 'it is recommended that divider highlight group names end with ":divider"')
	).optional(),
).func(check_full_segment_data).copy
subsegment_spec = segment_spec_base().update(
	type=Spec().oneof(set((key for key in type_keys if key != 'segment_list'))).optional(),
)
segment_spec = segment_spec_base().update(
	type=Spec().oneof(type_keys).optional(),
	segments=Spec().optional().list(subsegment_spec),
)
segments_spec = Spec().optional().list(segment_spec).copy
segdict_spec = Spec(
	left=segments_spec().context_message('Error while loading segments from left side (key {key})'),
	right=segments_spec().context_message('Error while loading segments from right side (key {key})'),
).func(
	(lambda value, *args: (True, True, not (('left' in value) or ('right' in value)))),
	(lambda value: 'segments dictionary must contain either left, right or both keys')
).context_message('Error while loading segments (key {key})').copy
divside_spec = Spec(
	hard=divider_spec(),
	soft=divider_spec(),
).copy
segment_data_value_spec = Spec(
	after=Spec().printable().optional(),
	before=Spec().printable().optional(),
	display=Spec().type(bool).optional(),
	args=args_spec().func(lambda *args, **kwargs: check_args(get_all_possible_functions, *args, **kwargs)),
	contents=Spec().printable().optional(),
).copy
dividers_spec = Spec(
	left=divside_spec(),
	right=divside_spec(),
).copy
spaces_spec = Spec().unsigned().cmp(
	'le', 2, (lambda value: 'Are you sure you need such a big ({0}) number of spaces?'.format(value))
).copy
common_theme_spec = Spec(
	default_module=segment_module_spec().optional(),
	cursor_space=Spec().type(int, float).cmp('le', 100).cmp('gt', 0).optional(),
	cursor_columns=Spec().type(int).cmp('gt', 0).optional(),
).context_message('Error while loading theme').copy
top_theme_spec = common_theme_spec().update(
	dividers=dividers_spec(),
	spaces=spaces_spec(),
	use_non_breaking_spaces=Spec().type(bool).optional(),
	segment_data=Spec().unknown_spec(
		Spec().func(check_segment_data_key),
		segment_data_value_spec(),
	).optional().context_message('Error while loading segment data (key {key})'),
)
main_theme_spec = common_theme_spec().update(
	dividers=dividers_spec().optional(),
	spaces=spaces_spec().optional(),
	segment_data=Spec().unknown_spec(
		Spec().func(check_segment_data_key),
		segment_data_value_spec(),
	).optional().context_message('Error while loading segment data (key {key})'),
)
theme_spec = common_theme_spec().update(
	dividers=dividers_spec().optional(),
	spaces=spaces_spec().optional(),
	segment_data=Spec().unknown_spec(
		Spec().func(check_segment_data_key),
		segment_data_value_spec(),
	).optional().context_message('Error while loading segment data (key {key})'),
	segments=segdict_spec().update(above=Spec().list(segdict_spec()).optional()),
)


def register_common_names():
	register_common_name('player', 'powerline.segments.common.players', '_player')


def load_json_file(path):
	with open_file(path) as F:
		try:
			config, hadproblem = load(F)
		except MarkedError as e:
			return True, None, str(e)
		else:
			return hadproblem, config, None


def updated_with_config(d):
	hadproblem, config, error = load_json_file(d['path'])
	d.update(
		hadproblem=hadproblem,
		config=config,
		error=error,
	)
	return d


def find_all_ext_config_files(search_paths, subdir):
	for config_root in search_paths:
		top_config_subpath = join(config_root, subdir)
		if not os.path.isdir(top_config_subpath):
			if os.path.exists(top_config_subpath):
				yield {
					'error': 'Path {0} is not a directory'.format(top_config_subpath),
					'path': top_config_subpath,
				}
			continue
		for ext_name in os.listdir(top_config_subpath):
			ext_path = os.path.join(top_config_subpath, ext_name)
			if not os.path.isdir(ext_path):
				if ext_name.endswith('.json') and os.path.isfile(ext_path):
					yield updated_with_config({
						'error': False,
						'path': ext_path,
						'name': ext_name[:-5],
						'ext': None,
						'type': 'top_' + subdir,
					})
				else:
					yield {
						'error': 'Path {0} is not a directory or configuration file'.format(ext_path),
						'path': ext_path,
					}
				continue
			for config_file_name in os.listdir(ext_path):
				config_file_path = os.path.join(ext_path, config_file_name)
				if config_file_name.endswith('.json') and os.path.isfile(config_file_path):
					yield updated_with_config({
						'error': False,
						'path': config_file_path,
						'name': config_file_name[:-5],
						'ext': ext_name,
						'type': subdir,
					})
				else:
					yield {
						'error': 'Path {0} is not a configuration file'.format(config_file_path),
						'path': config_file_path,
					}


def dict2(d):
	return defaultdict(dict, ((k, dict(v)) for k, v in d.items()))


def check(paths=None, debug=False, echoerr=echoerr, require_ext=None):
	'''Check configuration sanity

	:param list paths:
		Paths from which configuration should be loaded.
	:param bool debug:
		Determines whether some information useful for debugging linter should 
		be output.
	:param function echoerr:
		Function that will be used to echo the error(s). Should accept four 
		optional keyword parameters: ``problem`` and ``problem_mark``, and 
		``context`` and ``context_mark``.
	:param str require_ext:
		Require configuration for some extension to be present.

	:return:
		``False`` if user configuration seems to be completely sane and ``True`` 
		if some problems were found.
	'''
	hadproblem = False

	register_common_names()
	search_paths = paths or get_config_paths()
	find_config_files = generate_config_finder(lambda: search_paths)

	logger = logging.getLogger('powerline-lint')
	logger.setLevel(logging.DEBUG if debug else logging.ERROR)
	logger.addHandler(logging.StreamHandler())

	ee = EchoErr(echoerr, logger)

	if require_ext:
		used_main_spec = main_spec.copy()
		try:
			used_main_spec['ext'][require_ext].required()
		except KeyError:
			used_main_spec['ext'][require_ext] = ext_spec()
	else:
		used_main_spec = main_spec

	lhadproblem = [False]
	load_json_config = generate_json_config_loader(lhadproblem)

	config_loader = ConfigLoader(run_once=True, load=load_json_config)

	lists = {
		'colorschemes': set(),
		'themes': set(),
		'exts': set(),
	}
	found_dir = {
		'themes': False,
		'colorschemes': False,
	}
	config_paths = defaultdict(lambda: defaultdict(dict))
	loaded_configs = defaultdict(lambda: defaultdict(dict))
	for d in chain(
		find_all_ext_config_files(search_paths, 'colorschemes'),
		find_all_ext_config_files(search_paths, 'themes'),
	):
		if d['error']:
			hadproblem = True
			ee(problem=d['error'])
			continue
		if d['hadproblem']:
			hadproblem = True
		if d['ext']:
			found_dir[d['type']] = True
			lists['exts'].add(d['ext'])
			if d['name'] == '__main__':
				pass
			elif d['name'].startswith('__') or d['name'].endswith('__'):
				hadproblem = True
				ee(problem='File name is not supposed to start or end with “__”: {0}'.format(
					d['path']))
			else:
				lists[d['type']].add(d['name'])
			config_paths[d['type']][d['ext']][d['name']] = d['path']
			loaded_configs[d['type']][d['ext']][d['name']] = d['config']
		else:
			config_paths[d['type']][d['name']] = d['path']
			loaded_configs[d['type']][d['name']] = d['config']

	for typ in ('themes', 'colorschemes'):
		if not found_dir[typ]:
			hadproblem = True
			ee(problem='Subdirectory {0} was not found in paths {1}'.format(typ, ', '.join(search_paths)))

	diff = set(config_paths['colorschemes']) - set(config_paths['themes'])
	if diff:
		hadproblem = True
		for ext in diff:
			typ = 'colorschemes' if ext in config_paths['themes'] else 'themes'
			if not config_paths['top_' + typ] or typ == 'themes':
				ee(problem='{0} extension {1} not present in {2}'.format(
					ext,
					'configuration' if (
						ext in loaded_configs['themes'] and ext in loaded_configs['colorschemes']
					) else 'directory',
					typ,
				))

	try:
		main_config = load_config('config', find_config_files, config_loader)
	except IOError:
		main_config = {}
		ee(problem='Configuration file not found: config.json')
		hadproblem = True
	except MarkedError as e:
		main_config = {}
		ee(problem=str(e))
		hadproblem = True
	else:
		if used_main_spec.match(
			main_config,
			data={'configs': config_paths, 'lists': lists},
			context=Context(main_config),
			echoerr=ee
		)[1]:
			hadproblem = True

	import_paths = [os.path.expanduser(path) for path in main_config.get('common', {}).get('paths', [])]

	try:
		colors_config = load_config('colors', find_config_files, config_loader)
	except IOError:
		colors_config = {}
		ee(problem='Configuration file not found: colors.json')
		hadproblem = True
	except MarkedError as e:
		colors_config = {}
		ee(problem=str(e))
		hadproblem = True
	else:
		if colors_spec.match(colors_config, context=Context(colors_config), echoerr=ee)[1]:
			hadproblem = True

	if lhadproblem[0]:
		hadproblem = True

	top_colorscheme_configs = dict(loaded_configs['top_colorschemes'])
	data = {
		'ext': None,
		'top_colorscheme_configs': top_colorscheme_configs,
		'ext_colorscheme_configs': {},
		'colors_config': colors_config
	}
	for colorscheme, config in loaded_configs['top_colorschemes'].items():
		data['colorscheme'] = colorscheme
		if top_colorscheme_spec.match(config, context=Context(config), data=data, echoerr=ee)[1]:
			hadproblem = True

	ext_colorscheme_configs = dict2(loaded_configs['colorschemes'])
	for ext, econfigs in ext_colorscheme_configs.items():
		data = {
			'ext': ext,
			'top_colorscheme_configs': top_colorscheme_configs,
			'ext_colorscheme_configs': ext_colorscheme_configs,
			'colors_config': colors_config,
		}
		for colorscheme, config in econfigs.items():
			data['colorscheme'] = colorscheme
			if ext == 'vim':
				spec = vim_colorscheme_spec
			elif ext == 'shell':
				spec = shell_colorscheme_spec
			else:
				spec = colorscheme_spec
			if spec.match(config, context=Context(config), data=data, echoerr=ee)[1]:
				hadproblem = True

	colorscheme_configs = {}
	for ext in lists['exts']:
		colorscheme_configs[ext] = {}
		for colorscheme in lists['colorschemes']:
			econfigs = ext_colorscheme_configs[ext]
			ecconfigs = econfigs.get(colorscheme)
			mconfigs = (
				top_colorscheme_configs.get(colorscheme),
				econfigs.get('__main__'),
				ecconfigs,
			)
			if not (mconfigs[0] or mconfigs[2]):
				continue
			config = None
			for mconfig in mconfigs:
				if not mconfig:
					continue
				if config:
					config = mergedicts_copy(config, mconfig)
				else:
					config = mconfig
			colorscheme_configs[ext][colorscheme] = config

	theme_configs = dict2(loaded_configs['themes'])
	top_theme_configs = dict(loaded_configs['top_themes'])
	for ext, configs in theme_configs.items():
		data = {
			'ext': ext,
			'colorscheme_configs': colorscheme_configs,
			'import_paths': import_paths,
			'main_config': main_config,
			'top_themes': top_theme_configs,
			'ext_theme_configs': configs,
			'colors_config': colors_config
		}
		for theme, config in configs.items():
			data['theme'] = theme
			if theme == '__main__':
				data['theme_type'] = 'main'
				spec = main_theme_spec
			else:
				data['theme_type'] = 'regular'
				spec = theme_spec
			if spec.match(config, context=Context(config), data=data, echoerr=ee)[1]:
				hadproblem = True

	for top_theme, config in top_theme_configs.items():
		data = {
			'ext': None,
			'colorscheme_configs': colorscheme_configs,
			'import_paths': import_paths,
			'main_config': main_config,
			'theme_configs': theme_configs,
			'ext_theme_configs': None,
			'colors_config': colors_config
		}
		data['theme_type'] = 'top'
		data['theme'] = top_theme
		if top_theme_spec.match(config, context=Context(config), data=data, echoerr=ee)[1]:
			hadproblem = True

	return hadproblem
