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

from __future__ import absolute_import, division, print_function

# Standard library imports
from collections import OrderedDict
import os
import tempfile

# Third party imports
from qtpy.QtCore import QPoint, Qt, QTimer, Signal
from qtpy.QtGui import QColor, QIcon
from qtpy.QtWidgets import QHBoxLayout, QMenu, QSizePolicy, QVBoxLayout
import yaml

# Local imports
from anaconda_navigator.api.anaconda_api import AnacondaAPI
from anaconda_navigator.config import CHANNELS_PATH, CONF
from anaconda_navigator.static import images
from anaconda_navigator.utils import launch
from anaconda_navigator.utils.analytics import GATracker
from anaconda_navigator.utils.logs import logger
from anaconda_navigator.utils.qthelpers import update_pointer
from anaconda_navigator.utils.styles import load_style_sheet
from anaconda_navigator.widgets import (ButtonToolNormal,
                                        FrameEnvironmentsList,
                                        FrameEnvironmentsPackages,
                                        FrameTabHeader, QToolButton,
                                        WidgetBase)
from anaconda_navigator.widgets.dialogs import ActionsDialog, MessageBoxError
from anaconda_navigator.widgets.dialogs.environment import (CloneDialog,
                                                            CreateDialog,
                                                            ImportDialog,
                                                            RemoveDialog)
from anaconda_navigator.widgets.dialogs.quit import ClosePackageManagerDialog
from anaconda_navigator.widgets.helperwidgets import LineEditSearch
from anaconda_navigator.widgets.lists.environments import (ListItemEnv,
                                                           ListWidgetEnv)
from anaconda_navigator.widgets.manager.packages import CondaPackagesWidget


class ButtonToggleCollapse(QToolButton):
    """Button to collapse the environment list."""

    def __init__(self, *args, **kwargs):
        """Button to collapse the environment list."""
        super(ButtonToggleCollapse, self).__init__(*args, **kwargs)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setFocusPolicy(Qt.NoFocus)
        self.setCheckable(True)

        self.clicked.connect(self._update_icon)

    def setProperty(self, name, value):
        """Override Qt method."""
        QToolButton.setProperty(self, name, value)
        self.style().unpolish(self)
        self.style().polish(self)
        self.update()

    def _update_icon(self):
        self.setProperty('checked', self.isChecked())


class EnvironmentsTab(WidgetBase):
    """Conda environments tab.

    This tab holds the list of named and application environments in the local
    machine.

    Available options include, `create`, `clone` and `remove` and package
    management.
    """

    BLACKLIST = ['anaconda-navigator']  # Do not show in package manager.

    sig_status_updated = Signal(object, object, object, object)
    sig_error_popped_up = Signal()

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

    def __init__(self, parent=None):
        """Conda environments tab."""
        super(EnvironmentsTab, self).__init__(parent)

        self.api = AnacondaAPI()
        self.last_env_prefix = None
        self.last_env_name = None
        self.previous_environments = None
        self.tracker = GATracker()
        self.metadata = {}
        self.config = CONF

        active_channels = CONF.get('main', 'conda_active_channels')
        if not active_channels:
            channels = self.api.conda_get_condarc_channels()
            CONF.set('main', 'conda_active_channels', channels)

        # Widgets
        self.button_clone = ButtonToolNormal(text="Clone")
        self.button_create = ButtonToolNormal(text="Create")
        self.button_import = ButtonToolNormal(text="Import")
        self.button_remove = ButtonToolNormal(text="Remove")

        self.frame_environments_list = FrameEnvironmentsList(self)
        self.frame_environments_packages = FrameEnvironmentsPackages(self)
        self.frame_header_left = FrameTabHeader()
        self.frame_header_right = FrameTabHeader()

        self.button_toggle_collapse = ButtonToggleCollapse()
        self.list_environments = ListWidgetEnv()
        self.packages_widget = CondaPackagesWidget(
            self,
            setup=False,
            active_channels=active_channels,
            data_directory=CHANNELS_PATH,
        )
        self.menu_list = QMenu()
        self.text_search = LineEditSearch()
        self.timer_environments = QTimer()

        # Widgets setup
        self.frame_environments_list.is_expanded = True
        self.list_environments.setAttribute(Qt.WA_MacShowFocusRect, False)
        self.list_environments.setContextMenuPolicy(Qt.CustomContextMenu)
        self.packages_widget.textbox_search.setAttribute(
            Qt.WA_MacShowFocusRect, False)
        self.packages_widget.textbox_search.set_icon_visibility(False)
        self.text_search.setPlaceholderText("Search Environments")
        self.text_search.setAttribute(Qt.WA_MacShowFocusRect, False)
        self.timer_environments.setInterval(5000)

        self.button_clone.setObjectName("clone")
        self.button_create.setObjectName("create")
        self.button_import.setObjectName("import")
        self.button_remove.setObjectName("remove")

        # Layouts
        layout_header_left = QVBoxLayout()
        layout_header_left.addWidget(self.text_search)
        self.frame_header_left.setLayout(layout_header_left)

        layout_buttons = QHBoxLayout()
        layout_buttons.addWidget(self.button_create)
        layout_buttons.addWidget(self.button_clone)
        layout_buttons.addWidget(self.button_import)
        layout_buttons.addWidget(self.button_remove)

        layout_list_buttons = QVBoxLayout()
        layout_list_buttons.addWidget(self.frame_header_left)
        layout_list_buttons.addWidget(self.list_environments)
        layout_list_buttons.addLayout(layout_buttons)
        self.frame_environments_list.setLayout(layout_list_buttons)

        layout_packages = QHBoxLayout()
        layout_packages.addWidget(self.packages_widget)
        self.frame_environments_packages.setLayout(layout_packages)

        layout_main = QHBoxLayout()
        layout_main.addWidget(self.frame_environments_list, 10)
        layout_main.addWidget(self.button_toggle_collapse, 1)
        layout_main.addWidget(self.frame_environments_packages, 30)

        self.setLayout(layout_main)

        # Signals
        self.button_toggle_collapse.clicked.connect(self.expand_collapse)
        self.button_clone.clicked.connect(self.clone_environment)
        self.button_create.clicked.connect(self.create_environment)
        self.button_import.clicked.connect(self.import_environment)
        self.button_remove.clicked.connect(self.remove_environment)
        self.list_environments.sig_item_selected.connect(
            self.load_environment)
