# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------------
# Copyright 2016 Continuum Analytics, Inc.
#
# May be copied and distributed freely only as part of an Anaconda or
# Miniconda installation.
# -----------------------------------------------------------------------------
"""Widgets to list applications available to launch from the Home tab."""

from __future__ import absolute_import, division, print_function

# Standard library imports
import os
import sys

# Third party imports
from qtpy.QtCore import QPoint, QSize, Qt, QTimer, Signal
from qtpy.QtGui import QMovie, QPixmap
from qtpy.QtWidgets import QHBoxLayout, QListWidget, QMenu, QVBoxLayout

# Local imports
from anaconda_navigator.api.anaconda_api import AnacondaAPI
from anaconda_navigator.static.images import (ANACONDA_ICON_256_PATH,
                                              SPINNER_WHITE_16_PATH)
from anaconda_navigator.utils.launch import launch
from anaconda_navigator.utils.logs import logger
from anaconda_navigator.utils.py3compat import to_text_string
from anaconda_navigator.utils.qthelpers import (add_actions, create_action,
                                                update_pointer)
from anaconda_navigator.utils.styles import SASS_VARIABLES, load_style_sheet
from anaconda_navigator.widgets import (ButtonLabel, ButtonNormal, FrameBase,
                                        LabelBase)
from anaconda_navigator.widgets.dialogs import MessageBoxError
from anaconda_navigator.widgets.lists import ListWidgetBase, ListWidgetItemBase


# --- Widgets used in CSS styling
# -----------------------------------------------------------------------------
class ButtonApplicationInstall(ButtonNormal):
    """Button used in CSS styling."""


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


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


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


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


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


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


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


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


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


class WidgetApplication(FrameBase):
    """Widget used in CSS styling."""

    # application_name, application_version
    sig_application_updated = Signal(object, object)
    # action, status, progress value, max progress
    sig_status_updated = Signal(object, object, object, object)


# --- Main Widgets
# -----------------------------------------------------------------------------
class ListWidgetApplication(ListWidgetBase):
    """Widget that holds the whole list of applications to launch."""

    # application_name, application_version
    sig_application_updated = Signal(object, object)

    # action, status, progress value, max progress
    sig_status_updated = Signal(object, object, object, object)

    def __init__(self, *args, **kwargs):
        """Widget that holds the whole list of applications to launch."""
        super(ListWidgetApplication, self).__init__(*args, **kwargs)
        self.setViewMode(QListWidget.IconMode)

    def setup_item(self, item):
        """Override base method."""
        item.widget.sig_application_updated.connect(
            self.sig_application_updated)
        item.widget.sig_status_updated.connect(
            self.sig_status_updated)


class ListItemApplication(ListWidgetItemBase):
    """Item with custom widget for the applications list."""

    ICON_SIZE = 64

    def __init__(self,
                 name=None,
                 description=None,
                 command=None,
                 versions=None,
                 image_path=None,
                 prefix=None):
        """Item with custom widget for the applications list."""
        super(ListItemApplication, self).__init__()

        self.api = AnacondaAPI()
        self.prefix = prefix
        self.name = name
        self.description = description
        self.command = command
        self.versions = versions
        self.image_path = image_path if image_path else ANACONDA_ICON_256_PATH

        self.pixmap = QPixmap(self.image_path)
        self.progress_max_value = 0
        self.progress_value = 0
        self.message = ''
        self.style_sheet = None
        self.timeout = 2000

        # Widgets
        self.button_install = ButtonApplicationInstall("Install")
        self.button_launch = ButtonApplicationLaunch("Launch")
        self.button_options = ButtonApplicationOptions()
        self.label_icon = LabelApplicationIcon()
        self.label_name = LabelApplicationName(self.name)
        self.label_description = LabelApplicationDescription(
            self.description)
        self.button_version = ButtonApplicationVersion(
            to_text_string(self.version))
        self.label_spinner = LabelApplicationSpinner()
        self.menu_options = QMenu('Application options')
        self.menu_versions = QMenu('Install specific version')
        self.movie_spinner = QMovie(SPINNER_WHITE_16_PATH)
        self.timer = QTimer()
        self.widget = WidgetApplication()

        # Widget setup
        self.button_version.setFocusPolicy(Qt.NoFocus)
        self.button_version.setEnabled(True)
        self.label_description.setAlignment(Qt.AlignCenter)
        self.movie_spinner.start()
        self.timer.setInterval(self.timeout)
        self.timer.setSingleShot(True)
#        self.label_icon.setToolTip(description)
        self.label_icon.setPixmap(self.pixmap.scaled(self.ICON_SIZE,
                                                     self.ICON_SIZE,
                                                     Qt.KeepAspectRatio,
                                                     Qt.SmoothTransformation))
        self.label_icon.setAlignment(Qt.AlignCenter)
        self.label_name.setAlignment(Qt.AlignCenter)
        self.label_name.setWordWrap(True)
        self.label_description.setWordWrap(True)
        self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
        self.label_spinner.setVisible(False)
