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

# yapf: disable

from __future__ import absolute_import, division

# Standard library imports
import json
import os
import pickle
import sys
import tempfile

# Third party imports
from qtpy.QtCore import (QCoreApplication, QPoint, QSize, Qt, QTimer, QUrl,
                         Signal)
from qtpy.QtGui import QDesktopServices, QIcon
from qtpy.QtSvg import QSvgWidget
from qtpy.QtWidgets import (QApplication, QHBoxLayout, QMainWindow,
                            QVBoxLayout, QWidget)
import psutil

# Local imports
from anaconda_navigator.api.anaconda_api import AnacondaAPI
from anaconda_navigator.config import (CHANNELS_PATH, CONF, MAC,
                                       PACKAGES_WITH_LICENSE, WIN)
from anaconda_navigator.static import images
from anaconda_navigator.utils import constants as C
from anaconda_navigator.utils.analytics import GATracker
from anaconda_navigator.utils.launch import launch
from anaconda_navigator.utils.logs import logger
from anaconda_navigator.utils.qthelpers import create_action
from anaconda_navigator.utils.styles import BLUR_SIZE, load_style_sheet
from anaconda_navigator.widgets import (ButtonBase, ButtonLabel, ButtonLink,
                                        ButtonPrimary, FrameBase, LabelBase,
                                        SpacerHorizontal)
from anaconda_navigator.widgets.dialogs import (MessageBoxError,
                                                MessageBoxInformation,
                                                MessageBoxQuestion)
from anaconda_navigator.widgets.dialogs.about import AboutDialog
from anaconda_navigator.widgets.dialogs.channels import DialogChannels
from anaconda_navigator.widgets.dialogs.environment import CreateConflictDialog
from anaconda_navigator.widgets.dialogs.license import LicenseManagerDialog
from anaconda_navigator.widgets.dialogs.logger import LogViewerDialog
from anaconda_navigator.widgets.dialogs.login import AuthenticationDialog
from anaconda_navigator.widgets.dialogs.preferences import PreferencesDialog
from anaconda_navigator.widgets.dialogs.quit import (QuitApplicationDialog,
                                                     QuitBusyDialog,
                                                     QuitRunningAppsDialog)
from anaconda_navigator.widgets.dialogs.update import DialogUpdateApplication
from anaconda_navigator.widgets.tabs.community import CommunityTab
from anaconda_navigator.widgets.tabs.environments import EnvironmentsTab
from anaconda_navigator.widgets.tabs.home import HomeTab
from anaconda_navigator.widgets.tabs.projects import ProjectsTab
from anaconda_navigator.widgets.tabs.tabwidget import TabWidget


# yapf: enable


# --- Widgets used with CSS styling
# -----------------------------------------------------------------------------
class ButtonLinkLogin(ButtonLink):
    """Button used in CSS styling."""


class ButtonLabelLogin(ButtonLabel):
    """Button used in CSS styling."""