#        self.packages_widget.sig_channels_updated.connect(self.update_channels)
        self.packages_widget.sig_packages_ready.connect(self.refresh)
        self.packages_widget.sig_channels_dialog_requested.connect(
            self.sig_channels_dialog_requested)
        self.text_search.textChanged.connect(self.filter_environments)
        self.timer_environments.timeout.connect(self.refresh_environments)
        self.packages_widget.sig_process_cancelled.connect(
            lambda: self.update_visibility(True))
        self.packages_widget.sig_packages_busy.connect(
            lambda: self.update_visibility(False))

    # --- Helpers
    # -------------------------------------------------------------------------
    def update_visibility(self, enabled=True):
        """Update visibility of buttons."""
        self.button_create.setDisabled(not enabled)
        self.button_remove.setDisabled(not enabled)
        self.button_clone.setDisabled(not enabled)
        self.button_import.setDisabled(not enabled)
        self.list_environments.setDisabled(not enabled)
        update_pointer()

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

        DEPRECATE.
        """
        if style_sheet is None:
            style_sheet = load_style_sheet()

        self.setStyleSheet(style_sheet)
        self.menu_list.setStyleSheet(style_sheet)
        self.list_environments.update_style_sheet(style_sheet)
#        self.list_environments.setFrameStyle(QFrame.NoFrame)
#        self.list_environments.setFrameShape(QFrame.NoFrame)
#        self.packages_widget.table.setFrameStyle(QFrame.NoFrame)
#        self.packages_widget.table.setFrameShape(QFrame.NoFrame)

#        size = QSize(16, 16)

        palette = {
            'icon.action.not_installed': QIcon(
                images.CONDA_MANAGER_NOT_INSTALLED),
            'icon.action.installed': QIcon(
                images.CONDA_MANAGER_INSTALLED),
            'icon.action.remove': QIcon(
                images.CONDA_MANAGER_REMOVE),
            'icon.action.add': QIcon(
                images.CONDA_MANAGER_ADD),
            'icon.action.upgrade': QIcon(
                images.CONDA_MANAGER_UPGRADE),
            'icon.action.downgrade': QIcon(
                images.CONDA_MANAGER_DOWNGRADE),
            'icon.upgrade.arrow': QIcon(
                images.CONDA_MANAGER_UPGRADE_ARROW),
            'spacer': QIcon(images.CONDA_MANAGER_SPACER),
            'background.remove': QColor(0, 0, 0, 0),
            'background.install': QColor(0, 0, 0, 0),
            'background.upgrade': QColor(0, 0, 0, 0),
            'background.downgrade': QColor(0, 0, 0, 0),
            'foreground.not.installed': QColor("#666"),
            'foreground.upgrade': QColor("#00A3E0"), }

        self.packages_widget.update_style_sheet(
            style_sheet=style_sheet,
            extra_dialogs={'cancel_dialog': ClosePackageManagerDialog,
                           'apply_actions_dialog': ActionsDialog,
                           'message_box_error': MessageBoxError,
                           },
            palette=palette, )

    @property
    def environments(self):
        """
        Return an ordered dictionary of all existing named environments.

        The dictionary includes the root environment as the first entry.
        """
        environments = OrderedDict()
        environments_prefix = sorted(self.api.conda_get_envs(log=False))
        environments['root'] = self.api.ROOT_PREFIX

        for prefix in environments_prefix:
            name = os.path.basename(prefix)
            environments[name] = prefix

        return environments

    def refresh_environments(self):
        """Check for newly created environments via CLI."""
        environments = self.environments
        if self.previous_environments is None:
            self.previous_environments = environments.copy()

        if self.previous_environments != environments:
            self.previous_environments = environments.copy()
            self.setup_tab()

    def open_environment_in(self, which):
        """Open selected environment in console terminal."""
        environment_prefix = self.list_environments.currentItem().prefix()
        logger.debug("%s, %s", which, environment_prefix)

        if which == 'terminal':
            launch.console(environment_prefix)
        else:
            launch.py_in_console(environment_prefix, which)

    def set_last_active_prefix(self):
        """Set the last active selected environment in the list."""
        current_item = self.list_environments.currentItem()
        if current_item:
            self.last_env_prefix = getattr(current_item, '_prefix')
        else:
            self.last_env_prefix = self.api.ROOT_PREFIX
        CONF.set('main', 'last_active_prefix', self.last_env_prefix)

    def setup_tab(self, metadata=None, load_environment=True,
                  selected_prefix=None):
        """Setup tab content."""
        self.metadata = metadata if metadata else {}

        envs = self.environments
        self.timer_environments.start()
        self.menu_list.clear()

        menu_item = self.menu_list.addAction('Open Terminal')
        menu_item.triggered.connect(
            lambda: self.open_environment_in('terminal'))

        for word in ['Python', 'IPython', 'Jupyter Notebook']:
            menu_item = self.menu_list.addAction("Open with " + word)
            menu_item.triggered.connect(
                lambda x, w=word: self.open_environment_in(w.lower()))

        def select(value=None, position=None):
            current_item = self.list_environments.currentItem()
            prefix = current_item.prefix()

            if isinstance(position, bool) or position is None:
                width = current_item.button_options.width()
                position = QPoint(width, 0)

            point = QPoint(0, 0)
            parent_position = current_item.button_options.mapToGlobal(point)
            self.menu_list.move(parent_position + position)
            self.menu_list.actions()[2].setEnabled(
                launch.check_prog('ipython', prefix))
            self.menu_list.actions()[3].setEnabled(
                launch.check_prog('notebook', prefix))
            self.menu_list.exec_()

#        self.set_last_active_prefix()
        self.list_environments.clear()

        selected_item_row = 0
        for i, env in enumerate(envs):
            prefix = envs[env]
            item = ListItemEnv(env, prefix=prefix)
            item.button_options.clicked.connect(select)
            if selected_prefix and prefix == selected_prefix:
                selected_item_row = i
            self.list_environments.addItem(item)

        self.list_environments.setCurrentRow(selected_item_row, loading=True)

        if load_environment:
            self.load_environment()
        else:
            return

        # Adjust Tab Order
        self.setTabOrder(self.text_search,
                         self.list_environments._items[0].widget)
        for i in range(len(self.list_environments._items) - 1):
            self.setTabOrder(self.list_environments._items[i].widget,
                             self.list_environments._items[i + 1].widget)
        self.setTabOrder(self.list_environments._items[-1].button_name,
                         self.button_create)
        self.setTabOrder(self.button_create, self.button_clone)
        self.setTabOrder(self.button_clone, self.button_import)
        self.setTabOrder(self.button_import, self.button_remove)
        self.setTabOrder(self.button_remove,
                         self.packages_widget.combobox_filter)
        self.setTabOrder(self.packages_widget.combobox_filter,
                         self.packages_widget.button_channels)
        self.setTabOrder(self.packages_widget.button_channels,
                         self.packages_widget.button_update)
        self.setTabOrder(self.packages_widget.button_update,
                         self.packages_widget.textbox_search)
        self.setTabOrder(self.packages_widget.textbox_search,
                         self.packages_widget.table_first_row)
        self.setTabOrder(self.packages_widget.table_last_row,
                         self.packages_widget.button_apply)
        self.setTabOrder(self.packages_widget.button_apply,
                         self.packages_widget.button_clear)
        self.setTabOrder(self.packages_widget.button_clear,
                         self.packages_widget.button_cancel)

    def filter_environments(self):
        """Filter displayed environments by matching search text."""
        text = self.text_search.text().lower()

        for i in range(self.list_environments.count()):
            item = self.list_environments.item(i)
            item.setHidden(text not in item.text().lower())

            if not item.widget.isVisible():
                item.widget.repaint()

    def load_environment(self, item=None):
        """Load selected environment into the conda package manager."""
        self.update_visibility(False)

        if item is None:
            item = self.list_environments.currentItem()

        if item is None or not isinstance(item, ListItemEnv):
            prefix = self.api.ROOT_PREFIX
            index = 0
        elif item and isinstance(item, ListItemEnv):
            prefix = item.prefix()
        else:
            prefix = self.last_env_prefix if self.last_env_prefix else None

        index = [i for i, it in enumerate(self.list_environments._items)
                 if prefix in it.prefix()]
        index = index[0] if len(index) else 0

        self.list_environments.setCurrentRow(index, loading=True)
        self.packages_widget.set_environment(prefix=prefix)
        self.packages_widget.setup(check_updates=False,
                                   blacklist=self.BLACKLIST,
                                   metadata=self.metadata)
        self.list_environments.setDisabled(True)
        self.update_visibility(False)
        self.set_last_active_prefix()

    def refresh(self):
        """Refresh the enabled/disabled status of buttons."""
        self.update_visibility(True)
        self.list_environments.setDisabled(False)
        item = self.list_environments.currentItem()

        try:
            if item:
                item.set_loading(False)
        except RuntimeError:
            pass
            # C/C++ object not found

        if item:
            is_root = item.text() == 'root'
            self.button_remove.setDisabled(is_root)

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

        for ch in add_channels:
            self.api.conda_config_add('channels', ch)
        for ch in remove_channels:
            self.api.conda_config_remove('channels', ch)
        CONF.set('main', 'conda_active_channels', active_channels)

    # --- Callbacks
    # -------------------------------------------------------------------------
    def _environment_created(self, worker, output, error):
        """Callback for create environment."""
        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(str(error))
            alert = MessageBoxError(text="Environment Create Error",
                                    error=error)
            self.sig_error_popped_up.emit()
            alert.exec_()

        self.update_visibility(False)
        for row, environment in enumerate(self.environments):
            if worker.name == environment:
                break

        self.last_env_prefix = self.api.conda_get_prefix_envname(environment)
        self.setup_tab(load_environment=False)
        self.list_environments.setCurrentRow(row)
        self.load_environment()
        self.refresh()
        self.update_visibility(True)
        self.sig_environments_updated.emit()
        update_pointer()

    def _environment_removed(self, worker, output, error):
        """Callback for remove environment."""
        self.update_visibility(True)
        if error:
            logger.error(str(error))
            if "error" in error:
                alert = MessageBoxError(text="Environment Removal Error",
                                        error=error)
                self.sig_error_popped_up.emit()
                alert.exec_()
        self.sig_environments_updated.emit()

    # --- Public API
    # -------------------------------------------------------------------------
    def update_domains(self, anaconda_api_url, conda_url):
        """
        Update the anaconda api and conda channel urls.

        DEPRECATE.
        """
        self.packages_widget.update_domains(
            anaconda_api_url=anaconda_api_url,
            conda_url=conda_url, )

    def import_environment(self):
        """Create environment based on specfile or environment YAML."""
        self.tracker.track_page('/environments/import',
                                pagetitle='Create new environment by import')

        dlg = ImportDialog(parent=None)

        if dlg.exec_():
            data = None
            path = dlg.path
            name = dlg.text_name.text()
            selected_filter = dlg.selected_file_filter

            try:
                with open(path, 'r') as f:
                    raw = f.read()
            except Exception:
                raw = ''

            # Its using a conda env specification
            if selected_filter == dlg.CONDA_ENV_FILES:
                try:
                    data = yaml.load(raw)
                    data['name'] = name
                except Exception:
                    pass
            # Its using a conda spec file
            elif selected_filter == dlg.CONDA_SPEC_FILES:
                worker = self.packages_widget.create_environment(
                    name=name, packages=path, specfile=True)
            # Its using a pip requirement file
            elif selected_filter == dlg.PIP_REQUIREMENT_FILES:
                ls = raw.split('\n')
                deps = [l for l in ls if l and not l.strip().startswith('#')]
                data = {'name': name,
                        'dependencies': ['python',
                                         {'pip': deps}]}

            # Either conda env or pip requirements
            if data:
                fd, fn = tempfile.mkstemp(suffix='.yaml')
                with open(fn, 'w') as f:
                    yaml.dump(data, f, default_flow_style=False)
                worker = self.packages_widget.import_yaml(name=name, yaml=fn)

            worker.name = name
            worker.sig_finished.connect(self._environment_created)
            self.update_visibility(False)

        self.tracker.track_page('/environments')

    def create_environment(self):
        """
        Create new basic environment with selectable python version.

        Actually makes new env on disc, in directory within the project
        whose name depends on the env name. New project state is saved.
        Should also sync to spec file.
        """
        dlg = CreateDialog(parent=self, environments=self.environments)
        self.tracker.track_page('/environments/create',
                                pagetitle='Create new environment dialog')

        if dlg.exec_():
            name = dlg.text_name.text().strip()
            pyver = dlg.combo_version.currentText()
            py = dlg.check_python.checkState()
            r = dlg.check_r.checkState()

            channels = set(self.api.channels)
            active_channels = set(self.api.active_channels)

            if name:
                pkgs = []
                logger.debug(str('{0}, {1}'.format(name, pyver)))

                self.update_visibility(False)
                update_pointer(Qt.BusyCursor)

                if py:
                    pkgs.append('python={0}'.format(pyver))

                if r:
                    pkgs.append('r-essentials')
                    if 'r' not in channels:
                        r_channel = 'r'
                        active_channels.add(r_channel)
                        worker = self.api.conda_config_add('channels',
                                                           r_channel)
                        self.config.set('main', 'conda_active_channels',
                                        list(active_channels))
                        worker.communicate()

                logger.debug(str((name, pkgs, channels, active_channels)))
                self.update_visibility(False)
                worker = self.packages_widget.create_environment(name=name,
                                                                 packages=pkgs)
                worker.name = name
                worker.sig_finished.connect(self._environment_created)

        self.tracker.track_page('/environments')

    def remove_environment(self):
        """Clone currently selected environment."""
        current_item = self.list_environments.currentItem()
        if current_item is not None:
            name = current_item.text()

            if name == 'root':
                return

            dlg = RemoveDialog(environment=name)
            self.tracker.track_page('/environments/remove',
                                    pagetitle='Remove environment dialog')
            if dlg.exec_():
                logger.debug(str(name))
                self.update_visibility(False)
                update_pointer(Qt.BusyCursor)
                worker = self.packages_widget.remove_environment(name=name)
#                worker = self.api.conda_remove(name=name, all_=True)
                worker.sig_finished.connect(self._environment_removed)
#                self.sig_status_updated.emit('Deleting environment '
#                                             '"{0}"'.format(name),
#                                             0, -1, -1)
            self.tracker.track_page('/environments')

    def clone_environment(self):
        """Clone currently selected environment."""
        current_item = self.list_environments.currentItem()
        if current_item is not None:
            current_name = current_item.text()
            dlg = CloneDialog(parent=self, environments=self.environments)
            self.tracker.track_page('/environments/clone',
                                    pagetitle='Clone environment dialog')

            if dlg.exec_():
                name = dlg.text_name.text().strip()

                if name and current_name:
                    logger.debug(str("{0}, {1}".format(current_name, name)))

                    self.update_visibility(False)
                    update_pointer(Qt.BusyCursor)
                    worker = self.packages_widget.clone_environment(
                        clone=current_name, name=name)
                    worker.name = name
                    worker.sig_finished.connect(self._environment_created)
            self.tracker.track_page('/environments')

    def expand_collapse(self):
        """Expand or collapse the environment selector."""
        if self.frame_environments_list.is_expanded:
            self.frame_environments_list.hide()
            self.frame_environments_list.is_expanded = False
        else:
            self.frame_environments_list.show()
            self.frame_environments_list.is_expanded = True


# --- Local testing
# -----------------------------------------------------------------------------
def test():  # pragma : no cover
    """Run local test."""
    from anaconda_navigator.utils.qthelpers import qapplication
    from anaconda_navigator.utils.styles import load_style_sheet

    app = qapplication()
    widget = EnvironmentsTab()
    widget.setStyleSheet(load_style_sheet())
    widget.setup_tab()
    widget.show()
    app.exec_()


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