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

from __future__ import (absolute_import, division, print_function,
                        with_statement)

# Standard library imports
import os
import sys

# Third party imports
from qtpy.QtCore import QEvent, QPoint, QRegExp, QSize, Qt, Signal
from qtpy.QtGui import (QBrush, QColor, QFont, QKeySequence, QPalette,
                        QRegExpValidator)
from qtpy.QtWidgets import (QAbstractItemView, QAction, QApplication, QFrame,
                            QHBoxLayout, QListWidget, QListWidgetItem, QMenu,
                            QSizePolicy, QToolTip, QVBoxLayout)

# Local imports
from anaconda_navigator.api.anaconda_api import AnacondaAPI
from anaconda_navigator.utils.styles import SASS_VARIABLES, load_style_sheet
from anaconda_navigator.widgets import (ButtonDanger, ButtonNormal,
                                        ButtonPrimary, CheckBoxBase, FrameBase,
                                        LabelBase, LineEditBase,
                                        SpacerVertical)
from anaconda_navigator.widgets.dialogs import DialogBase


# --- Widgets used in CSS styling
# -----------------------------------------------------------------------------
class FrameChannels(FrameBase):
    """Frame used in CSS styling."""


class ButtonRemoveChannel(ButtonDanger):
    """Button that emits signal on focus."""

    sig_focused = Signal()

    def focusInEvent(self, event):
        """Override Qt method."""
        super(ButtonRemoveChannel, self).focusInEvent(event)
        self.sig_focused.emit()


class CheckBoxChannel(CheckBoxBase):
    """Checkbox that emits signal on focus."""

    sig_focused = Signal()
    sig_unfocused = Signal()

    def focusInEvent(self, event):
        """Override Qt method."""
        super(CheckBoxChannel, self).focusInEvent(event)
        self.sig_focused.emit()

    def focusOutEvent(self, event):
        """Override Qt method."""
        super(CheckBoxChannel, self).focusOutEvent(event)
        self.sig_unfocused.emit()


# --- Heper widgets
# -----------------------------------------------------------------------------
class WorkerMock:
    """Worker mock to handle `defaults` channel special case."""

    def __init__(self, item=None, url=None, repodata_url=None):
        """Worker mock to handle `defaults` channel special case."""
        self.item = item
        self.url = url
        self.repodata_url = url


class LineEditChannel(LineEditBase):
    """
    Custom line edit that uses different validators for text and url.

    More info:
    http://conda.pydata.org/docs/config.html#channel-locations-channels

    Valid entries:
    - defaults  <- Special case
    - <some-channel-name>
    - https://conda.anaconda.org/<channel>/<package>
    - https://conda.anaconda.org/t/<token>/<package>
    - http://<some.custom.url>/<channel>
    - https://<some.custom.url>/<channel>
    - file:///<some-local-directory>
    """

    VALID_RE = QRegExp('^[A-Za-z][A-Za-z0-9_-]+$|'
                       '^https?://.*|'
                       '^file:///.*'
                       )

    sig_return_pressed = Signal()
    sig_escape_pressed = Signal()

    def __init__(self, *args, **kwargs):
        """Custom line edit that uses different validators for text and url."""
        super(LineEditChannel, self).__init__(*args, **kwargs)
        self._validator = QRegExpValidator(self.VALID_RE)
        self.menu = QMenu(parent=self)

        self.setValidator(self._validator)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

    def event(self, event):
        """Override Qt method."""
        if (event.type() == QEvent.MouseButtonPress and
                event.buttons() & Qt.RightButton and not self.isEnabled()):
            self.show_menu(event.pos())
            return True
        return super(LineEditChannel, self).event(event)

    def keyPressEvent(self, event):
        """Override Qt method."""
        key = event.key()

        # Display a copy menu in case the widget is disabled.
        if event.matches(QKeySequence.Paste):
            clipboard = QApplication.clipboard()
            text = clipboard.text()
            if self.VALID_RE.exactMatch(text):
                self.setText(text)
                return
        else:
            if key in [Qt.Key_Return, Qt.Key_Enter]:
                self.sig_return_pressed.emit()
            elif key in [Qt.Key_Escape]:
                self.sig_escape_pressed.emit()
        super(LineEditChannel, self).keyPressEvent(event)

    def show_menu(self, pos):
        """Show copy menu for channel item."""
        self.menu.clear()
        clipboard = QApplication.clipboard()
        copy = QAction("&Copy", self.menu)
        copy.triggered.connect(lambda: clipboard.setText(self.text()))
        self.menu.addAction(copy)
        self.menu.exec_(self.mapToGlobal(pos))


