# webconf.py - Widget to show/edit hgweb config
#
# Copyright 2010 Yuya Nishihara <yuya@tcha.org>
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2, incorporated herein by reference.

import os
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from tortoisehg.util import hglib, wconfig
from tortoisehg.hgqt import qtlib
from tortoisehg.hgqt.i18n import _
from tortoisehg.hgqt.webconf_ui import Ui_WebconfForm

_FILE_FILTER = _('Config files (*.conf *.config *.ini);;Any files (*)')

class WebconfForm(QWidget):
    """Widget to show/edit webconf"""
    def __init__(self, parent=None, webconf=None):
        super(WebconfForm, self).__init__(parent, acceptDrops=True)
        self._qui = Ui_WebconfForm()
        self._qui.setupUi(self)
        self._initicons()
        self._qui.path_edit.currentIndexChanged.connect(self._updateview)
        self._qui.path_edit.currentIndexChanged.connect(self._updateform)
        self._qui.add_button.clicked.connect(self._addpathmap)

        self.setwebconf(webconf or wconfig.config())
        self._updateform()

    def _initicons(self):
        def setstdicon(w, name):
            w.setIcon(self.style().standardIcon(name))

        setstdicon(self._qui.open_button, QStyle.SP_DialogOpenButton)
        setstdicon(self._qui.save_button, QStyle.SP_DialogSaveButton)
        self._qui.add_button.setIcon(qtlib.geticon('fileadd'))
        self._qui.edit_button.setIcon(qtlib.geticon('edit-file'))
        self._qui.remove_button.setIcon(qtlib.geticon('filedelete'))

    def dragEnterEvent(self, event):
        if self._getlocalpath_from_dropevent(event):
            event.setDropAction(Qt.LinkAction)
            event.accept()

    def dropEvent(self, event):
        localpath = self._getlocalpath_from_dropevent(event)
        if localpath:
            event.setDropAction(Qt.LinkAction)
            event.accept()
            self._addpathmap(localpath=localpath)

    @staticmethod
    def _getlocalpath_from_dropevent(event):
        m = event.mimeData()
        if m.hasFormat('text/uri-list') and len(m.urls()) == 1:
            return unicode(m.urls()[0].toLocalFile())

    def setwebconf(self, webconf):
        """set current webconf object"""
        path = hglib.tounicode(getattr(webconf, 'path', None) or '')
        i = self._qui.path_edit.findText(path)
        if i < 0:
            i = 0
            self._qui.path_edit.insertItem(i, path, webconf)
        self._qui.path_edit.setCurrentIndex(i)

    @property
    def webconf(self):
        """current webconf object"""
        def curconf(w):
            i = w.currentIndex()
            _path, conf = unicode(w.itemText(i)), w.itemData(i).toPyObject()
            return conf

        return curconf(self._qui.path_edit)

    @property
    def _webconfmodel(self):
        """current model object of webconf"""
        return self._qui.repos_view.model()

    @pyqtSlot()
    def _updateview(self):
        m = WebconfModel(config=self.webconf, parent=self)
        self._qui.repos_view.setModel(m)
        self._qui.repos_view.selectionModel().currentChanged.connect(
            self._updateform)

    def _updateform(self):
        """Update availability of each widget"""
        self._qui.repos_view.setEnabled(hasattr(self.webconf, 'write'))
        self._qui.add_button.setEnabled(hasattr(self.webconf, 'write'))
        self._qui.edit_button.setEnabled(
            hasattr(self.webconf, 'write')
            and self._qui.repos_view.currentIndex().isValid())
        self._qui.remove_button.setEnabled(
            hasattr(self.webconf, 'write')
            and self._qui.repos_view.currentIndex().isValid())

    @pyqtSlot()
    def on_open_button_clicked(self):
        path = QFileDialog.getOpenFileName(
            self, _('Open hgweb config'),
            getattr(self.webconf, 'path', None) or '', _FILE_FILTER)
        if path:
            self.openwebconf(path)

    def openwebconf(self, path):
        """load the specified webconf file"""
        path = hglib.fromunicode(path)
        c = wconfig.readfile(path)
        c.path = os.path.abspath(path)
        self.setwebconf(c)

    @pyqtSlot()
    def on_save_button_clicked(self):
        path = QFileDialog.getSaveFileName(
            self, _('Save hgweb config'),
            getattr(self.webconf, 'path', None) or '', _FILE_FILTER)
        if path:
            self.savewebconf(path)

    def savewebconf(self, path):
        """save current webconf to the specified file"""
        path = hglib.fromunicode(path)
        wconfig.writefile(self.webconf, path)
        self.openwebconf(path)  # reopen in case file path changed

    @pyqtSlot()
    def _addpathmap(self, path=None, localpath=None):
        path, localpath = _PathDialog.getaddpathmap(
            self, path=path, localpath=localpath,
            invalidpaths=self._webconfmodel.paths)
        if path:
            self._webconfmodel.addpathmap(path, localpath)

    @pyqtSlot()
    def on_edit_button_clicked(self):
        self.on_repos_view_doubleClicked(self._qui.repos_view.currentIndex())

    @pyqtSlot(QModelIndex)
    def on_repos_view_doubleClicked(self, index):
        assert index.isValid()
        origpath, origlocalpath = self._webconfmodel.getpathmapat(index.row())
        path, localpath = _PathDialog.geteditpathmap(
            self, path=origpath, localpath=origlocalpath,
            invalidpaths=set(self._webconfmodel.paths) - set([origpath]))
        if not path:
            return
        if path != origpath:
            # we cannot change config key without reordering
            self._webconfmodel.removepathmap(origpath)
            self._webconfmodel.addpathmap(path, localpath)
        else:
            self._webconfmodel.setpathmap(path, localpath)

    @pyqtSlot()
    def on_remove_button_clicked(self):
        index = self._qui.repos_view.currentIndex()
        assert index.isValid()
        path, _localpath = self._webconfmodel.getpathmapat(index.row())
        self._webconfmodel.removepathmap(path)

