import collections
import logging
import os
import stat
import threading

from . import tasks
from . import directory
from . import sftp_utilities
from . import editor

from king_phisher.client import gui_utilities
from king_phisher.client.widget import extras

from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import GLib

logger = logging.getLogger('KingPhisher.Plugins.SFTPClient')

class StatusDisplay(object):
	"""
	Class representing the bottom treeview of the GUI. This contains the logging
	and graphical representation of all queued transfers.
	"""
	def __init__(self, queue):
		self.queue = queue
		self.scroll = sftp_utilities.get_object('SFTPClient.notebook.page_stfp.scrolledwindow_transfer_statuses')
		self.treeview_transfer = sftp_utilities.get_object('SFTPClient.notebook.page_stfp.treeview_transfer_statuses')
		self._tv_lock = threading.RLock()

		col_img = Gtk.CellRendererPixbuf()
		col = Gtk.TreeViewColumn('')
		col.pack_start(col_img, False)
		col.add_attribute(col_img, 'pixbuf', 0)
		self.treeview_transfer.append_column(col)
		gui_utilities.gtk_treeview_set_column_titles(self.treeview_transfer, ('Local File', 'Remote File', 'Status'), column_offset=1)

		col_bar = Gtk.TreeViewColumn('Progress')
		progress = Gtk.CellRendererProgress()
		col_bar.pack_start(progress, True)
		col_bar.add_attribute(progress, 'value', 4)
		col_bar.set_property('resizable', True)
		col_bar.set_min_width(125)
		self.treeview_transfer.append_column(col_bar)

		# todo: make this a CellRendererBytes
		gui_utilities.gtk_treeview_set_column_titles(self.treeview_transfer, ('Size',), column_offset=5, renderers=(extras.CellRendererBytes(),))
		self._tv_model = Gtk.TreeStore(GdkPixbuf.Pixbuf, str, str, str, int, int, object)
		self.treeview_transfer.connect('size-allocate', self.signal_tv_size_allocate)
		self.treeview_transfer.connect('button_press_event', self.signal_tv_button_pressed)

		self.treeview_transfer.set_model(self._tv_model)
		self.treeview_transfer.show_all()

		self.popup_menu = Gtk.Menu.new()

		self.menu_item_paused = Gtk.CheckMenuItem.new_with_label('Paused')
		menu_item = self.menu_item_paused
		menu_item.connect('toggled', self.signal_menu_toggled_paused)
		self.popup_menu.append(menu_item)

		self.menu_item_cancel = Gtk.MenuItem.new_with_label('Cancel')
		menu_item = self.menu_item_cancel
		menu_item.connect('activate', self.signal_menu_activate_cancel)
		self.popup_menu.append(menu_item)

		menu_item = Gtk.SeparatorMenuItem()
		self.popup_menu.append(menu_item)

		menu_item = Gtk.MenuItem.new_with_label('Clear')
		menu_item.connect('activate', self.signal_menu_activate_clear)
		self.popup_menu.append(menu_item)
		self.popup_menu.show_all()

	def _get_selected_tasks(self):
		treepaths = self._get_selected_treepaths()
		if treepaths is None:
			return None
		selected_tasks = set()
		for treepath in treepaths:
			treeiter = self._tv_model.get_iter(treepath)
			selected_tasks.add(self._tv_model[treeiter][6])
			self._tv_model.foreach(lambda _, path, treeiter: selected_tasks.add(self._tv_model[treeiter][6]) if path.is_descendant(treepath) else 0)
		return selected_tasks

	def _get_selected_treepaths(self):
		selection = self.treeview_transfer.get_selection()
		model, treeiter = selection.get_selected()
		if treeiter is None:
			return None
		treepaths = []
		treepaths.append(model.get_path(treeiter))
		return treepaths

	def _change_task_state(self, state_from, state_to):
		modified_tasks = []
		with self.queue.mutex:
			selected_tasks = set([task for task in self._get_selected_tasks() if task.state in state_from])
			for task in selected_tasks:
				modified_tasks.append(task)
				modified_tasks.extend(task.parents)  # ensure parents are also synced because state changes may affect them
				task.state = state_to
		self.sync_view(set(modified_tasks))

	def _sync_view(self, sftp_tasks=None):
		# This value was set to True to prevent the treeview from freezing.
		if not self.queue.mutex.acquire(blocking=True):
			return
		if not self._tv_lock.acquire(blocking=False):
			self.queue.mutex.release()
			return
		sftp_tasks = (sftp_tasks or self.queue.queue)
		for task in sftp_tasks:
			if not isinstance(task, tasks.TransferTask):
				continue
			if task.treerowref is None:
				parent_treeiter = None
				if task.parent:
					parent_treerowref = task.parent.treerowref
					if parent_treerowref is None:
						continue
					parent_treepath = parent_treerowref.get_path()
					if parent_treepath is None:
						continue
					parent_treeiter = self._tv_model.get_iter(parent_treepath)
				direction = Gtk.STOCK_GO_FORWARD if task.transfer_direction == 'upload' else Gtk.STOCK_GO_BACK
				image = self.treeview_transfer.render_icon(direction, Gtk.IconSize.BUTTON, None) if parent_treeiter is None else Gtk.Image()
				treeiter = self._tv_model.append(parent_treeiter, [
					image,
					task.local_path,
					task.remote_path,
					task.state,
					0,
					None if isinstance(task, tasks.TransferDirectoryTask) else task.size,
					task
				])
				task.treerowref = Gtk.TreeRowReference.new(self._tv_model, self._tv_model.get_path(treeiter))
			elif task.treerowref.valid():
				row = self._tv_model[task.treerowref.get_path()]  # pylint: disable=unsubscriptable-object
				row[3] = task.state
				row[4] = task.progress
		self.queue.mutex.release()
		return False

	def sync_view(self, sftp_tasks=None):
		if isinstance(sftp_tasks, tasks.Task):
			sftp_tasks = (sftp_tasks,)
		GLib.idle_add(self._sync_view, sftp_tasks, priority=GLib.PRIORITY_DEFAULT_IDLE)

	def signal_menu_activate_clear(self, _):
		with self.queue.mutex:
			for task in list(self.queue.queue):
				if not task.is_done:
					continue
				if task.treerowref is not None and task.treerowref.valid():
					self._tv_model.remove(self._tv_model.get_iter(task.treerowref.get_path()))
					task.treerowref = None
				self.queue.queue.remove(task)
			self.queue.not_full.notify()

	def signal_menu_toggled_paused(self, _):
		if self.menu_item_paused.get_active():
			self._change_task_state(('Active', 'Pending', 'Transferring'), 'Paused')
		else:
			self._change_task_state(('Paused',), 'Pending')

	def signal_menu_activate_cancel(self, _):
		self._change_task_state(('Active', 'Paused', 'Pending', 'Transferring'), 'Cancelled')

	def signal_tv_button_pressed(self, _, event):
		if event.button == Gdk.BUTTON_SECONDARY:
			selected_tasks = self._get_selected_tasks()
			if not selected_tasks:
				self.menu_item_cancel.set_sensitive(False)
				self.menu_item_paused.set_sensitive(False)
			else:
				self.menu_item_cancel.set_sensitive(True)
				self.menu_item_paused.set_sensitive(True)
				tasks_are_paused = [task.state == 'Paused' for task in selected_tasks]
				if any(tasks_are_paused):
					self.menu_item_paused.set_active(True)
					self.menu_item_paused.set_inconsistent(not all(tasks_are_paused))
				else:
					self.menu_item_paused.set_active(False)
					self.menu_item_paused.set_inconsistent(False)
			self.popup_menu.popup(None, None, None, None, event.button, Gtk.get_current_event_time())
			return True
		return

	def signal_tv_size_allocate(self, _, event, data=None):
		adj = self.scroll.get_vadjustment()
		adj.set_value(0)