# --- Main list widgets
# -----------------------------------------------------------------------------
class ListWidgetItemChannel(QListWidgetItem):
    """Conda channels list widget item used in CSS styling."""

    def __init__(self, channel):
        """Conda channels list widget item used in CSS styling."""
        super(ListWidgetItemChannel, self).__init__()
        self.channel = channel

        # Widgets
        self.widget = FrameChannels()
        self.checkbox_channel = CheckBoxChannel()
        self.text_channel = LineEditChannel(channel)
        self.label_info = LabelBase()
        self.button_remove = ButtonRemoveChannel()

        # Layouts
        layout_frame = QHBoxLayout()
        layout_frame.addWidget(self.checkbox_channel)
        layout_frame.addWidget(self.text_channel)
        layout_frame.addStretch()
        layout_frame.addWidget(self.label_info)
        layout_frame.addWidget(self.button_remove)
        self.widget.setLayout(layout_frame)
        self.setSizeHint(self.widget_size())

    def set_checked(self, value):
        """Set the checkbox state to value."""
        if value:
            self.checkbox_channel.setCheckState(Qt.Checked)
        else:
            self.checkbox_channel.setCheckState(Qt.Unchecked)

    def set_editable(self, value):
        """Set the editable status of the channel textbox."""
        self.text_channel.setEnabled(value)
        self.text_channel.setFocus()

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


