import logging
import time
from pathlib import Path
from typing import List, Type, Set

from PyQt5.QtCore import QEvent, Qt, QSize, pyqtSignal, QCoreApplication
from PyQt5.QtGui import QIcon, QWindowStateChangeEvent
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QCheckBox, QHeaderView, QToolBar, \
    QLabel, QPlainTextEdit, QLineEdit, QProgressBar, QPushButton, QComboBox, QApplication, QListView, QSizePolicy

from bauh.api.abstract.cache import MemoryCache
from bauh.api.abstract.context import ApplicationContext
from bauh.api.abstract.controller import SoftwareManager
from bauh.api.abstract.model import SoftwarePackage, PackageAction
from bauh.api.abstract.view import MessageType
from bauh.api.http import HttpClient
from bauh.commons import user
from bauh.commons.html import bold
from bauh.gems.web import TEMP_PATH
from bauh.view.qt import dialog, commons, qt_utils, root
from bauh.view.qt.about import AboutDialog
from bauh.view.qt.apps_table import AppsTable, UpdateToggleButton
from bauh.view.qt.components import new_spacer, InputFilter, IconButton
from bauh.view.qt.confirmation import ConfirmationDialog
from bauh.view.qt.history import HistoryDialog
from bauh.view.qt.info import InfoDialog
from bauh.view.qt.root import ask_root_password
from bauh.view.qt.screenshots import ScreenshotsDialog
from bauh.view.qt.settings import SettingsWindow
from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, DowngradeApp, GetAppInfo, \
    GetAppHistory, SearchPackages, InstallPackage, AnimateProgress, VerifyModels, FindSuggestions, ListWarnings, \
    AsyncAction, LaunchApp, ApplyFilters, CustomAction, GetScreenshots
from bauh.view.qt.view_model import PackageView
from bauh.view.util import util, resource
from bauh.view.util.translation import I18n

DARK_ORANGE = '#FF4500'


def toolbar_button_style(bg: str):
    return 'QPushButton { color: white; font-weight: bold; background: ' + bg + '}'