class FileManager(object):
	"""
	File manager that manages the Transfer Queue by adding new tasks and
	handling tasks put in, as well as handles communication between all the
	other classes.
	"""
	def __init__(self, application, ssh, config):
		self.application = application
		self.config = config
		self.queue = tasks.TaskQueue()
		self._threads = []
		self._threads_max = 1
		self._threads_shutdown = threading.Event()
		for _ in range(self._threads_max):
			thread = threading.Thread(target=self._thread_routine)
			thread.start()
			self._threads.append(thread)
		self.editor = None
		self.window = sftp_utilities.get_object('SFTPClient.window')
		self.notebook = sftp_utilities.get_object('SFTPClient.notebook')
		self.notebook.set_show_tabs(False)
		self.notebook.connect('switch-page', self.signal_change_page)
		self.editor_tab_save_button = sftp_utilities.get_object('SFTPClient.notebook.page_editor.toolbutton_save_html_file')
		self.editor_tab_save_button.set_sensitive(False)
		self.editor_tab_save_button.connect('clicked', self.signal_editor_save)
		self.status_display = StatusDisplay(self.queue)
		self.local = directory.LocalDirectory(self.application, config)
		self.remote = directory.RemoteDirectory(self.application, config, ssh)
		sftp_utilities.get_object('SFTPClient.notebook.page_stfp.button_upload').connect('button-press-event', lambda widget, event: self._queue_transfer_from_selection(tasks.UploadTask))
		sftp_utilities.get_object('SFTPClient.notebook.page_stfp.button_download').connect('button-press-event', lambda widget, event: self._queue_transfer_from_selection(tasks.DownloadTask))
		self.local.menu_item_transfer.connect('activate', lambda widget: self._queue_transfer_from_selection(tasks.UploadTask))
		self.remote.menu_item_transfer.connect('activate', lambda widget: self._queue_transfer_from_selection(tasks.DownloadTask))
		self.local.menu_item_edit.connect('activate', self.signal_edit_file, self.local)
		self.remote.menu_item_edit.connect('activate', self.signal_edit_file, self.remote)
		menu_item = sftp_utilities.get_object('SFTPClient.notebook.page_stfp.menuitem_opts_transfer_hidden')
		menu_item.set_active(self.config['transfer_hidden'])
		menu_item.connect('toggled', self.signal_toggled_config_option, 'transfer_hidden')
		menu_item = sftp_utilities.get_object('SFTPClient.notebook.page_stfp.menuitem_opts_show_hidden')
		menu_item.set_active(self.config['show_hidden'])
		menu_item.connect('toggled', self.signal_toggled_config_option_show_hidden)
		menu_item = sftp_utilities.get_object('SFTPClient.notebook.page_stfp.menuitem_exit')
		menu_item.connect('activate', lambda _: self.window.destroy())
		self.window.connect('destroy', self.signal_window_destroy)
		self.window.show_all()

	def signal_change_page(self, _, __, page_number):
		"""
		will check to is if the page change is from editor to sftp, and then ask if the user if they
		want to save detected changes. If yes it passes to the save editor file to take action.
		"""
		# page_number is the page switched from
		if page_number:
			return
		if not self.editor_tab_save_button.is_sensitive():
			return
		if not gui_utilities.show_dialog_yes_no('Changes not saved', self.application.get_active_window(), 'Do you want to save your changes?'):
			return

		self._save_editor_file()

	def signal_edit_file(self, _, directory):
		"""
		Handles the signal when edit is selected on a file.

		:param _: Gtkmenuitem unused
		:param directory: The local or remote directory
		"""
		selection = directory.treeview.get_selection()
		model, treeiter = selection.get_selected()
		try:
			file_path = directory.get_abspath(model[treeiter][2])
		except TypeError:
			logger.warning('no file selected to edit')
			return

		self.editor = editor.SFTPEditor(self.application, file_path, directory)
		self._load_editor_file()

	def signal_editor_save(self, _):
		self._save_editor_file()

	def _save_editor_file(self):
		"""
		Handles the save file action for the editor instance when button is pressed or when tabs are changed
		"""
		if not self.editor:
			self.editor_tab_save_button.set_sensitive(False)
			self.notebook.set_current_page(0)
			self.notebook.set_show_tabs(False)
			return

		buffer_contents = self.editor.sourceview_buffer.get_text(
			self.editor.sourceview_buffer.get_start_iter(),
			self.editor.sourceview_buffer.get_end_iter(),
			False
		)
		if buffer_contents == self.editor.file_contents:
			logger.debug('editor found nothing to save')
			self.editor_tab_save_button.set_sensitive(False)
			return

		buffer_contents = buffer_contents.encode('utf-8')

		try:
			self.editor.directory.write_file(self.editor.file_path, buffer_contents)
			self.editor.file_contents = buffer_contents
			logger.info("saved editor contents to {} file path {}".format(self.editor.file_location, self.editor.file_path))
		except IOError:
			logger.warning("could not write to {} file: {}".format(self.editor.file_location, self.editor.file_path))
			self.editor_tab_save_button.set_sensitive(False)
			gui_utilities.show_dialog_error(
				'Permission Denied',
				self.application.get_active_window(),
				"Cannot write to {} file".format(self.editor.file_location)
			)
			return
		self.editor_tab_save_button.set_sensitive(False)

	def _load_editor_file(self):
		"""
		Used to get and load the file contains of the SFTPEditor instance,
		and handle any errors found during the process
		"""
		if not self.editor:
			return

		try:
			file_contents = self.editor.directory.read_file(self.editor.file_path)
			file_contents = file_contents.decode('utf-8')
		except IOError:
			logger.warning("cannot read {} file {}".format(self.editor.file_location, self.editor.file_path))
			gui_utilities.show_dialog_error(
				'Permission Denied',
				self.application.get_active_window(),
				"Cannot read {} file".format(self.editor.file_location)
			)
			return
		except UnicodeDecodeError:
			logger.warning("could not decode content of {} file {}".format(self.editor.file_location, self.editor.file_path))
			gui_utilities.show_dialog_error(
				'Error decoding file',
				self.application.get_active_window(),
				'Can only edit utf-8 encoded file types.'
			)
			return

		if isinstance(file_contents, bytes):
			try:
				file_contents = file_contents.decode('utf-8')
			except UnicodeDecodeError:
				logger.warning("could not decode content of {} file {}".format(self.editor.file_location, self.editor.file_path))
				gui_utilities.show_dialog_error(
					'Error decoding file',
					self.application.get_active_window(),
					'Can only edit utf-8 encoded file types.'
				)
				return

		self.notebook.set_show_tabs(True)
		self.editor.load_file(file_contents)
		self.notebook.set_current_page(1)

	def signal_toggled_config_option(self, menuitem, config_key):
		self.config[config_key] = menuitem.get_active()

	def signal_toggled_config_option_show_hidden(self, menuitem):
		self.config['show_hidden'] = menuitem.get_active()
		self.local.refilter()
		self.remote.refilter()

	def _transfer_dir(self, task):
		task.state = 'Transferring'
		if isinstance(task, tasks.DownloadTask):
			dst, dst_path = self.local, task.local_path
		elif isinstance(task, tasks.UploadTask):
			dst, dst_path = self.remote, task.remote_path
		else:
			raise ValueError('task_cls must be a subclass of TransferTask')
		if not stat.S_ISDIR(dst.path_mode(dst_path)):
			dst.make_dir(dst_path)

		if not task.size:
			task.state = 'Completed'

	def _transfer_file(self, task, chunk=0x1000):
		task.state = 'Transferring'
		self.status_display.sync_view(task)
		ftp = self.remote.ftp_acquire()
		write_mode = 'ab+' if task.transferred > 0 else 'wb+'
		if isinstance(task, tasks.UploadTask):
			src_file_h = open(task.local_path, 'rb')
			dst_file_h = ftp.file(task.remote_path, write_mode)
		elif isinstance(task, tasks.DownloadTask):
			src_file_h = ftp.file(task.remote_path, 'rb')
			dst_file_h = open(task.local_path, write_mode)
		else:
			self.remote.ftp_release()
			raise ValueError('unsupported task type passed to _transfer_file')
		self.remote.ftp_release()
		src_file_h.seek(task.transferred)
		try:
			while task.transferred < task.size:
				if self._threads_shutdown.is_set():
					task.state = 'Cancelled'
				if task.state != 'Transferring':
					break
				temp = src_file_h.read(chunk)
				dst_file_h.write(temp)
				task.transferred += chunk
				self.status_display.sync_view(task)
		except Exception as error:
			raise error
		finally:
			src_file_h.close()
			dst_file_h.close()
		if task.state == 'Cancelled':
			if isinstance(task, tasks.UploadTask):
				self.remote.remove_by_file_name(task.remote_path)
			elif isinstance(task, tasks.DownloadTask):
				self.local.remove_by_file_name(task.local_path)
		elif task.state != 'Paused':
			task.state = 'Completed'
			GLib.idle_add(self._idle_refresh_directories)

	def _idle_refresh_directories(self):
		self.local.refresh()
		self.remote.refresh()

	def _thread_routine(self):
		while not self._threads_shutdown.is_set():
			task = self.queue.get()
			if isinstance(task, tasks.ShutdownTask):
				logger.info('processing task: ' + str(task))
				task.state = 'Completed'
				self.queue.remove(task)
				break
			elif isinstance(task, tasks.TransferTask):
				logger.debug('processing task: ' + str(task))
				try:
					if isinstance(task, tasks.TransferDirectoryTask):
						self._transfer_dir(task)
					else:
						self._transfer_file(task)
				except Exception:
					logger.error("unknown error processing task: {0!r}".format(task), exc_info=True)
					if not task.is_done:
						task.state = 'Error'
						for parent in task.parents:
							parent.state = 'Error'
				self.status_display.sync_view([task] + task.parents)

	def signal_window_destroy(self, _):
		self.window.set_sensitive(False)
		self._threads_shutdown.set()
		for _ in self._threads:
			self.queue.put(tasks.ShutdownTask())
		for thread in self._threads:
			thread.join()
		self.local.shutdown()
		self.remote.shutdown()
		directories = self.config.get('directories', {})
		directories['local'] = {
			'current': self.local.cwd,
			'history': list(self.local.wd_history)
		}
		if 'remote' not in directories:
			directories['remote'] = {}
		directories['remote'][self.application.config['server'].split(':', 1)[0]] = list(self.remote.wd_history)
		self.config['directories'] = directories
		self.editor = None
		sftp_utilities._gtk_objects = {}
		sftp_utilities._builder = None

	def _queue_transfer_from_selection(self, task_cls):
		selection = self.local.treeview.get_selection()
		model, treeiter = selection.get_selected()
		local_path = self.local.cwd if treeiter is None else model[treeiter][2]
		if local_path is None:
			logger.warning('can not queue a transfer when the local path is unspecified')
			return

		selection = self.remote.treeview.get_selection()
		model, treeiter = selection.get_selected()
		remote_path = self.remote.cwd if treeiter is None else model[treeiter][2]
		if remote_path is None:
			logger.warning('can not queue a transfer when the remote path is unspecified')
			return

		if issubclass(task_cls, tasks.DownloadTask):
			src_path, dst_path = remote_path, local_path
		elif issubclass(task_cls, tasks.UploadTask):
			src_path, dst_path = local_path, remote_path
		else:
			raise ValueError('task_cls must be a subclass of TransferTask')
		self.queue_transfer(task_cls, src_path, dst_path)

	def queue_transfer(self, task_cls, src_path, dst_path):
		if issubclass(task_cls, tasks.DownloadTask):
			src, dst = self.remote, self.local
		elif issubclass(task_cls, tasks.UploadTask):
			src, dst = self.local, self.remote
		else:
			raise ValueError('task_cls must be a subclass of TransferTask')
		if dst.get_is_folder(dst_path):
			dst_path = dst.path_mod.join(dst_path, src.path_mod.basename(src_path))
		if src.get_is_folder(src_path):
			self._queue_dir_transfer(task_cls, src_path, dst_path)
		else:
			self._queue_file_transfer(task_cls, src_path, dst_path)

	def _queue_file_transfer(self, task_cls, src_path, dst_path):
		"""
		Handles the file transfer by stopping bad transfers, creating tasks for
		transfers, and placing them in the queue.

		:param task_cls: The type of task the transfer will be.
		:param str src_path: The source path to be uploaded or downloaded.
		:param str dst_path: The destination path to be created and data transferred into.
		"""
		if issubclass(task_cls, tasks.DownloadTask):
			if not os.access(os.path.dirname(dst_path), os.W_OK):
				gui_utilities.show_dialog_error(
					'Permission Denied',
					self.application.get_active_window(),
					'Cannot write to the destination folder.'
				)
				return
			local_path, remote_path = self.local.get_abspath(dst_path), self.remote.get_abspath(src_path)
		elif issubclass(task_cls, tasks.UploadTask):
			if not os.access(src_path, os.R_OK):
				gui_utilities.show_dialog_error(
					'Permission Denied',
					self.application.get_active_window(),
					'Cannot read the source file.'
				)
				return
			local_path, remote_path = self.local.get_abspath(src_path), self.remote.get_abspath(dst_path)
		file_task = task_cls(local_path, remote_path)
		if isinstance(file_task, tasks.UploadTask):
			file_size = self.local.get_file_size(local_path)
		elif isinstance(file_task, tasks.DownloadTask):
			file_size = self.remote.get_file_size(remote_path)
		file_task.size = file_size
		self.queue.put(file_task)
		self.status_display.sync_view(file_task)

	def _queue_dir_transfer(self, task_cls, src_path, dst_path):
		"""
		Handles the folder transfer by stopping bad transfers, creating tasks
		for transfers, and placing them in the queue.

		:param task_cls: The type of task the transfer will be.
		:param str src_path: The path to be uploaded or downloaded.
		:param str dst_path: The path to be created.
		"""
		if issubclass(task_cls, tasks.DownloadTask):
			src, dst = self.remote, self.local
			if not os.access(dst.path_mod.dirname(dst_path), os.W_OK):
				gui_utilities.show_dialog_error('Permission Denied', self.application.get_active_window(), 'Can not write to the destination directory.')
				return
			task = task_cls.dir_cls(dst_path, src_path, size=0)
		elif issubclass(task_cls, tasks.UploadTask):
			if not os.access(src_path, os.R_OK):
				gui_utilities.show_dialog_error('Permission Denied', self.application.get_active_window(), 'Can not read the source directory.')
				return
			src, dst = self.local, self.remote
			task = task_cls.dir_cls(src_path, dst_path, size=0)
			if not stat.S_ISDIR(dst.path_mode(dst_path)):
				try:
					dst.make_dir(dst_path)
				except (IOError, OSError):
					gui_utilities.show_dialog_error('Permission Denied', self.application.get_active_window(), 'Can not create the destination directory.')
					return
		else:
			raise ValueError('unknown task class')

		queued_tasks = []
		parent_directory_tasks = collections.OrderedDict({src_path: task})

		for dir_cont in src.walk(src_path):
			dst_base_path = dst.path_mod.normpath(dst.path_mod.join(dst_path, src.get_relpath(dir_cont.dirpath, start=src_path)))
			src_base_path = dir_cont.dirpath
			parent_task = parent_directory_tasks.pop(src_base_path, None)
			if parent_task is None:
				continue
			queued_tasks.append(parent_task)

			new_task_count = 0
			if issubclass(task_cls, tasks.DownloadTask):
				local_base_path, remote_base_path = (dst_base_path, src_base_path)
			else:
				local_base_path, remote_base_path = (src_base_path, dst_base_path)

			for filename in dir_cont.filenames:
				if not self.config['transfer_hidden'] and src.path_is_hidden(src.path_mod.join(src_base_path, filename)):
					continue
				try:
					file_size = src.get_file_size(src.path_mod.join(dir_cont.dirpath, filename))
				except (IOError, OSError):
					continue  # skip this file if we can't get it's size
				task = task_cls(
					self.local.path_mod.join(local_base_path, filename),
					self.remote.path_mod.join(remote_base_path, filename),
					parent=parent_task,
					size=file_size
				)
				queued_tasks.append(task)
				new_task_count += 1

			for dirname in dir_cont.dirnames:
				if not self.config['transfer_hidden'] and src.path_is_hidden(src.path_mod.join(src_base_path, dirname)):
					continue
				task = task_cls.dir_cls(
					self.local.path_mod.join(local_base_path, dirname),
					self.remote.path_mod.join(remote_base_path, dirname),
					parent=parent_task,
					size=0
				)
				parent_directory_tasks[src.path_mod.join(src_base_path, dirname)] = task
				new_task_count += 1

			parent_task.size += new_task_count
			for grandparent_task in parent_task.parents:
				grandparent_task.size += new_task_count
		for task in queued_tasks:
			self.queue.put(task)
		self.status_display.sync_view(queued_tasks)