class ListWidgetChannels(QListWidget):
    """Conda channels list widget."""

    sig_channel_added = Signal(object)
    sig_channel_removed = Signal(object)
    sig_channel_status = Signal(bool)
    sig_channel_checked = Signal()

    def __init__(self,
                 parent=None,
                 api=None,
                 main_url='https://anaconda.org',
                 api_url='https://api.anaconda.org',
                 conda_url='https://conda.anaconda.org'):
        """Conda channels list widget."""
        super(ListWidgetChannels, self).__init__(parent)

        # Variables
        self._items = []
        self.api = api
        self.api_url = api_url
        self.main_url = main_url
        self.conda_url = conda_url
        self.style_sheet = None
        self.repeat_error = False

        # Widget setup
        self.setObjectName('ListWidgetChannels')
        self.setResizeMode(QListWidget.Adjust)
        self.setMovement(QListWidget.Static)
        self.setFrameStyle(QListWidget.Plain)
        self.setSelectionMode(QAbstractItemView.NoSelection)
        self.setViewMode(QListWidget.ListMode)
        self.setFocusPolicy(Qt.NoFocus)
        self.setUniformItemSizes(True)

    def addItem(self, item):
        """
        Override Qt method.

        Add a content item to the list.
        """
        super(ListWidgetChannels, self).addItem(item)
        self.setItemWidget(item, item.widget)

        item.text_channel.sig_return_pressed.connect(
            lambda: self.validate_channel(item))
        item.text_channel.sig_escape_pressed.connect(
            lambda: self.escape_pressed(item))
        item.button_remove.clicked.connect(lambda: self.remove_channel(item))
        item.checkbox_channel.stateChanged.connect(
            lambda v=None: self.sig_channel_checked.emit())
        item.button_remove.sig_focused.connect(
            lambda v=None: self.fix_focus(item))
        item.checkbox_channel.sig_focused.connect(
            lambda v=None: self.activate_name(item, True))
        item.checkbox_channel.sig_unfocused.connect(
            lambda v=None: self.activate_name(item, False))

        if not item.channel.strip().lower():
            self.is_editing = True
            self.scrollToBottom()
            item.set_editable(True)
            item.text_channel.textEdited.connect(self.check_repeat)
            item.button_remove.setVisible(False)
            item.checkbox_channel.setDisabled(True)
        else:
            self.is_editing = False
            self._items.append(item)

        item.text_channel.setToolTip(item.channel)
        item.label_info.setDisabled(True)

    def escape_pressed(self, item):
        """Handle cancelation of ongoing new channel editing."""
        self.takeItem(self.count() - 1)

    @staticmethod
    def activate_name(item, value):
        """Change line edit property to activate underline effect."""
        item.text_channel.setProperty('activated', value)

    def fix_focus(self, item):
        """Set the current row based on focus of child widgets."""
        row = self._items.index(item)
        self.setCurrentRow(row)

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

        for item in self._items:
            try:
                item_widget = self.itemWidget(item)
                item_widget.setStyleSheet(self.style_sheet)
                item.text_channel.menu.setStyleSheet(self.style_sheet)
                item.setSizeHint(item.widget_size())
            except Exception:
                pass
                # This error is just in case the C++ object has been
                # deleted and it is not crucial to log.
        self.update()
        self.repaint()

    def remove_channel(self, item):
        """Remove the selected channel."""
        if item in self._items:
            index = self._items.index(item)
            self.takeItem(index)
            self._items.remove(item)
            self.sig_channel_removed.emit(item.channel)

    def _channel_url_validated(self, worker, valid, error):
        """Callback for channel url validation."""
        self.setDisabled(False)
        item = worker.item
        if valid:
            item.set_editable(False)
            item.set_checked(True)
            item.channel = worker.channel
            item.text_channel.setText(worker.channel)
            item.text_channel.setToolTip(worker.channel)
            item.button_remove.setVisible(True)
            item.checkbox_channel.setDisabled(False)
            self.show_tool_tip(item)
            self._items.append(item)
            self.sig_channel_added.emit(worker.channel)
            self.sig_channel_status.emit(True)
            self.update_style_sheet(self.style_sheet)
        else:
            text = ('<b>{0}</b> is not a valid conda '
                    'channel.'.format(worker.url))
            item.set_editable(True)
            self.show_tool_tip(item, text=text)
            self.sig_channel_status.emit(False)

    def show_tool_tip(self, item, text=''):
        """Set the tooltip in case of errors."""
        widget = item.label_info
        widget.setToolTip(text)
        widget.setEnabled(bool(text))

        brush_tool_tip_base = QBrush(QColor('#253746'))
        brush_tool_tip_text = QBrush(Qt.red)
        palette = QToolTip.palette()
        palette.setBrush(QPalette.ToolTipBase, brush_tool_tip_base)
        palette.setBrush(QPalette.ToolTipText, brush_tool_tip_text)
        palette.setColor(QPalette.ToolTipBase, QColor('#253746'))
        palette.setColor(QPalette.ToolTipText, Qt.white)
        font = QFont("Ubuntu", 10, QFont.Normal)

        QToolTip.setFont(font)
        QToolTip.setPalette(palette)
        QToolTip.showText(widget.mapToGlobal(QPoint(0, 0)), text)
        if text:
            self.sig_channel_status.emit(False)

    def check_repeat(self, text):
        """Check that given channel (text) is not already in list."""
        channels_lower = [channel.lower() for channel in self.channels]
        self.repeat_error = text.lower() in channels_lower

    def validate_channel(self, item):
        """Validate entered channel with current api url."""
        channel = item.text_channel.text().strip()
        item.channel = channel

        if self.repeat_error:
            # Channel is already in list
            self.show_tool_tip(item, text='Channel <b>{0}</b> is in list '
                               'already.'.format(channel))
        elif channel:
            # Try to validate
            if channel == 'defaults':
                url = []  # no need to check this special channel
                worker = WorkerMock(item=item, url=channel,
                                    repodata_url=channel)
                worker.channel = channel
                return self._channel_url_validated(worker, True, None)
            elif (channel.startswith('https://') or
                  channel.startswith('http://')):
                if channel.startswith(self.main_url):
                    # User entered https://anaconda.org/<channel> or
                    # User entered https://anaconda.org/t/<token>/<package>
                    url = channel.replace(self.main_url, self.conda_url)
                    channel = url
                else:
                    url = channel
            elif channel.startswith('file:///'):
                # Its a local folder, only check that the folder exists
                url = []  # no need to check this special channel
                worker = WorkerMock(item=item, url=channel,
                                    repodata_url=channel)
                worker.channel = channel
                if os.name == 'nt':
                    path = channel.replace('file:///', '')
                else:
                    path = channel.replace('file://', '')
                valid = os.path.isdir(path)
                return self._channel_url_validated(worker, valid, None)
            else:
                url = "{0}/{1}".format(self.conda_url, channel)

            url = url[:-1] if url[-1] == '/' else url
            plat = self.api.conda_platform()
            repodata_url = "{0}/{1}/{2}".format(url, plat, 'repodata.json')

            worker = self.api.download_is_valid_url(repodata_url)
            worker.sig_finished.connect(self._channel_url_validated)
            worker.item = item
            worker.url = url
            worker.channel = channel
            worker.repodata_url = repodata_url
            self.setDisabled(True)
            self.show_tool_tip(item)
        else:
            # Inform user channel is empty!
            self.show_tool_tip(item, text='Channel cannot be empty.')

    @property
    def channels(self):
        """Return the channels."""
        channels = []
        for item in self._items:
            channels.append(item.channel)
        return channels

    @property
    def active_channels(self):
        """Return the active channels."""
        channels = []
        for item in self._items:
            if item.checkbox_channel.isChecked():
                channels.append(item.channel)
        return channels