#        self.label_spinner.setMinimumWidth(16)
#        self.label_spinner.setMinimumHeight(16)

        # Layouts
        layout_spinner = QHBoxLayout()
        layout_spinner.addWidget(self.button_version, 0, Qt.AlignCenter)
        layout_spinner.addWidget(self.label_spinner, 0, Qt.AlignCenter)

        layout = QVBoxLayout()
        layout.addWidget(self.button_options, 0, Qt.AlignRight)
        layout.addWidget(self.label_icon, 0, Qt.AlignCenter)
        layout.addWidget(self.label_name, 0, Qt.AlignCenter)
        layout.addLayout(layout_spinner)
#        layout.addWidget(self.button_version, 0, Qt.AlignCenter)
#        layout.addWidget(self.label_spinner, 0, Qt.AlignCenter)
        layout.addWidget(self.label_description, 0, Qt.AlignCenter)

#        hlayout = QHBoxLayout()
#        hlayout.addWidget(self.label_update)
#        hlayout.addWidget(self.label_version)
#        layout.addLayout(hlayout)
#        layout.addWidget(self.label_version, 0, Qt.AlignCenter)
        layout.addWidget(self.button_launch, 0, Qt.AlignCenter)
        layout.addWidget(self.button_install, 0, Qt.AlignCenter)

        self.widget.setLayout(layout)
        self.widget.setStyleSheet(load_style_sheet())
        self.setSizeHint(self.widget_size())

        # Signals
        self.button_install.clicked.connect(self.install_application)
        self.button_launch.clicked.connect(self.launch_application)
        self.button_options.clicked.connect(self.actions_menu_requested)
        self.timer.timeout.connect(self._application_launched)

        # Setup
        self.update_status()

    @property
    def version(self):
        """Return the current installed version or the highest version."""
        version = None
        if self.prefix:
            version = self.api.conda_package_version(prefix=self.prefix,
                                                     pkg=self.name,
                                                     build=False)
        if not version:
            version = self.versions[-1]

        return version

    # --- Callbacks
    # -------------------------------------------------------------------------
    def _application_launched(self):
        self.button_launch.setDisabled(False)
        update_pointer()

    def _application_installed(self, worker, output, error):
        self.handle_action_finished(worker, output, error)

    def _application_updated(self, worker, output, error):
        self.handle_action_finished(worker, output, error)

    def _application_removed(self, worker, output, error):
        self.handle_action_finished(worker, output, error)

    # --- Helpers
    # -------------------------------------------------------------------------
    def _partial_output_ready(self, worker, output, error):
        """Callback."""
        if isinstance(output, dict):
            self.progress_value = output.get('progress',
                                             self.progress_value)
            self.progress_max_value = output.get('maxval',
                                                 self.progress_max_value)
            name = output.get('name')
            fetch = output.get('fetch')

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

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

        logger.debug(self.message)
        self.widget.sig_status_updated.emit(worker.action, self.message,
                                            self.progress_value,
                                            self.progress_max_value)

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

        self.menu_options.setStyleSheet(self.style_sheet)
        self.menu_versions.setStyleSheet(self.style_sheet)

    def actions_menu_requested(self):
        """Create and display menu for the currently selected application."""
        self.menu_options.clear()
        self.menu_versions.clear()

        # Add versions menu
        versions = self.versions if self.versions else []
        version_actions = []
        for version in reversed(versions):
            action = create_action(
                self.widget,
                version,
                triggered=lambda value, version=version:
                    self.install_application(version=version))

            action.setCheckable(True)
            if self.version == version:
                action.setChecked(True)
                action.setDisabled(True)

            version_actions.append(action)

        update_action = create_action(
            self.widget,
            'Update application',
            triggered=lambda: self.update_application())

        if versions and versions[-1] == self.version:
            update_action.setDisabled(True)
        else:
            update_action.setDisabled(False)

        remove_action = create_action(
            self.widget,
            'Remove application',
            triggered=lambda: self.remove_application())
        remove_action.setEnabled(self.installed)

        actions = [update_action, remove_action, None, self.menu_versions]
        add_actions(self.menu_options, actions)
        add_actions(self.menu_versions, version_actions)
        offset = QPoint(self.button_options.width(), 0)
        position = self.button_options.mapToGlobal(QPoint(0, 0))
        self.menu_versions.setEnabled(bool(versions))
        self.menu_options.move(position + offset)
        self.menu_options.exec_()

    @property
    def installed(self):
        """Return the installed status of the package."""
        version = None
        if self.prefix:
            version = self.api.conda_package_version(prefix=self.prefix,
                                                     pkg=self.name,
                                                     build=False)
        return bool(version)

    def update_status(self):
        """Update status."""
        if (self.versions and self.version != self.versions[-1] and
                self.installed):
            self.button_version.setProperty('pressed', True)
            self.button_version.setToolTip(
                'Version {0} available'.format(self.versions[-1]))
        else:
            self.button_version.setProperty('pressed', False)

        self.button_install.setVisible(not self.installed)
        self.button_launch.setVisible(self.installed)

    def set_loading(self, value):
        """Set loading status."""
        self.progress_max_value = 0
        self.progress_value = 0
        self.message = ''
        self.button_launch.setDisabled(value)
        self.button_install.setDisabled(value)
        self.button_options.setDisabled(value)

        if value:
            self.label_spinner.setMovie(self.movie_spinner)
        else:
            self.label_spinner.setMovie(None)
            if self.version is None and self.versions is not None:
                version = self.versions[-1]
            else:
                version = self.version
            self.button_version.setText(version)

        self.label_spinner.setVisible(value)
        self.button_version.setVisible(not value)

    def handle_action_finished(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)
        success = bool(output.get('error', success))

        if (error or not success) and "Extra data: line " not in error:
            # Error might be harmless if no decoding was possible...
            MessageBoxError(text="Application action error",
                            error=to_text_string(error)).exec_()
            logger.error(error)
        self.widget.sig_application_updated.emit(self.name, self.version)

        self.update_status()
        self.set_loading(False)

    def update_versions(self, version=None, versions=None):
        """Update button visibility depending on update availability."""
