# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright 2016 Continuum Analytics, Inc.
#
# May be copied and distributed freely only as part of an Anaconda or
# Miniconda installation.
# -----------------------------------------------------------------------------
"""Conda Packager Manager Widget."""

from __future__ import (absolute_import, division, print_function,
                        with_statement)

# Standard library imports
from collections import deque
import gettext
import json
import os.path as osp
import sys

# Third party imports
from qtpy.QtCore import QEvent, QSize, Qt, Signal
from qtpy.QtWidgets import (QDialog, QDialogButtonBox, QHBoxLayout,
                            QMessageBox, QProgressBar, QPushButton,
                            QVBoxLayout, QWidget)

# Local imports
from anaconda_navigator.api.anaconda_api import AnacondaAPI
from anaconda_navigator.config import CONF
from anaconda_navigator.utils import constants as C
from anaconda_navigator.utils.logs import logger
from anaconda_navigator.utils.styles import BLUR_SIZE
from anaconda_navigator.widgets import (ButtonBase, ButtonNormal, ComboBoxBase,
                                        FrameBase, FrameTabFooter,
                                        FrameTabHeader, LabelBase,
                                        SpacerHorizontal)
from anaconda_navigator.widgets.dialogs.channels import DialogChannels
from anaconda_navigator.widgets.helperwidgets import LineEditSearch
from anaconda_navigator.widgets.manager.table import TableCondaPackages

_ = gettext.gettext


# --- Widgtes defined for CSS styling
# -----------------------------------------------------------------------------
class ComboBoxPackageFilter(ComboBoxBase):
    """Combobox used in CSS styling."""


class ButtonPackageChannels(ButtonNormal):
    """Button used in CSS styling."""


class ButtonPackageOk(ButtonNormal):
    """Button used in CSS styling."""


class ButtonPackageApply(ButtonNormal):
    """Button used in CSS styling."""


class ButtonPackageCancel(ButtonNormal):
    """Button used in CSS styling."""


class ButtonPackageUpdate(ButtonNormal):
    """Button used in CSS styling."""


class ButtonPackageClear(ButtonNormal):
    """Button used in CSS styling."""


class ProgressBarPackage(QProgressBar):
    """Progress bar used in CSS styling."""


class LabelPackageStatus(LabelBase):
    """Label used in CSS styling."""


class FramePackageTop(FrameBase):
    """Frame used in CSS styling."""


class FramePackageBottom(FrameBase):
    """Frame used in CSS styling."""

    pass


# --- Navigationr helper widgets
# -----------------------------------------------------------------------------
class FirstRowWidget(ButtonBase):
    """Widget located before table to handle focus in and tab focus."""

    sig_enter_first = Signal()

    def __init__(self, widget_before=None):
        """Widget located before table to handle focus in and tab focus."""
        QPushButton.__init__(self)
        self.widget_before = widget_before
        self.setAttribute(Qt.WA_LayoutUsesWidgetRect)  # Needed on mac

    @staticmethod
    def sizeHint():
        """Override Qt method."""
        return QSize(0, 0)

    def focusInEvent(self, event):
        """Override Qt method."""
        self.sig_enter_first.emit()

    def event(self, event):
        """Override Qt method."""
        if event.type() == QEvent.KeyPress:
            key = event.key()
            if key in [Qt.Key_Tab]:
                self.sig_enter_first.emit()
                return True
            else:
                return QPushButton.event(self, event)
        else:
            return QPushButton.event(self, event)


class LastRowWidget(ButtonBase):
    """Widget located after table to handle focus out and tab focus."""

    sig_enter_last = Signal()

    def __init__(self, widgets_after=None):
        """Widget located after table to handle focus out and tab focus."""
        QPushButton.__init__(self)
        self.widgets_after = widgets_after
        self.setAttribute(Qt.WA_LayoutUsesWidgetRect)  # Needed on mac

    def focusInEvent(self, event):
        """Qt override."""
        self.sig_enter_last.emit()

    def add_focus_widget(self, widget):
        """Add after focus widget."""
        if widget in self.widgets_after:
            return
        else:
            self.widgets_after[-1] = widget

    def handle_tab(self):
        """Handle tab focus widget."""
        for w in self.widgets_after:
            if w.isVisible():
                w.setFocus()
                return

    def event(self, event):
        """Override Qt method."""
        if event.type() == QEvent.KeyPress:
            key = event.key()
            if key in [Qt.Key_Backtab]:
                self.sig_enter_last.emit()
                return True
            else:
                return QPushButton.event(self, event)
        else:
            return QPushButton.event(self, event)

    @staticmethod
    def sizeHint():
        """Override Qt method."""
        return QSize(0, 0)


# --- Main widget
# -----------------------------------------------------------------------------
class CondaPackagesWidget(QWidget):
    """Conda Packages Widget."""

    # Location of updated repo.json files from continuum/binstar
    CONDA_CONF_PATH = ''  # DEPRECATE

    # Location of continuum/anaconda default repos shipped with conda-manager
    DATA_PATH = None

    # file inside DATA_PATH with metadata for conda packages
    DATABASE_FILE = 'packages.ini'

    sig_worker_ready = Signal()
    sig_packages_ready = Signal()
    sig_environment_created = Signal(object, object)
    sig_environment_removed = Signal(object, object)
    sig_environment_cloned = Signal(object, object)
    sig_channels_updated = Signal(tuple, tuple)  # channels, active_channels
    sig_process_cancelled = Signal()
    sig_next_focus = Signal()
    sig_packages_busy = Signal()

    def __init__(self,
                 parent,
                 name=None,
                 prefix=None,
                 active_channels=(),
                 setup=True,
                 data_directory=None,
                 extra_metadata=None,
                 config=CONF):
        """Conda Packages Widget."""
        super(CondaPackagesWidget, self).__init__(parent)

        # Check arguments: active channels, must be witbhin channels
