'''
SublimeTodoReview
A SublimeText 3 plugin for reviewing todo (and other) comments within your code.

@author Jonathan Delgado (Initial Repo by @robcowie and ST3 update by @dnatag)
'''

import datetime
import fnmatch
import io
import itertools
import os
import re
import sublime
import sublime_plugin
import sys
import threading
import timeit

class Settings():
	def __init__(self, view, args):
		self.user = sublime.load_settings('TodoReview.sublime-settings')
		if not args:
			self.proj = view.settings().get('todoreview', {})
		else:
			self.proj = args

	def get(self, key, default):
		return self.proj.get(key, self.user.get(key, default))

class Engine():
	def __init__(self, dirpaths, filepaths, view):
		self.view = view
		self.dirpaths = dirpaths
		self.filepaths = filepaths
		if settings.get('case_sensitive', False):
			case = 0
		else:
			case = re.IGNORECASE
		patt_patterns = settings.get('patterns', {})
		patt_files = settings.get('exclude_files', [])
		patt_folders = settings.get('exclude_folders', [])
		match_patterns = '|'.join(patt_patterns.values())
		match_files = [fnmatch.translate(p) for p in patt_files]
		match_folders = [fnmatch.translate(p) for p in patt_folders]

		self.patterns = re.compile(match_patterns, case)
		self.priority = re.compile(r'\(([0-9]{1,2})\)')
		self.exclude_files = [re.compile(p) for p in match_files]
		self.exclude_folders = [re.compile(p) for p in match_folders]
		self.open = self.view.window().views()
		self.open_files = [v.file_name() for v in self.open if v.file_name()]

	def files(self):
		seen_paths = []
		for dirpath in self.dirpaths:
			dirpath = self.resolve(dirpath)
			for dirp, dirnames, filepaths in os.walk(dirpath, followlinks=True):
				if any(p.search(dirp) for p in self.exclude_folders):
					continue
				for filepath in filepaths:
					self.filepaths.append(os.path.join(dirp, filepath))
		for filepath in self.filepaths:
			p = self.resolve(filepath)
			if p in seen_paths:
				continue
			if any(p.search(filepath) for p in self.exclude_folders):
				continue
			if any(p.search(filepath) for p in self.exclude_files):
				continue
			seen_paths.append(p)
			yield p

	def extract(self, files):
		encoding = settings.get('encoding', 'utf-8')
		for p in files:
			try:
				if p in self.open_files:
					for view in self.open:
						if view.file_name() == p:
							f = []
							lines = view.lines(sublime.Region(0, view.size()))
							for line in lines:
								f.append(view.substr(line))
							break
				else:
					f = io.open(p, 'r', encoding=encoding)
				for num, line in enumerate(f, 1):
					for result in self.patterns.finditer(line):
						for patt, note in result.groupdict().items():
							if not note and note != '':
								continue
							priority_match = self.priority.search(note)
							if(priority_match):
								priority = int(priority_match.group(1))
							else:
								priority = 50
							yield {
								'file': p,
								'patt': patt,
								'note': note,
								'line': num,
								'priority': priority
							}
			except(IOError, UnicodeDecodeError):
				f = None
			finally:
				thread.increment()
				if f is not None and type(f) is not list:
					f.close()

	def process(self):
		return self.extract(self.files())

	def resolve(self, directory):
		if settings.get('resolve_symlinks', True):
			return os.path.realpath(os.path.expanduser(os.path.abspath(directory)))
		else:
			return os.path.expanduser(os.path.abspath(directory))

class Thread(threading.Thread):
	def __init__(self, engine, callback):
		self.i = 0
		self.engine = engine
		self.callback = callback
		self.lock = threading.RLock()
		threading.Thread.__init__(self)

	def run(self):
		self.start = timeit.default_timer()
		if sys.version_info < (3,0,0):
			sublime.set_timeout(self.thread, 1)
		else:
			self.thread()

	def thread(self):
		results = list(self.engine.process())
		self.callback(results, self.finish(), self.i)

	def finish(self):
		return round(timeit.default_timer() - self.start, 2)

	def increment(self):
		with self.lock:
			self.i += 1
			sublime.status_message("TodoReview: {0} files scanned".format(self.i))

class TodoReviewCommand(sublime_plugin.TextCommand):
	def run(self, edit, **args):
		global settings, thread
		filepaths = []
		self.args = args
		window = self.view.window()
		paths = args.get('paths', None)
		settings = Settings(self.view, args.get('settings', False))
		if args.get('current_file', False):
			if self.view.file_name():
				paths = []
				filepaths = [self.view.file_name()]
			else:
				print('TodoReview: File must be saved first')
				return
		else:
			if not paths and settings.get('include_paths', False):
				paths = settings.get('include_paths', False)
			if args.get('open_files', False):
				filepaths = [v.file_name() for v in window.views() if v.file_name()]
			if not args.get('open_files_only', False):
				if not paths:
					paths = window.folders()
				else:
					for p in paths:
						if os.path.isfile(p):
							filepaths.append(p)
			else:
				paths = []
		engine = Engine(paths, filepaths, self.view)
		thread = Thread(engine, self.render)
		thread.start()

	def render(self, results, time, count):
		self.view.run_command('todo_review_render', {
			"results": results,
			"time": time,
			"count": count,
			"args": self.args
		})

