# acqlib

# Copyright (c) 2011 Christopher Reichert and Claude Rubinson

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.

import sys
import signal
import logging
import itertools
import md5

from PyQt4.QtCore import Qt, QSettings, SIGNAL, QDir, QAbstractTableModel,\
                         QVariant, QCoreApplication, pyqtSignal
from PyQt4.QtGui import QAction, QIcon, QMessageBox, QTextEdit, QCursor,\
                        QDesktopServices, QTableView, QStyledItemDelegate,\
                        QWidget, QAbstractItemView, QPrinter, QPrintDialog, \
                        QDialog, QTextDocument, QMainWindow, QKeySequence, \
                        QErrorMessage, QSortFilterProxyModel, QFileDialog

from libfsqca import fznot, QcaDataset, concov_nec

AcqSettings = QSettings( 'acq' )
# this does not pollute the acq namespace with kirq
# it is simply an identifier
KirqSettings = QSettings( 'kirq' )
GlobalSettings = AcqSettings

# allow SIGTERM & SIGKIL
# SIGINT is translated into a KeyboardInterrupt exception.
signal.signal(signal.SIGINT, signal.SIG_DFL)
HistoryItemRole = 99

# icon paths
ICON_SM_PATH=':/images/22x22/'
ICON_LRG_PATH=':/images/48x48/'

# window icons
KIRQ_APP_ICON = ICON_LRG_PATH + 'basket.png'
GTT_APP_ICON = ICON_LRG_PATH + 'qelectrotech.png'
CONCOV_APP_ICON = ICON_LRG_PATH + 'kdf.png'
OBS_EDITOR_ICON = ICON_LRG_PATH + 'view-process-users.png'

# action icons
DOCUMENT_OPEN_ICON = ICON_SM_PATH + 'document-open.png'
DOCUMENT_SAVE_ICON = ICON_SM_PATH + 'document-save.png'
PRINTER_ICON = ICON_SM_PATH + 'printer.png'
APP_EXIT_ICON = ICON_SM_PATH + 'application-exit.png'
MULTI_SORT_ICON = ICON_SM_PATH + 'view-sort-ascending.png'
FILTER_DLG_ICON = ICON_SM_PATH + 'edit-find-project.png'
PREF_DLG_ICON = ICON_SM_PATH + 'preferences-other.png'
HELP_ICON = ICON_SM_PATH + 'help-contents.png'
ABOUT_ICON = ICON_SM_PATH + 'help-about.png'
PREF_ICON = ICON_SM_PATH + 'preferences-other.png'
PRINT_ICON = ICON_SM_PATH + 'printer.png'

# file type icons
GTT_FILE_TYPE_ICON = ICON_SM_PATH + 'x-office-spreadsheet.png'
KIRQ_FILE_TYPE_ICON = ICON_LRG_PATH + 'x-office-document.png'
CONCOV_FILE_TYPE_ICON = ICON_LRG_PATH + 'application-x-sharedlib.png'