#        for ch in active_channels:
#            if ch not in channels:
#                raise Exception("'active_channels' must be also within "
#                                "'channels'")

        if data_directory is None:
            data_directory = self.CONDA_CONF_PATH

        self._parent = parent
        self._current_action_name = ''
        self._hide_widgets = False
        # From repo.continuum
        self._metadata = extra_metadata if extra_metadata else {}
        self._metadata_links = {}        # Bundled metadata
        self.api = AnacondaAPI()
        self.busy = False
        self.data_directory = data_directory
        self.name = name
        self.package_blacklist = []
        self.root_prefix = self.api.ROOT_PREFIX
        self.prefix = prefix if prefix else self.root_prefix
        self.style_sheet = None
        self.message = ''
        self.apply_actions_dialog = None
        self.conda_errors = []
        self.message_box_error = None
        self.config = config
        self.token = None
        self._active_channels = self.config.get('main',
                                                'conda_active_channels')
        self._channels = self.api.conda_get_condarc_channels()

        self.progress_value = 0
        self.progress_value_max = 0

#        if channels:
#            self._channels = channels
#            self._active_channels = active_channels
#        else:
#            self._channels = self.api.conda_get_condarc_channels()
#            self._active_channels = self._channels[:]

        # Widgets
#        self.cancel_dialog = ClosePackageManagerDialog
        self.bbox = QDialogButtonBox(Qt.Horizontal)
        self.button_cancel = ButtonPackageCancel('Cancel')
        self.button_channels = ButtonPackageChannels(_('Channels'))
        self.button_ok = ButtonPackageOk(_('Ok'))
        self.button_update = ButtonPackageUpdate(_('Update index...'))
        self.button_apply = ButtonPackageApply(_('Apply'))
        self.button_clear = ButtonPackageClear(_('Clear'))
        self.combobox_filter = ComboBoxPackageFilter(self)
        self.frame_top = FrameTabHeader()
        self.frame_bottom = FrameTabFooter()
        self.progress_bar = ProgressBarPackage(self)
        self.status_bar = LabelPackageStatus(self)
        self.table = TableCondaPackages(self)
        self.textbox_search = LineEditSearch(self)
        self.widgets = [self.button_update, self.button_channels,
                        self.combobox_filter, self.textbox_search, self.table,
                        self.button_ok, self.button_apply, self.button_clear]
        self.table_first_row = FirstRowWidget(
            widget_before=self.textbox_search)
        self.table_last_row = LastRowWidget(
            widgets_after=[self.button_apply, self.button_clear,
                           self.button_cancel, self.combobox_filter])

        # Widgets setup
#        for button in [self.button_cancel, self.button_apply,
#                       self.button_clear, self.button_ok, self.button_update,
#                       self.button_channels]:
#             button.setDefault(True)
        max_height = self.status_bar.fontMetrics().height()
        max_width = self.textbox_search.fontMetrics().width('M' * 23)
        self.bbox.addButton(self.button_ok, QDialogButtonBox.ActionRole)
