#!/usr/bin/env python3

# This file is a part of settingsctl.

# settingsctl is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# settingsctl is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with settingsctl.  If not, see <http://www.gnu.org/licenses/gpl.txt>.

# A copy of the license is included in the LICENSE file, which should have
# been distributed along with this program.
# If not, see http://www.gnu.org/licenses/gpl.txt


# Copyright © Bharadwaj Raju and other contributors (list of contributors: https://github.com/bharadwaj-raju/settingsctl/graphs/contributors)

import os
import sys
import argparse
import json
from textwrap import dedent
import subprocess as sp
from collections import namedtuple
import builtins
import time

__version__ = '0.0.1'


# -- Finding path to the settings lib (see Documentation::Settings Lib)

# Order: $SETTINGCTL_LIB ./lib/ ~/.local/lib/settingsctl /usr/lib/settingsctl

if os.path.isdir(os.environ.get('SETTINGSCTL_LIB', '')):
	lib = os.environ.get('SETTINGSCTL_LIB')

elif os.path.isdir('lib'):
	lib = 'lib/'

elif os.path.isdir(os.path.join(os.path.dirname(__file__), 'lib')):
	lib = os.path.join(os.path.dirname(__file__), 'lib/')

elif os.path.isdir(os.path.expanduser('~/.local/lib/settingsctl')):
	lib = os.path.expanduser('~/.local/lib/settingsctl')

elif os.path.isdir('/usr/lib/settingsctl'):
	lib = '/usr/lib/settingsctl/'

else:
	print(dedent('''settingsctl: settingsctl settings library not found.\n
		It should be included with the settingsctl distribution in lib/,\
		see Documentation::Settings Lib" <https://bharadwaj-raju.github.io/settingsctl/documentation/settingsctl-lib.html>'''))


# -- Inter-process Communication
# for monitor command

if os.path.isdir(os.environ.get('XDG_RUNTIME_DIR')):
	tmp_dir = os.environ.get('XDG_RUNTIME_DIR')

elif os.path.isdir(os.environ.get('TMPDIR')):
	tmp_dir = os.environ.get('TMPDIR')

else:
	tmp_dir = '/var/tmp'

ipc_file = os.path.join(tmp_dir, 'SETTINGSCTL_IPC')

if not os.path.exists(ipc_file):
	open(ipc_file, 'w').close()

# Generates a filesystem tree (as a dictionary) recursively

global recursion_number  # This needs to modified later in fstree() → __tree()
recursion_number = 0

def fstree(dir_, maxdepth=None):

	if maxdepth == 0:
		maxdepth = None

	def __tree(dir_):

		global recursion_number

		tree = {}

		for i in os.listdir(dir_):

			if os.path.isfile(os.path.join(dir_, i)):
				tree[i.replace('.py', '')] = ''

			else:
				recursion_number += 1

				if maxdepth is not None:
					if recursion_number <= maxdepth:
						tree[i + '/'] = __tree(os.path.join(dir_, i))

				else:
					tree[i + '/'] = __tree(os.path.join(dir_, i))

		return tree

	return __tree(dir_)

# -- Functions provided to all settings
#    Must be documented

# Convenience function exported to settings

def module_message(message, level='info'):

	'''Causes a message to be produced in the standard way.
	This allows for settings not having to implement JSON, etc.

	Arguments:
	  message  The message to be produced. Required.
	  level    The severity of the message. One of 'info', 'warning' or 'error'. Default: info
	'''

	msg = {
				'setting': os.environ.get('SETTINGSCTL_CURRENT_SETTING'),
				'message': message,
				'level': level
			}

	if os.environ.get('SETTINGSCTL_USE_JSON', None) is not None:
		sys.stderr.write(json.dumps(msg, indent=4) + '\n')
		sys.stderr.flush()

	else:
		sys.stderr.write('{setting}: {level}: {message}\n'.format_map(msg))
		sys.stderr.flush()

# Convenience function exported to settings