#        update = versions[-1] != version
        logger.debug(str((self.name, self.dev_tool, self.installed)))

        if self.installed and version:
            self.button_options.setVisible(True)
            self.button_version.setText(version)
            self.button_version.setVisible(True)
        elif not self.installed and versions:
            self.button_install.setEnabled(True)
            self.button_version.setText(versions[-1])
            self.button_version.setVisible(True)

        self.versions = versions
        self.version = version
        self.update_status()

    # --- Public API
    # ------------------------------------------------------------------------
    def install_application(self, value=None, version=None, install=True):
        """
        Update the application on the defined prefix environment.

        This is used for both normal install and specific version install.
        """
        if not version:
            version = self.versions[-1]

        pkg = '{0}={1}'.format(self.name, version)
        pkgs = [pkg]
        logger.debug(str((pkg)))

        # Check if environment exists and then create or install
#        is_installed = self.api.conda_package_version(prefix=self.prefix,
#                                                      pkg=self.name)
#        pkgs = [pkg] + self.BASIC_PACKAGES
#        if is_installed:
#            worker = self.api.conda_install(prefix=self.prefix, pkgs=pkgs)
#        else:
#            worker = self.api.conda_create(prefix=self.prefix, pkgs=pkgs)

        action = 'Installing' if install else 'Updating'
        worker = self.api.conda_install(prefix=self.prefix, pkgs=pkgs)
        worker.action = '{0} application <b>{1}</b>'.format(action, self.name)
        worker.sig_finished.connect(self._application_installed)
        worker.sig_partial.connect(self._partial_output_ready)
        self.set_loading(True)
        self.widget.sig_status_updated.emit(worker.action, '', 0, 0)

    def launch_application(self):
        """Launch application installed in prefix environment."""
        if self.command is not None:
            if self.command.startswith('open'):
                command = self.command.replace("${PREFIX}", self.prefix)
            elif self.prefix:
                command = os.sep.join([self.prefix, 'bin', self.command])
            else:
                command = self.command

            self.button_launch.setDisabled(True)
            self.timer.setInterval(self.timeout)
            self.timer.start()
            update_pointer(Qt.BusyCursor)
#            launch(self.path, command)
            launch(self.prefix, command)

    def remove_application(self):
        """Remove the application from the defined prefix environment."""
        pkg = self.name
        pkgs = [pkg]
        logger.debug(str((self.name)))
        worker = self.api.conda_remove(prefix=self.prefix, pkgs=pkgs)
        worker.sig_finished.connect(self._application_removed)
        worker.sig_partial.connect(self._partial_output_ready)
        worker.action = 'Removing application <b>{0}</b>'.format(self.name)
        self.set_loading(True)
        self.widget.sig_status_updated.emit(worker.action, '', 0, 0)

    def update_application(self):
        """Update the application on the defined prefix environment."""
        logger.debug(str((self.name, self.installed)))
        self.install_application(version=self.versions[-1], install=False)

    @staticmethod
    def widget_size():
        """Return the size defined in the SASS file."""
        return QSize(SASS_VARIABLES.WIDGET_APPLICATION_TOTAL_WIDTH,
                     SASS_VARIABLES.WIDGET_APPLICATION_TOTAL_HEIGHT)


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

    app = qapplication(test_time=5)
    widget = ListWidgetApplication()
    for i in range(10):
        item = ListItemApplication(
            name="Package {0}".format(i),
            description="Scientific PYthon Development EnviRonment",
            versions=[str(i), str(i + 1)],
            image_path=ANACONDA_ICON_256_PATH,
            prefix=None)
        widget.addItem(item)

    widget.update_style_sheet()
    widget.show()
    sys.exit(app.exec_())


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