#        self.button_ok.setAutoDefault(True)
#        self.button_ok.setDefault(True)
        self.button_ok.setMaximumSize(QSize(0, 0))
        self.button_ok.setVisible(False)
        self.button_channels.setCheckable(True)
        self.combobox_filter.addItems([k for k in C.COMBOBOX_VALUES_ORDERED])
        self.combobox_filter.setMinimumWidth(120)
        self.progress_bar.setMaximumHeight(max_height * 1.2)
        self.progress_bar.setMaximumWidth(max_height * 12)
        self.progress_bar.setTextVisible(False)
        self.progress_bar.setVisible(False)
        self.setMinimumSize(QSize(480, 300))
        self.setWindowTitle(_("Conda Package Manager"))
        self.status_bar.setFixedHeight(max_height * 1.5)
        self.textbox_search.setMaximumWidth(max_width)
        self.textbox_search.setPlaceholderText('Search Packages')
        self.table_first_row.setMaximumHeight(0)
        self.table_last_row.setMaximumHeight(0)
        self.table_last_row.setVisible(False)
        self.table_first_row.setVisible(False)

        # Layout
        top_layout = QHBoxLayout()
        top_layout.addWidget(self.combobox_filter, 0, Qt.AlignCenter)
        top_layout.addWidget(SpacerHorizontal())
        top_layout.addWidget(self.button_channels, 0, Qt.AlignCenter)
        top_layout.addWidget(SpacerHorizontal())
        top_layout.addWidget(self.button_update, 0, Qt.AlignCenter)
        top_layout.addWidget(SpacerHorizontal())
        top_layout.addWidget(self.textbox_search, 0, Qt.AlignCenter)
        top_layout.addStretch()
        self.frame_top.setLayout(top_layout)

        middle_layout = QVBoxLayout()
        middle_layout.addWidget(self.table_first_row)
        middle_layout.addWidget(self.table)
        middle_layout.addWidget(self.table_last_row)

        bottom_layout = QHBoxLayout()
        bottom_layout.addWidget(self.status_bar)
        bottom_layout.addStretch()
        bottom_layout.addWidget(self.progress_bar)
        bottom_layout.addWidget(SpacerHorizontal())
        bottom_layout.addWidget(self.button_cancel)
        bottom_layout.addWidget(SpacerHorizontal())
        bottom_layout.addWidget(self.button_apply)
        bottom_layout.addWidget(SpacerHorizontal())
        bottom_layout.addWidget(self.button_clear)
        self.frame_bottom.setLayout(bottom_layout)

        layout = QVBoxLayout(self)
        layout.addWidget(self.frame_top)
        layout.addLayout(middle_layout)
        layout.addWidget(self.frame_bottom)
        self.setLayout(layout)

        self.setTabOrder(self.combobox_filter, self.button_channels)
        self.setTabOrder(self.button_channels, self.button_update)
        self.setTabOrder(self.button_update, self.textbox_search)
        self.setTabOrder(self.textbox_search, self.table_first_row)
        self.setTabOrder(self.table, self.table_last_row)
        self.setTabOrder(self.table_last_row, self.button_apply)
        self.setTabOrder(self.button_apply, self.button_clear)
        self.setTabOrder(self.button_clear, self.button_cancel)

        # Signals and slots
        self.api.sig_repodata_updated.connect(self._repodata_updated)
        self.combobox_filter.currentIndexChanged.connect(self.filter_package)
        self.button_apply.clicked.connect(self.apply_multiple_actions)
        self.button_clear.clicked.connect(self.clear_actions)
        self.button_cancel.clicked.connect(self.cancel_process)
        self.button_channels.clicked.connect(self.show_channels_dialog)
        self.button_update.clicked.connect(self.update_package_index)
        self.textbox_search.textChanged.connect(self.search_package)
        self.table.sig_actions_updated.connect(self.update_actions)
        self.table.sig_status_updated.connect(self.update_status)
        self.table.sig_next_focus.connect(self.table_last_row.handle_tab)
        self.table.sig_previous_focus.connect(
            lambda: self.table_first_row.widget_before.setFocus())
        self.table_first_row.sig_enter_first.connect(self._handle_tab_focus)
        self.table_last_row.sig_enter_last.connect(self._handle_backtab_focus)

        # Setup
        self.api.client_set_domain(self.api.client_get_api_url())
        self.api.set_data_directory(self.data_directory)
        self._load_bundled_metadata()
        self.update_actions(0)

        if setup:
            self.set_environment(name=name, prefix=prefix)
            self.setup()

    # --- Helpers
    # -------------------------------------------------------------------------
    def _handle_tab_focus(self):
        self.table.setFocus()
        if self.table.proxy_model:
            index = self.table.proxy_model.index(0, 0)
            self.table.setCurrentIndex(index)

    def _handle_backtab_focus(self):
        self.table.setFocus()
        if self.table.proxy_model:
            row = self.table.proxy_model.rowCount() - 1
            index = self.table.proxy_model.index(row, 0)
            self.table.setCurrentIndex(index)

    # --- Callbacks
    # -------------------------------------------------------------------------
    def _load_bundled_metadata(self):
        pass
#        logger.debug('')
#
#        parser = cp.ConfigParser()
#        db_file = CondaPackagesWidget.DATABASE_FILE
#        with open(osp.join(self.DATA_PATH, db_file)) as f:
#            parser.readfp(f)
#
#        for name in parser.sections():
#            metadata = {}
#            for key, data in parser.items(name):
#                metadata[key] = data
#            self._metadata_links[name] = metadata

    def _setup_packages(self, worker, data, error):
        """Callback."""
        if error:
            logger.error(error)
        else:
            logger.debug('')

        combobox_index = self.combobox_filter.currentIndex()
        status = C.PACKAGE_STATUS[combobox_index]

        packages = worker.packages

        # Remove blacklisted packages
        for package in self.package_blacklist:
            if package in packages:
                packages.pop(package)
            for i, row in enumerate(data):
                if package == data[i][C.COL_NAME]:
                    data.pop(i)

        self.table.setup_model(packages, data, self._metadata_links)
        self.combobox_filter.setCurrentIndex(combobox_index)
        self.filter_package(status)

        if self._current_model_index:
            self.table.setCurrentIndex(self._current_model_index)
            self.table.verticalScrollBar().setValue(self._current_table_scroll)
        if error:
            self.update_status(error, False)
        self.sig_packages_ready.emit()
        self.table.setFocus()

    def get_logged_user_list_channels(self):
        """Return normalized list of logged user channels."""
        channels = []
        condarc_channels = self.api.conda_get_condarc_channels()
        # Check that they are active!!!
        for ch in condarc_channels:
            if 'repo.continuum' not in ch and ch != 'defaults':
                channels.append(ch)

        active_channels = self.config.get('main', 'conda_active_channels')
        channels = [ch for ch in channels if ch in active_channels]
        return channels

    def _prepare_model_data(self, worker=None, output=None, error=None):
        """Callback."""
        if error:
            logger.error(error)
        else:
            logger.debug('')

        packages, apps = output