def module_Process(cmd, shell=False, strip=True):

	'''Run a command.
	This allows settings to not implement their own newline handling, encoding etc.

	Arguments:
	  cmd      The command to be run. Required. If shell is True, will be a string; otherwise a list of arguments
	  shell    Whether to run the command using /bin/sh. Default: False
	  strip    Whether to strip extra whitespace and newlines from the end of the command output. Default: True

	Returns:
	  object    (stdout, stderr, return_code)

	  Can be accessed as:
		proc = Process(...)
		proc.stdout
	'''

	process = sp.Popen(cmd, shell=shell, universal_newlines=True,
					   stdout=sp.PIPE, stderr=sp.PIPE)

	stdout, stderr = process.communicate()
	return_code = process.returncode

	if strip:
		stdout, stderr = [out.rstrip() for out in (stdout, stderr)]

	Proc = namedtuple('Process', 'stdout stderr return_code')

	return Proc(stdout, stderr, return_code)


# -- Miscellaneous functions

# Get a Python module from a dot-separated setting name

def get_setting_module(setting):

	setting = setting.split('.')
	last = setting.pop()
	setting = '/'.join(setting)

	sys.path.insert(0, os.path.join(lib, setting))

	builtins.Process = module_Process
	builtins.message = module_message

	module = __import__(last)

	return module

# Pretty-print a filesystem tree as provided by fstree()

def pretty(d, indent=0):

	total = ''

	if isinstance(d, dict):
		for key, value in d.items():
			total += '  ' * indent + str(key) + '\n'
			if isinstance(value, (dict, list)):
				total += pretty(value, indent+1)
			else:
				total += '  ' * (indent+1) + str(value) + '\n'
	elif isinstance(d, list):
		for item in d:
			if isinstance(item, (dict, list)):
				total += pretty(item, indent+1)
			else:
				total += '  ' * (indent+1) + str(item) + '\n'
	else:
		pass

	return total

#-- Main settingsctl CLI interface (arguments etc)

# Template for --help outputs

HELP_BOILERPLATE = dedent('''\
		usage: {usage}

		settingsctl — a cross-desktop-settings tool

		{options_etc}

		Report bugs at: <https://github.com/bharadwaj-raju/settingsctl/issues/new>
		settingsctl home page: <https://bharadwaj-raju.github.io/settingsctl>
		Full documentation: <https://bharadwaj-raju.github.io/settingsctl/documentation>

		''')

# Custom ArgumentParser extension to allow custom --help output

class ArgumentParser(argparse.ArgumentParser):

	def set_help(self, help_str):

		self.format_help = lambda: help_str

# settingsctl CLI class
# main command is implemented in __init__
# subcommands are implemented in the methods