class ButtonHeaderUpdate(ButtonBase):
    """Button used in CSS styling."""


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


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

    Top application header.
    """


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


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


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


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

    pass


# --- Main widget
# -----------------------------------------------------------------------------
class MainWindow(QMainWindow):
    """Main window widget."""

    sig_logged_in = Signal()
    sig_logged_out = Signal()

    DOCS_URL = 'https://docs.continuum.io/anaconda/navigator'
    FEATURED_CHANNELS = ('anaconda-fusion', )

    # Web content
    VIDEOS_URL = "http://content.continuum.io/api/videos"
    EVENTS_URL = "http://content.continuum.io/api/events"
    WEBINARS_URL = "http://content.continuum.io/api/webinars"

    # Web content development site
    TEST_URL = "http://api-dev-continuum-content.pantheonsite.io/"
    TEST_VIDEOS_URL = TEST_URL + "api/videos"
    TEST_EVENTS_URL = TEST_URL + "api/events"
    TEST_WEBINARS_URL = TEST_URL + "api/webinars"

    # VIDEOS_URL = TEST_VIDEOS_URL
    # EVENTS_URL = TEST_EVENTS_URL
    # WEBINARS_URL = TEST_WEBINARS_URL

    def __init__(self, splash=None, config=CONF):
        """Main window widget."""
        super(MainWindow, self).__init__()

        self._login_text = 'Sign in to Anaconda Cloud'
        self._current_action_name = None

        self.setup_ready = False
        self.initial_setup = None
        self.app = QCoreApplication.instance()
        self.tracker = None
        self.splash = splash
        self.config = config
        self.fullscreen_flag = False
        self.maximized_flag = True
        self.style_sheet = None
        self.running_processes = []

        # Status messages
        self.status_progress_value = 0
        self.status_progress_max_value = 0
        self.status_message = ''

        # Anaconda API
        self.api = AnacondaAPI()

        # Start navigator with the activated prefix in case it was installed
        # in an environment different from root.
        self.current_prefix = os.environ.get(
            'CONDA_PREFIX', self.api.ROOT_PREFIX
        )
        self.api_info = self.api.download_get_api_info()
        self.busy = False
        self.logged = False
        self.username = ''
        self.first_run = self.config.get('main', 'first_run')
        self.application_update_version = None

        # Widgets
        self.frame_header = FrameHeader(self)
        self.frame_body = FrameBody(self)
        self.label_logo = QSvgWidget(images.ANACONDA_NAVIGATOR_LOGO)
        self.button_logged_text = ButtonLabelLogin('')
        self.button_logged_username = ButtonLinkLogin('')
        self.button_update_available = ButtonHeaderUpdate('Upgrade Now')
        self.button_login = ButtonLogin(self._login_text)
        self.central_widget = QWidget()

        self.stack = TabWidget(self)
        self.stack.add_link(
            'Documentation',
            url="https://docs.continuum.io/anaconda/navigator",
        )
        self.stack.add_link(
            'Developer Blog',
            url="https://www.continuum.io/blog/developer-blog",
        )
        self.stack.add_link(
            'Feedback',
            url="https://continuum.typeform.com/to/ABe8FA",
        )
        self.stack.add_social('Twitter', url="https://twitter.com/ContinuumIO")
        self.stack.add_social(
            'Youtube',
            url="https://www.youtube.com/c/ContinuumIo",
        )
        self.stack.add_social('Github', url="https://github.com/ContinuumIO")
        self.home_tab = HomeTab(parent=self)
        self.environments_tab = EnvironmentsTab(parent=self)
        self.learning_tab = CommunityTab(
            parent=self,
            tags=['webinar', 'documentation', 'video', 'training'],
            content_urls=[self.VIDEOS_URL, self.WEBINARS_URL],
        )
        self.community_tab = CommunityTab(
            parent=self,
            tags=['event', 'forum', 'social'],
            content_urls=[self.EVENTS_URL],
        )
        self.projects_tab = ProjectsTab(parent=self)

        # Note: Icons are set in /static/css/styles.scss
        self.stack.addTab(self.home_tab, text='Home')
        self.stack.addTab(self.environments_tab, text='Environments')
        self.stack.addTab(self.projects_tab, text='Projects')
        self.stack.addTab(self.learning_tab, text='Learning')
        self.stack.addTab(self.community_tab, text='Community')

        # Widget setup
        self.setWindowTitle("Anaconda Navigator")
        self.button_login.setDefault(True)
        self.label_logo.setMaximumSize(*(729 / 2, 92 / 2))

        # Layout
        header_layout = QHBoxLayout()
        header_layout.addWidget(self.label_logo)
        header_layout.addStretch()
        header_layout.addWidget(self.button_update_available)
        header_layout.addWidget(SpacerHorizontal())
        header_layout.addWidget(self.button_logged_text)
        header_layout.addWidget(self.button_logged_username)
        header_layout.addWidget(SpacerHorizontal())
        header_layout.addWidget(self.button_login)
        self.frame_header.setLayout(header_layout)

        body_layout = QHBoxLayout()
        body_layout.addWidget(self.stack)
        body_layout.setContentsMargins(0, 0, 0, 0)
        body_layout.setSpacing(0)
        self.frame_body.setLayout(body_layout)

        main_layout = QVBoxLayout()
        main_layout.addWidget(self.frame_header)
        main_layout.addWidget(self.frame_body)
        main_layout.setContentsMargins(0, 0, 0, 0)
        main_layout.setSpacing(0)
        self.central_widget.setLayout(main_layout)
        self.setContentsMargins(0, 0, 0, 0)
        self.setCentralWidget(self.central_widget)

        # Signals
        self.button_login.clicked.connect(self.login)
        self.button_logged_username.clicked.connect(self.open_login_page)
        self.button_update_available.clicked.connect(self.update_application)
        self.stack.sig_current_changed.connect(self._track_tab)
        self.stack.sig_current_changed.connect(self.fix_tab_order)
        self.stack.sig_url_clicked.connect(self.open_url)

        # Move to a separate method per tab?
        self.home_tab.sig_channels_dialog_requested.connect(self.show_channels)
        self.home_tab.sig_url_clicked.connect(self.open_url)
        self.home_tab.sig_conda_action_requested.connect(self.run_conda_action)
        self.home_tab.sig_launch_action_requested.connect(
            self.launch_application
        )
        self.home_tab.sig_environment_changed.connect(self.change_environment)
        self.home_tab.sig_environment_updated.connect(self.fix_tab_order)
        self.environments_tab.sig_channels_dialog_requested.connect(
            self.show_channels
        )
        self.environments_tab.sig_environments_updated.connect(
            self.home_tab.setup_tab
        )
        self.environments_tab.sig_environments_updated.connect(
            self.fix_tab_order
        )
        self.environments_tab.sig_status_changed.connect(
            self.update_busy_status
        )
        self.projects_tab.projects_widget.sig_login_requested.connect(
            self.login
        )

        # Setup
        self.api.set_data_directory(CHANNELS_PATH)
        self.update_style_sheet()

    # Helpers
    # -------------------------------------------------------------------------
    def _track_tab(self, index=None):
        """Track the active tab by index, or set `Home` when index is None."""
        if index is None:
            index = self.stack.currentIndex()

        text = self.stack.currentText().lower()

        if self.tracker:
            page = '/{0}'.format(text)
            self.tracker.track_page(page)

    def _metadata_updated(self, worker, path, error):
        """Callback for update_metadata."""
        if self.initial_setup:
            self.set_splash('Updating repodata...')

        if error:
            logger.error(str(error))

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

        active_channels = self.config.get(
            'main', 'conda_active_channels', default=tuple()
        )
        if not active_channels:
            active_channels = self.api.conda_get_condarc_channels()
            self.config.set('main', 'conda_active_channels', active_channels)

        self.api.update_repodata()
        self.api.sig_repodata_updated.connect(self._repodata_updated)

    def _repodata_updated(self, paths):
        """Callback for update repodata."""
        if self.initial_setup:
            self.set_splash('Loading repodata...')

        self.api.sig_repodata_updated.disconnect(self._repodata_updated)

        if self.first_run:
            self.set_splash('Initial configuration...')

        worker = self.api.client_load_repodata(paths, self._metadata)
        worker.sig_finished.connect(self.process_application_info)

    # --- Public API
    # -------------------------------------------------------------------------
    def setup(self, initial_setup=True):
        """
        Perform initial setup and configuration.

        This is the first method called after the main window has been created.
        """
        self.initial_setup = initial_setup

        if self.initial_setup:
            self.add_featured_channels()
            self.set_splash('Updating metadata...')

            # Get user info if it has previously logged in via anaconda client
            user = self.api.client_user()
            self.update_login_status(user)

            self.setup_toolbars()
            self.set_application_icon()
            worker = self.api.update_metadata()
            worker.sig_finished.connect(self._metadata_updated)
        else:
            self.api.update_repodata()
            self.api.sig_repodata_updated.connect(self._repodata_updated)
            self.environments_tab.update_visibility(enabled=False)
            self.environments_tab.packages_widget.update_status(
                'Updating channels',
            )

    def add_featured_channels(self):
        """Automatically add featured channels on startup if not found."""
        current_channels = self.api.conda_get_condarc_channels()
        active_channels = set(self.config.get('main', 'conda_active_channels'))
        add_channels = self.config.get('main', 'add_default_channels')
        if 'anaconda.org' in self.api_url and add_channels:
            for ch in self.FEATURED_CHANNELS:
                if ch not in current_channels:
                    active_channels.add(ch)
                    worker = self.api.conda_config_add('channels', ch)
                    worker.communicate()
            self.config.set('main', 'add_default_channels', False)
        self.config.set(
            'main', 'conda_active_channels', tuple(active_channels)
        )

    def check_outdated_channels(self):
        """Check if the user has binstar channels as part of the config."""
        current_channels = self.api.conda_get_condarc_channels()
        outdated_channels = []
        active_channels = set(self.config.get('main', 'conda_active_channels'))
        if 'anaconda.org' in self.api_url:
            for ch in current_channels:
                if 'binstar.org' in ch:
                    outdated_channels.append(ch)

        if outdated_channels:
            chs = ['- <b>{0}</b><br>'.format(ch) for ch in outdated_channels]
            chs = ''.join(chs)
            text = (
                "We detected some outdated channels making use of "
                "<b>binstar.org</b><br><br>{chs}<br><br>"
                "Do you want to update them?"
            ).format(chs=chs)

            dlg = MessageBoxQuestion(
                text=text, title='Outdated channels detected'
            )
            dlg.setMinimumWidth(300)

            if dlg.exec_():
                for ch in outdated_channels:
                    new_channel = ch.replace('binstar.org', 'anaconda.org')
                    worker = self.api.conda_config_add('channels', new_channel)
                    worker.communicate()
                    worker = self.api.conda_config_remove('channels', ch)
                    worker.communicate()

                    if ch in active_channels:
                        active_channels.remove(ch)
                    active_channels.add(new_channel)

                self.config.set(
                    'main', 'conda_active_channels', tuple(active_channels)
                )

    def process_application_info(self, worker, output, error):
        """Create application projects."""
        if error:
            logger.error(str(error))

        packages, apps = output
        processed_apps = self.api.process_apps(apps)

        if self.initial_setup:
            self.post_setup(processed_apps)
            self.check_for_updates(packages)
        else:
            self.home_tab.setup_tab(processed_apps)
            self.environments_tab.setup_tab(metadata=self._metadata)
            self.update_status()

        self.fix_tab_order(0)
        self.stack.tabbar.buttons[self.stack.currentIndex()].setFocus()

    def post_setup(self, apps):
        """Setup tab widgets."""
        self.setup_ready = True
        self.config.set('main', 'first_run', False)
        self.set_splash('Loading applications...')
        self.home_tab.setup_tab(apps, prefix=self.current_prefix)
        self.set_splash('Loading environments...')
        self.environments_tab.setup_tab(
            metadata=self._metadata, selected_prefix=self.current_prefix
        )
        self.set_splash('Loading projects...')
        self.projects_tab.setup_tab()
        self.set_splash('Loading content...')
        self.community_tab.setup_tab()
        self.learning_tab.setup_tab()
        self.update_style_sheet()

        geo = self.config.get('main', 'geo', None)
        if geo:
            # In case Navigator is installed in an env that has a different
            # python version than root (root == py3, env == py2 or viceversa)
            try:
                self.restoreGeometry(pickle.loads(geo))
                self.show()
            except Exception:
                self.showMaximized()
        else:
            self.showMaximized()
        self.post_visible_setup()

    def set_application_icon(self):
        """Set application icon."""
        app_icon = QIcon()
        app_icon.addFile(images.ANACONDA_ICON_16_PATH, QSize(16, 16))
        app_icon.addFile(images.ANACONDA_ICON_24_PATH, QSize(24, 24))
        app_icon.addFile(images.ANACONDA_ICON_32_PATH, QSize(32, 32))
        app_icon.addFile(images.ANACONDA_ICON_48_PATH, QSize(48, 48))
        app_icon.addFile(images.ANACONDA_ICON_256_PATH, QSize(256, 256))
        self.app.setWindowIcon(app_icon)

    def setup_toolbars(self):
        """Setup toolbar menus and actions."""
        menubar = self.menuBar()

        file_menu = menubar.addMenu('&File')
        file_menu.addAction(
            create_action(
                self,
                "&Preferences",
                triggered=self.show_preferences,
                shortcut="Ctrl+P"
            )
        )
        # file_menu.addAction(create_action(self, "&Restart",
        #                                   triggered=self.restart,
        #                                   shortcut="Ctrl+R"))
        file_menu.addAction(
            create_action(
                self, "&Quit", triggered=self.close, shortcut="Ctrl+Q"
            )
        )

        helpmenu = menubar.addMenu('&Help')
        helpmenu.addAction(
            create_action(
                self,
                "&Online Documentation",
                triggered=lambda: self.open_url(self.DOCS_URL)
            )
        )
        helpmenu.addAction(
            create_action(
                self, "License &manager", triggered=self.show_license_manager
            )
        )
        helpmenu.addAction(
            create_action(
                self,
                "&Logs viewer",
                triggered=self.show_log_viewer,
                shortcut="F6"
            )
        )
        helpmenu.addSeparator()
        helpmenu.addAction(
            create_action(
                self, "&About", triggered=self.show_about
            )
        )

    def post_visible_setup(self):
        """Setup after show method has been applied."""
        if self.splash:
            self.splash.hide()

        self.config.set('main', 'first_run', False)

        # Start the tracker only after post_visible_setup
        self.tracker = GATracker()
        self._track_tab(0)  # Start tracking home
        self.show_welcome_screen()
        self.check_outdated_channels()

        # Fix tab order
        self.fix_tab_order(0)
        self.stack.tabbar.buttons[0].setFocus()

    def check_for_updates(self, packages=None, version=None):
        """Check for application updates."""
        from distutils.version import LooseVersion as lv
        # Check if there is an update for navigator!
        version = version or self.api.conda_package_version(
            name='root', pkg='anaconda-navigator'
        )
        version = version or "0.0.1"
        # Temporal mock test
        # mock_versions = [version, '1.5.0']
        # packages['anaconda-navigator'] = {'versions': mock_versions}
        self.button_update_available.setEnabled(False)
        self.button_update_available.setVisible(False)

        if packages:
            package_data = packages.get('anaconda-navigator')
            if package_data:
                versions = package_data.get('versions')
                if versions and (
                    lv(versions[-1]) > lv(version) or 'dev' in version and
                    versions[-1] == version.replace('dev', '')
                ):
                    self.application_update_version = versions[-1]
                    self.button_update_available.setEnabled(True)
                    self.button_update_available.setVisible(True)
                    if not self.config.get('main', 'hide_update_dialog'):
                        self.update_application(center_dialog=True)

    def fix_tab_order(self, tab_index=None):
        """Fix tab order of UI widgets."""
        current_widget = self.stack.currentWidget()
        if current_widget is not None:
            # print('\n\n{}\n'.format(current_widget))
            ordered_widgets = [
                self.button_update_available,
                self.button_logged_username,
                self.button_login,
            ]
            ordered_widgets += self.stack.tabbar.buttons
            next_widdget = self.stack.tabbar.links[0]
            ordered_widgets += current_widget.ordered_widgets(next_widdget)
            ordered_widgets += self.stack.tabbar.links
            ordered_widgets += self.stack.tabbar.links_social
            ordered_widgets += [self.button_update_available]

            for i, widget in enumerate(ordered_widgets[:-1]):
                try:
                    text = widget.text()
                except:
                    text = ''
                logger.debug('{0}, {1}, {2}'.format(i, widget, text))

                # print('{0}, {1}, {2}'.format(i, widget, text))
                # print('{0}, {1}'.format(i + 1, ordered_widgets[i + 1]))
                self.setTabOrder(ordered_widgets[i], ordered_widgets[i + 1])
                # print()

    def restart(self):
        """
        Restart application.

        FIXME: The restart method needs to be called via an external script
        So that a new version can be loaded. The current method keeps loading
        a potentially outdated versions after an update.
        """
        self.splash.show_message("Restarting...")
        w = MainWindow(splash=self.splash)
        self.successor = w
        w.setup()
        w.show()
        self.close()

    def update_style_sheet(self):
        """Update custom CSS style sheet."""
        self.style_sheet = load_style_sheet()
        self.home_tab.update_style_sheet(style_sheet=self.style_sheet)
        self.environments_tab.update_style_sheet(style_sheet=self.style_sheet)
        self.community_tab.update_style_sheet(style_sheet=self.style_sheet)
        self.learning_tab.update_style_sheet(style_sheet=self.style_sheet)
        self.setStyleSheet(self.style_sheet)

    def set_splash(self, message):
        """Set splash message."""
        if self.splash:
            self.splash.show_message(message)
        QApplication.processEvents()

    # --- Login
    # -------------------------------------------------------------------------
    @property
    def conda_url(self):
        """Return the conda url based on the api info from config url."""
        api_info = self.api.download_get_api_info()
        conda_url = api_info.get('conda_url', 'https://conda.anaconda.org')
        conda_url = conda_url[:-1] if conda_url[-1] == '/' else conda_url
        return conda_url

    @property
    def api_url(self):
        """Return the api url from anaconda client config."""
        return self.api.client_get_api_url()

    def update_project_login_status(self, username, token):
        self.projects_tab.projects_widget.update_login_status(username, token)

    def update_login_status(self, user_data=None):
        """Update login button and information."""
        if user_data:
            self.username = user_data.get('login', '')
            self.logged = True

        if self.logged:
            username = self.username
            token = self.api.client_load_token(self.api_url)
            url = "{0}/{1}".format(self.conda_url, username)
            self.button_logged_text.setText('Signed in as ')
            self.button_logged_username.setText(username)
            self.button_logged_username.setToolTip(url)
            self.button_login.setText('Sign out')
            self.environments_tab.packages_widget.set_token(token)
            self.button_logged_text.setVisible(True)
            self.button_logged_username.setVisible(True)
            self.update_project_login_status(username, token)
        else:
            self.button_logged_text.setText('')
            self.button_logged_username.setText('')
            self.button_logged_text.setVisible(False)
            self.button_logged_username.setVisible(False)
            self.button_login.setText(self._login_text)
            self.update_project_login_status(None, None)
        QApplication.restoreOverrideCursor()

    def login(self, logout=False):
        """Open up login dialog or log out depending on logged status."""
        if logout:  # Force a logout
            self.logged = True
            self.api.client_remove_token()

        if self.logged:
            QApplication.setOverrideCursor(Qt.WaitCursor)
            self.api.client_logout()
            self.api.client_remove_token()
            self.logged = False
            self.sig_logged_out.emit()
            self.tracker.track_event(
                'authenticate', 'logout', label=self.username
            )
        else:
            dlg = AuthenticationDialog(self.api, parent=self)

            if self.tracker:
                self.tracker.track_page('/login', pagetitle='Login dialog')

            if dlg.exec_():
                self.api.client_store_token(dlg.token)
                self.username = dlg.username
                self.logged = True
                self.sig_logged_in.emit()

                if self.tracker:
                    self.tracker.track_event(
                        'authenticate', 'login', label=self.username
                    )
            self._track_tab()

        self.update_login_status()
        logger.debug(str((self.logged, self.username)))

    # --- Dialogs
    # -------------------------------------------------------------------------
    def show_channels(self, button=None, sender=None):
        """Show the channels dialog."""
        if sender == C.HOME_APPLICATION:
            pass
        elif sender == C.ENVIRONMENT_PACKAGE_MANAGER:
            pass

        if button:
            button.setDisabled(True)

        dlg = DialogChannels(
            self,
            channels=self.api.channels,
            active_channels=self.api.active_channels,
        )
        dlg.update_style_sheet(style_sheet=self.style_sheet)
        dlg.sig_channels_updated.connect(self.update_channels)
        dlg.rejected.connect(lambda: button.setEnabled(True))
        dlg.rejected.connect(button.toggle)
        dlg.rejected.connect(button.setFocus)
        dlg.accepted.connect(
            lambda v=None: self._accept_channels_dialog(button)
        )

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

    @staticmethod
    def _accept_channels_dialog(button):
        button.setEnabled(True)
        button.setFocus()
        button.toggle()

    def show_preferences(self):
        """Display the preferences dialog and apply the needed actions."""
        dlg = PreferencesDialog(self)
        self.tracker.track_page('/preferences', pagetitle='Preferences dialog')
        set_domains = self.environments_tab.packages_widget.update_domains

        dlg.sig_urls_updated.connect(set_domains)
        dlg.sig_urls_updated.connect(lambda au, cu: self.login(logout=True))
        dlg.exec_()
        self._track_tab()

    def show_about(self):
        """Display the `About` dialog with information on the project."""
        dlg = AboutDialog(self)
        self.tracker.track_page('/about', pagetitle='About dialog')
        dlg.exec_()
        self._track_tab()

    def show_license_manager(self):
        """Show license manager dialog."""
        license_dialog = LicenseManagerDialog(parent=self)
        license_dialog.sig_url_clicked.connect(self.open_url)
        reply = license_dialog.exec_()
        if reply:
            self.home_tab.setup_tab(prefix=self.home_tab.current_environment())

    def show_log_viewer(self):
        """Display the logs viewer to the user."""
        dlg = LogViewerDialog()
        self.tracker.track_page('/logs', pagetitle='Log Viewer Dialog')
        dlg.exec_()
        self._track_tab()

    def show_welcome_screen(self):
        """Show initial splash screen."""
        if (getattr(self, 'showme', True) and
                self.config.get('main', 'show_startup', True)):
            from anaconda_navigator.widgets.dialogs.splash import FirstSplash

            self.showme = False
            if self.splash:
                self.splash.hide()
                dlg = FirstSplash()
                dlg.raise_()
                dlg.exec_()

    @staticmethod
    def show_error_message(worker, output, error):
        """Display application error message."""
        if error:
            # error_type = output.get('exception_type')
            error_name = output.get('exception_name', '')
            error_message = output.get('message', '')

            if 'UnsatisfiableSpecifications' in error_name:
                report = False
            else:
                report = True

        if report:
            title = 'Conda process error'
            text = "The following errors occurred:"
            error_msg = error_message
        else:
            title = "Unsatisfiable package specifications:"
            text = (
                "The following specifications were found to be "
                "in conflict:"
            )
            package_errors = [
                e.strip() for e in error_message.split('\n') if '-' in e
            ]
            error_msg = '\n'.join(package_errors)
            error_msg += (
                '\n\n\nA package you tried to install '
                'conflicts with another.'
            )

            if worker.sender == C.ENVIRONMENT_PACKAGE_MANAGER:
                error_msg += (
                    '\n\nPlease change '
                    'your installation request so that its '
                    'package specifications are not in '
                    'conflict, and then select "Apply" again.'
                )

        dlg = MessageBoxError(
            text=text,
            error=error_msg,
            title=title,
            report=report,
            learn_more='http://conda.pydata.org/docs/'
            'troubleshooting.html#unsatisfiable'
        )
        dlg.setMinimumWidth(400)

        if dlg.exec_():
            pass

    # --- Update Navigator
    # -------------------------------------------------------------------------
    def _update_application(self, worker, output, error):
        """Update application callback."""
        if error:
            self.button_update_available.setEnabled(True)
            text = 'Anaconda Navigator Update error:'
            dlg = MessageBoxError(
                text=text, error=error, title='Application Update Error'
            )
            self.tracker.track_page(
                '/update/error',
                pagetitle='Update Application Error '
                'Message Box'
            )
            dlg.exec_()
        else:
            self.button_update_available.setEnabled(False)
            text = (
                'Anaconda Navigator Updated succefully.\n\n'
                'Please restart the application'
            )
            dlg = MessageBoxInformation(text=text, title='Application Update')
            self.tracker.track_page(
                '/update/successful',
                pagetitle='Application Update Succesful '
                'Message Box'
            )
            dlg.exec_()
            self.update_busy_status(False)
        self.update_status()
        self._track_tab()

    def update_application(self, center_dialog=False):
        """Update application to latest available version."""
        version = self.application_update_version
        if version:
            dlg = DialogUpdateApplication(
                version=version, startup=center_dialog
            )
            if not center_dialog:
                height = self.button_update_available.height()
                point = self.button_update_available.mapToGlobal(
                    QPoint(-dlg.WIDTH, height)
                )
                dlg.move(point)

            if self.tracker:
                self.tracker.track_page(
                    '/update', pagetitle='Update Application Dialog'
                )

            reply = dlg.exec_()
            if reply:
                self.tracker.track_event('application', 'updated', version)
                self.update_busy_status(True)
                self.update_status(
                    'Updating navigator to version '
                    '{}'.format(version)
                )
                pkg = 'anaconda-navigator={}'.format(version)
                worker = self.api.conda_install(name='root', pkgs=[pkg])
                worker.sig_finished.connect(self._update_application)
                self.button_update_available.setEnabled(False)
                self.button_update_available.setVisible(False)
            self._track_tab()

    # --- Url handling
    # -------------------------------------------------------------------------
    # TODO: Route ALL url handling to this method? or Make a global func?
    def open_url(self, url, category=None, action=None):
        """Open url and track event."""
        qurl = QUrl(url)
        QDesktopServices.openUrl(qurl)
        self.tracker.track_event('help', 'documentation', url)

    def open_login_page(self):
        """Open logged in user anaconda page."""
        url = "{0}/{1}".format(self.conda_url, self.username)
        qurl = QUrl(url)
        QDesktopServices.openUrl(qurl)
        self.tracker.track_event('content', 'clicked', url)

    # --- Conda handling
    # -------------------------------------------------------------------------
    def change_environment(self, name, prefix, sender):
        """TODO:."""
        #  print('MAIN:change_environment', name, prefix, sender)
        self.current_prefix = prefix
        if sender == C.ENVIRONMENT_LIST:
            self.home_tab.set_environments(prefix=prefix)
        elif sender == C.HOME_APPLICATION:
            pass
            # self.environments_tab.set_environment(prefix=prefix)

    def update_channels(self, channels, active_channels):
        """Save updated channels to the conda config."""
        old_channels = self.api.conda_get_condarc_channels()
        remove_channels = list((set(old_channels) - set(channels)))
        add_channels = list(set(channels) - set(old_channels))

        self.update_status(
            action='Updating channels and repository data',
            progress_value=0,
            progress_max_value=0
        )

        for ch in add_channels:
            worker = self.api.conda_config_add('channels', ch)
            worker.communicate()
        for ch in remove_channels:
            worker = self.api.conda_config_remove('channels', ch)
            worker.communicate()

        self.config.set('main', 'conda_active_channels', active_channels)
        self.setup(initial_setup=False)

    def _conda_partial_output_ready(self, worker, output, error):
        """Callback."""
        if isinstance(output, dict):
            self.status_progress_value = output.get(
                'progress', self.status_progress_value
            )
            self.status_progress_max_value = output.get(
                'maxval', self.status_progress_max_value
            )
            name = output.get('name')
            fetch = output.get('fetch')

            if fetch:
                self.status_message = ('- Downloading <b>{0}</b>'
                                       '...').format(fetch)

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

        logger.debug(self.status_message)
        self.update_status(
            worker.action_msg, self.status_message, self.status_progress_value,
            self.status_progress_max_value, worker.sender
        )

    def _conda_output_ready(self, worker, output, error):
        """
        Callback for handling action finished.

        TODO: Move out of this widget.
        """
        if not isinstance(output, dict):
            output = {}

        success = output.get('success', True)
        exception_name = output.get('exception_name')
        exception_type = output.get('exception_type')
        is_error = exception_name or exception_type

        if is_error or error or not success:
            logger.error(error)
            self.show_error_message(worker, output, error)
            self.home_tab.set_widgets_enabled(True)
        else:
            action = getattr(worker, 'action', None)

            if action == C.ACTION_CREATE:
                env_name = getattr(worker, 'env_name', self.current_prefix)
                prefix = self.api.conda_get_prefix_envname(env_name)
            else:
                prefix = self.current_prefix

            self.home_tab.setup_tab(prefix=prefix)
            self.update_busy_status(False)

        self.update_status()

    def launch_application(
        self,
        package_name,
        command,
        leave_path_alone,
        prefix,
        sender,
    ):
        """Launch application from home screen."""
        self.update_busy_status(True)
        self.update_status(action='Launching <b>{0}</b>'.format(package_name))
        process = launch(
            prefix,
            command,
            leave_path_alone,
            package_name=package_name,
            root_prefix=self.api.ROOT_PREFIX
        )
        # Find process and subprocesses after launching it
        pids = [process]
        if psutil.pid_exists(process):
            proc = psutil.Process(process)
            for p in proc.children(recursive=True):
                pids.append(p.pid)

        data = {
            'pids': pids,
            'package': package_name,
            'command': command,
            'prefix': prefix,
        }
        self.running_processes.append(data)

        # Set timer
        time_seconds = 10 if WIN else 5
        self._launch_timer = QTimer()
        self._launch_timer.setSingleShot(True)
        self._launch_timer.setInterval(time_seconds * 1000)
        self._launch_timer.timeout.connect(lambda: self.update_status())
        self._launch_timer.timeout.connect(
            lambda: self.update_busy_status(False)
        )
        self._launch_timer.start()

    def check_dependencies_before_install(self, worker, output, error):
        """
        Check if the package to be installed changes navigator dependencies.

        This check is made for Orange3 which is not qt5 compatible.
        """
        navigator_dependencies = ['qt', 'pyqt']
        actions = output.get('actions', {})
        exception_type = str(output.get('exception_type', ''))

        # It seems conda now provides the whole result as a list?
        if isinstance(actions, list):
            actions = actions[0]

        linked = actions.get('LINK', [])
        unlinked = actions.get('UNLINK', [])

        split_cano = self.api.conda_split_canonical_name
        try:
            # Old conda json format
            linked = {split_cano(p)[0]: split_cano(p) for p in linked}
        except AttributeError:
            # New conda json format
            linked = {
                split_cano(p['dist_name'])[0]: split_cano(p['dist_name'])
                for p in linked
            }

        try:
            # Old conda json format
            unlinked = {split_cano(p)[0]: split_cano(p) for p in unlinked}
        except AttributeError:
            # New conda json format
            unlinked = {
                split_cano(p['dist_name'])[0]: split_cano(p['dist_name'])
                for p in unlinked
            }

        # TODO: Use fallback package?
        from pkg_resources import parse_version

        downgraded_deps = []
        for pkg in unlinked:
            u_pkg_ver = parse_version(unlinked[pkg][1])
            l_pkg = linked.get(pkg)
            l_pkg_ver = parse_version(linked[pkg][1]) if l_pkg else None

            if l_pkg and u_pkg_ver > l_pkg_ver:
                downgraded_deps.append(pkg)

        conflict_message = ''
        conflicts = False
        for down_dep in downgraded_deps:
            for nav_dep in navigator_dependencies:
                if (down_dep == nav_dep and
                        worker.prefix == self.api.ROOT_PREFIX):
                    conflicts = True
                    break

        # Try to install in a new environment
        if 'UnsatisfiableError' in exception_type:
            conflicts = True
            # Try to set the default python to None to avoid issues that
            # prevent a package to be installed in a new environment due to
            # python pinning, fusion for 2.7, rstudio on win for 2.7 etc.
            self.api.conda_config_set('default_python', None)

        if conflicts:
            dlg = CreateConflictDialog(
                parent=self,
                environments=self.api.environments,
                package=worker.package,
                extra_message=conflict_message,
            )
            if dlg.exec_():
                env_name = dlg.environment
                action_msg = (
                    'Install application <b>{0}</b> on '
                    '<b>{1}</b>'
                ).format(worker.package, env_name)

                if env_name not in self.api.environments:
                    self.create_environment(
                        name=env_name, packages=worker.pkgs
                    )
                else:
                    worker = self.api.conda_install(
                        name=env_name,
                        channels=self.api.active_normalized_channels,
                        pkgs=worker.pkgs
                    )
                    worker.action = C.ACTION_INSTALL

                    worker.action_msg = action_msg
                    worker.sig_finished.connect(self._conda_output_ready)
                    worker.sig_partial.connect(
                        self._conda_partial_output_ready
                    )
                self.update_status(action_msg)
            else:
                self.update_status()
                self.home_tab.set_widgets_enabled(True)
        else:
            if worker.action == C.APPLICATION_INSTALL:
                action_msg = (
                    'Install application <b>{0}</b> on '
                    '<b>{1}</b>'
                ).format(worker.package, worker.prefix)
            elif worker.action == C.APPLICATION_UPDATE:
                action_msg = (
                    'Updating application <b>{0}</b> on '
                    '<b>{1}</b>'
                ).format(worker.package, worker.prefix)
            new_worker = self.api.conda_install(
                prefix=worker.prefix,
                channels=self.api.active_normalized_channels,
                pkgs=worker.pkgs
            )
            new_worker.action_msg = action_msg
            new_worker.action = worker.action
            new_worker.sender = worker.sender
            new_worker.sig_finished.connect(self._conda_output_ready)
            new_worker.sig_partial.connect(self._conda_partial_output_ready)
            self.update_status(action_msg)

    def _check_license_requirements(self, worker, output, error):
        """Check license requirement callback."""
        if output:
            temp_license_dir = tempfile.mkdtemp()
            file_name = 'license_bundle_trial.txt'
            temp_license_path = os.path.join(temp_license_dir, file_name)
            with open(temp_license_path, 'w') as f:
                f.write(json.dumps(output))
            self.api.add_license([temp_license_path])
        else:
            question = (
                'There was a problem requesting a trial license for '
                '<b>{package}</b>. <br><br>'
                '<a style="color: #43B02A" href="{url}">'
                'Contact us</a>'.format(
                    url='mailto:sales@continuum.io', package=worker.package
                )
            )
            msg_box = MessageBoxInformation(
                text=question, title='Trial license request'
            )
            msg_box.sig_url_clicked.connect(self.open_url)
            msg_box.exec_()

        self.check_dependencies_before_install(worker, worker.output, error)

    def check_license_requirements(self, worker, output, error):
        """Check if package requires licensing and try to get a trial."""
        package_name = worker.package

        # See if there is a license and if it has expired
        package_with_license = package_name.lower() in PACKAGES_WITH_LICENSE
        license_info = self.api.get_package_license(package_name)
        license_days = self.api.get_days_left(license_info)
        expired = license_days == 0

        if package_with_license and self.logged and expired:
            question = (
                'Do you want to install <b>{package}</b> and <br>'
                'get a trial license valid for <b>{days} days</b>?'
                ''.format(
                    package=package_name, days=60
                )
            )
            msg_box = MessageBoxQuestion(
                text=question, title='Request trial license'
            )
            action_msg = 'Installing package requiring a license...'
            self.update_status(action_msg)
            accept = msg_box.exec_() if package_with_license else False

            if accept:
                new_worker = self.api.client_get_user_licenses()
                new_worker.prefix = worker.prefix
                new_worker.action = worker.action
                new_worker.action_msg = worker.action_msg
                new_worker.sender = worker.sender
                new_worker.package = package_name
                new_worker.pkgs = worker.pkgs
                new_worker.output = output
                new_worker.sig_finished.connect(
                    self._check_license_requirements
                )
            else:
                self.update_status()
                self.home_tab.set_widgets_enabled(True)
        elif package_with_license and not self.logged:
            dlg = AuthenticationDialog(self.api, parent=self)
            dlg.update_links()
            anaconda_register_url = dlg.base_url

            question = (
                'To get a license trial for <b>{package}</b>, '
                'you need to login to <b>Anaconda Cloud</b>.<br><br>'
                'If you do not have an account, you can register at '
                '<a style="color: #43B02A" href="{url}">'
                'Anaconda Cloud</a>'
                ''.format(
                    package=package_name, url=anaconda_register_url
                )
            )
            msg_box = MessageBoxInformation(
                text=question, title='Login/Register with '
                'Anaconda Cloud'
            )
            msg_box.sig_url_clicked.connect(self.open_url)
            msg_box.exec_()
            self.update_status()
            self.home_tab.set_widgets_enabled(True)
        else:
            worker.output = output
            self.check_dependencies_before_install(worker, output, error)

    def run_conda_action(self, action, package_name, version, sender):
        """FIXME:."""
        self.update_busy_status(True)
        self.home_tab.set_widgets_enabled(False)

        if version:
            pkgs = ['{0}=={1}'.format(package_name, version)]
        else:
            pkgs = ['{0}'.format(package_name)]

        if action == C.APPLICATION_INSTALL:
            worker = self.api.conda_dependencies(
                prefix=self.current_prefix,
                channels=self.api.active_normalized_channels,
                pkgs=pkgs
            )
            action_msg = (
                'Installing application <b>{0}</b> on '
                '<b>{1}</b>'
            ).format(package_name, self.current_prefix)
            worker.prefix = self.current_prefix
            worker.action = action
            worker.action_msg = action_msg
            worker.sender = sender
            worker.package = package_name
            worker.pkgs = pkgs
            worker.sig_finished.connect(self.check_license_requirements)
            worker.sig_partial.connect(self._conda_partial_output_ready)
        elif action == C.APPLICATION_UPDATE:
            worker = self.api.conda_install(
                prefix=self.current_prefix,
                channels=self.api.active_normalized_channels,
                pkgs=pkgs
            )
            action_msg = ('Updating application <b>{0}</b> on '
                          '<b>{1}</b>'
                          ).format(package_name, self.current_prefix)
            worker.prefix = self.current_prefix
            worker.action = action
            worker.action_msg = action_msg
            worker.sender = sender
            worker.package = package_name
            worker.pkgs = pkgs
            worker.sig_finished.connect(self.check_license_requirements)
            worker.sig_partial.connect(self._conda_partial_output_ready)
        elif action == C.APPLICATION_REMOVE:
            worker = self.api.conda_remove(
                prefix=self.current_prefix, pkgs=pkgs
            )
            action_msg = (
                'Removing application <b>{0}</b> from '
                '<b>{1}</b>'
            ).format(package_name, self.current_prefix)
            worker.action = action
            worker.action_msg = action_msg
            worker.sender = sender
            worker.sig_finished.connect(self._conda_output_ready)
            worker.sig_partial.connect(self._conda_partial_output_ready)
        self.update_status(action_msg)

    def create_environment(self, name, packages=('python', )):
        """Create environment and install `packages`."""
        action_msg = (
            'Installing application <b>{0}</b> on new '
            'environment <b>{1}</b>'
        ).format(packages[0], name)
        self.update_status(action_msg)
        worker = self.api.conda_create(
            name=name,
            pkgs=packages,
            channels=self.api.active_normalized_channels
        )
        worker.action_msg = action_msg
        worker.action = C.ACTION_CREATE
        worker.env_name = name
        worker.sig_finished.connect(self._conda_output_ready)
        worker.sig_partial.connect(self._conda_partial_output_ready)

    # --- Other
    # -------------------------------------------------------------------------
    def toggle_fullscreen(self):
        """Toggle fullscreen status."""
        if self.isFullScreen():
            self.fullscreen_flag = False

            if self.maximized_flag:
                self.showMaximized()
            else:
                self.showNormal()
        else:
            self.maximized_flag = self.isMaximized()
            self.fullscreen_flag = True
            self.showFullScreen()

    def update_busy_status(self, value):
        """Update the busy status of the application."""
        self.busy = value

    def update_status(
        self,
        action=None,
        message=None,
        progress_value=0,
        progress_max_value=0,
        sender=None
    ):
        """Update status bar."""
        self.home_tab.update_status(
            action=action,
            status=message,
            progress=progress_value,
            max_value=progress_max_value
        )

    # --- Qt methods
    # -------------------------------------------------------------------------
    def closeEvent(self, event):
        """Catch close event."""
        if not self.busy:
            show_apps_dialog = not self.config.get(
                'main', 'hide_running_apps_dialog'
            )
            close_apps = self.config.get('main', 'running_apps_to_close')

            # Check if any of the stored processes is still alive and if not
            # remove from the list of processes
            pids_exist = []
            if self.running_processes:
                for process_data in self.running_processes[:]:
                    pids = process_data['pids']

                    for pid in pids:
                        if psutil.pid_exists(pid):
                            pids_exist.append(pid)
                            proc = psutil.Process(pid)
                            for p in proc.children(recursive=True):
                                pids_exist.append(psutil.pid_exists(p.pid))

                    if not any(pids_exist):
                        self.running_processes.remove(process_data)

            if any(pids_exist) and show_apps_dialog:
                dlg = QuitRunningAppsDialog(
                    parent=self, running_processes=self.running_processes
                )
                reply = dlg.exec_()
                if reply:
                    for process_data in self.running_processes[:]:
                        if process_data['package'] in close_apps:
                            pids = process_data['pids']
                            for pid in pids:
                                if psutil.pid_exists(pid):
                                    proc = psutil.Process(pid)
                                    for p in proc.children(recursive=True):
                                        logger.debug(str((p.name(), p.pid)))
                                        try:
                                            p.kill()
                                        except Exception:
                                            pass
                                    logger.debug(str((proc.name(), proc.pid)))
                                    try:
                                        proc.kill()
                                    except Exception:
                                        pass
                                    self.running_processes.remove(process_data)
                else:
                    event.ignore()
                    return

        if self.busy:
            dlg = QuitBusyDialog(parent=self)
            reply = dlg.exec_()
            if not reply:
                event.ignore()
        else:
            show_dialog = not self.config.get('main', 'hide_quit_dialog')
            if show_dialog:
                if self.tracker:
                    self.tracker.track_page('/quit', pagetitle='Quit dialog')
                dlg = QuitApplicationDialog(parent=self)
                reply = dlg.exec_()

                if not reply:
                    event.ignore()
                    self._track_tab()
        try:
            geo = pickle.dumps(self.saveGeometry())
            self.config.set('main', 'geo', geo)
        except Exception as e:
            logger.error(e)

    def keyPressEvent(self, event):
        """Override Qt method."""
        key = event.key()
        modifiers = event.modifiers()
        if (key == Qt.Key_F5 or key == Qt.Key_R and modifiers &
                Qt.ControlModifier):
            # print('Refreshing stylesheets...')
            self.update_style_sheet()
        elif key == Qt.Key_F11 and not MAC:
            self.toggle_fullscreen()
        elif key == Qt.Key_F and modifiers & Qt.ControlModifier and MAC:
            self.toggle_fullscreen()

        super(MainWindow, self).keyPressEvent(event)


# --- Local testing
# -----------------------------------------------------------------------------
def test():  # pragma: no cover
    """Run local test."""
    from anaconda_navigator.utils.qthelpers import qapplication
    app = qapplication()
    w = MainWindow()
    w.setup()
    w.showMaximized()
    sys.exit(app.exec_())


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