#        worker = self.api.pip_list(prefix=self.prefix)
#        worker.sig_finished.connect(self._pip_list_ready)
        logins = self.get_logged_user_list_channels()
        worker = self.api.client_multi_packages(logins=logins,
                                                access='private')
        worker.sig_finished.connect(self._user_private_packages_ready)
        worker.packages = packages
        worker.apps = apps

    def _user_private_packages_ready(self, worker, output, error):
        """Callback."""
        if error:
            logger.error(error)
        else:
            logger.debug('')

        packages = worker.packages
        apps = worker.apps
        worker = self.api.pip_list(prefix=self.prefix)
        worker.sig_finished.connect(self._pip_list_ready)
        worker.packages = packages
        worker.apps = apps
        worker.private_packages = output

    def _pip_list_ready(self, worker, pip_packages, error):
        """Callback for pip listing ready."""
        if error:
            logger.error(error)
        else:
            logger.debug('')

        packages = worker.packages
        private_packages = worker.private_packages
        linked_packages = self.api.conda_linked(prefix=self.prefix)
        data = self.api.client_prepare_packages_data(packages,
                                                     linked_packages,
                                                     pip_packages,
                                                     private_packages)

        combobox_index = self.combobox_filter.currentIndex()
        status = C.PACKAGE_STATUS[combobox_index]

        # Remove blacklisted packages
        for package in self.package_blacklist:
            if package in packages:
                packages.pop(package)

            for i, row in enumerate(data):
                if package == data[i][C.COL_NAME]:
                    data.pop(i)

        self.table.setup_model(packages, data, self._metadata_links)
        self.combobox_filter.setCurrentIndex(combobox_index)
        self.filter_package(status)

        if self._current_model_index:
            self.table.setCurrentIndex(self._current_model_index)
            self.table.verticalScrollBar().setValue(self._current_table_scroll)

        if error:
            self.update_status(str(error), False)
        self.sig_packages_ready.emit()
        self.table.setFocus()

    def _repodata_updated(self, paths):
        """Callback for updated repodata."""
        worker = self.api.client_load_repodata(paths, extra_data={},
                                               metadata=self._metadata)
        worker.paths = paths
        worker.sig_finished.connect(self._prepare_model_data)

    def _metadata_updated(self, worker, path, error):
        """Callback for update metadada."""
        if error:
            logger.error(error)
        else:
            logger.debug('')

        if path and osp.isfile(path):
            with open(path, 'r') as f:
                data = f.read()
            try:
                self._metadata = json.loads(data)
            except Exception:
                self._metadata = {}
        else:
            self._metadata = {}

        self.api.update_repodata(
            channels=self._get_normalized_active_repos())

    # ---
    # -------------------------------------------------------------------------
    def _run_multiple_actions(self, worker=None, output=None, error=None):
        """Run and process actions for selected packages."""
        self.progress_value = 0
        self.progress_value_max = 0

        logger.error(str(error))

        if output and isinstance(output, dict):
            conda_error_type = output.get('error_type')
            conda_error = output.get('error')

            if conda_error_type or conda_error:
                self.conda_errors.append((conda_error_type, conda_error))
                logger.error((conda_error_type, conda_error))

        if self._multiple_process:
            status, func = self._multiple_process.popleft()
            self.update_status(status)
            worker = func()
            worker.sig_finished.connect(self._run_multiple_actions)
            worker.sig_partial.connect(self._partial_output_ready)
        else:
            if self.conda_errors and self.message_box_error:
                text = "The following errors occured:"
                error = ''
                for conda_error in self.conda_errors:
                    error += str(conda_error[0]) + ':\n'
                    error += str(conda_error[1]) + '\n\n'
                dlg = self.message_box_error(text=text, error=error,
                                             title='Conda process error')
                dlg.setMinimumWidth(400)
                dlg.exec_()

            self.update_status('', hide=False)
            self.setup()

    def _pip_process_ready(self, worker, output, error):
        """Pip process ready callback."""
        if error is not None:
            status = _('there was an error')
            self.update_status(hide=False, message=status)
        else:
            self.update_status(hide=True)

        self.setup()

    def _conda_process_ready(self, worker, output, error):
        """Conda process ready callback."""
        if error is not None:
            status = _('there was an error')
            self.update_status(hide=False, message=status)
        else:
            self.update_status(hide=True)

        conda_error = None
        conda_error_type = None
        if output and isinstance(output, dict):
            conda_error_type = output.get('error_type')
            conda_error = output.get('error')

            if conda_error_type or conda_error:
                logger.error((conda_error_type, conda_error))

        dic = self._temporal_action_dic

        if dic['action'] == C.ACTION_CREATE:
            self.sig_environment_created.emit(conda_error, conda_error_type)
        elif dic['action'] == C.ACTION_CLONE:
            self.sig_environment_cloned.emit(conda_error, conda_error_type)
        elif dic['action'] == C.ACTION_REMOVE_ENV:
            self.sig_environment_removed.emit(conda_error, conda_error_type)

        self.setup()

    def _partial_output_ready(self, worker, output, error):
        """Process output available callback."""
        message = None
        progress = (self.progress_value, self.progress_value_max)

        if isinstance(output, dict):
            progress = (output.get('progress', self.progress_value),
                        output.get('maxval', self.progress_value_max))
            name = output.get('name')
            fetch = output.get('fetch')

            if fetch:
                message = "Downloading <b>{0}</b>...".format(fetch)

            if name:
                self._current_action_name = name
                message = "Linking <b>{0}</b>...".format(name)

        logger.debug(message)
        self.update_status(message=message, progress=progress)

    def _run_pip_action(self, package_name, action):
        """DEPRECATED."""
        prefix = self.prefix

        if prefix == self.root_prefix:
            name = 'root'
        elif self.api.conda_environment_exists(prefix=prefix):
            name = osp.basename(prefix)
        else:
            name = prefix

        if action == C.ACTION_REMOVE:
            msgbox = QMessageBox.question(self,
                                          "Remove pip package: "
                                          "{0}".format(package_name),
                                          "Do you want to proceed?",
                                          QMessageBox.Yes | QMessageBox.No)
            if msgbox == QMessageBox.Yes:
                self.update_status()
                worker = self.api.pip_remove(prefix=self.prefix,
                                             pkgs=[package_name])
                worker.sig_finished.connect(self._pip_process_ready)
                status = (_('Removing pip package <b>') + package_name +
                          '</b>' + _(' from <i>') + name + '</i>')
                self.update_status(hide=True, message=status,
                                   progress=[0, 0])