class AcqMainWindow( QMainWindow ):
    """ Base class for acq standalone dialogs.
        Intended to be subclasses and instantiated outside of kirq.
    """
    def __init__(self, parent=None):
        """ Initialize an AcqMainWindow. """
        super(AcqMainWindow, self).__init__(parent)
        
        # primary central widget (gtt/concov)
        self.setAcqWidget(None)
        self.setCurrentDataFile(None)

        # functions that construct main dialog:
        self.setupActions()
        self.setupMenu()
        self.openSavedState()

    def setAcqWidget(self, acqWidget):
        """ Set the acq widget and also make acqWidget the
            central Widget. """
        self.acqWidget = acqWidget
        self.setCentralWidget(self.acqWidget)

    def getAcqWidget(self):
        return self.acqWidget

    def setCurrentDataFile(self, FILE):
        self.currentDataFile = FILE

    def getCurrentDataFile(self):
        return self.currentDataFile

    def getFileOpenAction(self):
        return self.fileOpenAction

    def getToolbar(self):
        return self.toolbar

    def showDocumentation( self ):
        createDocumentationDialog( self, self.__class__.__name__ )

    def setupActions(self):
        """ Setup file menu/toolbar actions.
            Internally the button on the toolbar and the item
            in the file menu are the same thing. """
        # Open action
        self.fileOpenAction = createAction( self,
                                            '&Open',
                                            self.slotOpenTable,
                                            QKeySequence.Open,
                                            DOCUMENT_OPEN_ICON,
                                            'Open a new table' )
        # Export/Save action
        self.exportTableAction = createAction( self,
                                               '&Export Table',
                                               self.slotExportTable,
                                               'Ctrl+E',
                                               DOCUMENT_SAVE_ICON,
                                               'Save table' )
        self.exportTableAction.setEnabled( False )

        # close action
        self.closeAction = createAction( self,
                                         '&Close',
                                         self.close,
                                         QKeySequence.Close,
                                         APP_EXIT_ICON,
                                         'Close the window' )
        # about action
        self.documentationAction = createAction( self,
                                                 '&Documentation',
                                                 self.showDocumentation,
                                                 'Ctrl+H',
                                                 HELP_ICON,
                                                 'Documentation' )

        self.aboutAction = createAction( self,
                                         '&About',
                                         self.helpAbout,
                                         'Ctrl+a',
                                         ABOUT_ICON,
                                         'About' )
        # print
        self.printAction = createAction( self,
                                         '&Print',
                                          self.slotPrintTable,
                                          QKeySequence.Print,
                                          PRINT_ICON,
                                         'Print' )
        self.printAction.setEnabled( False )
        # multi-sort action
        self.multiSortAction = createAction( self, '&Multi-sort',
                                             self.slotMultiSortStateChanged,
                                             'Ctrl+m', MULTI_SORT_ICON,
                                             'Toggle multi-sort',
                                             True, 'toggled(bool)' )
        self.multiSortAction.setEnabled( False )

    def setupToolbar(self):
        # Toolbar
        self.toolbar = self.addToolBar('')
        self.toolbar.addAction( self.exportTableAction )
        self.toolbar.addAction( self.printAction )
        self.toolbar.addAction( self.multiSortAction )
        self.setToolButtonStyle( Qt.ToolButtonTextUnderIcon )

    def setupMenu( self ):
        """ Create the menu items for our defined actions. """
        fileMenu = self.menuBar().addMenu('&File')
        fileMenu.addAction( self.fileOpenAction )
        fileMenu.addAction( self.exportTableAction )
        fileMenu.addAction( self.printAction )
        fileMenu.addSeparator()
        fileMenu.addAction( self.closeAction )

    def slotExportTable( self ):
        """ Export table to file. """
        self.getAcqWidget().exportTable()

    def slotPrintTable( self ):
        self.getAcqWidget().printTable()

    def inputFromFile( self, inputFile ):
        """ Returns the raw table from file. """
        rawData = []
        try:
            for line in open( inputFile ):
                rawData.append( line.strip() )
            return rawData
        except IOError as e:
            print >> sys.stderr, e

    def slotMultiSortStateChanged( self, state ):
        """ Toggle the state of multi-sort. """
        self.getAcqWidget().proxyModel.slotMultiSortStateChanged( state )


class AcqWidget( QWidget ):
    def __init__( self, parent=None ):
        super(AcqWidget, self).__init__(parent)

    def __hash__( self ):
        return hash(tuple([tuple(x) for x in self.getModel().formattedTableData()]))

    def setModel(self, model):
        self._model = model

    def getModel(self):
        return self._model

    def setView(self, view):
        self._view = view

    def getView(self):
        return self._view

    def setupView( self ):
        self.getView().setShowGrid( False )
        self.getView().setAlternatingRowColors(True)
        self.getView().setSortingEnabled( True )
        self.getView().setHorizontalScrollMode( QAbstractItemView.ScrollPerPixel )
        self.getView().horizontalHeader().setVisible( True )
        self.getView().horizontalHeader().setCascadingSectionResizes( True )
        self.getView().horizontalHeader().setStretchLastSection( True )
        self.getView().verticalHeader().setVisible( False )

    def slotKillObsEditor( self ):
        """ Close the obs editor and remove it from the list. """
        try:
            self.obsEditors.remove(self.sender())
        except ValueError: # if it doesn't exist yet
            pass