class TodoReviewRender(sublime_plugin.TextCommand):
	def run(self, edit, results, time, count, args):
		self.args = args
		self.edit = edit
		self.time = time
		self.count = count
		self.results = results
		self.sorted = self.sort()
		self.rview = self.get_view()
		self.draw_header()
		self.draw_results()
		self.window.focus_view(self.rview)
		self.args['settings'] = settings.proj
		self.rview.settings().set('review_args', self.args)

	def sort(self):
		self.largest = 0
		for item in self.results:
			self.largest = max(len(self.draw_file(item)), self.largest)
		self.largest = min(self.largest, settings.get('render_maxspaces', 50)) + 6
		w = settings.get('patterns_weight', {})
		key = lambda m: (str(w.get(m['patt'].upper(), m['patt'])), m['priority'])
		results = sorted(self.results, key=key)
		return itertools.groupby(results, key=lambda m: m['patt'])

	def get_view(self):
		self.window = sublime.active_window()
		for view in self.window.views():
			if view.settings().get('todo_results', False):
				view.erase(self.edit, sublime.Region(0, view.size()))
				return view
		view = self.window.new_file()
		view.set_name('TodoReview')
		view.set_scratch(True)
		view.settings().set('todo_results', True)
		if sys.version_info < (3,0,0):
			view.set_syntax_file('Packages/TodoReview/TodoReview.hidden-tmLanguage')
		else:
			view.assign_syntax('Packages/TodoReview/TodoReview.hidden-tmLanguage')
		view.settings().set('line_padding_bottom', 2)
		view.settings().set('line_padding_top', 2)
		view.settings().set('word_wrap', False)
		view.settings().set('command_mode', True)
		return view

	def draw_header(self):
		forms = settings.get('render_header_format', '%d - %c files in %t secs')
		datestr = settings.get('render_header_date', '%A %m/%d/%y at %I:%M%p')
		if not forms:
			forms = '%d - %c files in %t secs'
		if not datestr:
			datestr = '%A %m/%d/%y at %I:%M%p'
		if len(forms) == 0:
			return
		date = datetime.datetime.now().strftime(datestr)
		res = '// '
		res += forms \
			.replace('%d', date) \
			.replace('%t', str(self.time)) \
			.replace('%c', str(self.count))
		res += '\n'
		self.rview.insert(self.edit, self.rview.size(), res)

	def draw_results(self):
		data = [x[:] for x in [[]] * 2]
		for patt, items in self.sorted:
			items = list(items)
			res = '\n## %t (%n)\n' \
				.replace('%t', patt.upper()) \
				.replace('%n', str(len(items)))
			self.rview.insert(self.edit, self.rview.size(), res)
			for idx, item in enumerate(items, 1):
				line = '%i. %f' \
					.replace('%i', str(idx)) \
					.replace('%f', self.draw_file(item))
				res = '%f%s%n\n' \
					.replace('%f', line) \
					.replace('%s', ' '*max((self.largest - len(line)), 1)) \
					.replace('%n', item['note'])
				start = self.rview.size()
				self.rview.insert(self.edit, start, res)
				region = sublime.Region(start, self.rview.size())
				data[0].append(region)
				data[1].append(item)
		self.rview.add_regions('results', data[0], '')
		d = dict(('{0},{1}'.format(k.a, k.b), v) for k, v in zip(data[0], data[1]))
		self.rview.settings().set('review_results', d)

	def draw_file(self, item):
		if settings.get('render_include_folder', False):
			depth = settings.get('render_folder_depth', 1)
			if depth == 'auto':
				f = item['file']
				for folder in sublime.active_window().folders():
					if f.startswith(folder):
						f = os.path.relpath(f, folder)
						break
				f = f.replace('\\', '/')
			else:
				f = os.path.dirname(item['file']).replace('\\', '/').split('/')
				f = '/'.join(f[-depth:] + [os.path.basename(item['file'])])
		else:
			f = os.path.basename(item['file'])
		return '%f:%l' \
			.replace('%f', f) \
			.replace('%l', str(item['line']))

class TodoReviewResults(sublime_plugin.TextCommand):
	def run(self, edit, **args):
		self.settings = self.view.settings()
		if not self.settings.get('review_results'):
			return
		if args.get('open'):
			window = self.view.window()
			index = int(self.settings.get('selected_result', -1))
			result = self.view.get_regions('results')[index]
			coords = '{0},{1}'.format(result.a, result.b)
			i = self.settings.get('review_results')[coords]
			p = "%f:%l".replace('%f', i['file']).replace('%l', str(i['line']))
			view = window.open_file(p, sublime.ENCODED_POSITION)
			window.focus_view(view)
			return
		if args.get('refresh'):
			args = self.settings.get('review_args')
			self.view.run_command('todo_review', args)
			self.settings.erase('selected_result')
			return
		if args.get('direction'):
			d = args.get('direction')
			results = self.view.get_regions('results')
			if not results:
				return
			start_arr = {
				'down': -1,
				'up': 0,
				'down_skip': -1,
				'up_skip': 0
			}
			dir_arr = {
				'down': 1,
				'up': -1,
				'down_skip': settings.get('navigation_forward_skip', 10),
				'up_skip': settings.get('navigation_backward_skip', 10) * -1
			}
			sel = int(self.settings.get('selected_result', start_arr[d]))
			sel = sel + dir_arr[d]
			if sel == -1:
				target = results[len(results) - 1]
				sel = len(results) - 1
			if sel < 0:
				target = results[0]
				sel = 0
			if sel >= len(results):
				target = results[0]
				sel = 0
			target = results[sel]
			self.settings.set('selected_result', sel)
			region = target.cover(target)
			self.view.add_regions('selection', [region], 'selected', 'dot')
			self.view.show(sublime.Region(region.a, region.a + 5))
			return