import os
from schrodinger.models import advanced_mappers
from schrodinger.models import mappers
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.tasks import gui
from schrodinger.tasks import jobtasks
from schrodinger.tasks import taskmanager
from schrodinger.tasks import tasks
from schrodinger.ui.qt import basewidgets
from schrodinger.ui.qt import jobwidgets
from schrodinger.ui.qt import utils
from schrodinger.ui.qt.tasks import taskbarwidgets
from schrodinger.ui.qt.tasks.taskbarwidgets import TaskWidgetMixin
DEFAULT_START_LATENCY = 750
IMAGE_PATH = os.path.join(":/schrodinger/ui/qt/icons_dir/spinner/")
ICON_PATH_TEMPLATE = os.path.join(IMAGE_PATH, "{}.png")
SPIN_ICONS = None
ERROR_ICON = None
[docs]def get_spin_icons():
    global SPIN_ICONS
    if SPIN_ICONS is None:
        SPIN_ICONS = [
            QtGui.QIcon(ICON_PATH_TEMPLATE.format(num)) for num in range(0, 9)
        ]
    return SPIN_ICONS 
[docs]def get_error_icon():
    global ERROR_ICON
    ERROR_ICON = QtGui.QIcon(ICON_PATH_TEMPLATE.format('error'))
    return ERROR_ICON 
class _SpinnerMixin:
    running_pics = NotImplemented
    stop_pic = NotImplemented
    error_pic = NotImplemented
    def setImage(self, image):
        raise NotImplementedError
    def __init__(self, *args, **kwargs):
        self.current_num = 0
        self.timer = QtCore.QTimer()
        self.timer.timeout.connect(self._advanceSpinner)
        super().__init__(*args, **kwargs)
    def _advanceSpinner(self):
        pic = self.running_pics[self.current_num]
        self.setImage(pic)
        self.current_num += 1
        if self.current_num == len(self.running_pics):
            self.current_num = 0
    def _startSpinner(self):
        if not self.timer.isActive():
            self.timer.start(250)
    def _stopSpinner(self):
        self.timer.stop()
        task = self.model
        if task is None:
            return
        if task.status is task.FAILED:
            self.setImage(self.error_pic)
        else:
            self.setImage(self.stop_pic)
    def defineMappings(self):
        mappings = super().defineMappings()
        M = self.model_class
        target = mappers.TargetSpec(slot=self.updateSpinnerState)
        mappings += [(target, M.status)]
        return mappings
    def updateSpinnerState(self):
        status = self.model.status
        if status != tasks.Status.RUNNING:
            self._stopSpinner()
        else:
            self._startSpinner()
[docs]class SpinnerLabel(_SpinnerMixin, TaskWidgetMixin, QtWidgets.QLabel):
    clicked = QtCore.pyqtSignal()
[docs]    def __init__(self, *args, **kwargs):
        height = 16
        icons = get_spin_icons()
        self.running_pics = [icon.pixmap(height, height) for icon in icons[1:]]
        self.stop_pic = icons[0].pixmap(height, height)
        self.error_pic = get_error_icon().pixmap(height, height)
        super().__init__(*args, **kwargs) 
[docs]    def mousePressEvent(self, event):
        QtWidgets.QLabel.mousePressEvent(self, event)
        self.clicked.emit() 
[docs]    def setImage(self, image):
        self.setPixmap(image)  
[docs]class ProgressBar(TaskWidgetMixin, QtWidgets.QProgressBar):
    """
    Progress bar for a running task. For AbstractTask, this monitors the
    task.status, task.progress, and task.max_progress and updates accordingly.
    For TaskManager, this monitors taskman.status only.
    Set max_progress to 0 for an indeterminate progress bar. Both progress and
    max_progress may be adjusted while the task is running.
    Determinate progress is only supported for tasks. For a taskmanager, the
    progress dialog will run as long as any task in the taskmanager is running.
    """
[docs]    def __init__(self, *args, **kwargs):
        self._is_running = False
        super().__init__(*args, **kwargs)
        self.setMinimum(0)
        self.setMaximum(1)
        self._stop() 