class AcqTableModel( QAbstractTableModel ):
    """
        Base class for acq models.
    """
    def __init__( self, headerData, tableData, parent=None):
        super( AcqTableModel, self ).__init__( parent )
        self.setParent(parent)
        self.setHeaderData(headerData)
        self.setTableData(tableData)

    def setParent(self, parent):
        self._parent = parent

    def getParent(self):
        return self._parent

    def setTableData(self, tableData):
        self._tableData = tableData

    def getTableData(self):
        """ Returns an actual referencee to the data.
            Use formattedTableData to get a copy."""
        return self._tableData

    def formattedTableData( self ):
        """ Returns the table data. """
        return self.getTableData()[:]

    def setHeaderData(self, headerData):
        self._headerData = headerData

    def getHeaderData(self):
        """ Returns an actual reference to the header data.
            Use formattedHeaderData to get a copy."""
        return self._headerData

    def formattedHeaderData( self ):
        """ Returns the header data. """
        return self.getHeaderData()[:]

    def formattedModelData( self ):
        """ Returns the complete list of data in this model. """
        formattedModelData =  self.getTableData()[:]
        formattedModelData.insert( 0, self.getHeaderData()[:] )
        return formattedModelData
        
    def formattedTableData( self ):
        """ Return a new list of lists containing tableData. """
        return self.getTableData()[:]

    def columnCount( self, parent ):
        """ return the number of columns ( header count ) """
        try:
            return len( self.getTableData()[0] )
        except IndexError as e:
            # if we open a concov table with no input, we want to show an
            # an empty table, so we need to catch the index error to have
            # a bare model
            logging.debug(str(e) + " -- Used to safely show an empty table")
            return 0

    def rowCount( self, parent ):
        """ return the number of rows """
        return len( self.getTableData() )

    def data( self, index, role ):
        """ """
        if role == Qt.DisplayRole:
            return QVariant( self.getTableData()[ index.row() ][ index.column() ] )

        elif role == Qt.TextAlignmentRole:
            return Qt.AlignCenter

        elif role == Qt.ToolTipRole and self.getHeaderData() and \
                   'Obs' in self.getHeaderData()[index.column()]:
            obsList = str( index.data( Qt.DisplayRole ).toString() ).split(';')
            if len( obsList ) <= 10:
                return QVariant( '\n'.join( obsList[:10] ))
            else:
                return QVariant( '\n'.join( obsList[:10] ) + '\n...' )

        return QVariant()

    def headerData( self, col, orientation, role ):
        """ This is slightly different then our getHeaderData base class method.
            This is a virtual protected Qt method that we are leaving here to keep
            out model intact. """
        if self.getHeaderData() is None:
            return
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return QVariant( self.getHeaderData()[ col ] )
        return QAbstractTableModel.headerData( self, col, orientation, role )

    def setData( self, index, data, role=Qt.EditRole ):
        return False

    def flags( self, index ):
        if not index.isValid():
            return Qt.NoItemFlags
        return Qt.ItemFlags( Qt.ItemIsEnabled | Qt.ItemIsSelectable )