#    def _run_conda_action(package_name, action, version, versions,
#                          packages_sizes):
#        """
#        DEPRECATED
#        """
#        prefix = self.prefix
#        dlg = CondaPackageActionDialog(self, prefix, package_name, action,
#                                       version, versions, packages_sizes,
#                                       self._active_channels)
#
#        if dlg.exec_():
#            dic = {}
#
#            self.status = 'Processing'
#            self.update_status(hide=True)
#            self.repaint()
#
#            ver1 = dlg.label_version.text()
#            ver2 = dlg.combobox_version.currentText()
#            pkg = u'{0}={1}{2}'.format(package_name, ver1, ver2)
#            dep = dlg.checkbox.checkState()
#            state = dlg.checkbox.isEnabled()
#            dlg.close()
#
#            dic['pkg'] = pkg
#            dic['dep'] = not (dep == 0 and state)
#            dic['action'] = None
#            self._run_conda_process(action, dic)

    def _get_normalized_active_repos(self):
        """Return the normalized repos for the active channels."""
        active_channels = self.config.get('main', 'conda_active_channels')
        norm_channels = self.api.conda_get_condarc_channels(
            normalize=True,
            channels=active_channels)

        repos_to_download = []
        for ch in norm_channels:
            repos_to_download.append(ch)
        return repos_to_download

    def _run_conda_process(self, action, dic):
        """DEPRECATED."""
        self.progress_value = 0
        self.progress_value_max = 0
        self._temporal_action_dic = dic

        if 'pkgs' in dic and 'dep' in dic:
            #  dep = dic['dep']
            pkgs = dic['pkgs']
            if not isinstance(pkgs, list):
                pkgs = [pkgs]

        # Temporal arrangement, needs to be better organized
        if 'specfile' in dic:
            pkgs = dic['pkgs']

        # --- Environment management actions
        name = dic['name']
        if action == C.ACTION_CREATE:
            status = _('Creating environment <b>') + name + '</b>'
            worker = self.api.conda_create(
                name=name,
                pkgs=pkgs,
                channels=self.active_normalized_channels)
        elif action == C.ACTION_CLONE:
            clone = dic['clone']
            status = (_('Cloning ') + '<i>' + clone +
                      _('</i> into <b>') + name + '</b>')
            worker = self.api.conda_clone(clone, name=name)
        elif action == C.ACTION_REMOVE_ENV:
            status = _('Removing environment <b>') + name + '</b>'
            worker = self.api.conda_remove(name=name, all_=True)
        elif action == C.ACTION_IMPORT:
            status = _('Importing environment <b>') + name + '</b>'
            worker = self.api.conda_create_yaml(name, pkgs)

        worker.sig_finished.connect(self._conda_process_ready)
        worker.sig_partial.connect(self._partial_output_ready)
        self.update_status(hide=True, message=status, progress=None)
        self._temporal_action_dic = dic
        return worker

    # Public API
    # -------------------------------------------------------------------------
    def prepare_model_data(self, packages, apps):
        """Prepare model data."""
        # DEPRECATE.
        logger.debug('')
        self._prepare_model_data(output=(packages, apps))

    # These should be private methods....
    def enable_widgets(self):
        """Enable child widgets."""
        self.table.hide_columns()

    def disable_widgets(self):
        """Disable child widgets."""
        self.table.hide_action_columns()

    def accept_channels_dialog(self):
        """Return focus to channels button after accepting channel update."""
        self.button_channels.setFocus()
        self.button_channels.toggle()

    def update_actions(self, number_of_actions):
        """Update visibility of buttons based on actions."""
        self.button_apply.setVisible(bool(number_of_actions))
        self.button_clear.setVisible(bool(number_of_actions))

    # --- Non UI API
    # -------------------------------------------------------------------------
    def setup(self, check_updates=False, blacklist=None, metadata=None):
        """
        Setup packages.

        Main triger method to download repodata, load repodata, prepare and
        updating the data model.

        Parameters
        ----------
        check_updates : bool
            If `True`, checks that the latest repodata is available on the
            listed channels. If `False`, the data will be loaded from the
            downloaded files without checking for newer versions.
        blacklist: list of str
            List of conda package names to be excluded from the actual package
            manager view.
        """
        self.sig_packages_busy.emit()

        if self.busy:
            logger.debug('Busy...')
            return
        else:
            logger.debug('')

        if blacklist:
            self.package_blacklist = [p.lower() for p in blacklist]

        if metadata:
            self._metadata = metadata

        self._current_model_index = self.table.currentIndex()
        self._current_table_scroll = self.table.verticalScrollBar().value()
        self.update_status('Updating package index', True)

        if check_updates:
            worker = self.api.update_metadata()
            worker.sig_finished.connect(self._metadata_updated)
        else:
            paths = self.api.repodata_files(self.active_normalized_channels)
            self._repodata_updated(paths)

    @property
    def channels(self):
        """Convenience property for returning active channels."""
        return self.api.conda_get_condarc_channels()

    @property
    def active_channels(self):
        """Convenience property for returning active channels."""
        return self.config.get('main', 'conda_active_channels')

    @property
    def active_normalized_channels(self):
        """Convenience property for returning active normalized channels."""
        return self.api.conda_get_condarc_channels(
            channels=self.active_channels, normalize=True)

    def update_domains(self, anaconda_api_url=None, conda_url=None):
        """
        Update anaconda_api and conda_url.

        DEPRECATE.
        """
        logger.debug(str((anaconda_api_url, conda_url)))

        if anaconda_api_url:
            self.conda_api_url = anaconda_api_url
            self.api.client_set_domain(anaconda_api_url)

    def set_environment(self, name=None, prefix=None):
        """
        Set the package manager environment.

        This does not update the package manager!.
        """
        logger.debug(str((name, prefix)))

        if (prefix and osp.exists(prefix) and
                self.api.conda_environment_exists(prefix=prefix)):
            self.prefix = prefix
        elif name and self.api.conda_environment_exists(name=name):
            self.prefix = self.get_prefix_envname(name)
        else:
            self.prefix = self.root_prefix

    def set_token(self, token):
        """Set the user login token."""
        self.token = token

    def update_channels(self, channels, active_channels, setup=True):
        """Update the channels and active channels."""
        logger.debug(str((channels, active_channels)))

