# -*- coding: utf-8 -*-
import os

from encodings.aliases import aliases as _encodings

import pandas

from pandasqt.compat import Qt, QtCore, QtGui, Slot, Signal
from pandasqt.encoding import Detector
from pandasqt.models.DataFrameModel import DataFrameModel
from pandasqt.views.CustomDelegates import DtypeComboDelegate
from pandasqt.views._ui import icons_rc

from pandasqt.utils import fillNoneValues, convertTimestamps

class DelimiterValidator(QtGui.QRegExpValidator):
    """A Custom RegEx Validator.

    The validator checks, if the input has a length of 1.
    The input may contain any non-whitespace-character
    as denoted by the RegEx term `\S`.

    """

    def __init__(self, parent=None):
        """Constructs the object with the given parent.

        Args:
            parent (QObject, optional): Causes the objected to be owned
                by `parent` instead of Qt. Defaults to `None`.

        """
        super(DelimiterValidator, self).__init__(parent)
        re = QtCore.QRegExp('\S{1}')
        self.setRegExp(re)


class DelimiterSelectionWidget(QtGui.QGroupBox):
    """A custom widget with different text delimiter signs.

    A user can choose between 3 predefined and one user defined
    text delimiter characters. Default delimiters include `semicolon`,
    `colon` and `tabulator`. The user defined delimiter may only have
    a length of 1 and may not include any whitespace character.

    Attributes:
        delimiter (QtCore.pyqtSignal): This signal is emitted, whenever a
            delimiter character is selected by the user.
        semicolonRadioButton (QtGui.QRadioButton): A radio button to
            select the `semicolon` character as delimiter.
        commaRadioButton (QtGui.QRadioButton): A radio button to select
            the `comma` character as delimiter.
        tabRadioButton (QtGui.QRadioButton): A radio button to select
            the `tabulator` character as delimiter.
        otherRadioButton (QtGui.QRadioButton): A radio button to select
            the given input text as delimiter.
        otherSeparatorLineEdit (QtGui.QLineEdit): An input line to let the
            user enter one character only, which may be used as delimiter.

    """

    delimiter = Signal('QString')

    def __init__(self, parent=None):
        """Constructs the object with the given parent.

        Args:
            parent (QObject, optional): Causes the objected to be owned
                by `parent` instead of Qt. Defaults to `None`.

        """
        super(DelimiterSelectionWidget, self).__init__(parent)
        self.semicolonRadioButton = None
        self.commaRadioButton = None
        self.tabRadioButton = None
        self.otherRadioButton = None
        self.otherSeparatorLineEdit = None
        self._initUI()


    def _initUI(self):
        """Creates the inital layout with all subwidgets.

        The layout is a `QHBoxLayout`. Each time a radio button is
        selected or unselected, a slot
        `DelimiterSelectionWidget._delimiter` is called.
        Furthermore the `QLineEdit` widget has a custom regex validator
        `DelimiterValidator` enabled.

        """
        #layout = QtGui.QHBoxLayout(self)

        self.semicolonRadioButton = QtGui.QRadioButton(u'Semicolon')
        self.commaRadioButton = QtGui.QRadioButton(u'Comma')
        self.tabRadioButton = QtGui.QRadioButton(u'Tab')
        self.otherRadioButton = QtGui.QRadioButton(u'Other')
        self.semicolonRadioButton.setChecked(True)

        self.otherSeparatorLineEdit = QtGui.QLineEdit(self)
        self.otherSeparatorLineEdit.setEnabled(False)

        self.semicolonRadioButton.toggled.connect(self._delimiter)
        self.commaRadioButton.toggled.connect(self._delimiter)
        self.tabRadioButton.toggled.connect(self._delimiter)

        self.otherRadioButton.toggled.connect(self._enableLine)
        self.otherSeparatorLineEdit.textChanged.connect(lambda: self._delimiter(True))
        self.otherSeparatorLineEdit.setValidator(DelimiterValidator(self))

        currentLayout = self.layout()
        # unset and delete the current layout in order to set a new one
        if currentLayout is not None:
            del currentLayout

        layout = QtGui.QHBoxLayout()
        layout.addWidget(self.semicolonRadioButton)
        layout.addWidget(self.commaRadioButton)
        layout.addWidget(self.tabRadioButton)
        layout.addWidget(self.otherRadioButton)
        layout.addWidget(self.otherSeparatorLineEdit)
        self.setLayout(layout)

    @Slot('QBool')
    def _enableLine(self, toggled):
        self.otherSeparatorLineEdit.setEnabled(toggled)

    def currentSelected(self):
        """Returns the currently selected delimiter character.

        Returns:
            str: One of `,`, `;`, `\t`, `*other*`.

        """
        if self.commaRadioButton.isChecked():
            return ','
        elif self.semicolonRadioButton.isChecked():
            return ';'
        elif self.tabRadioButton.isChecked():
            return '\t'
        elif self.otherRadioButton.isChecked():
            return self.otherSeparatorLineEdit.text()
        return


    @Slot('QBool')
    def _delimiter(self, checked):
        if checked:
            if self.commaRadioButton.isChecked():
                self.delimiter.emit(',')
            elif self.semicolonRadioButton.isChecked():
                self.delimiter.emit(';')
            elif self.tabRadioButton.isChecked():
                self.delimiter.emit('\t')
            elif self.otherRadioButton.isChecked():
                ret = self.otherSeparatorLineEdit.text()
                if len(ret) > 0:
                    self.delimiter.emit(ret)

    def reset(self):
        """Resets this widget to its initial state.

        """
        self.semicolonRadioButton.setChecked(True)
        self.otherSeparatorLineEdit.setText('')