class ManageWindow(QWidget):
    __BASE_HEIGHT__ = 400

    signal_user_res = pyqtSignal(bool)
    signal_root_password = pyqtSignal(str, bool)
    signal_table_update = pyqtSignal()

    def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, screen_size, config: dict,
                 context: ApplicationContext, http_client: HttpClient, logger: logging.Logger, icon: QIcon, tray_icon=None):
        super(ManageWindow, self).__init__()
        self.i18n = i18n
        self.logger = logger
        self.manager = manager
        self.tray_icon = tray_icon
        self.working = False  # restrict the number of threaded actions
        self.pkgs = []  # packages current loaded in the table
        self.pkgs_available = []  # all packages loaded in memory
        self.pkgs_installed = []  # cached installed packages
        self.display_limit = config['ui']['table']['max_displayed']
        self.icon_cache = icon_cache
        self.screen_size = screen_size
        self.config = config
        self.context = context
        self.http_client = http_client

        self.icon_app = icon
        self.resize(ManageWindow.__BASE_HEIGHT__, ManageWindow.__BASE_HEIGHT__)
        self.setWindowIcon(self.icon_app)

        self.layout = QVBoxLayout()
        self.setLayout(self.layout)

        self.toolbar_top = QToolBar()
        self.toolbar_top.addWidget(new_spacer())

        self.label_status = QLabel()
        self.label_status.setText('')
        self.label_status.setStyleSheet("font-weight: bold")
        self.toolbar_top.addWidget(self.label_status)

        self.toolbar_search = QToolBar()
        self.toolbar_search.setStyleSheet("spacing: 0px;")
        self.toolbar_search.setContentsMargins(0, 0, 0, 0)

        label_pre_search = QLabel()
        label_pre_search.setStyleSheet("""
            background: white; 
            border-top-left-radius: 5px; 
            border-bottom-left-radius: 5px;
            border-left: 1px solid lightgrey; 
            border-top: 1px solid lightgrey; 
            border-bottom: 1px solid lightgrey;
        """)

        self.toolbar_search.addWidget(label_pre_search)

        self.input_search = QLineEdit()
        self.input_search.setFrame(False)
        self.input_search.setPlaceholderText(self.i18n['window_manage.input_search.placeholder'] + "...")
        self.input_search.setToolTip(self.i18n['window_manage.input_search.tooltip'])
        self.input_search.setStyleSheet("""QLineEdit { 
                background-color: white; 
                color: grey; 
                spacing: 0; 
                height: 30px; 
                font-size: 12px; 
                width: 300px; 
                border-bottom: 1px solid lightgrey; 
                border-top: 1px solid lightgrey; 
        } 
        """)
        self.input_search.returnPressed.connect(self.search)
        self.toolbar_search.addWidget(self.input_search)

        label_pos_search = QLabel()
        label_pos_search.setPixmap(QIcon(resource.get_path('img/search.svg')).pixmap(QSize(10, 10)))
        label_pos_search.setStyleSheet("""
            background: white; padding-right: 10px; 
            border-top-right-radius: 5px; 
            border-bottom-right-radius: 5px; 
            border-right: 1px solid lightgrey; 
            border-top: 1px solid lightgrey; 
            border-bottom: 1px solid lightgrey;
        """)

        self.toolbar_search.addWidget(label_pos_search)

        self.ref_toolbar_search = self.toolbar_top.addWidget(self.toolbar_search)
        self.toolbar_top.addWidget(new_spacer())
        self.layout.addWidget(self.toolbar_top)

        self.toolbar = QToolBar()
        self.toolbar.setStyleSheet('QToolBar {spacing: 4px; margin-top: 15px; margin-bottom: 5px}')
        self.toolbar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)

        self.checkbox_updates = QCheckBox()
        self.checkbox_updates.setText(self.i18n['updates'].capitalize())
        self.checkbox_updates.stateChanged.connect(self._handle_updates_filter)
        self.ref_checkbox_updates = self.toolbar.addWidget(self.checkbox_updates)

        self.checkbox_only_apps = QCheckBox()
        self.checkbox_only_apps.setText(self.i18n['manage_window.checkbox.only_apps'])
        self.checkbox_only_apps.setChecked(True)
        self.checkbox_only_apps.stateChanged.connect(self._handle_filter_only_apps)
        self.ref_checkbox_only_apps = self.toolbar.addWidget(self.checkbox_only_apps)

        self.any_type_filter = 'any'
        self.cache_type_filter_icons = {}
        self.combo_filter_type = QComboBox()
        self.combo_filter_type.setView(QListView())
        self.combo_filter_type.setStyleSheet('QLineEdit { height: 2px; }')
        self.combo_filter_type.setIconSize(QSize(14, 14))
        self.combo_filter_type.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.combo_filter_type.setEditable(True)
        self.combo_filter_type.lineEdit().setReadOnly(True)
        self.combo_filter_type.lineEdit().setAlignment(Qt.AlignCenter)
        self.combo_filter_type.activated.connect(self._handle_type_filter)
        self.combo_filter_type.addItem('--- {} ---'.format(self.i18n['type'].capitalize()), self.any_type_filter)
        self.ref_combo_filter_type = self.toolbar.addWidget(self.combo_filter_type)

        self.any_category_filter = 'any'
        self.combo_categories = QComboBox()
        self.combo_categories.setStyleSheet('QLineEdit { height: 2px; }')
        self.combo_categories.setSizeAdjustPolicy(QComboBox.AdjustToContents)
        self.combo_categories.setEditable(True)
        self.combo_categories.lineEdit().setReadOnly(True)
        self.combo_categories.lineEdit().setAlignment(Qt.AlignCenter)
        self.combo_categories.activated.connect(self._handle_category_filter)
        self.combo_categories.addItem('--- {} ---'.format(self.i18n['category'].capitalize()), self.any_category_filter)
        self.ref_combo_categories = self.toolbar.addWidget(self.combo_categories)

        self.input_name_filter = InputFilter(self.apply_filters_async)
        self.input_name_filter.setMaxLength(10)
        self.input_name_filter.setPlaceholderText(self.i18n['manage_window.name_filter.placeholder'] + '...')
        self.input_name_filter.setToolTip(self.i18n['manage_window.name_filter.tooltip'])
        self.input_name_filter.setStyleSheet("QLineEdit { background-color: white; color: gray;}")
        self.input_name_filter.setFixedWidth(130)
        self.ref_input_name_filter = self.toolbar.addWidget(self.input_name_filter)

        self.toolbar.addWidget(new_spacer())

        toolbar_bts = []

        self.bt_installed = QPushButton()
        self.bt_installed.setToolTip(self.i18n['manage_window.bt.installed.tooltip'])
        self.bt_installed.setIcon(QIcon(resource.get_path('img/disk.svg')))
        self.bt_installed.setText(self.i18n['manage_window.bt.installed.text'].capitalize())
        self.bt_installed.clicked.connect(self._show_installed)
        self.bt_installed.setStyleSheet(toolbar_button_style('#A94E0A'))
        self.ref_bt_installed = self.toolbar.addWidget(self.bt_installed)
        toolbar_bts.append(self.bt_installed)

        if config['suggestions']['enabled']:
            self.bt_suggestions = QPushButton()
            self.bt_installed.setToolTip(self.i18n['manage_window.bt.suggestions.tooltip'])
            self.bt_suggestions.setText(self.i18n['manage_window.bt.suggestions.text'].capitalize())
            self.bt_suggestions.setIcon(QIcon(resource.get_path('img/suggestions.svg')))
            self.bt_suggestions.setStyleSheet(toolbar_button_style('#FF8000'))
            self.bt_suggestions.clicked.connect(self.read_suggestions)
            self.ref_bt_suggestions = self.toolbar.addWidget(self.bt_suggestions)
            toolbar_bts.append(self.bt_suggestions)
        else:
            self.bt_suggestions = None
            self.ref_bt_suggestions = None

        self.bt_refresh = QPushButton()
        self.bt_refresh.setToolTip(i18n['manage_window.bt.refresh.tooltip'])
        self.bt_refresh.setIcon(QIcon(resource.get_path('img/refresh.svg')))
        self.bt_refresh.setText(self.i18n['manage_window.bt.refresh.text'])
        self.bt_refresh.setStyleSheet(toolbar_button_style('#2368AD'))
        self.bt_refresh.clicked.connect(lambda: self.refresh_apps(keep_console=False))
        toolbar_bts.append(self.bt_refresh)
        self.ref_bt_refresh = self.toolbar.addWidget(self.bt_refresh)

        self.bt_upgrade = QPushButton()
        self.bt_upgrade.setToolTip(i18n['manage_window.bt.upgrade.tooltip'])
        self.bt_upgrade.setIcon(QIcon(resource.get_path('img/app_update.svg')))
        self.bt_upgrade.setText(i18n['manage_window.bt.upgrade.text'])
        self.bt_upgrade.setStyleSheet(toolbar_button_style('#20A435'))
        self.bt_upgrade.clicked.connect(self.update_selected)
        toolbar_bts.append(self.bt_upgrade)
        self.ref_bt_upgrade = self.toolbar.addWidget(self.bt_upgrade)

        # setting all buttons to the same size:
        bt_biggest_size = 0
        for bt in toolbar_bts:
            bt_width = bt.sizeHint().width()
            if bt_width > bt_biggest_size:
                bt_biggest_size = bt_width

        for bt in toolbar_bts:
            bt_width = bt.sizeHint().width()
            if bt_biggest_size > bt_width:
                bt.setFixedWidth(bt_biggest_size)

        self.layout.addWidget(self.toolbar)

        self.table_apps = AppsTable(self, self.icon_cache,
                                    disk_cache=bool(self.config['disk_cache']['enabled']),
                                    download_icons=bool(self.config['download']['icons']))
        self.table_apps.change_headers_policy()

        self.layout.addWidget(self.table_apps)

        toolbar_console = QToolBar()

        self.checkbox_console = QCheckBox()
        self.checkbox_console.setText(self.i18n['manage_window.checkbox.show_details'])
        self.checkbox_console.stateChanged.connect(self._handle_console)
        self.checkbox_console.setVisible(False)
        self.ref_checkbox_console = toolbar_console.addWidget(self.checkbox_console)

        toolbar_console.addWidget(new_spacer())

        self.label_displayed = QLabel()
        toolbar_console.addWidget(self.label_displayed)

        self.layout.addWidget(toolbar_console)

        self.textarea_output = QPlainTextEdit(self)
        self.textarea_output.resize(self.table_apps.size())
        self.textarea_output.setStyleSheet("background: black; color: white;")
        self.layout.addWidget(self.textarea_output)
        self.textarea_output.setVisible(False)
        self.textarea_output.setReadOnly(True)

        self.toolbar_substatus = QToolBar()
        self.toolbar_substatus.addWidget(new_spacer())
        self.label_substatus = QLabel()
        self.toolbar_substatus.addWidget(self.label_substatus)
        self.toolbar_substatus.addWidget(new_spacer())
        self.layout.addWidget(self.toolbar_substatus)
        self._change_label_substatus('')

        self.thread_update = self._bind_async_action(UpdateSelectedApps(self.manager, self.i18n), finished_call=self._finish_update_selected)
        self.thread_refresh = self._bind_async_action(RefreshApps(self.manager), finished_call=self._finish_refresh_apps, only_finished=True)
        self.thread_uninstall = self._bind_async_action(UninstallApp(self.manager, self.icon_cache), finished_call=self._finish_uninstall)
        self.thread_get_info = self._bind_async_action(GetAppInfo(self.manager), finished_call=self._finish_get_info)
        self.thread_get_history = self._bind_async_action(GetAppHistory(self.manager, self.i18n), finished_call=self._finish_get_history)
        self.thread_search = self._bind_async_action(SearchPackages(self.manager), finished_call=self._finish_search, only_finished=True)
        self.thread_downgrade = self._bind_async_action(DowngradeApp(self.manager, self.i18n), finished_call=self._finish_downgrade)
        self.thread_suggestions = self._bind_async_action(FindSuggestions(man=self.manager), finished_call=self._finish_search, only_finished=True)
        self.thread_run_app = self._bind_async_action(LaunchApp(self.manager), finished_call=self._finish_run_app, only_finished=False)
        self.thread_custom_action = self._bind_async_action(CustomAction(manager=self.manager, i18n=self.i18n), finished_call=self._finish_custom_action)
        self.thread_screenshots = self._bind_async_action(GetScreenshots(self.manager), finished_call=self._finish_get_screenshots)

        self.thread_apply_filters = ApplyFilters()
        self.thread_apply_filters.signal_finished.connect(self._finish_apply_filters_async)
        self.thread_apply_filters.signal_table.connect(self._update_table_and_upgrades)
        self.signal_table_update.connect(self.thread_apply_filters.stop_waiting)

        self.thread_install = InstallPackage(manager=self.manager, disk_cache=bool(self.config['disk_cache']['enabled']), icon_cache=self.icon_cache, i18n=self.i18n)
        self._bind_async_action(self.thread_install, finished_call=self._finish_install)

        self.thread_animate_progress = AnimateProgress()
        self.thread_animate_progress.signal_change.connect(self._update_progress)

        self.thread_verify_models = VerifyModels()
        self.thread_verify_models.signal_updates.connect(self._notify_model_data_change)

        self.toolbar_bottom = QToolBar()
        self.toolbar_bottom.setIconSize(QSize(16, 16))
        self.toolbar_bottom.setStyleSheet('QToolBar { spacing: 3px }')

        self.toolbar_bottom.addWidget(new_spacer())

        self.progress_bar = QProgressBar()
        self.progress_bar.setMaximumHeight(10 if QApplication.instance().style().objectName().lower() == 'windows' else 4)

        self.progress_bar.setTextVisible(False)
        self.ref_progress_bar = self.toolbar_bottom.addWidget(self.progress_bar)

        self.toolbar_bottom.addWidget(new_spacer())

        bt_settings = IconButton(QIcon(resource.get_path('img/app_settings.svg')),
                                 action=self.show_settings,
                                 background='#12ABAB',
                                 i18n=self.i18n,
                                 tooltip=self.i18n['manage_window.bt_settings.tooltip'])
        self.ref_bt_settings = self.toolbar_bottom.addWidget(bt_settings)

        bt_about = IconButton(QIcon(resource.get_path('img/question.svg')),
                              action=self._show_about,
                              background='#2E68D3',
                              i18n=self.i18n,
                              tooltip=self.i18n['manage_window.settings.about'])
        self.ref_bt_about = self.toolbar_bottom.addWidget(bt_about)

        self.layout.addWidget(self.toolbar_bottom)

        qt_utils.centralize(self)

        self.filter_only_apps = True
        self.type_filter = self.any_type_filter
        self.category_filter = self.any_category_filter
        self.filter_updates = False
        self._maximized = False
        self.progress_controll_enabled = True
        self.recent_installation = False
        self.recent_uninstall = False
        self.types_changed = False

        self.dialog_about = None
        self.first_refresh = bool(config['suggestions']['enabled'])

        self.thread_warnings = ListWarnings(man=manager, i18n=i18n)
        self.thread_warnings.signal_warnings.connect(self._show_warnings)
        self.settings_window = None
        self.search_performed = False

    def set_tray_icon(self, tray_icon):
        self.tray_icon = tray_icon
        # self.combo_styles.show_panel_after_restart = bool(tray_icon)

    def _update_process_progress(self, val: int):
        if self.progress_controll_enabled:
            self.thread_animate_progress.set_progress(val)

    def apply_filters_async(self):
        self.label_status.setText(self.i18n['manage_window.status.filtering'] + '...')

        self.ref_toolbar_search.setVisible(False)

        if self.ref_input_name_filter.isVisible():
            self.input_name_filter.setReadOnly(True)

        self.thread_apply_filters.filters = self._gen_filters()
        self.thread_apply_filters.pkgs = self.pkgs_available
        self.thread_apply_filters.start()

    def _update_table_and_upgrades(self, pkgs_info: dict):
        self._update_table(pkgs_info=pkgs_info, signal=True)
        self.update_bt_upgrade(pkgs_info)

        if self.pkgs_available:
            self._notify_model_data_change()
            self.thread_verify_models.work = False
            self.thread_verify_models.wait(50)
            self.thread_verify_models.apps = self.pkgs_available
            self.thread_verify_models.start()

    def _finish_apply_filters_async(self, success: bool):
        self.label_status.setText('')
        self.ref_toolbar_search.setVisible(True)

        if self.ref_input_name_filter.isVisible():
            self.input_name_filter.setReadOnly(False)

    def _bind_async_action(self, action: AsyncAction, finished_call, only_finished: bool = False) -> AsyncAction:
        action.signal_finished.connect(finished_call)

        if not only_finished:
            action.signal_confirmation.connect(self._ask_confirmation)
            action.signal_output.connect(self._update_action_output)
            action.signal_message.connect(self._show_message)
            action.signal_status.connect(self._change_label_status)
            action.signal_substatus.connect(self._change_label_substatus)
            action.signal_progress.connect(self._update_process_progress)
            action.signal_root_password.connect(self._ask_root_password)

            self.signal_user_res.connect(action.confirm)
            self.signal_root_password.connect(action.set_root_password)

        return action

    def _ask_confirmation(self, msg: dict):
        self.thread_animate_progress.pause()
        diag = ConfirmationDialog(title=msg['title'],
                                  body=msg['body'],
                                  i18n=self.i18n,
                                  components=msg['components'],
                                  confirmation_label=msg['confirmation_label'],
                                  deny_label=msg['deny_label'],
                                  screen_size=self.screen_size)
        res = diag.is_confirmed()
        self.thread_animate_progress.animate()
        self.signal_user_res.emit(res)

    def _ask_root_password(self):
        self.thread_animate_progress.pause()
        password, valid = root.ask_root_password(self.i18n)
        self.thread_animate_progress.animate()
        self.signal_root_password.emit(password, valid)

    def _show_message(self, msg: dict):
        self.thread_animate_progress.pause()
        dialog.show_message(title=msg['title'], body=msg['body'], type_=msg['type'])
        self.thread_animate_progress.animate()

    def _show_warnings(self, warnings: List[str]):
        if warnings:
            dialog.show_message(title=self.i18n['warning'].capitalize(), body='<p>{}</p>'.format('<br/><br/>'.join(warnings)), type_=MessageType.WARNING)

    def show(self):
        super(ManageWindow, self).show()

        if not self.thread_warnings.isFinished():
            self.thread_warnings.start()

    def verify_warnings(self):
        self.thread_warnings.start()

    def _show_installed(self):
        if self.pkgs_installed:
            self.finish_action()
            self.search_performed = False
            self.ref_bt_upgrade.setVisible(True)
            self.ref_checkbox_only_apps.setVisible(True)
            self.input_search.setText('')
            self.input_name_filter.setText('')
            self.update_pkgs(new_pkgs=None, as_installed=True)

    def _show_about(self):
        if self.dialog_about is None:
            self.dialog_about = AboutDialog(self.i18n)

        self.dialog_about.show()

    def _handle_updates_filter(self, status: int):
        self.filter_updates = status == 2
        self.apply_filters_async()

    def _handle_filter_only_apps(self, status: int):
        self.filter_only_apps = status == 2
        self.apply_filters_async()

    def _handle_type_filter(self, idx: int):
        self.type_filter = self.combo_filter_type.itemData(idx)
        self.combo_filter_type.adjustSize()
        self.apply_filters_async()

    def _handle_category_filter(self, idx: int):
        self.category_filter = self.combo_categories.itemData(idx)
        self.apply_filters_async()

    def _notify_model_data_change(self):
        self.table_apps.fill_async_data()

        if not self.recent_installation:
            self._reload_categories()

    def _reload_categories(self):

        categories = set()

        for p in self.pkgs_available:
            if p.model.categories:
                for c in p.model.categories:
                    categories.add(c.lower())

        if categories:
            self._update_categories(categories, keep_selected=True)

    def changeEvent(self, e: QEvent):
        if isinstance(e, QWindowStateChangeEvent):
            self._maximized = self.isMaximized()
            policy = QHeaderView.Stretch if self._maximized else QHeaderView.ResizeToContents
            self.table_apps.change_headers_policy(policy)

    def closeEvent(self, event):
        if self.tray_icon:
            event.ignore()
            self.hide()
            self._handle_console_option(False)
        else:
            QCoreApplication.exit()  # needed because QuitOnLastWindowClosed is disabled

    def _handle_console(self, checked: bool):

        if checked:
            self.textarea_output.show()
        else:
            self.textarea_output.hide()

    def _handle_console_option(self, enable: bool):

        if enable:
            self.textarea_output.clear()

        self.ref_checkbox_console.setVisible(enable)
        self.checkbox_console.setChecked(False)
        self.textarea_output.hide()

    def refresh_apps(self, keep_console: bool = True, top_app: PackageView = None, pkg_types: Set[Type[SoftwarePackage]] = None):
        self.recent_installation = False
        self.input_search.clear()

        if not keep_console:
            self._handle_console_option(False)

        self.ref_checkbox_updates.setVisible(False)
        self.ref_checkbox_only_apps.setVisible(False)
        self._begin_action(self.i18n['manage_window.status.refreshing'], keep_bt_installed=False, clear_filters=not self.recent_uninstall)

        self.thread_refresh.app = top_app  # the app will be on top when refresh happens
        self.thread_refresh.pkg_types = pkg_types
        self.thread_refresh.start()

    def read_suggestions(self):
        self.input_search.clear()
        self._handle_console_option(False)
        self.ref_checkbox_updates.setVisible(False)
        self.ref_checkbox_only_apps.setVisible(False)
        self._begin_action(self.i18n['manage_window.status.suggestions'], keep_bt_installed=False, clear_filters=not self.recent_uninstall)
        self.thread_suggestions.filter_installed = True
        self.thread_suggestions.start()

    def _finish_refresh_apps(self, res: dict, as_installed: bool = True):
        self.finish_action()
        self.search_performed = False

        self.ref_checkbox_only_apps.setVisible(bool(res['installed']))
        self.ref_bt_upgrade.setVisible(True)
        self.update_pkgs(res['installed'], as_installed=as_installed, types=res['types'], keep_filters=self.recent_uninstall and res['types'])
        self.first_refresh = False
        self.recent_uninstall = False
        self.types_changed = False
        self._hide_fields_after_recent_installation()

    def uninstall_app(self, app: PackageView):
        pwd = None
        requires_root = self.manager.requires_root('uninstall', app.model)

        if not user.is_root() and requires_root:
            pwd, ok = ask_root_password(self.i18n)

            if not ok:
                return

        self._handle_console_option(True)
        self._begin_action('{} {}'.format(self.i18n['manage_window.status.uninstalling'], app.model.name), clear_filters=False)

        self.thread_uninstall.app = app
        self.thread_uninstall.root_password = pwd
        self.thread_uninstall.start()

    def run_app(self, app: PackageView):
        self._begin_action(self.i18n['manage_window.status.running_app'].format(app.model.name))
        self.thread_run_app.app = app
        self.thread_run_app.start()

    def _finish_uninstall(self, pkgv: PackageView):
        self.finish_action()

        if pkgv:
            if self._can_notify_user():
                util.notify_user('{} ({}) {}'.format(pkgv.model.name, pkgv.model.get_type(), self.i18n['uninstalled']))

            if not self.search_performed:
                only_pkg_type = len([p for p in self.pkgs if p.model.get_type() == pkgv.model.get_type()]) >= 2
            else:
                only_pkg_type = False

            self.recent_uninstall = True
            self.refresh_apps(pkg_types={pkgv.model.__class__} if only_pkg_type else None)

            if self.tray_icon:
                self.tray_icon.verify_updates()
        else:
            if self._can_notify_user():
                util.notify_user('{}: {}'.format(pkgv.model.name, self.i18n['notification.uninstall.failed']))

            self.checkbox_console.setChecked(True)

    def _can_notify_user(self):
        return bool(self.config['system']['notifications']) and (self.isHidden() or self.isMinimized())

    def _finish_downgrade(self, res: dict):
        self.finish_action()

        if res['success']:
            if self._can_notify_user():
                util.notify_user('{} {}'.format(res['app'], self.i18n['downgraded']))

            self.refresh_apps(pkg_types={res['app'].model.__class__} if len(self.pkgs) > 1 else None)

            if self.tray_icon:
                self.tray_icon.verify_updates(notify_user=False)
        else:
            if self._can_notify_user():
                util.notify_user(self.i18n['notification.downgrade.failed'])

            self.checkbox_console.setChecked(True)

    def _change_label_status(self, status: str):
        self.label_status.setText(status)

    def _change_label_substatus(self, substatus: str):
        self.label_substatus.setText('<p>{}</p>'.format(substatus))
        if not substatus:
            self.toolbar_substatus.hide()
        elif not self.toolbar_substatus.isVisible():
            self.toolbar_substatus.show()

    def _update_table(self, pkgs_info: dict, signal: bool = False):
        self.pkgs = pkgs_info['pkgs_displayed']

        self.table_apps.update_pkgs(self.pkgs, update_check_enabled=pkgs_info['not_installed'] == 0)

        if not self._maximized:
            self.table_apps.change_headers_policy(QHeaderView.Stretch)
            self.table_apps.change_headers_policy()
            self.resize_and_center(accept_lower_width=len(self.pkgs) > 0)
            self.label_displayed.setText('{} / {}'.format(len(self.pkgs), len(self.pkgs_available)))
        else:
            self.label_displayed.setText('')

        if signal:
            self.signal_table_update.emit()

    def update_bt_upgrade(self, pkgs_info: dict = None):
        show_bt_upgrade = False

        if not pkgs_info or pkgs_info['not_installed'] == 0:
            for app_v in (pkgs_info['pkgs_displayed'] if pkgs_info else self.pkgs):
                if app_v.update_checked:
                    show_bt_upgrade = True
                    break

        self.ref_bt_upgrade.setVisible(show_bt_upgrade)

    def change_update_state(self, pkgs_info: dict, trigger_filters: bool = True, keep_selected: bool = False):
        self.update_bt_upgrade(pkgs_info)

        if pkgs_info['updates'] > 0:

            if pkgs_info['not_installed'] == 0:
                if not self.ref_checkbox_updates.isVisible():
                    self.ref_checkbox_updates.setVisible(True)

                if not self.filter_updates and not keep_selected:
                    self._change_checkbox(self.checkbox_updates, True, 'filter_updates', trigger_filters)

            if pkgs_info['napp_updates'] > 0 and self.filter_only_apps and not keep_selected:
                self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger_filters)
        else:
            if not keep_selected:
                self._change_checkbox(self.checkbox_updates, False, 'filter_updates', trigger_filters)

            self.ref_checkbox_updates.setVisible(False)

    def _change_checkbox(self, checkbox: QCheckBox, checked: bool, attr: str = None, trigger: bool = True):
        if not trigger:
            checkbox.blockSignals(True)

        checkbox.setChecked(checked)

        if not trigger:
            setattr(self, attr, checked)
            checkbox.blockSignals(False)

    def _gen_filters(self, updates: int = 0, ignore_updates: bool = False) -> dict:
        return {
            'only_apps': False if self.search_performed else self.filter_only_apps,
            'type': self.type_filter,
            'category': self.category_filter,
            'updates': False if ignore_updates else self.filter_updates,
            'name': self.input_name_filter.get_text().lower() if self.input_name_filter.get_text() else None,
            'display_limit': self.display_limit if updates <= 0 else None
        }

    def update_pkgs(self, new_pkgs: List[SoftwarePackage], as_installed: bool, types: Set[type] = None, ignore_updates: bool = False, keep_filters: bool = False):
        self.input_name_filter.setText('')
        pkgs_info = commons.new_pkgs_info()
        filters = self._gen_filters(ignore_updates)

        if new_pkgs is not None:
            old_installed = None

            if as_installed:
                old_installed = self.pkgs_installed
                self.pkgs_installed = []

            for pkg in new_pkgs:
                app_model = PackageView(model=pkg)
                commons.update_info(app_model, pkgs_info)
                commons.apply_filters(app_model, filters, pkgs_info)

            if old_installed and types:
                for pkgv in old_installed:
                    if not pkgv.model.__class__ in types:
                        commons.update_info(pkgv, pkgs_info)
                        commons.apply_filters(pkgv, filters, pkgs_info)

        else:  # use installed
            for pkgv in self.pkgs_installed:
                commons.update_info(pkgv, pkgs_info)
                commons.apply_filters(pkgv, filters, pkgs_info)

        if pkgs_info['apps_count'] == 0:
            if self.first_refresh or self.types_changed:
                self._begin_search('')
                self.thread_suggestions.filter_installed = False
                self.thread_suggestions.start()
                return
            else:
                if not keep_filters:
                    self._change_checkbox(self.checkbox_only_apps, False, 'filter_only_apps', trigger=False)
                    self.checkbox_only_apps.setCheckable(False)
        else:
            if not keep_filters:
                self.checkbox_only_apps.setCheckable(True)
                self._change_checkbox(self.checkbox_only_apps, True, 'filter_only_apps', trigger=False)

        self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed']))
        self._update_categories(pkgs_info['categories'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed']))
        self._update_type_filters(pkgs_info['available_types'], keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed']))
        self._apply_filters(pkgs_info, ignore_updates=ignore_updates)
        self.change_update_state(pkgs_info=pkgs_info, trigger_filters=False, keep_selected=keep_filters and bool(pkgs_info['pkgs_displayed']))

        self.pkgs_available = pkgs_info['pkgs']

        if as_installed:
            self.pkgs_installed = pkgs_info['pkgs']

        self.pkgs = pkgs_info['pkgs_displayed']

        if self.pkgs:
            self.ref_input_name_filter.setVisible(True)

        self._update_table(pkgs_info=pkgs_info)

        if new_pkgs:
            self.thread_verify_models.apps = self.pkgs
            self.thread_verify_models.start()

        if self.pkgs_installed:
            self.ref_bt_installed.setVisible(not as_installed and not self.recent_installation)

        self.resize_and_center(accept_lower_width=self.pkgs_installed)

    def _apply_filters(self, pkgs_info: dict, ignore_updates: bool):
        pkgs_info['pkgs_displayed'] = []
        filters = self._gen_filters(updates=pkgs_info['updates'], ignore_updates=ignore_updates)
        for pkgv in pkgs_info['pkgs']:
            commons.apply_filters(pkgv, filters, pkgs_info)

    def _update_type_filters(self, available_types: dict = None, keep_selected: bool = False):

        if available_types is None:
            self.ref_combo_filter_type.setVisible(self.combo_filter_type.count() > 1)
        else:
            keeping_selected = keep_selected and available_types and self.type_filter in available_types

            if not keeping_selected:
                self.type_filter = self.any_type_filter

            if available_types and len(available_types) > 1:
                if self.combo_filter_type.count() > 1:
                    for _ in range(self.combo_filter_type.count() - 1):
                        self.combo_filter_type.removeItem(1)

                sel_type = -1
                for idx, item in enumerate(available_types.items()):
                    app_type, icon_path = item[0], item[1]

                    icon = self.cache_type_filter_icons.get(app_type)

                    if not icon:
                        icon = QIcon(icon_path)
                        self.cache_type_filter_icons[app_type] = icon

                    self.combo_filter_type.addItem(icon, app_type.capitalize(), app_type)

                    if keeping_selected and app_type == self.type_filter:
                        sel_type = idx + 1

                self.combo_filter_type.blockSignals(True)
                self.combo_filter_type.setCurrentIndex(sel_type if sel_type > -1 else 0)
                self.combo_filter_type.blockSignals(False)
                self.ref_combo_filter_type.setVisible(True)
            else:
                self.ref_combo_filter_type.setVisible(False)

    def _update_categories(self, categories: Set[str] = None, keep_selected: bool = False):

        if categories is None:
            self.ref_combo_categories.setVisible(self.combo_categories.count() > 1)
        else:
            keeping_selected = keep_selected and categories and self.category_filter in categories

            if not keeping_selected:
                self.category_filter = self.any_category_filter

            if categories:
                if self.combo_categories.count() > 1:
                    for _ in range(self.combo_categories.count() - 1):
                        self.combo_categories.removeItem(1)

                selected_cat = -1
                cat_list = list(categories)
                cat_list.sort()

                for idx, c in enumerate(cat_list):
                    i18n_cat = self.i18n.get(c)
                    cat_label = i18n_cat if i18n_cat else c
                    self.combo_categories.addItem(cat_label.capitalize(), c)

                    if keeping_selected and c == self.category_filter:
                        selected_cat = idx + 1

                self.combo_categories.blockSignals(True)
                self.combo_categories.setCurrentIndex(selected_cat if selected_cat > -1 else 0)
                self.combo_categories.blockSignals(False)
                self.ref_combo_categories.setVisible(True)

            else:
                self.ref_combo_categories.setVisible(False)

    def resize_and_center(self, accept_lower_width: bool = True):
        table_width = self.table_apps.get_width()
        toolbar_width = self.toolbar.sizeHint().width()
        topbar_width = self.toolbar_top.sizeHint().width()

        new_width = max(table_width, toolbar_width, topbar_width)
        new_width *= 1.05  # this extra size is not because of the toolbar button, but the table upgrade buttons

        if (self.pkgs and accept_lower_width) or new_width > self.width():
            self.resize(new_width, self.height())

        qt_utils.centralize(self)

    def update_selected(self):
        if self.pkgs:
            requires_root = False

            to_update = []

            for app_v in self.pkgs:
                if app_v.update_checked:
                    to_update.append(app_v)

                    if self.manager.requires_root('update', app_v.model):
                        requires_root = True

            if to_update and dialog.ask_confirmation(title=self.i18n['manage_window.upgrade_all.popup.title'],
                                                     body=self.i18n['manage_window.upgrade_all.popup.body'],
                                                     i18n=self.i18n,
                                                     widgets=[UpdateToggleButton(None, self, self.i18n, clickable=False)]):
                pwd = None

                if not user.is_root() and requires_root:
                    pwd, ok = ask_root_password(self.i18n)

                    if not ok:
                        return

                self._handle_console_option(True)
                self.progress_controll_enabled = len(to_update) == 1
                self._begin_action(self.i18n['manage_window.status.upgrading'])
                self.thread_update.pkgs = to_update
                self.thread_update.root_password = pwd
                self.thread_update.start()

    def _finish_update_selected(self, res: dict):
        self.finish_action()

        if res['success']:
            if self._can_notify_user():
                util.notify_user('{} {}'.format(res['updated'], self.i18n['notification.update_selected.success']))

            self.refresh_apps(pkg_types=res['types'])

            if self.tray_icon:
                self.tray_icon.verify_updates()
        else:
            if self._can_notify_user():
                util.notify_user(self.i18n['notification.update_selected.failed'])

            self.ref_bt_upgrade.setVisible(True)
            self.checkbox_console.setChecked(True)

    def _update_action_output(self, output: str):
        self.textarea_output.appendPlainText(output)

    def _begin_action(self, action_label: str, keep_search: bool = False, keep_bt_installed: bool = True, clear_filters: bool = False):
        self.ref_input_name_filter.setVisible(False)
        self.ref_combo_filter_type.setVisible(False)
        self.ref_combo_categories.setVisible(False)
        self.ref_bt_settings.setVisible(False)
        self.ref_bt_about.setVisible(False)
        self.thread_animate_progress.stop = False
        self.thread_animate_progress.start()
        self.ref_progress_bar.setVisible(True)

        self.label_status.setText(action_label + "...")
        self.ref_bt_upgrade.setVisible(False)
        self.ref_bt_refresh.setVisible(False)

        if self.ref_bt_suggestions:
            self.ref_bt_suggestions.setVisible(False)

        self.checkbox_only_apps.setEnabled(False)
        self.table_apps.setEnabled(False)
        self.checkbox_updates.setEnabled(False)

        if not keep_bt_installed:
            self.ref_bt_installed.setVisible(False)
        elif self.ref_bt_installed.isVisible():
            self.ref_bt_installed.setEnabled(False)

        if keep_search:
            self.ref_toolbar_search.setVisible(True)
        else:
            self.ref_toolbar_search.setVisible(False)

        if clear_filters:
            self._update_type_filters({})
            self._update_categories(set())
        else:
            self.combo_filter_type.setEnabled(False)

    def finish_action(self, keep_filters: bool = False):
        self.thread_animate_progress.stop = True
        self.thread_animate_progress.wait(msecs=1000)
        self.ref_progress_bar.setVisible(False)
        self.progress_bar.setValue(0)
        self.progress_bar.setTextVisible(False)

        self._change_label_substatus('')
        self.ref_bt_settings.setVisible(True)
        self.ref_bt_about.setVisible(True)

        self.ref_bt_refresh.setVisible(True)
        self.checkbox_only_apps.setEnabled(True)
        self.table_apps.setEnabled(True)
        self.input_search.setEnabled(True)
        self.label_status.setText('')
        self.label_substatus.setText('')
        self.ref_toolbar_search.setVisible(True)
        self.ref_toolbar_search.setEnabled(True)
        self.combo_filter_type.setEnabled(True)
        self.checkbox_updates.setEnabled(True)
        self.progress_controll_enabled = True

        if self.ref_bt_suggestions:
            self.ref_bt_suggestions.setVisible(True)

        if self.pkgs:
            self.ref_input_name_filter.setVisible(True)
            self.update_bt_upgrade()
            self._update_type_filters(keep_selected=keep_filters)
            self._update_categories(keep_selected=keep_filters)

            if self.ref_bt_installed.isVisible():
                self.ref_bt_installed.setEnabled(True)

        self._hide_fields_after_recent_installation()

    def _hide_fields_after_recent_installation(self):
        if self.recent_installation:
            self.ref_combo_filter_type.setVisible(False)
            self.ref_combo_categories.setVisible(False)
            self.ref_input_name_filter.setVisible(False)

    def downgrade(self, pkgv: PackageView):
        pwd = None
        requires_root = self.manager.requires_root('downgrade', pkgv.model)

        if not user.is_root() and requires_root:
            pwd, ok = ask_root_password(self.i18n)

            if not ok:
                return

        self._handle_console_option(True)
        self._begin_action('{} {}'.format(self.i18n['manage_window.status.downgrading'], pkgv.model.name))

        self.thread_downgrade.app = pkgv
        self.thread_downgrade.root_password = pwd
        self.thread_downgrade.start()

    def get_app_info(self, pkg: dict):
        self._handle_console_option(False)
        self._begin_action(self.i18n['manage_window.status.info'])

        self.thread_get_info.app = pkg
        self.thread_get_info.start()

    def get_screenshots(self, pkg: PackageView):
        self._handle_console_option(False)
        self._begin_action(self.i18n['manage_window.status.screenshots'].format(bold(pkg.model.name)))

        self.thread_screenshots.pkg = pkg
        self.thread_screenshots.start()

    def _finish_get_screenshots(self, res: dict):
        self.finish_action()

        if res.get('screenshots'):
            diag = ScreenshotsDialog(pkg=res['pkg'],
                                     http_client=self.http_client,
                                     icon_cache=self.icon_cache,
                                     logger=self.logger,
                                     i18n=self.i18n,
                                     screenshots=res['screenshots'])
            diag.exec_()
        else:
            dialog.show_message(title=self.i18n['error'],
                                body=self.i18n['popup.screenshots.no_screenshot.body'].format(bold(res['pkg'].model.name)),
                                type_=MessageType.ERROR)

    def get_app_history(self, app: dict):
        self._handle_console_option(False)
        self._begin_action(self.i18n['manage_window.status.history'])

        self.thread_get_history.app = app
        self.thread_get_history.start()

    def _finish_get_info(self, app_info: dict):
        self.finish_action()
        dialog_info = InfoDialog(app=app_info, icon_cache=self.icon_cache, i18n=self.i18n, screen_size=self.screen_size)
        dialog_info.exec_()

    def _finish_get_history(self, res: dict):
        self.finish_action()

        if res.get('error'):
            self._handle_console_option(True)
            self.textarea_output.appendPlainText(res['error'])
            self.checkbox_console.setChecked(True)
        else:
            dialog_history = HistoryDialog(res['history'], self.icon_cache, self.i18n)
            dialog_history.exec_()

    def _begin_search(self, word):
        self._handle_console_option(False)
        self.ref_checkbox_only_apps.setVisible(False)
        self.ref_checkbox_updates.setVisible(False)
        self.filter_updates = False
        self._begin_action('{} {}'.format(self.i18n['manage_window.status.searching'], word if word else ''), clear_filters=True)

    def search(self):
        word = self.input_search.text().strip()
        if word:
            self._begin_search(word)
            self.thread_search.word = word
            self.thread_search.start()

    def _finish_search(self, res: dict):
        self.finish_action()
        self.search_performed = True

        if not res['error']:
            self.ref_bt_upgrade.setVisible(False)
            self.update_pkgs(res['pkgs_found'], as_installed=False, ignore_updates=True)
        else:
            dialog.show_message(title=self.i18n['warning'].capitalize(), body=self.i18n[res['error']], type_=MessageType.WARNING)

    def install(self, pkg: PackageView):
        pwd = None
        requires_root = self.manager.requires_root('install', pkg.model)

        if not user.is_root() and requires_root:
            pwd, ok = ask_root_password(self.i18n)

            if not ok:
                return

        self._handle_console_option(True)
        self._begin_action('{} {}'.format(self.i18n['manage_window.status.installing'], pkg.model.name))

        self.thread_install.pkg = pkg
        self.thread_install.root_password = pwd
        self.thread_install.start()

    def _finish_install(self, res: dict):
        self.input_search.setText('')
        self.finish_action()

        console_output = self.textarea_output.toPlainText()

        if console_output:
            log_path = '{}/logs/install/{}/{}'.format(TEMP_PATH, res['pkg'].model.get_type(), res['pkg'].model.name)
            try:
                Path(log_path).mkdir(parents=True, exist_ok=True)

                log_file = log_path + '/{}.log'.format(int(time.time()))
                with open(log_file, 'w+') as f:
                    f.write(console_output)

                self.textarea_output.appendPlainText(self.i18n['console.install_logs.path'].format('"{}"'.format(log_file)))
            except:
                self.textarea_output.appendPlainText("[warning] Could not write install log file to '{}'".format(log_path))

        if res['success']:
            self.recent_installation = True
            if self._can_notify_user():
                util.notify_user(msg='{} ({}) {}'.format(res['pkg'].model.name, res['pkg'].model.get_type(), self.i18n['installed']))

            self._finish_refresh_apps({'installed': [res['pkg'].model], 'total': 1, 'types': None})
            self.ref_bt_installed.setVisible(False)
            self.ref_checkbox_only_apps.setVisible(False)
        else:
            if self._can_notify_user():
                util.notify_user('{}: {}'.format(res['pkg'].model.name, self.i18n['notification.install.failed']))

            self.checkbox_console.setChecked(True)

    def _update_progress(self, value: int):
        self.progress_bar.setValue(value)

    def _finish_run_app(self, success: bool):
        self.finish_action()

    def execute_custom_action(self, pkg: PackageView, action: PackageAction):
        pwd = None

        if not user.is_root() and action.requires_root:
            pwd, ok = ask_root_password(self.i18n)

            if not ok:
                return

        self._handle_console_option(True)
        self._begin_action('{} {}'.format(self.i18n[action.i18n_status_key], pkg.model.name))

        self.thread_custom_action.pkg = pkg
        self.thread_custom_action.root_password = pwd
        self.thread_custom_action.custom_action = action
        self.thread_custom_action.start()

    def _finish_custom_action(self, res: dict):
        self.finish_action()
        if res['success']:
            self.refresh_apps(pkg_types={res['pkg'].model.__class__})
        else:
            self.checkbox_console.setChecked(True)

    def show_settings(self):
        if self.settings_window:
            self.settings_window.handle_display()
        else:
            self.settings_window = SettingsWindow(self.manager, self.i18n, self.screen_size, self.tray_icon, self)
            self.settings_window.setMinimumWidth(int(self.screen_size.width() / 4))
            self.settings_window.resize(self.size())
            self.settings_window.adjustSize()
            qt_utils.centralize(self.settings_window)
            self.settings_window.show()