[docs]    def defineMappings(self):
        M = self.model_class
        status_target = mappers.TargetSpec(slot=self.updateStatus)
        mappings = [(status_target, M.status)]
        if M is tasks.AbstractTask:
            progress_target = mappers.TargetSpec(slot=self.updateProgress)
            mappings.append((progress_target, M.progress))
            mappings.append((progress_target, M.max_progress))
        return mappings 
[docs]    def setModel(self, model):
        if model is None:
            self._stop()
        super().setModel(model) 
[docs]    def updateStatus(self):
        M = self.model_class
        status = self.model.status
        if status == tasks.Status.RUNNING:
            if M is tasks.AbstractTask:
                task = self.model
                self.setMaximum(task.max_progress)
            if not self.isRunning():
                self._start()
        else:
            if self.isRunning():
                self._stop() 
[docs]    def updateProgress(self):
        """
        Updates the numerical progress. Not called for TaskManager.
        """
        task = self.model
        self.setMaximum(task.max_progress)
        self.setValue(task.progress)
        self._setDeterminate(task.max_progress > 0) 
    def _setDeterminate(self, is_determinate):
        self.setTextVisible(is_determinate)
[docs]    def isRunning(self):
        return self._is_running 
    def _start(self):
        M = self.model_class
        if M is taskmanager.TaskManager:
            self.setMaximum(0)
        self._is_running = True
    def _stop(self):
        M = self.model_class
        if (M is taskmanager.TaskManager or self.model is None or
                self.model.max_progress == 0):
            # This is how you stop an indeterminate progress bar
            self.setMaximum(1)
        self._is_running = False 
[docs]class ProgressDialog(advanced_mappers.MultiModelClassMapperMixin,
                     basewidgets.BaseWidget):
    """
    A basic modal progress dialog that shows itself whenever its model task is
    running and hides itself whenever it's not running. The dialog contains a
    ProgressBar instance and a text label, which can be set with setLabelText.
    Calling setTask with a running task will result in the dialog immediately
    being shown.
    """
    model_classes = (tasks.AbstractTask, taskmanager.TaskManager)
    SHOW_AS_WINDOW = True
[docs]    def initSetUp(self):
        super().initSetUp()
        self._is_running = False
        self.progress_bar = ProgressBar()
        self.main_lbl = QtWidgets.QLabel() 
[docs]    def initLayOut(self):
        super().initLayOut()
        self.main_layout.addWidget(self.main_lbl)
        self.main_lbl.setAlignment(Qt.AlignHCenter)
        self.main_layout.addWidget(self.progress_bar)
        self.main_layout.setContentsMargins(9, 9, 9, 9)
        self.main_layout.setSpacing(9) 
[docs]    def setTask(self, task):
        self.setModel(task) 
[docs]    def setModel(self, model):
        if model is None:
            self._stop()
        super().setModel(model) 
[docs]    def setLabelText(self, text):
        self.main_lbl.setText(text) 
[docs]    def defineMappings(self):
        M = self.model_class
        status_target = mappers.TargetSpec(slot=self.updateStatus)
        return [(self.progress_bar, M), (status_target, M.status)] 
[docs]    def updateStatus(self):
        task = self.model
        if task.isRunning():
            self._start()
        else:
            self._stop() 
[docs]    def isRunning(self):
        return self._is_running 
    def _start(self):
        self._is_running = True
        self.run(modal=True)
    def _stop(self):
        self._is_running = False
        self.hide() 
[docs]class KillableProgressDialog(ProgressDialog):
    """
    A `ProgressDialog` with a "Cancel" button that kills the running task.
    """
[docs]    def initSetUp(self):
        super().initSetUp()
        self.kill_task_btn = QtWidgets.QPushButton('Cancel', self)
        self.kill_task_btn.clicked.connect(self._onKillTaskClicked) 
[docs]    def initLayOut(self):
        super().initLayOut()
        self.bottom_middle_layout.addWidget(self.kill_task_btn) 
    def _onKillTaskClicked(self):
        task = self.model
        task.kill() 