class CSVImportDialog(QtGui.QDialog):
    """A dialog to import any csv file into a pandas data frame.

    This modal dialog enables the user to enter any path to a csv
    file and parse this file with or without a header and with special
    delimiter characters.

    On a successful load, the data can be previewed and the column data
    types may be edited by the user.

    After all configuration is done, the dataframe and the underlying model
    may be used by the main application.

    Attributes:
        load (QtCore.pyqtSignal): This signal is emitted, whenever the
            dialog is successfully closed, e.g. when the ok button is
            pressed. Returns DataFrameModel and path of chosen csv file.
    """

    load = Signal('QAbstractItemModel', str)

    def __init__(self, parent=None):
        """Constructs the object with the given parent.

        Args:
            parent (QObject, optional): Causes the objected to be owned
                by `parent` instead of Qt. Defaults to `None`.

        """
        super(CSVImportDialog, self).__init__(parent)
        self._modal = True
        self._windowTitle = u'Import CSV'
        self._encodingKey = None
        self._filename = None
        self._delimiter = None
        self._header = None
        self._detector = Detector()
        self._initUI()

    def _initUI(self):
        """Initiates the user interface with a grid layout and several widgets.

        """
        self.setModal(self._modal)
        self.setWindowTitle(self._windowTitle)

        layout = QtGui.QGridLayout()

        self._filenameLabel = QtGui.QLabel(u'Choose File', self)
        self._filenameLineEdit = QtGui.QLineEdit(self)
        self._filenameLineEdit.textEdited.connect(self._updateFilename)
        chooseFileButtonIcon = QtGui.QIcon(QtGui.QPixmap(':/icons/document-open.png'))
        self._chooseFileAction = QtGui.QAction(self)
        self._chooseFileAction.setIcon(chooseFileButtonIcon)
        self._chooseFileAction.triggered.connect(self._openFile)

        self._chooseFileButton = QtGui.QToolButton(self)
        self._chooseFileButton.setDefaultAction(self._chooseFileAction)

        layout.addWidget(self._filenameLabel, 0, 0)
        layout.addWidget(self._filenameLineEdit, 0, 1, 1, 2)
        layout.addWidget(self._chooseFileButton, 0, 3)

        self._encodingLabel = QtGui.QLabel(u'File Encoding', self)

        encoding_names = map(lambda x: x.upper(), sorted(list(set(_encodings.viewvalues()))))
        self._encodingComboBox = QtGui.QComboBox(self)
        self._encodingComboBox.addItems(encoding_names)
        self._encodingComboBox.activated.connect(self._updateEncoding)

        layout.addWidget(self._encodingLabel, 1, 0)
        layout.addWidget(self._encodingComboBox, 1, 1, 1, 1)

        self._hasHeaderLabel = QtGui.QLabel(u'Header Available?', self)
        self._headerCheckBox = QtGui.QCheckBox(self)
        self._headerCheckBox.toggled.connect(self._updateHeader)

        layout.addWidget(self._hasHeaderLabel, 2, 0)
        layout.addWidget(self._headerCheckBox, 2, 1)

        self._delimiterLabel = QtGui.QLabel(u'Column Delimiter', self)
        self._delimiterBox = DelimiterSelectionWidget(self)
        self._delimiter = self._delimiterBox.currentSelected()
        self._delimiterBox.delimiter.connect(self._updateDelimiter)

        layout.addWidget(self._delimiterLabel, 3, 0)
        layout.addWidget(self._delimiterBox, 3, 1, 1, 3)

        self._tabWidget = QtGui.QTabWidget(self)
        self._previewTableView = QtGui.QTableView(self)
        self._datatypeTableView = QtGui.QTableView(self)
        self._tabWidget.addTab(self._previewTableView, u'Preview')
        self._tabWidget.addTab(self._datatypeTableView, u'Change Column Types')
        layout.addWidget(self._tabWidget, 4, 0, 3, 4)

        self._datatypeTableView.horizontalHeader().setDefaultSectionSize(200)
        self._datatypeTableView.setItemDelegateForColumn(1, DtypeComboDelegate(self._datatypeTableView))


        self._loadButton = QtGui.QPushButton(u'Load Data', self)
        #self.loadButton.setAutoDefault(False)

        self._cancelButton = QtGui.QPushButton(u'Cancel', self)
        # self.cancelButton.setDefault(False)
        # self.cancelButton.setAutoDefault(True)

        self._buttonBox = QtGui.QDialogButtonBox(self)
        self._buttonBox.addButton(self._loadButton, QtGui.QDialogButtonBox.AcceptRole)
        self._buttonBox.addButton(self._cancelButton, QtGui.QDialogButtonBox.RejectRole)
        self._buttonBox.accepted.connect(self.accepted)
        self._buttonBox.rejected.connect(self.rejected)
        layout.addWidget(self._buttonBox, 9, 2, 1, 2)
        self._loadButton.setDefault(False)
        self._filenameLineEdit.setFocus()

        self._statusBar = QtGui.QStatusBar(self)
        self._statusBar.setSizeGripEnabled(False)
        layout.addWidget(self._statusBar, 8, 0, 1, 4)
        self.setLayout(layout)

    @Slot('QString')
    def updateStatusBar(self, message):
        """Updates the status bar widget of this dialog with the given message.

        This method is also a `SLOT()`.
        The message will be shown for only 5 seconds.

        Args:
            message (QString): The new message which will be displayed.

        """
        self._statusBar.showMessage(message, 5000)

    @Slot()
    def _openFile(self):
        """Opens a file dialog and sets a value for the QLineEdit widget.

        This method is also a `SLOT`.

        """
        ret = QtGui.QFileDialog.getOpenFileName(self, self.tr(u'open file'), filter='Comma Separated Values (*.csv)')
        if ret:
            self._filenameLineEdit.setText(ret)
            self._updateFilename()

    @Slot('QBool')
    def _updateHeader(self, toggled):
        """Changes the internal flag, whether the csv file contains a header or not.

        This method is also a `SLOT`.

        In addition, after toggling the corresponding checkbox, the
        `_previewFile` method will be called.

        Args:
            toggled (boolean): A flag indicating the status of the checkbox.
                The flag will be used to update an internal variable.

        """
        self._header = 0 if toggled else None
        self._previewFile()

    @Slot()
    def _updateFilename(self):
        """Calls several methods after the filename changed.

        This method is also a `SLOT`.
        It checks the encoding of the changed filename and generates a
        preview of the data.

        """
        self._filename = self._filenameLineEdit.text()
        self._guessEncoding(self._filename)
        self._previewFile()

    def _guessEncoding(self, path):
        """Opens a file from the given `path` and checks the file encoding.

        The file must exists on the file system and end with the extension
        `.csv`. The file is read line by line until the encoding could be
        guessed.
        On a successfull identification, the widgets of this dialog will be
        updated.

        Args:
            path (string): Path to a csv file on the file system.

        """
        if os.path.exists(path) and path.lower().endswith('csv'):
            encoding = self._detector.detect(path)

            if encoding is not None:
                if encoding.startswith('utf'):
                    encoding = encoding.replace('-', '')
                encoding = encoding.replace('-','_')

                viewValue = _encodings.get(encoding)

                self._encodingKey = encoding

                index = self._encodingComboBox.findText(viewValue.upper())
                self._encodingComboBox.setCurrentIndex(index)

    @Slot('int')
    def _updateEncoding(self, index):
        """Changes the value of the encoding combo box to the value of given index.

        This method is also a `SLOT`.
        After the encoding is changed, the file will be reloaded and previewed.

        Args:
            index (int): An valid index of the combo box.

        """
        encoding = self._encodingComboBox.itemText(index)
        encoding = encoding.lower()

        self._encodingKey = _calculateEncodingKey(encoding)
        self._previewFile()

    @Slot('QString')
    def _updateDelimiter(self, delimiter):
        """Changes the value of the delimiter for the csv file.

        This method is also a `SLOT`.

        Args:
            delimiter (string): The new delimiter.

        """
        self._delimiter = delimiter
        self._previewFile()

    def _previewFile(self):
        """Updates the preview widgets with new models for both tab panes.

        """
        dataFrame = self._loadCSVDataFrame()
        dataFrameModel = DataFrameModel(dataFrame)
        dataFrameModel.enableEditing(True)
        self._previewTableView.setModel(dataFrameModel)
        columnModel = dataFrameModel.columnDtypeModel()
        columnModel.changeFailed.connect(self.updateStatusBar)
        self._datatypeTableView.setModel(columnModel)

    def _loadCSVDataFrame(self):
        """Loads the given csv file with pandas and generate a new dataframe.

        The file will be loaded with the configured encoding, delimiter
        and header.git
        If any execptions will occur, an empty Dataframe is generated
        and a message will appear in the status bar.

        Returns:
            pandas.DataFrame: A dataframe containing all the available
                information of the csv file.

        """
        if self._filename and os.path.exists(self._filename) and self._filename.endswith('.csv'):
            # default fallback if no encoding was found/selected
            encoding = self._encodingKey or 'uft8'

            try:
                dataFrame = pandas.read_csv(self._filename,
                    sep=self._delimiter, encoding=encoding,
                    header=self._header)
                dataFrame = dataFrame.apply(fillNoneValues)
                dataFrame = dataFrame.apply(convertTimestamps)
            except Exception, err:
                self.updateStatusBar(str(err))
                return pandas.DataFrame()
            self.updateStatusBar('Preview generated.')
            return dataFrame
        self.updateStatusBar('File does not exists or does not end with .csv')
        return pandas.DataFrame()

    def _resetWidgets(self):
        """Resets all widgets of this dialog to its inital state.

        """
        self._filenameLineEdit.setText('')
        self._encodingComboBox.setCurrentIndex(0)
        self._delimiterBox.reset()
        self._headerCheckBox.setChecked(False)
        self._statusBar.showMessage('')
        self._previewTableView.setModel(None)
        self._datatypeTableView.setModel(None)

    @Slot()
    def accepted(self):
        """Successfully close the widget and return the loaded model.

        This method is also a `SLOT`.
        The dialog will be closed, when the `ok` button is pressed. If
        a `DataFrame` was loaded, it will be emitted by the signal `load`.

        """
        model = self._previewTableView.model()
        if model is not None:
            df = model.dataFrame().copy()
            dfModel = DataFrameModel(df)
            self.load.emit(dfModel, self._filename)
        self._resetWidgets()
        self.accept()

    @Slot()
    def rejected(self):
        """Close the widget and reset its inital state.

        This method is also a `SLOT`.
        The dialog will be closed and all changes reverted, when the
        `cancel` button is pressed.

        """
        self._resetWidgets()
        self.reject()