#        if sorted(self._active_channels) != sorted(active_channels) or \
#                sorted(self._channels) != sorted(channels):
        self._channels = channels
        self._active_channels = active_channels
        self.sig_channels_updated.emit(tuple(channels), tuple(active_channels))
        if setup:
            self.setup(check_updates=True)

    def update_style_sheet(self, style_sheet=None, extra_dialogs=None,
                           palette=None):
        """
        Update custom CSS stylesheet.

        DEPRECATE.
        """
        if style_sheet:
            self.style_sheet = style_sheet
            self.textbox_search.update_style_sheet(style_sheet)
            self.setStyleSheet(style_sheet)

            if palette:
                self.table.update_style_palette(palette=palette)

        if extra_dialogs:
            cancel_dialog = extra_dialogs.get('cancel_dialog')
            apply_actions_dialog = extra_dialogs.get('apply_actions_dialog')
            message_box_error = extra_dialogs.get('message_box_error')

            if cancel_dialog:
                self.cancel_dialog = cancel_dialog
            if apply_actions_dialog:
                self.apply_actions_dialog = apply_actions_dialog
            if message_box_error:
                self.message_box_error = message_box_error

    # --- UI API
    # -------------------------------------------------------------------------
    def filter_package(self, value):
        """Filter packages by type."""
        self.table.filter_status_changed(value)

    def show_channels_dialog(self):
        """Show the channels dialog."""
        button_channels = self.button_channels
        button_channels.setDisabled(True)
        channels = self.api.conda_get_condarc_channels()
        active_channels = self.config.get('main', 'conda_active_channels')
        self.dlg = DialogChannels(self,
                                  channels=channels,
                                  active_channels=active_channels)
        self.dlg.update_style_sheet(style_sheet=self.style_sheet)
        self.dlg.sig_channels_updated.connect(self.update_channels)
        self.dlg.rejected.connect(lambda: button_channels.setEnabled(True))
        self.dlg.rejected.connect(button_channels.toggle)
        self.dlg.rejected.connect(button_channels.setFocus)
        self.dlg.accepted.connect(self.accept_channels_dialog)

        geo_tl = button_channels.geometry().topLeft()
        tl = button_channels.parentWidget().mapToGlobal(geo_tl)
        x = tl.x() - BLUR_SIZE
        y = tl.y() + button_channels.height() - BLUR_SIZE
        self.dlg.move(x, y)
        self.dlg.show()
        self.dlg.button_add.setFocus()

    def update_package_index(self):
        """Update pacakge index."""
        self.setup(check_updates=True)

    def search_package(self, text):
        """Search and filter pacakges by text."""
        self.table.search_string_changed(text)

    def apply_multiple_actions(self):
        """Apply multiple actions on packages."""
        logger.debug('')

        self.conda_errors = []

        prefix = self.prefix

        if prefix == self.root_prefix:
            name = 'root'
        elif self.api.conda_environment_exists(prefix=prefix):
            name = osp.basename(prefix)
        else:
            name = prefix

        actions = self.table.get_actions()

        if actions is None:
            return

        self._multiple_process = deque()

        pip_actions = actions[C.PIP_PACKAGE]
        conda_actions = actions[C.CONDA_PACKAGE]

        pip_remove = pip_actions.get(C.ACTION_REMOVE, [])
        conda_remove = conda_actions.get(C.ACTION_REMOVE, [])
        conda_install = conda_actions.get(C.ACTION_INSTALL, [])
        conda_upgrade = conda_actions.get(C.ACTION_UPGRADE, [])
        conda_downgrade = conda_actions.get(C.ACTION_DOWNGRADE, [])

        message = ''
        template_1 = '<li><b>{0}=={1}</b></li>'
        template_2 = '<li><b>{0}: {1} -> {2}</b></li>'

        if pip_remove:
            temp = [template_1.format(i['name'], i['version_to']) for i in
                    pip_remove]
            message += ('The following pip packages will be removed: '
                        '<ul>' + ''.join(temp) + '</ul>')
        if conda_remove:
            temp = [template_1.format(i['name'], i['version_to']) for i in
                    conda_remove]
            message += ('<br>The following conda packages will be removed: '
                        '<ul>' + ''.join(temp) + '</ul>')
        if conda_install:
            temp = [template_1.format(i['name'], i['version_to']) for i in
                    conda_install]
            message += ('<br>The following conda packages will be installed: '
                        '<ul>' + ''.join(temp) + '</ul>')
        if conda_downgrade:
            temp = [template_2.format(
                    i['name'], i['version_from'], i['version_to']) for i in
                    conda_downgrade]
            message += ('<br>The following conda packages will be downgraded: '
                        '<ul>' + ''.join(temp) + '</ul>')
        if conda_upgrade:
            temp = [template_2.format(
                    i['name'], i['version_from'], i['version_to']) for i in
                    conda_upgrade]
            message += ('<br>The following conda packages will be upgraded: '
                        '<ul>' + ''.join(temp) + '</ul>')
        message += '<br>'

        if self.apply_actions_dialog:
            dlg = self.apply_actions_dialog(message, parent=self)
            dlg.update_style_sheet(style_sheet=self.style_sheet)
            reply = dlg.exec_()
        else:
            reply = QMessageBox.question(self,
                                         'Proceed with the following actions?',
                                         message,
                                         buttons=QMessageBox.Ok |
                                         QMessageBox.Cancel)

        if reply:
            self.sig_packages_busy.emit()

            # Pip remove
            for pkg in pip_remove:
                status = (_('Removing pip package <b>') + pkg['name'] +
                          '</b>' + _(' from <i>') + name + '</i>')
                pkgs = [pkg['name']]

                def trigger(prefix=prefix, pkgs=pkgs):
                    return lambda: self.api.pip_remove(prefix=prefix,
                                                       pkgs=pkgs)

                self._multiple_process.append([status, trigger()])

            # Process conda actions
            if conda_remove:
                status = (_('Removing conda packages <b>') +
                          '</b>' + _(' from <i>') + name + '</i>')
                pkgs = [i['name'] for i in conda_remove]

                def trigger(prefix=prefix, pkgs=pkgs):
                    return lambda: self.api.conda_remove(pkgs=pkgs,
                                                         prefix=prefix)
                self._multiple_process.append([status, trigger()])

            if conda_install:
                pkgs = ['{0}=={1}'.format(i['name'], i['version_to']) for i in
                        conda_install]

                status = (_('Installing conda packages <b>') +
                          '</b>' + _(' on <i>') + name + '</i>')

                def trigger(prefix=prefix, pkgs=pkgs):
                    return lambda: self.api.conda_install(
                        prefix=prefix,
                        pkgs=pkgs,
                        channels=self.active_normalized_channels,
                        token=self.token)
                self._multiple_process.append([status, trigger()])

            # Conda downgrade
            if conda_downgrade:
                status = (_('Downgrading conda packages <b>') +
                          '</b>' + _(' on <i>') + name + '</i>')

                pkgs = ['{0}=={1}'.format(i['name'], i['version_to']) for i in
                        conda_downgrade]

                def trigger(prefix=prefix, pkgs=pkgs):
                    return lambda: self.api.conda_install(
                        prefix=prefix,
                        pkgs=pkgs,
                        channels=self.active_normalized_channels,
                        token=self.token)

                self._multiple_process.append([status, trigger()])

            # Conda update
            if conda_upgrade:
                status = (_('Upgrading conda packages <b>') +
                          '</b>' + _(' on <i>') + name + '</i>')

                pkgs = ['{0}=={1}'.format(i['name'], i['version_to']) for i in
                        conda_upgrade]

                def trigger(prefix=prefix, pkgs=pkgs):
                    return lambda: self.api.conda_install(
                        prefix=prefix,
                        pkgs=pkgs,
                        channels=self.active_normalized_channels,
                        token=self.token)

                self._multiple_process.append([status, trigger()])

            self._run_multiple_actions()

    def clear_actions(self):
        """Clear the table actions."""
        self.table.clear_actions()

    def cancel_process(self):
        """Allow user to cancel an ongoing process."""
        logger.debug(str('process canceled by user.'))
        if self.busy:
            dlg = self.cancel_dialog()
            reply = dlg.exec_()
            if reply:
                self.update_status(hide=False, message='Process cancelled')
                self.api.conda_terminate()
                self.api.download_terminate()
                self.api.conda_clear_lock()
                self.table.clear_actions()
                self.sig_process_cancelled.emit()
        else:
            QDialog.reject(self)

    def update_status(self, message=None, hide=True, progress=None,
                      env=False):
        """
        Update status bar, progress bar display and widget visibility.

        Parameters
        ----------
        message : str
            Message to display in status bar.
        hide : bool
            Enable/Disable widgets.
        progress : [int, int]
            Show status bar progress. [0, 0] means spinning statusbar.
        """
        self.busy = hide
        for widget in self.widgets:
            widget.setDisabled(hide)
        self.table.verticalScrollBar().setValue(self._current_table_scroll)

        self.button_apply.setVisible(False)
        self.button_clear.setVisible(False)

        self.progress_bar.setVisible(hide)
        self.button_cancel.setVisible(hide)

        if message is not None:
            self.message = message

        if self.prefix == self.root_prefix:
            short_env = 'root'