class AcqProxyModel( QSortFilterProxyModel ):

    def __init__( self, parent=None, *args ):
        super( AcqProxyModel, self ).__init__( parent )
        self.multiSortColumns = []
        self.multiSortEnabled = False

    def sort( self, column, order ):
        """ Handle the type of sorting we are doing, we only want to pass
             in each column we multi sort on once. """
        if self.multiSortEnabled is True and column not in self.multiSortColumns:
            self.multiSortColumns.append( column )
        return QSortFilterProxyModel.sort( self, column, order )

    def lessThan( self, left, right ):
        """ Return the lesser between index left and right
                -- custom implementation to handle multi sort """
        if not self.multiSortEnabled:
            return QSortFilterProxyModel.lessThan( self, left, right )
        else:
            # if rights value in the previous columns are equal to lefts values 
            # in the previous columns then you can sort them, else just return
            # that they are them same; If we make it out of this loop then we have 
            # met the conditions to sort normally which is a multi sort in this case
            for col in self.multiSortColumns:
                if self.sourceModel().data( self.sourceModel().index( right.row(), col ), Qt.DisplayRole ) != \
                   self.sourceModel().data( self.sourceModel().index( left.row(), col ), Qt.DisplayRole ) and \
                   col != left.column() and col != right.column():
                    return 0
            return QSortFilterProxyModel.lessThan( self, left, right )

    def slotMultiSortStateChanged( self, state ):
        """ Handle multi sort state. """
        logging.debug( "multi-sorting on columns {0} ".format(
                                         self.multiSortColumns ))
        if state == Qt.Unchecked:
            self.multiSortColumns = []
            self.multiSortEnabled = False
        else:
            self.multiSortEnabled = True


class AcqItemDelegate( QStyledItemDelegate ):
    """
        Handles specific functionality to edit a cell in our table.
    """
    def __init__(self, parent=None):
        super(AcqItemDelegate, self).__init__(parent)

    def setModelData(self, editor, model, index):
        QStyledItemDelegate.setModelData(self, editor, model, index)

    def createEditor(self, parent, option, index):
        return QStyledItemDelegate.createEditor(self, parent, option, index)

    def setEditorData( self, editor, index ):
        QStyledItemDelegate.setEditorData( self, editor, index )


class AcqView( QTableView ):
    def __init__( self, parent=None ):
        super( AcqView, self ).__init__( parent )


class ObsEditor( QTextEdit ):
    """ Custom Text Editor for Obs columns.

        The reason we need a custom class for this is
        reimplementing the HoverLeave event.
    """
    killObsEditor = pyqtSignal()
    def __init__( self, text, title, parent=None ):
        super( ObsEditor, self ).__init__( parent )
        self.setWindowIcon( QIcon(OBS_EDITOR_ICON) )
        self.setWindowFlags( Qt.WindowStaysOnTopHint )
        self.setWindowTitle(title)
        self.resize(350,200)
        self.setText( text )
        self.setReadOnly( True )
        self.move( QCursor.pos().x() - 8, QCursor.pos().y() - 8 )

    def closeEvent( self, event ):
        self.killObsEditor.emit()
        return QTextEdit.closeEvent( self, event )


class AcqPrintDialog(object):

    def __init__(self):
        self.printer = QPrinter()

    def acceptPrintJob(self):
        printDlg = QPrintDialog(self.printer)
        if (printDlg.exec_() == QDialog.Accepted):
            return True
        return False

    def print_(self, doc):
        document = QTextDocument( doc )
        document.print_(self.printer)


class InvalidTruthTableElement( Exception ):
    """ Marks a cells contents as invalid in a truth table. """
    def __init__( self, val ):
        super( InvalidTruthTableElement, self ).__init__()
        self.value = val
    def __str__( self ):
        return repr( self.value )


class ExportTableDlg(QFileDialog):

    def __init__(self, parent = None):
        super(ExportTableDlg, self).__init__(parent)
        self.setAcceptMode(QFileDialog.AcceptSave)
        self.setWindowTitle("Export Table")
        self.setLabelText(QFileDialog.Accept, "Export")


def createAction( sender, text, slot=None, shortcut=None, icon=None,
                  tip=None, checkable=False, signal="triggered()" ):
    """ Generic action creator. """
    action = QAction( text, sender )
    if icon is not None:
        action.setIcon( QIcon(icon) )
    if shortcut is not None:
        action.setShortcut( shortcut )
    if tip is not None:
        action.setToolTip( tip )
        action.setStatusTip( tip )
    if slot is not None:
        sender.connect( action, SIGNAL( signal ), slot )
    if checkable:
        action.setCheckable( True )
    return action


