# -*- coding: utf-8 -*-

# pylint: disable=no-name-in-module

# -----------------------------------------------------------------------------
# Copyright (c) 2016-2017 Anaconda, 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.

This widget does not perform the actual conda actions or command launch, but it
emits signals that should be connected to the parents and final controller on
the main window.
"""

from __future__ import absolute_import, division, print_function

import typing

from qtpy.QtCore import QPoint, QSize, Qt, QTimer, Signal
from qtpy.QtGui import QPixmap
from qtpy.QtWidgets import QHBoxLayout, QListWidget, QMenu, QVBoxLayout

from anaconda_navigator.api import external_apps
from anaconda_navigator.api.anaconda_api import AnacondaAPI
from anaconda_navigator.static.images import ANACONDA_ICON_256_PATH
from anaconda_navigator.utils import constants as C
from anaconda_navigator.utils.constants import AppType
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
from anaconda_navigator.utils import telemetry
from anaconda_navigator.widgets import ButtonLabel, ButtonNormal, FrameBase, LabelBase
from anaconda_navigator.widgets.lists import ListWidgetBase, ListWidgetItemBase
from anaconda_navigator.widgets.spinner import NavigatorSpinner


# --- 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 FrameApplicationSpinner(FrameBase):
    """Label used in CSS styling."""


class ButtonApplicationVersion(ButtonLabel):  # pylint: disable=too-few-public-methods
    """Button used in CSS styling."""


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

    # application_name, command, leave_path_alone, prefix, sender, non_conda, app_type
    sig_launch_action_requested = Signal(object, object, object, bool, object, object, object, object)

    # action, application_name, version, sender, non_conda, app_type
    sig_conda_action_requested = Signal(object, object, object, object, object, object)

    sig_url_clicked = Signal(object)


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

    # application_name, command, extra_arguments, leave_path_alone, prefix, sender, non_conda, app_type
    sig_launch_action_requested = Signal(object, object, object, bool, object, object, object, object)

    # action, application_name, version, sender, non_conda, app_type
    sig_conda_action_requested = Signal(object, object, object, object, object, object)

    sig_url_clicked = Signal(object)

    def __init__(self, *args, **kwargs):
        """Widget that holds the whole list of applications to launch."""
        super().__init__(*args, **kwargs)
        self.setGridSize(ListItemApplication.widget_size())
        self.setWrapping(True)
        self.setViewMode(QListWidget.IconMode)
        self.setLayoutMode(ListWidgetApplication.Batched)
        self.setFocusPolicy(Qt.NoFocus)

    def ordered_widgets(self):
        """Return a list of the ordered widgets."""
        ordered_widgets = []
        for item in self.items():
            ordered_widgets += item.ordered_widgets()
        return ordered_widgets

    def setup_item(self, item):
        """Override base method."""
        item.widget.sig_conda_action_requested.connect(self.sig_conda_action_requested)
        item.widget.sig_launch_action_requested.connect(self.sig_launch_action_requested)
        item.widget.sig_url_clicked.connect(self.sig_url_clicked)


class ListItemApplication(ListWidgetItemBase):  # pylint: disable=too-many-instance-attributes
    """Item with custom widget for the applications list."""

    ICON_HEIGHT = 64  # pylint: disable=invalid-name
    ICON_WIDTH = 192  # pylint: disable=invalid-name

    def __init__(  # pylint: disable=too-many-arguments,too-many-locals,too-many-statements
        self,
        name: str,
        display_name: typing.Optional[str] = None,
        description: typing.Optional[str] = None,
        command: typing.Optional[str] = None,
        extra_arguments: typing.Sequence[typing.Any] = (),
        version: typing.Optional[str] = None,
        versions: typing.Optional[typing.Sequence[str]] = None,
        image_path: typing.Optional[str] = None,
        prefix: typing.Optional[str] = None,
        non_conda: bool = False,
        installed: bool = False,
        summary: typing.Optional[str] = None,
        app_type: AppType = AppType.CONDA,
        rank: int = 0,
    ) -> None:
        """Item with custom widget for the applications list."""
        super().__init__()

        self.api = AnacondaAPI()
        self.prefix = prefix
        self.name: str = name
        self.display_name = display_name if display_name else name
        self.url = ''
        self.expired = False
        self.description = description if description else summary
        self.command = command
        self.extra_arguments = extra_arguments
        self.version = version
        self.versions = versions
        self.image_path = image_path or ANACONDA_ICON_256_PATH
        self.timeout = 2000
        self.non_conda = non_conda
        self.installed = installed
        self.app_type = app_type
        self.rank = rank

        # Widgets
        self.button_install = ButtonApplicationInstall('Install')  # or Try!
        self.button_launch = ButtonApplicationLaunch('Launch')
        self.button_options = ButtonApplicationOptions()
        self.button_options.setDisabled(True)
        self.label_icon = LabelApplicationIcon()
        self.label_name = LabelApplicationName(self.display_name)
        self.label_description = LabelApplicationDescription(self.description)
        self.button_version = ButtonApplicationVersion(to_text_string(self.version or ''))
        self.menu_options = QMenu('Application options')
        self.menu_versions = QMenu('Install specific version')
        self.timer = QTimer()
        self.widget = WidgetApplication()
        self.frame_spinner = FrameApplicationSpinner()
        self.spinner = NavigatorSpinner(self.widget, total_width=16)
        lay = QHBoxLayout()
        lay.addWidget(self.spinner)
        self.frame_spinner.setLayout(lay)

        # Widget setup
        self.button_version.setFocusPolicy(Qt.NoFocus)
        self.button_version.setEnabled(True)
        self.label_description.setAlignment(Qt.AlignCenter)
        self.timer.setInterval(self.timeout)
        self.timer.setSingleShot(True)
        self.label_icon.setScaledContents(True)  # important on High DPI!
        self.label_icon.setAlignment(Qt.AlignCenter)
        if self.image_path == ANACONDA_ICON_256_PATH:
            if not installed:
                self.label_icon.setToolTip('Application icon will show when installed')
            else:
                self.label_icon.setToolTip('Application icon is missing from the package recipe')

        self.label_name.setAlignment(Qt.AlignCenter)
        self.label_name.setWordWrap(True)
        self.label_name.setFixedWidth(200)
        self.label_description.setWordWrap(True)
        self.label_description.setAlignment(Qt.AlignTop | Qt.AlignHCenter)
        self.frame_spinner.setVisible(False)

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

        layout_main = QVBoxLayout()
        layout_main.addWidget(self.button_options, 0, Qt.AlignRight)
        layout_main.addWidget(self.label_icon, 0, Qt.AlignCenter)
        layout_main.addWidget(self.label_name, 0, Qt.AlignCenter)
        layout_main.addLayout(layout_spinner)
        layout_main.addWidget(self.label_description, 0, Qt.AlignCenter)
        layout_main.addWidget(self.button_launch, 0, Qt.AlignCenter)
        layout_main.addWidget(self.button_install, 0, Qt.AlignCenter)

        self.widget.setLayout(layout_main)
        self.setSizeHint(self.widget_size())
        # This might help with visual quirks on the home screen
        self.widget.setMinimumSize(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.set_icon(self.image_path)
        self.update_status()

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

    # --- Helpers
    # -------------------------------------------------------------------------
    def update_style_sheet(self):
        """Update custom CSS stylesheet."""

    def ordered_widgets(self):
        """Return a list of the ordered widgets."""
        return [self.button_install, self.button_launch, self.button_options]

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

    def launch_url(self):
        """Launch signal for url click."""
        self.widget.sig_url_clicked.emit(self.url)

    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 and self.installed:
                action.setChecked(True)
                action.setDisabled(True)

            version_actions.append(action)

        install_action = create_action(self.widget, 'Install application', triggered=self.install_application)
        install_action.setEnabled(not self.installed)

        if self.non_conda:
            install_action.setDisabled(True)

        update_action = create_action(self.widget, 'Update application', triggered=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=self.remove_application)
        remove_action.setEnabled(self.installed)

        actions = [install_action, update_action, remove_action, None, self.menu_versions]

        if self.non_conda:
            # we're not going to support messing
            # with vscode/pycharm via navigator for now
            update_action.setDisabled(True)
            remove_action.setDisabled(True)
            install_action.setDisabled(True)
            versions = []
            self.menu_versions.setDisabled(True)

        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(len(versions) > 1)
        self.menu_options.move(position + offset)
        self.menu_options.exec_()

    def update_status(self):  # pylint: disable=too-many-branches,too-many-statements
        """Update status."""
        # License check
        self.url = ''
        self.expired = False
        button_label = 'Install'

        # Version and version updates
        if self.versions and self.version != self.versions[-1] and self.installed:
            # The property is used with CSS to display updatable packages.
            self.button_version.setProperty('pressed', True)
            self.button_version.setToolTip(f'Version {self.versions[-1]} available')
        else:
            self.button_version.setProperty('pressed', False)

        # For VScode app do not display if new updates are available
        # See: https://github.com/ContinuumIO/navigator/issues/1504
        if self.non_conda:
            self.button_version.setProperty('pressed', False)
            self.button_version.setToolTip('')

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

        self.button_launch.setEnabled(True)

    def update_versions(self, version=None, versions=None):
        """Update button visibility depending on update availability."""
        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()

    def set_loading(self, value):
        """Set loading status."""
        self.button_install.setDisabled(value)
        self.button_options.setDisabled(self.non_conda or value)
        self.button_launch.setDisabled(value)

        if value:
            self.spinner.start()
        else:
            self.spinner.stop()
            if self.version is None and self.versions:
                version = self.versions[-1]
            else:
                version = self.version
            self.button_version.setText(version)
            self.button_launch.setDisabled(self.expired)

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

    def set_icon(self, icon: typing.Union[str, QPixmap]) -> None:
        """Set new icon for the tile."""
        if isinstance(icon, str):
            icon = QPixmap(icon)
        if icon.width() <= 0:
            icon = QPixmap(ANACONDA_ICON_256_PATH)

        icon_width: int = icon.width() * self.ICON_HEIGHT
        icon_height: int = icon.height() * self.ICON_WIDTH
        if icon_width >= icon_height:
            icon_width = self.ICON_WIDTH
            icon_height //= icon.width()
        else:
            icon_width //= icon.height()
            icon_height = self.ICON_HEIGHT

        self.label_icon.setPixmap(icon)
        self.label_icon.setFixedSize(icon_width, icon_height)

    # --- Application actions
    # ------------------------------------------------------------------------
    def install_application(self, value=None, version=None, install=True):  # pylint: disable=unused-argument
        """
        Update the application on the defined prefix environment.

        This is used for both normal install and specific version install.
        """
        action = C.APPLICATION_INSTALL if install else C.APPLICATION_UPDATE
        telemetry.ANALYTICS.instance.event(
            f'{action}-application', {'application': self.name, 'version': version or ''})
        self.widget.sig_conda_action_requested.emit(
            action, self.name, version, C.TAB_HOME, self.non_conda, self.app_type
        )
        if self.app_type == AppType.CONDA:
            self.set_loading(True)

    def remove_application(self):
        """Remove the application from the defined prefix environment."""
        telemetry.ANALYTICS.instance.event(
            'remove-application', {'application': self.name, 'version': self.version or ''})
        self.widget.sig_conda_action_requested.emit(
            C.APPLICATION_REMOVE, self.name, None, C.TAB_HOME, self.non_conda, self.app_type
        )
        self.set_loading(True)

    def update_application(self):
        """Update the application on the defined prefix environment."""
        # version = None is equivalent to saying
        # "most recent version that is compatible with my env"

        self.install_application(version=None, install=False)

    def launch_application(self) -> None:
        """Launch an application (all types)."""
        telemetry.ANALYTICS.instance.event('launch-application', {
            'application': self.name, 'version': self.version or ''})

        command: str
        if self.app_type == AppType.WEB:
            command = external_apps.get_applications(cached=True).web_apps[self.name].url
        else:
            if self.command is None:
                raise ValueError('command not set')

            command = self.command
            update_pointer(Qt.BusyCursor)
            self.button_launch.setDisabled(True)
            self.timer.setInterval(self.timeout)
            self.timer.start()

        self.widget.sig_launch_action_requested.emit(
            self.name,  # package_name
            command,  # command
            self.extra_arguments,
            True,  # leave_path_alone
            self.prefix,  # prefix
            C.TAB_HOME,  # sender
            self.non_conda,  # non_conda
            self.app_type,  # app_type
        )