#        elif self.api.environment_exists(prefix=self.prefix):
#            short_env = osp.basename(self.prefix)
        else:
            short_env = self.prefix

        if env:
            self.message = '{0} (<b>{1}</b>)'.format(
                self.message, short_env)
        self.status_bar.setText(self.message)

        if progress is None:
            progress = (self.progress_value, self.progress_value_max)
        elif progress[0] == 0 and progress[1] == 0:
            progress = (self.progress_value, self.progress_value_max)

        self.progress_bar.setMinimum(0)
        self.progress_bar.setMaximum(progress[1])
        self.progress_bar.setValue(progress[0])
#        if progress is not None:
#            current_progress, max_progress = 0, 0
#
#            if progress[1]:
#                max_progress = progress[1]
#
#            if progress[0]:
#                current_progress = progress[0]
#
#            self.progress_bar.setMinimum(0)
#            self.progress_bar.setMaximum(max_progress)
#            self.progress_bar.setValue(current_progress)
#        else:
#            self.progress_bar.setMinimum(0)
#            self.progress_bar.setMaximum(0)

    # --- Conda helpers
    # -------------------------------------------------------------------------
    def get_environment_prefix(self):
        """Return the active environment prefix."""
        return self._prefix

    def get_environment_name(self):
        """
        Return the active environment name or prefix.

        Returns the name if it is located in the default conda environments
        directory, otherwise it returns the prefix.
        """
        name = osp.basename(self._prefix)

        if not (name and self.api.environment_exists(name=name)):
            name = self._prefix

        return name

    @property
    def environments(self):
        """Return list of named conda environments."""
        return self.api.conda_get_envs()

    def get_prefix_envname(self, name):
        """Return the prefix for a given environment by name."""
        return self.api.conda_get_prefix_envname(name)

    def get_package_versions(self, name):
        """Return available package versions for package `name`."""
        return self.table.source_model.get_package_versions(name)

    # --- Conda actions
    # -------------------------------------------------------------------------
    def create_environment(self, name=None, prefix=None, packages=('python',),
                           specfile=False):
        """Create environment by `name` or `prefix` and install `pacakges`."""
        # If environment exists already? GUI should take care of this
        # BUT the api call should simply set that env as the env
        dic = {}
        dic['name'] = name
        dic['prefix'] = prefix
        dic['pkgs'] = packages
        if specfile:
            dic['specfile'] = packages
        dic['dep'] = True  # Not really needed but for the moment!
        dic['action'] = C.ACTION_CREATE
        return self._run_conda_process(dic['action'], dic)

    def clone_environment(self, name=None, prefix=None, clone=None):
        """Clone environment by `name` or `prefix`."""
        dic = {}
        dic['name'] = name
        dic['prefix'] = prefix
        dic['clone'] = clone
        dic['pkgs'] = None
        dic['dep'] = True  # Not really needed but for the moment!
        dic['action'] = C.ACTION_CLONE
        return self._run_conda_process(dic['action'], dic)

    def remove_environment(self, name=None, prefix=None):
        """Remove environment by `name` or `prefix`."""
        dic = {}
        dic['name'] = name
        dic['pkgs'] = None
        dic['dep'] = True  # Not really needed but for the moment!
        dic['action'] = C.ACTION_REMOVE_ENV
        return self._run_conda_process(dic['action'], dic)

    def import_yaml(self, name=None, yaml=None):
        """Import environment using conda env."""
        dic = {}
        dic['name'] = name
        dic['pkgs'] = yaml
        dic['dep'] = True  # Not really needed but for the moment!
        dic['action'] = C.ACTION_IMPORT
        dic['specfile'] = yaml
        return self._run_conda_process(dic['action'], dic)


# --- Local testing
# -----------------------------------------------------------------------------
def test():  # pragma: no cover
    """Run conda packages widget test."""
    from anaconda_navigator.utils.qthelpers import qapplication
    app = qapplication()
    widget = CondaPackagesWidget(None, prefix='/home/goanpeca/anaconda2')
    widget.show()
    sys.exit(app.exec_())


if __name__ == '__main__':  # pragma: no cover
    test()