class CSVExportDialog(QtGui.QDialog):
    """An widget to serialize a `DataFrameModel` to a `CSV-File`.

    """
    exported = Signal('QBool')

    def __init__(self, model=None, parent=None):
        super(CSVExportDialog, self).__init__(parent)
        self._model = model
        self._modal = True
        self._windowTitle = u'Export to CSV'
        self._idx = -1
        self._initUI()

    def _initUI(self):
        """Initiates the user interface with a grid layout and several widgets.

        """
        self.setModal(self._modal)
        self.setWindowTitle(self._windowTitle)

        layout = QtGui.QGridLayout()

        self._filenameLabel = QtGui.QLabel(u'Output File', self)
        self._filenameLineEdit = QtGui.QLineEdit(self)
        chooseFileButtonIcon = QtGui.QIcon(QtGui.QPixmap(':/icons/document-save-as.png'))
        self._chooseFileAction = QtGui.QAction(self)
        self._chooseFileAction.setIcon(chooseFileButtonIcon)
        self._chooseFileAction.triggered.connect(self._createFile)

        self._chooseFileButton = QtGui.QToolButton(self)
        self._chooseFileButton.setDefaultAction(self._chooseFileAction)

        layout.addWidget(self._filenameLabel, 0, 0)
        layout.addWidget(self._filenameLineEdit, 0, 1, 1, 2)
        layout.addWidget(self._chooseFileButton, 0, 3)

        self._encodingLabel = QtGui.QLabel(u'File Encoding', self)

        encoding_names = map(lambda x: x.upper(), sorted(list(set(_encodings.viewvalues()))))
        self._encodingComboBox = QtGui.QComboBox(self)
        self._encodingComboBox.addItems(encoding_names)
        self._idx = encoding_names.index('UTF_8')
        self._encodingComboBox.setCurrentIndex(self._idx)
        #self._encodingComboBox.activated.connect(self._updateEncoding)

        layout.addWidget(self._encodingLabel, 1, 0)
        layout.addWidget(self._encodingComboBox, 1, 1, 1, 1)

        self._hasHeaderLabel = QtGui.QLabel(u'Header Available?', self)
        self._headerCheckBox = QtGui.QCheckBox(self)
        #self._headerCheckBox.toggled.connect(self._updateHeader)

        layout.addWidget(self._hasHeaderLabel, 2, 0)
        layout.addWidget(self._headerCheckBox, 2, 1)

        self._delimiterLabel = QtGui.QLabel(u'Column Delimiter', self)
        self._delimiterBox = DelimiterSelectionWidget(self)

        layout.addWidget(self._delimiterLabel, 3, 0)
        layout.addWidget(self._delimiterBox, 3, 1, 1, 3)

        self._exportButton = QtGui.QPushButton(u'Export Data', self)
        self._cancelButton = QtGui.QPushButton(u'Cancel', self)

        self._buttonBox = QtGui.QDialogButtonBox(self)
        self._buttonBox.addButton(self._exportButton, QtGui.QDialogButtonBox.AcceptRole)
        self._buttonBox.addButton(self._cancelButton, QtGui.QDialogButtonBox.RejectRole)

        self._buttonBox.accepted.connect(self.accepted)
        self._buttonBox.rejected.connect(self.rejected)

        layout.addWidget(self._buttonBox, 5, 2, 1, 2)
        self._exportButton.setDefault(False)
        self._filenameLineEdit.setFocus()

        self._statusBar = QtGui.QStatusBar(self)
        self._statusBar.setSizeGripEnabled(False)
        layout.addWidget(self._statusBar, 4, 0, 1, 4)
        self.setLayout(layout)

    def setExportModel(self, model):
        if not isinstance(model, DataFrameModel):
            return False

        self._model = model
        return True

    @Slot()
    def _createFile(self):
        ret = QtGui.QFileDialog.getSaveFileName(self, 'Save File', filter='Comma Separated Value (*.csv)')
        self._filenameLineEdit.setText(ret)

    def _saveModel(self):
        delimiter = self._delimiterBox.currentSelected()
        header = self._headerCheckBox.isChecked() # column labels
        filename = self._filenameLineEdit.text()
        index = False # row labels

        encodingIndex = self._encodingComboBox.currentIndex()
        encoding = self._encodingComboBox.itemText(encodingIndex)
        encoding = _calculateEncodingKey(encoding.lower())

        try:
            dataFrame = self._model.dataFrame()
        except AttributeError, err:
            raise AttributeError('No data loaded to export.')
        else:
            try:
                dataFrame.to_csv(filename, encoding=encoding, header=header, index=index, sep=delimiter)
            except IOError, err:
                raise IOError('No filename given')
            except UnicodeError, err:
                raise UnicodeError('Could not encode all data. Choose a different encoding')
            except Exception:
                raise

    def _resetWidgets(self):
        """Resets all widgets of this dialog to its inital state.

        """
        self._filenameLineEdit.setText('')
        self._encodingComboBox.setCurrentIndex(self._idx)
        self._delimiterBox.reset()
        self._headerCheckBox.setChecked(False)
        self._statusBar.showMessage('')

    @Slot()
    def accepted(self):
        """Successfully close the widget and emit an export signal.

        This method is also a `SLOT`.
        The dialog will be closed, when the `Export Data` button is
        pressed. If errors occur during the export, the status bar
        will show the error message and the dialog will not be closed.

        """
        try:
            self._saveModel()
        except Exception, err:
            self._statusBar.showMessage(str(err))
        else:
            self._resetWidgets()
            self.exported.emit(True)
            self.accept()

    @Slot()
    def rejected(self):
        """Close the widget and reset its inital state.

        This method is also a `SLOT`.
        The dialog will be closed and all changes reverted, when the
        `cancel` button is pressed.

        """
        self._resetWidgets()
        self.exported.emit(False)
        self.reject()



def _calculateEncodingKey(comparator):
    """Gets the first key of all available encodings where the corresponding
    value matches the comparator.

    Args:
        comparator (string): A view name for an encoding.

    Returns:
        str: A key for a specific encoding used by python.

    """
    encodingName = None
    for k, v in _encodings.viewitems():
        if v == comparator:
            encodingName = k
            break
    return encodingName