def createDocumentationDialog( obj, objname ):
    QMessageBox.about( obj, objname + ' Documentation',
        """<b>Documentation is available online at</b><p>
           <a href="http://grundrisse.org/qca/docs/">\
           http://grundrisse.org/qca/docs/</a>""" )


def deleteLayoutItems( layout ):
    """ Delets a layout and all its children. """
    if layout is not None:
        while layout.count():
            item = layout.takeAt( 0 )
            widget = item.widget()
            if widget is not None:
                widget.deleteLater()
            else:
                layout.removeItem( item.layout() )


def setupLogging( appname, verbose=False ):
    # debug logging
    logdir = str(QDesktopServices.storageLocation(
                         QDesktopServices.DataLocation ))
    logFormat = '[' + appname + '] => %(asctime)s %(levelname)s: %(message)s'

    if QDir(logdir).mkpath( logdir ):
        logging.basicConfig(level=logging.DEBUG, filename=logdir + '/acq.log',
                format=logFormat, datefmt='%Y-%m-%d %H:%M')
    else:
        print >> sys.stderr, 'Cannot create directory for logging.'

    console = logging.StreamHandler()
    console.setFormatter(logging.Formatter(logFormat))
    console.setLevel(logging.DEBUG) if verbose else console.setLevel(logging.ERROR)

    logging.getLogger().addHandler(console)

    logging.debug( 'QDesktopServices.DataLocation is  \'%s/\'' %
                   str(QDesktopServices.storageLocation(
                         QDesktopServices.DataLocation )) )
    logging.debug( 'Log directory set to \'%s/\'' % str( logdir ))

def getMd5(filename, blocksize=65536):
    ''' Get an md5 checksum for the file. '''
    return md5.md5(file(filename, 'r').read()).hexdigest()

def debug_trace():
    '''Set a tracepoint in the Python debugger that works with Qt'''
    from PyQt4.QtCore import pyqtRemoveInputHook
    from pdb import set_trace
    pyqtRemoveInputHook()
    set_trace()


def errorDlg(msg):
    errorMessage = QErrorMessage()
    errorMessage.showMessage( msg )
    errorMessage.exec_()


def reduceNecConcov(qcadata, consist_thresh, cov_thresh, causal_conds):
    """ Returns a concov_nec object """
    # construct new dataset with negated causal conditions
    with_negated=[['Cases'] + [cc.upper() for cc in qcadata.causal_conds()] + 
                  [cc.lower() for cc in qcadata.causal_conds()] + 
                  [qcadata.outcome()]]
    for obs_name, memberships, outcome_membership in \
            zip(qcadata.obs(),
                [cms + fznot(cms) for cms in qcadata.causal_memberships()],
                qcadata.outcome_memberships()):
        with_negated.append([obs_name] + memberships + [outcome_membership])
    qcadata_wn=QcaDataset(indata=with_negated)
    
    nec_conds = []
    if len( causal_conds) > 0:
        # if argument list is empty, test all possible combinations of conds
        causal_conditions = qcadata_wn.causal_conds()
        for i in range(1, len(causal_conditions)+1):
            # make sure to keep processing pending events in the event loop
            QCoreApplication.processEvents()
            test_conditions = itertools.combinations(causal_conditions, i)
            for condition in test_conditions:
                for nec_cond in nec_conds:  # ignore supersets of found
                                            # necessary conditions (e.g.,
                                            # if 'A' is a necessary
                                            # condition, 'A+B' is as well)
                    if set(nec_cond) < set(condition):
                        break
                else:
                    # ignore combinations with tautologies (e.g., A and a)
                    condition_upper = [term.upper() for term in condition]
                    for term in condition_upper:
                        if condition_upper.count(term) > 1:
                            break
                    else:
                        # test if this condition is a necessary condition
                        if qcadata_wn.isnec(condition, consist_thresh):
                            nec_conds.append(condition)
    return (concov_nec(qcadata, nec_conds, consist_thresh, cov_thresh))

