# -*- 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."""

# yapf: disable

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 load_style_sheet
from anaconda_navigator.widgets import (ButtonBase, ButtonDanger, ButtonNormal,
                                        ButtonPrimary, ComboBoxBase, FrameBase,
                                        FrameTabFooter, FrameTabHeader,
                                        LabelBase, SpacerHorizontal)
from anaconda_navigator.widgets.helperwidgets import LineEditSearch
from anaconda_navigator.widgets.manager.table import TableCondaPackages

# yapf: enable

_ = gettext.gettext


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

    currentTextChanged = Signal(str)

    def __init__(self, *args, **kwargs):
        """Combobox used in CSS styling."""
        super(ComboBoxPackageFilter, self).__init__(*args, **kwargs)
        self.currentIndexChanged.connect(self._handle_index_changed)

    def _handle_index_changed(self, index):
        """Recreate signal not present in qt4."""
        if isinstance(index, int):
            self.currentTextChanged.emit(self.itemText(index))

    def setCurrentText(self, text):
        """Recreate method not present in qt4."""
        for i in range(self.count()):
            if text == self.itemText(i):
                self.setCurrentIndex(i)
                return


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


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


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


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


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


class ButtonPackageClear(ButtonDanger):
    """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 or []
        self.setAttribute(Qt.WA_LayoutUsesWidgetRect)  # Needed on mac
        self.setFocusPolicy(Qt.StrongFocus)

    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.append(widget)

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

    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()

    # button_widget, sender
    sig_channels_dialog_requested = Signal(object, object)

    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)

        if data_directory is None:
            data_directory = self.CONDA_CONF_PATH

        self._parent = parent
        self._current_model_index = None
        self._current_action_name = ''
        self._current_table_scroll = None
        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

        # 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,
            ]
        )

        # Widgets setup
        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.setMaximumSize(QSize(0, 0))
        self.button_ok.setVisible(False)
        self.button_channels.setCheckable(True)
        combo_items = [k for k in C.COMBOBOX_VALUES_ORDERED]
        self.combobox_filter.addItems(combo_items)
        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)

        # Signals and slots
        self.api.sig_repodata_updated.connect(self._repodata_updated)
        self.combobox_filter.currentTextChanged.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.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 _setup_packages(self, worker, data, error):
        """Callback."""
        if error:
            logger.error(error)
        else:
            logger.debug('')

        combobox_text = self.combobox_filter.currentText()
        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.setCurrentText(combobox_text)
        self.filter_package(combobox_text)

        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' and
                    '/t/' not in ch):
                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('')
        if output:
            packages, apps = output
        else:
            packages, apps = {}, {}
        worker = self.api.pip_list(prefix=self.prefix)
        worker.sig_finished.connect(self._pip_list_ready)
        worker.packages = packages
        worker.apps = apps

    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
        linked_packages = self.api.conda_linked(prefix=self.prefix)
        data = self.api.client_prepare_packages_data(
            packages, linked_packages, pip_packages
        )

        combobox_text = self.combobox_filter.currentText()

        # 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.setCurrentText(combobox_text)
        self.filter_package(combobox_text)

        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."""
        v = self.api.conda_package_version(pkg='python', prefix=self.prefix)
        worker = self.api.client_load_repodata(
            paths, metadata=self._metadata, python_version=v
        )
        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('exception_name', '')
            conda_error_message = output.get('message', '')

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

        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:
                for conda_error in self.conda_errors:
                    if 'UnsatisfiableSpecifications' in conda_error[0]:
                        report = False
                        continue
                    else:
                        report = True

                error_msg = ''
                for conda_error in self.conda_errors:
                    error_msg += str(conda_error[0]) + ':\n'
                    error_msg += str(conda_error[1]) + '\n\n'

                if report:
                    title = 'Conda process error'
                    text = "The following errors occurred:"
                else:
                    title = "Unsatisfiable package specifications:"
                    text = (
                        "The following specifications were found to be "
                        "in conflict:"
                    )
                    package_errors = [
                        e.strip() for e in error_msg.split('\n') if '-' in e
                    ]
                    error_msg = '\n'.join(package_errors)
                    error_msg += (
                        '\n\n\nA package you tried to install '
                        'conflicts with another. \n\nPlease change '
                        'your installation request so that its '
                        'package specifications are not in '
                        'conflict, and then select "Apply" again.'
                        '\n\nUsing "conda info" in a terminal '
                        'window will show the dependencies for '
                        'each package.'
                    )

                dlg = self.message_box_error(
                    text=text,
                    error=error_msg,
                    title=title,
                    report=report,
                    learn_more='http://conda.pydata.org/docs/'
                    'troubleshooting.html#unsatisfiable'
                )
                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('exception_name', '')
            conda_error = output.get('message', '')

            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 _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.api.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.api.active_normalized_channels
            )
            self._repodata_updated(paths)

    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)))
        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 channel dialog."""
        self.sig_channels_dialog_requested.emit(
            self.button_channels,
            C.ENVIRONMENT_PACKAGE_MANAGER,
        )

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

    def search_package(self, text):
        """Search and filter packages 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 = 'The following actions will be applied: '
        template_0 = '  - {0}'
        template_1 = '  - {0}=={1}'
        template_2 = '  - {0}: {1} -> {2}'
        packages = []

        if pip_remove:
            packages += ['Remove (pip):']
            packages += [
                template_1.format(i['name'], i['version_to'])
                for i in pip_remove
            ]

        if conda_remove:
            packages += ['Remove:']
            packages += [
                template_1.format(i['name'], i['version_to'])
                for i in conda_remove
            ]

        if conda_install:
            packages += ['Install:']
            packages += [template_0.format(i['name']) for i in conda_install]

        if conda_downgrade:
            packages += ['Downgrade:']
            packages += [
                template_2.format(
                    i['name'], i['version_from'], i['version_to']
                ) for i in conda_downgrade
            ]

        if conda_upgrade:
            packages += ['Upgrade:']
            packages += [
                template_2.format(
                    i['name'], i['version_from'], i['version_to']
                ) for i in conda_upgrade
            ]

        if self.apply_actions_dialog:
            dlg = self.apply_actions_dialog(
                message, packages=packages, 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}'.format(i['name']) 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.api.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.api.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.api.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)

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

        self.button_apply.setVisible(
            self.button_apply.isVisible() and not hide
        )
        self.button_clear.setVisible(
            self.button_clear.isVisible() and not hide
        )

        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])

    # --- 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.setStyleSheet(load_style_sheet())
    widget.show()
    sys.exit(app.exec_())


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