class SettingsCtlCLI(object):

	def __init__(self):
		parser = ArgumentParser(
					description='Cross-desktop settings tool',
					usage='settingsctl <command> [args, ...]')


		parser.add_argument('command',
							help='Command to run. See manual settingsctl(1)',
							default='',
							nargs='?')

		parser.add_argument('--version',
							help='Show version and legal info.',
							action='store_true')

		parser.set_help(HELP_BOILERPLATE.format(
							usage=parser.format_usage(),
							options_etc=dedent('''\
								Commands:
								  get        get a setting's value
								  set        set a setting's value
								  list       list settings
								  tree       hierarchial tree of settings
								  info       get information about a setting
								  monitor    monitor for changes made via settingsctl
								  list-all   list all possible choices for a setting (may not be applicable to all settings)

								Options:
								  --help     print this help message, and exit
								  --version  print version and legal information, and exit
								''')
							)
						)



		args = parser.parse_args(sys.argv[1:2])

		if args.command == '':
			if args.version:
				print(
				dedent('''\
					settingsctl {version}
					Copyright © Bharadwaj Raju
					License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.txt>
					This is free software: you are free to change and redistribute it.
					There is NO WARRANTY, to the extent permitted by law.'''.format(version=__version__)))

				sys.exit(0)

			else:
				print('settingsctl: no command specified. See manual settingsctl(1) or the help printed below.\n')
				parser.print_help()
				sys.exit(1)


		if not hasattr(self, args.command.replace('-', '_')):
			print('settingsctl: unrecognized command "{cmd}".'.format(cmd=args.command))
			parser.print_help()
			sys.exit(1)

		os.environ['SETTINGSCTL_BIN'] = sys.argv[0]

		getattr(self, args.command.replace('-', '_'))()


	def list(self):

		parser = ArgumentParser(
					description='List settings',
					usage='settingsctl list [setting] [--json]')

		parser.set_help(HELP_BOILERPLATE.format(
							usage=parser.format_usage(),
							options_etc=dedent('''\
								Options:
								  setting		the setting to list
								''')
							)
						)



		parser.add_argument('setting', nargs='?', default='')
		parser.add_argument('--json', action='store_true')

		args = parser.parse_args(sys.argv[2:])

		args.setting = args.setting.replace('.', '/')

		ls_ = [x.replace('.py', '') for x in os.listdir(os.path.join(lib, args.setting))]
		ls = [x + '/' if os.path.isdir(os.path.join(lib, args.setting, x.replace('.', '/'))) else x for x in ls_]

		if args.json:
			print(json.dumps(ls))

		else:
			print('\n'.join(ls))


	def monitor(self):

		parser = ArgumentParser(
					description='Monitor settings for changes made via settingsctl',
					usage='settingsctl monitor [setting] [--json]')

		parser.set_help(HELP_BOILERPLATE.format(
							usage=parser.format_usage(),
							options_etc=dedent('''\
								Options:
								  setting		the setting to list
								  --json		 whether to print in JSON form
								''')
							)
						)

		parser.add_argument('setting', nargs='?', default='')
		parser.add_argument('--json', action='store_true')


		args = parser.parse_args(sys.argv[2:])

		start_time = time.time()

		while True:
			try:
				if os.path.getmtime(ipc_file) > start_time:
					# file changed
					start_time = time.time()
					with open(ipc_file, 'r') as f:
						message_json = json.loads(f.readline())

						if message_json['setting'].startswith(args.setting):
							if args.json:
								print(json.dumps(message_json))

							else:
								print(message_json['setting'], '→', message_json['value'])

						f.close()

			except KeyboardInterrupt:
					sys.exit(0)

	def tree(self):

		parser = ArgumentParser(
					description='Hierarchial tree of settings',
					usage='settingsctl tree [setting] [--json]')

		parser.set_help(HELP_BOILERPLATE.format(
							usage=parser.format_usage(),
							options_etc=dedent('''\
								Options:
								  setting		the setting to tree
								  --json		whether to display output in JSON form
								''')
							)
						)


		parser.add_argument('setting', nargs='?', default='')
		parser.add_argument('--json', action='store_true')


		args = parser.parse_args(sys.argv[2:])

		tree = fstree(os.path.join(lib, args.setting))

		if args.json:
			print(json.dumps(tree, indent=4))

		else:
			print(dedent(pretty(tree, indent=4)))


	def info(self):

		parser = ArgumentParser(
					description='Get information about a setting',
					usage='settingsctl info <setting> [--json]')

		parser.add_argument('setting', type=str)
		parser.add_argument('--json', action='store_true')


		parser.set_help(HELP_BOILERPLATE.format(
							usage=parser.format_usage(),
							options_etc=dedent('''\
								Arguments:
								  setting		the setting to get info about

								Options:
								  --json		whether to display output in JSON form
								''')
							)
						)


		args = parser.parse_args(sys.argv[2:])

		if args.json:
			os.environ['SETTINGSCTL_USE_JSON'] = args.setting

		os.environ['SETTINGSCTL_CURRENT_SETTING'] = args.setting

		module = get_setting_module(args.setting)

		data = module.info()

		try:
			if module.read_only:
				data['read-only'] = module.read_only

		except AttributeError:
			data['read-only'] = False

		if args.json:
			print(json.dumps(data, indent=4))

		else:
			print(', '.join(data['type']), '\n')
			print('Description:', data['description'], '\n')
			print('Data description:', ', '.join(data['data']), '\n')
			print('Read-only:', data['read-only'])

	def list_all(self):

		parser = ArgumentParser(
					description='List all choices of a setting (may not be applicable to all settings)',
					usage='settingsctl list-all <setting> [--json]')

		parser.add_argument('setting', type=str)
		parser.add_argument('--json', action='store_true')


		parser.set_help(HELP_BOILERPLATE.format(
							usage=parser.format_usage(),
							options_etc=dedent('''\
								Arguments:
								  setting		the setting to get info about

								Options:
								  --json		whether to display output in JSON form
								''')
							)
						)


		args = parser.parse_args(sys.argv[2:])

		if args.json:
			os.environ['SETTINGSCTL_USE_JSON'] = args.setting

		os.environ['SETTINGSCTL_CURRENT_SETTING'] = args.setting

		module = get_setting_module(args.setting)

		try:
			ls = module.list_all()

		except AttributeError:
			if args.json:
				print(json.dumps({
									'setting': args.setting,
									'message': 'list-all is not applicable for this setting',
									'level': 'error'
								}, indent=4))

				sys.exit(1)

			else:
				print('{name}: error: list-all is not applicable for this setting.'.format(name=args.setting))
				sys.exit(1)

		if args.json:
			print(json.dumps(ls, indent=4))

		else:
			print('\n'.join(ls))


	def get(self):

		parser = ArgumentParser(
					description='Get a setting',
					usage='settingsctl get <setting> [--json]')

		parser.set_help(HELP_BOILERPLATE.format(
							usage=parser.format_usage(),
							options_etc=dedent('''\
								Arguments:
								  setting		the setting whose value to get

								Options:
								  --json		 whether to display output in JSON form
								''')
							)
						)



		parser.add_argument('setting', type=str)
		parser.add_argument('--json', action='store_true')

		args = parser.parse_args(sys.argv[2:])

		if args.json:
			os.environ['SETTINGSCTL_USE_JSON'] = args.setting

		os.environ['SETTINGSCTL_CURRENT_SETTING'] = args.setting


		module = get_setting_module(args.setting)

		if args.json:
			print(json.dumps({
					'setting': args.setting,
					'value': module.get()
					}, indent=4))

		else:
			retval = module.get()

			if isinstance(retval, dict):
				pretty(retval)

			elif isinstance(retval, list):
				print('\n'.join([x.replace('\n', '\\n') for x in retval]))

			else:
				print(module.get())


	def set(self):

		parser = ArgumentParser(
					description='Set a setting',
					usage='settingsctl set <setting> <value> [--json]')

		parser.set_help(HELP_BOILERPLATE.format(
							usage=parser.format_usage(),
							options_etc=dedent('''\
								Arguments:
								  setting		    the setting whose value to set
								  value		        the value to set it to

								Options:
								  --json		    whether to display output in JSON form
								  --validate-only   only validate the input data, no changing the setting
								''')
							)
						)


		parser.add_argument('setting', type=str)
		parser.add_argument('value', type=str, nargs='*')
		parser.add_argument('--json', action='store_true')
		parser.add_argument('--validate-only', action='store_true')


		args = parser.parse_args(sys.argv[2:])

		if args.json:
			os.environ['SETTINGSCTL_USE_JSON'] = args.setting

		os.environ['SETTINGSCTL_CURRENT_SETTING'] = args.setting

		module = get_setting_module(args.setting)

		try:
			if module.read_only:
				if args.json:
					print(json.dumps({
										'setting': args.setting,
										'message': 'setting is read-only',
										'level': 'error'
									  }, indent=4))
				else:
					print(args.setting + ': error: this setting is read-only, and cannot be set')

				sys.exit(1)

		except AttributeError:
			# read_only not set
			pass


		value_fmt = module.validate(args.value)

		json_repr = {
						'setting': args.setting,
						'value': value_fmt,
						'value-raw': args.value
					}

		if args.json:
			print(json.dumps(json_repr, indent=4))

		else:
			print(args.value, 'formatted to (by the setting)', value_fmt)
			print(args.setting, '→', value_fmt)

		if args.validate_only:
			sys.exit(0)

		module.set(value_fmt)

		with open(ipc_file, 'w') as f:
			f.write(json.dumps(json_repr))


if __name__ == '__main__':
	SettingsCtlCLI()