class _PathDialog(QDialog):
    """Dialog to add/edit path mapping"""
    def __init__(self, title, acceptlabel, path=None, localpath=None,
                 invalidpaths=None, parent=None):
        super(_PathDialog, self).__init__(parent)
        self.setWindowFlags((self.windowFlags() | Qt.WindowMinimizeButtonHint)
                            & ~Qt.WindowContextHelpButtonHint)
        self.resize(QFontMetrics(self.font()).width('M') * 50, self.height())
        self.setWindowTitle(title)
        self._invalidpaths = set(invalidpaths or [])
        self.setLayout(QFormLayout(fieldGrowthPolicy=QFormLayout.ExpandingFieldsGrow))
        self._initfields()
        self._initbuttons(acceptlabel)
        self._path_edit.setText(path or os.path.basename(localpath or ''))
        self._localpath_edit.setText(localpath or '')
        self._updateform()

    def _initfields(self):
        """initialize input fields"""
        def addfield(key, label, *extras):
            edit = QLineEdit(self)
            edit.textChanged.connect(self._updateform)
            if extras:
                field = QHBoxLayout()
                field.addWidget(edit)
                for e in extras:
                    field.addWidget(e)
            else:
                field = edit
            self.layout().addRow(label, field)
            setattr(self, '_%s_edit' % key, edit)

        addfield('path', _('Path:'))
        self._localpath_browse_button = QToolButton(
            icon=self.style().standardIcon(QStyle.SP_DialogOpenButton))
        addfield('localpath', _('Local Path:'), self._localpath_browse_button)
        self._localpath_browse_button.clicked.connect(self._browse_localpath)

    def _initbuttons(self, acceptlabel):
        """initialize dialog buttons"""
        self._buttons = QDialogButtonBox(self)
        self._accept_button = self._buttons.addButton(QDialogButtonBox.Ok)
        self._reject_button = self._buttons.addButton(QDialogButtonBox.Cancel)
        self._accept_button.setText(acceptlabel)
        self._buttons.accepted.connect(self.accept)
        self._buttons.rejected.connect(self.reject)
        self.layout().addRow(self._buttons)

    @property
    def path(self):
        """value of path field"""
        return unicode(self._path_edit.text())

    @property
    def localpath(self):
        """value of localpath field"""
        return unicode(self._localpath_edit.text())

    @pyqtSlot()
    def _browse_localpath(self):
        path = QFileDialog.getExistingDirectory(self, _('Select Repository'),
                                                self.localpath)
        if not path:
            return

        self._localpath_edit.setText(path)
        if not self.path:
            self._path_edit.setText(os.path.basename(unicode(path)))

    @pyqtSlot()
    def _updateform(self):
        """update availability of form elements"""
        self._accept_button.setEnabled(self._isacceptable())

    def _isacceptable(self):
        return bool(self.path and self.localpath
                    and self.path not in self._invalidpaths)

    @classmethod
    def getaddpathmap(cls, parent, path=None, localpath=None, invalidpaths=None):
        d = cls(title=_('Add Path to Serve'), acceptlabel=_('Add'),
                path=path, localpath=localpath,
                invalidpaths=invalidpaths, parent=parent)
        if d.exec_():
            return d.path, d.localpath
        else:
            return None, None

    @classmethod
    def geteditpathmap(cls, parent, path=None, localpath=None, invalidpaths=None):
        d = cls(title=_('Edit Path to Serve'), acceptlabel=_('Edit'),
                path=path, localpath=localpath,
                invalidpaths=invalidpaths, parent=parent)
        if d.exec_():
            return d.path, d.localpath
        else:
            return None, None