[docs]class StatusLabel(TaskWidgetMixin, QtWidgets.QLabel):
[docs]    def defineMappings(self):
        M = self.model_class
        target = mappers.TargetSpec(setter=self.setStatus)
        return [(target, M.status)] 
[docs]    def setStatus(self, value):
        self.setText(value.name)  
[docs]class AbstractTaskBar(mappers.MapperMixin, basewidgets.BaseWidget):
    """
    Base class for all taskbars. To create a fully customized taskbar,
    subclass this class and make sure that startRequested is emitted
    whenever a task should be started.
    """
    model_class = taskmanager.TaskManager
    startRequested = QtCore.pyqtSignal()
[docs]    def setConfigDialog(self, config_dialog):
        """
        Set a config dialog for this taskbar.
        Subclasses should define this if they support a config dialog.
        """
        raise NotImplementedError 
[docs]    def makeInitialModel(self):
        """
        @overrides: mappers.MapperMixin
        """
        return None 
    def _getCurrentTask(self):
        if isinstance(self.model, taskmanager.TaskManager):
            return self.model.nextTask()
        else:
            return self.model
 
[docs]class TaskBar(AbstractTaskBar):
[docs]    def initSetUp(self):
        super().initSetUp()
        self.name_le = taskbarwidgets.NameLineEdit()
        self.task_name_lbl = QtWidgets.QLabel("Job name: ")
        self.start_btn = utils.AcceptsFocusPushButton("Run")
        self.spinner_label = SpinnerLabel()
        self.settings_btn = SettingsButton()
        self.config_dialog = None 
        # We connect signals later so subclasses can override these
        # widgets with their own.
[docs]    def initFinalize(self):
        super().initFinalize()
        self._connectSignals()
        if not isinstance(self.start_btn, utils.ButtonAcceptsFocusMixin):
            msg = (f'Expected `self.start_btn` class'
                   f'{self.start_btn.__class__.__name__} to inherit '
                   'ButtonAcceptsFocusMixin')
            raise TypeError(msg) 
    def _connectSignals(self):
        self.start_btn.clicked.connect(self.startRequested)
[docs]    def setConfigDialog(self, config_dialog):
        config_dialog.startRequested.connect(self.startRequested)
        self.settings_btn.clicked.connect(self.showConfigDialog)
        self.config_dialog = config_dialog 
[docs]    def showConfigDialog(self):
        if self.config_dialog is None:
            raise RuntimeError("No config dialog has been set for this taskbar")
        self.config_dialog.setParent(self)
        self.config_dialog.setModel(self._getCurrentTask().job_config)
        self.config_dialog.run(modal=True) 
[docs]    def initLayOut(self):
        super().initLayOut()
        hlayout = QtWidgets.QHBoxLayout()
        hlayout.addWidget(self.task_name_lbl)
        hlayout.addWidget(self.name_le)
        hlayout.addWidget(self.settings_btn)
        hlayout.addWidget(self.spinner_label)
        hlayout.addWidget(self.start_btn)
        self.main_layout.addLayout(hlayout) 
[docs]    def setModel(self, model):
        super().setModel(model)
        self._updateSettingsBtnVisibility() 
    def _updateSettingsBtnVisibility(self):
        """
        Show the settings button if the task is a jobtask, otherwise hide it.
        """
        task = self._getCurrentTask()
        should_show_settings = jobtasks.is_jobtask(task)
        self.settings_btn.setVisible(should_show_settings)
[docs]    def defineMappings(self):
        M = self.model_class
        mappings = [(self.spinner_label, M),
                    (self.name_le, M),
                    (self.settings_btn, M),
                    (self.start_btn, M)] # yapf: disable
        if M is taskmanager.TaskManager:
            t = mappers.TargetSpec(slot=self._updateSettingsBtnVisibility)
            mappings.append((t, M.TaskClass))
        return mappings  
[docs]class JobTaskBar(TaskBar):
[docs]    def initSetUp(self):
        super().initSetUp()
        self.spinner_label = jobwidgets.JobStatusButton(parent=self)