class DialogChannels(DialogBase):
    """Dialog to add delete and select active conda pacakge channels ."""

    sig_channels_updated = Signal(object, object)  # channels, active_channels

    def __init__(self,
                 parent=None,
                 channels=None,
                 active_channels=None,
                 ):
        """Dialog to add delete and select active conda pacakge channels ."""
        # Check arguments: active channels, must be within channels, otherwise
        # just remove that channel from active channels
        for channel in active_channels:
            if channel.lower() not in [ch.lower() for ch in channels]:
                if channel in active_channels:
                    active_channels.remove(channel)
                elif channel.lower() in active_channels:
                    active_channels.remove(channel.lower())

        super(DialogChannels, self).__init__(parent)
        self._parent = parent
        self._channels = channels
        self._active_channels = active_channels
        self._edited_channel_text = ''
        self._temp_channels = channels
        self.api = AnacondaAPI()
        self.style_sheet = None
        api_info = self.api.download_get_api_info()
        self._conda_url = api_info.get('conda_url',
                                       'https://conda.anaconda.org')

        # Widgets
        # FIXME:
        self.list = ListWidgetChannels(parent=self, api=self.api)
        self.label_info = LabelBase('Manage channels you want '
                                    'Navigator to include.')
        self.button_add = ButtonNormal('Add...')
        self.button_ok = ButtonPrimary('Update channels')

        # Widget setup
        self.frame_title_bar.setVisible(False)
        self.list.setFrameStyle(QFrame.NoFrame)
        self.list.setFrameShape(QFrame.NoFrame)
        self.setWindowFlags(Qt.Popup | Qt.FramelessWindowHint)
        self.setWindowOpacity(0.96)
        self.setModal(False)

        # Layout
        layout_button = QHBoxLayout()
        layout_button.addWidget(self.label_info)
        layout_button.addStretch()
        layout_button.addWidget(self.button_add)

        layout_ok = QHBoxLayout()
        layout_ok.addStretch()
        layout_ok.addWidget(self.button_ok)

        layout = QVBoxLayout()
        layout.addLayout(layout_button)
        layout.addWidget(SpacerVertical())
        layout.addWidget(self.list)
        layout.addWidget(SpacerVertical())
        layout.addWidget(SpacerVertical())
        layout.addLayout(layout_ok)
        self.setLayout(layout)

        # Signals
        self.button_add.clicked.connect(self.add_channel)
        self.button_ok.clicked.connect(self.update_channels)
        self.list.sig_channel_status.connect(self.refresh)
        self.list.sig_channel_added.connect(
            lambda v=None: self.set_tab_order())
        self.list.sig_channel_added.connect(
            lambda v=None: self.button_ok.setFocus())
        self.list.sig_channel_removed.connect(
            lambda v=None: self.set_tab_order())
        self.list.sig_channel_removed.connect(
            lambda v=None: self.button_ok.setFocus())
        self.list.sig_channel_checked.connect(self.refresh)

        self.setup()

    def keyPressEvent(self, event):
        """Override Qt method."""
        key = event.key()
        if key in [Qt.Key_Escape]:
            if self.list.is_editing:
                self.refresh()
                self.list.is_editing = False
            else:
                self.reject()

    # --- Public API
    # -------------------------------------------------------------------------
    def update_style_sheet(self, style_sheet=None):
        """Update custom css style sheets."""
        self.setMinimumWidth(SASS_VARIABLES.WIDGET_CHANNEL_DIALOG_WIDTH)
        self.setStyleSheet(style_sheet)
        try:
            self.list.update_style_sheet(style_sheet)
        except Exception:
            pass

    def setup(self):
        """Setup the channels widget."""
        lowered_active_channels = [ch.lower() for ch in self._active_channels]
        for channel in self._channels:
            item = ListWidgetItemChannel(channel)
            item.set_checked(channel.lower() in lowered_active_channels)
            item.set_editable(False)
            self.list.addItem(item)
        self.set_tab_order()
        self.button_add.setFocus()
        self.button_ok.setDefault(True)

    def set_tab_order(self):
        """Fix the tab ordering in the list."""
        for item in self.list._items:
            checkbox = item.checkbox_channel
            remove = item.button_remove
            self.setTabOrder(checkbox, remove)
        if self.list._items:
            self.setTabOrder(self.button_add,
                             self.list._items[0].checkbox_channel)
            self.setTabOrder(self.list._items[-1].button_remove,
                             self.button_ok)
        self.refresh()

    def add_channel(self):
        """Add new conda channel."""
        item = ListWidgetItemChannel('')
        self.list.addItem(item)
        self.refresh(False)

    def update_channels(self):
        """Update channels list and status."""
        temp_active_channels = self.list.active_channels
        channels = self.list.channels

        if (sorted(channels) != sorted(self._channels) or
                sorted(temp_active_channels) != sorted(self._active_channels)):
            self.sig_channels_updated.emit(tuple(channels),
                                           tuple(temp_active_channels))
            self.accept()
        else:
            self.reject()

    def refresh(self, channel_status=True):
        """Update enable/disable status based on item count."""
        self.button_add.setEnabled(channel_status and
                                   bool(self.list.channels))
        self.button_ok.setEnabled(channel_status and
                                  bool(self.list.active_channels))

        if len(self.list.channels) == 0:
            self.button_add.setEnabled(True)


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

    app = qapplication()
    widget = DialogChannels(
        None,
        channels=['https://conda.anaconda.org/malev', 'GOANPECA'],
        active_channels=['https://conda.anaconda.org/malev', 'boob',
                         'goanpEca'])
    widget.show()
    widget.update_style_sheet(load_style_sheet())
    sys.exit(app.exec_())


if __name__ == '__main__':  # pragma: nocover
    test()
