# -*- 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, QPixmap
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.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 = LabelHeaderLogo('ANACONDA NAVIGATOR')
        self.button_logged_text = ButtonLabelLogin('')
        self.button_logged_username = ButtonLinkLogin('')
        self.button_update_available = ButtonHeaderUpdate('')
        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],
        )

        # 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.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.setPixmap(QPixmap(images.ANACONDA_NAVIGATOR_LOGO))

        # 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.set_environments
        )
        self.environments_tab.sig_environments_updated.connect(
            self.fix_tab_order
        )
        self.environments_tab.sig_status_changed.connect(
            self.update_busy_status
        )

        # 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 content...')
        self.community_tab.setup_tab()
        self.set_splash('Loading content...')
        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'
        ) or "0.0.1"
        # Temporal mock test
        # mock_versions = [version, '1.1.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)

    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_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)
        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)
        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):
        """Update application to latest available version."""
        version = self.application_update_version
        if version:
            dlg = DialogUpdateApplication(version=version)
            height = self.button_update_available.height()
            point = self.button_update_available.mapToGlobal(
                QPoint(-dlg.WIDTH, height)
            )
            dlg.move(point)
            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:
            self.home_tab.setup_tab(prefix=self.current_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', {})

        # 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

        # Additional check for Rstudio on Windows
        if worker.package.lower() == 'rstudio' and WIN:
            pyver = self.api.conda_package_version(
                prefix=worker.prefix, pkg='python'
            )
            if pyver.startswith('2.7') or pyver.startswith('3.4'):
                conflicts = True
                conflict_message = (
                    'RStudio on windows is only '
                    'available for Python 3.5 and 3.6'
                )
        elif worker.package.lower() == 'anaconda-fusion':
            pyver = self.api.conda_package_version(
                prefix=worker.prefix, pkg='python'
            )
            if pyver.startswith('2.7') or pyver.startswith('3.4'):
                conflicts = True
                conflict_message = (
                    'Anaconda Fusion is only '
                    'available for Python 3.5 and 3.6'
                )

        if conflicts:
            dlg = CreateConflictDialog(
                parent=self,
                environments=self.api.environments,
                package=worker.package,
                extra_message=conflict_message,
            )
            if dlg.exec_():
                env_name = dlg.environment
                env_path = os.path.join(self.api.ROOT_PREFIX, 'envs', env_name)
                action_msg = (
                    'Install application <b>{0}</b> on '
                    '<b>{1}</b>'
                ).format(worker.package, env_path)
                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_msg = action_msg
                    worker.action = C.ACTION_CREATE
                    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.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()