class WebconfModel(QAbstractTableModel):
    """Wrapper for webconf object to be a Qt's model object"""
    _COLUMNS = [(_('Path'),),
                (_('Local Path'),)]

    def __init__(self, config, parent=None):
        super(WebconfModel, self).__init__(parent)
        self._config = config

    def data(self, index, role):
        if not index.isValid():
            return None
        if role == Qt.DisplayRole:
            v = self._config.items('paths')[index.row()][index.column()]
            return hglib.tounicode(v)
        return None

    def rowCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0  # no child
        return len(self._config['paths'])

    def columnCount(self, parent=QModelIndex()):
        if parent.isValid():
            return 0  # no child
        return len(self._COLUMNS)

    def headerData(self, section, orientation, role):
        if role != Qt.DisplayRole or orientation != Qt.Horizontal:
            return None
        return self._COLUMNS[section][0]

    @property
    def paths(self):
        """return list of known paths"""
        return [hglib.tounicode(e) for e in self._config['paths']]

    def getpathmapat(self, row):
        """return pair of (path, localpath) at the specified index"""
        assert 0 <= row and row < self.rowCount()
        return tuple(hglib.tounicode(e) for e in self._config.items('paths')[row])

    def addpathmap(self, path, localpath):
        """add path mapping to serve"""
        assert path not in self.paths
        self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
        try:
            self._config.set('paths', hglib.fromunicode(path),
                             hglib.fromunicode(localpath))
        finally:
            self.endInsertRows()

    def setpathmap(self, path, localpath):
        """change path mapping at the specified index"""
        self._config.set('paths', hglib.fromunicode(path),
                         hglib.fromunicode(localpath))
        row = self._indexofpath(path)
        self.dataChanged.emit(self.index(row, 0),
                              self.index(row, self.columnCount()))

    def removepathmap(self, path):
        """remove path from mapping"""
        row = self._indexofpath(path)
        self.beginRemoveRows(QModelIndex(), row, row)
        try:
            del self._config['paths'][hglib.fromunicode(path)]
        finally:
            self.endRemoveRows()

    def _indexofpath(self, path):
        path = hglib.fromunicode(path)
        assert path in self._config['paths']
        return list(self._config['paths']).index(path)
