import inspect
from PyQt4 import QtGui, QtCore, uic
import os.path
from queue import Queue
import sys

# Required to use resource file icons
# Compile the qrc file in terminal "pyrcc4.exe -py3 'icons.qrc' -o 'icon_rc.py'"
from icon_rc import *

from pySecMaster import maintenance, data_download

__author__ = 'Josh Schertz'
__copyright__ = 'Copyright (C) 2018 Josh Schertz'
__description__ = 'An automated system to store and maintain financial data.'
__email__ = 'josh[AT]joshschertz[DOT]com'
__license__ = 'GNU AGPLv3'
__maintainer__ = 'Josh Schertz'
__status__ = 'Development'
__url__ = 'https://joshschertz.com/'
__version__ = '1.5.0'

'''
    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/>.
'''


class MainWindow(QtGui.QMainWindow):

    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        # Set the default name of the ini file; used to load/save GUI settings
        self.ini_name = 'pySecMaster_gui.ini'

        # Load the GUI structure from the ui file
        uic.loadUi('main_gui.ui', self)

        # Establish all menu bar connections
        self.actionLoad_Settings.triggered.connect(lambda: self.select_restore())
        self.actionSave_Settings.triggered.connect(lambda: self.save_settings(self.ini_name))
        self.actionStart.triggered.connect(self.process)
        self.actionExit.triggered.connect(lambda: self.confirm_close(self.ini_name))
        self.actionPySecMaster.triggered.connect(lambda: self.open_url('https://github.com/camisatx/pySecMaster'))
        self.actionCSI_Data.triggered.connect(lambda: self.open_url('http://www.csidata.com/'))
        self.actionGoogle_Finance.triggered.connect(lambda: self.open_url('https://www.google.com/finance'))
        self.actionQuandl.triggered.connect(lambda: self.open_url('https://www.quandl.com/'))
        self.actionInstall_PostgreSQL.triggered.connect(lambda: self.open_url('http://www.postgresql.org/download/'))
        self.actionInstall_Psycopg.triggered.connect(lambda: self.open_url('http://initd.org/psycopg/docs/install.html'))
        self.actionJosh_Schertz.triggered.connect(lambda: self.open_url('https://joshschertz.com/'))

        # Establish all form button connections
        self.toolbtn_details.clicked.connect(self.txtbrwsr_details_toggle)
        self.btnbox_action.button(self.btnbox_action.Ok).\
            clicked.connect(self.process)
        self.btnbox_action.button(self.btnbox_action.Abort).\
            clicked.connect(self.worker_finished)
        self.btnbox_action.button(self.btnbox_action.Cancel).\
            clicked.connect(lambda: self.confirm_close(self.ini_name))

        # Set the default items for 'Quandl Databases'
        quandl_databases_index = self.cmb_tickers_quandl_db.findText('WIKI')
        self.cmb_tickers_quandl_db.setCurrentIndex(quandl_databases_index)

        # Hide the data fields if data won't be downloaded for them
        self.data_provider_toggle()
        # If 'Download Source' (Data tab) is changed, re-run the
        # data_provider_toggle method to re-process items
        self.cmb_data_source.currentIndexChanged.\
            connect(self.data_provider_toggle)
        self.cmb_data_source.currentIndexChanged.\
            connect(self.data_selection_toggle)

        # Modify the combobox items of 'Selection' (Data tab) to make sure it
        # only shows valid options.
        self.data_selection_toggle()

        # Hide the details text browser by default
        # ToDo: Doesn't hide at startup; .isVisible() always returned 'False'
        self.txtbrwsr_details_toggle()

        # Hide the Abort button; only show when pySecMaster function is running
        self.btnbox_action.button(self.btnbox_action.Abort).hide()
        # Change the default name from 'Abort' to 'Stop'
        self.btnbox_action.button(self.btnbox_action.Abort).setText('Stop')

        # ToDo: Integrate the progress bar
        self.progressBar.hide()

        # Load the prior settings if a ini files exists
        if os.path.isfile(self.ini_name):
            self.restore_settings(self.ini_name)

    def closeEvent(self, event):
        """
        closeEvent method is called when the user clicks window close button

        :param event: A default system variable specifying a user action (exit)
        """

        self.confirm_close(self.ini_name, event)

    def confirm_close(self, ini_name, event=None):
        """
        Popup message box requiring user consent to close program

        :param ini_name: String of the name of the ini file to save the
            settings to
        :param event: A Qt object that is only used via the closeEvent method
        """

        reply = QtGui.QMessageBox.question(self, 'Confirm Exit',
                                           'Do you want to save the current '
                                           'settings?',
                                           QtGui.QMessageBox.Yes |
                                           QtGui.QMessageBox.No |
                                           QtGui.QMessageBox.Cancel,
                                           QtGui.QMessageBox.Yes)

        if event:
            # Request originated from the closeEvent method
            if reply == QtGui.QMessageBox.Yes:
                self.save_settings(ini_name)
                event.accept()
            elif reply == QtGui.QMessageBox.No:
                event.accept()
            else:
                event.ignore()
        else:
            # Request originated from a specific exit feature
            if reply == QtGui.QMessageBox.Yes:
                self.save_settings(ini_name)
                sys.exit()
            elif reply == QtGui.QMessageBox.No:
                sys.exit()
            else:
                pass

    def data_provider_toggle(self):
        """
        Hides the data fields if the data won't be downloaded for them.
        """

        provider_selected = self.cmb_data_source.currentText()

        # The default interval is daily; all sources have daily data.
        intervals = ['daily']

        if provider_selected in ['google', 'yahoo']:
            # Downloading Google or Yahoo Finance data; hide Quandl options
            self.lbl_quandlkey.hide()
            self.lineedit_quandlkey.hide()
            self.lbl_tickers_quandl.hide()
            self.cmb_tickers_quandl.hide()
            self.lbl_tickers_quandl_db.hide()
            self.cmb_tickers_quandl_db.hide()

            # Set the data interval
            self.cmb_data_interval.clear()
            if provider_selected == 'google':
                google_intervals = ['daily', 'minute']
                self.cmb_data_interval.addItems(google_intervals)
                self.cmb_data_interval.setCurrentIndex(0)
            else:
                self.cmb_data_interval.addItems(intervals)
                self.cmb_data_interval.setCurrentIndex(0)

        elif provider_selected == 'quandl':
            # Downloading quandl data; hide all Google Fin options
            self.lbl_quandlkey.show()
            self.lineedit_quandlkey.show()
            self.lbl_tickers_quandl.show()
            self.cmb_tickers_quandl.show()
            self.lbl_tickers_quandl_db.show()
            self.cmb_tickers_quandl_db.show()

            # Set the data interval
            self.cmb_data_interval.clear()
            self.cmb_data_interval.addItems(intervals)
            self.cmb_data_interval.setCurrentIndex(0)

        else:
            raise NotImplementedError('%s is not implemented in the '
                                      'data_provider_toggle function within '
                                      'main_gui.py' % provider_selected)

    def data_selection_toggle(self):
        """
        Modify the combobox items of 'Selection' (Data tab) to make sure it
        only shows valid options. Each one of these options has explicit SQL
        queries established in the database_queries.query_codes function.
        """

        # The selected provider
        provider_selected = self.cmb_data_source.currentText()

        # The data selections for the currently selected data provider.
        google_fin_possible_selections = ['all', 'us_main',
                                          'us_main_no_end_date',
                                          'us_canada_london']
        google_default_selection = 1

        yahoo_fin_possible_selections = ['all', 'us_main',
                                         'us_main_no_end_date',
                                         'us_canada_london']
        yahoo_default_selection = 2

        quandl_possible_selections = ['wiki', 'goog', 'goog_us_main',
                                      'goog_us_main_no_end_date',
                                      'goog_us_canada_london']
        quandl_default_selection = 0

        self.cmb_data_selection.clear()
        if provider_selected == 'google':
            self.cmb_data_selection.addItems(google_fin_possible_selections)
            self.cmb_data_selection.setCurrentIndex(google_default_selection)
        elif provider_selected == 'yahoo':
            self.cmb_data_selection.addItems(yahoo_fin_possible_selections)
            self.cmb_data_selection.setCurrentIndex(yahoo_default_selection)
        elif provider_selected == 'quandl':
            self.cmb_data_selection.addItems(quandl_possible_selections)
            self.cmb_data_selection.setCurrentIndex(quandl_default_selection)
        else:
            raise NotImplementedError('%s is not implemented in the '
                                      'data_selection_toggle function within '
                                      'main_gui.py' % provider_selected)

    def onDataReady(self, string):
        """
        Special PyQt name; Write code output to txtbrwsr_details
        """
        # ToDo: Build functionality to handle stderr, using red font in GUI

        cursor = self.txtbrwsr_details.textCursor()
        cursor.movePosition(cursor.End)
        cursor.insertText(str(string))
        self.txtbrwsr_details.ensureCursorVisible()

    def open_url(self, url):
        """
        Open the provided url in the system default browser

        :param url: String of the url
        """

        print('Opening %s in the default browser' % (url,))
        q_url = QtCore.QUrl(url)
        if not QtGui.QDesktopServices.openUrl(q_url):
            QtGui.QMessageBox.warning(self, 'Open Url',
                                      'Could not open %s' % url)

    def process(self):
        """
        Invoke the thread worker, prepare the worker by providing it with the
        variables the function it's to run needs, and then pass the thread
        to the Worker class where it'll be executed.
        """

        # Determine if any of the postgres database options were not provided
        if (self.lineedit_admin_user.text() or
                self.lineedit_admin_password.text() or
                self.lineedit_name.text() or self.lineedit_user.text() or
                self.lineedit_password.text() or self.lineedit_host.text() or
                self.lineedit_port.text()) == '':
            raise ValueError('One or multiple database options were not '
                             'provided. Ensure there is a value in each field '
                             'within the PostgreSQL Database Options section.')

        # Determine if the Quandl API Key is required; if so, was it provided?
        if (self.cmb_data_source.currentText() in ['quandl'] and
                self.lineedit_quandlkey.text() == ''):
            raise ValueError('No Quandl API key provided')

        # # Depreciated when DB switched to PostgreSQL; kept for posterity
        # # Combine the directory path with the database name
        # db_link = os.path.abspath(os.path.join(self.lineedit_dbdir.text(),
        #                                        self.lineedit_dbname.text()))

        # PostgreSQL database options
        database_options = {'admin_user': self.lineedit_admin_user.text(),
                            'admin_password': self.lineedit_admin_password.text(),
                            'database': self.lineedit_name.text(),
                            'user': self.lineedit_user.text(),
                            'password': self.lineedit_password.text(),
                            'host': self.lineedit_host.text(),
                            'port': self.lineedit_port.text()}

        # Change the quandl database string to a list
        quandl_db_list = [self.cmb_tickers_quandl_db.currentText()]

        # ToDo: Add these source options as an interactive setup
        symbology_sources = ['csi_data', 'tsid', 'quandl_wiki', 'quandl_goog',
                             'seeking_alpha', 'yahoo']

        download_list = [{'source': self.cmb_data_source.currentText(),
                          'selection': self.cmb_data_selection.currentText(),
                          'interval': self.cmb_data_interval.currentText(),
                          'redownload_time': 60 * 60 * 12,
                          'data_process': 'replace',
                          'replace_days_back': 60,
                          'period': 60}]

        # Build the dictionary with all the pySecMaster settings
        settings_dict = {
            'database_options': database_options,
            'quandl_ticker_source': self.cmb_tickers_quandl.currentText(),
            'quandl_db_list': quandl_db_list,
            'download_list': download_list,
            'quandl_update_range': self.spinbx_settings_quandl_update.value(),
            'google_fin_update_range': self.spinbx_settings_csi_update.value(),
            'threads': self.spinbx_settings_threads.value(),
            'quandl_key': self.lineedit_quandlkey.text(),
            'symbology_sources': symbology_sources
        }

        self.thread_worker = QtCore.QThread()
        self.worker = Worker()
        self.worker.dataReady.connect(self.onDataReady)

        self.worker.moveToThread(self.thread_worker)

        # Stops the thread after the worker is done. To start it again, call
        #   thread.start()
        self.worker.finished.connect(self.thread_worker.quit)
        # ToDo: Figure out why worker_finished is unable to kill the thread
        # self.worker.finished.connect(self.worker_finished)
        self.worker.finished.connect(self.worker.deleteLater)

        # # Calls the Worker process directly, but it's difficult to send data
        # #     to the worker object from the main gui thread.
        # self.thread_worker.started.connect(self.worker.processA)
        # self.thread_worker.finished.connect(main().app.exit)

        # Tell the thread to start working
        self.thread_worker.start()

        # Invoke the Worker process with the ability of safely communicating
        #   with the worker through signals and slots. Worker must already be
        #   running in order for the process to be invoked. If you need to pass
        #   arguments to the worker process, add a "QtCore.Q_ARG(str, 'arg')"
        #   variable for each argument in the invokeMethod statement after
        #   the QueuedConnection variable. Only able to handle 10 arguments.
        # QtCore.Q_ARG(str, 'Hello'),
        # QtCore.Q_ARG(list, ['Hello', 0, 1]))
        QtCore.QMetaObject.invokeMethod(self.worker, 'pysecmaster',
                                        QtCore.Qt.QueuedConnection,
                                        QtCore.Q_ARG(dict, settings_dict))

        # Disable the 'Ok' button while the worker thread is running
        self.btnbox_action.button(self.btnbox_action.Ok).setEnabled(False)

        # ToDo: Figure out why worker_finished is unable to kill the thread
        # # Show the 'Stop' button and hide the 'Cancel' button
        # self.btnbox_action.button(self.btnbox_action.Abort).show()
        # self.btnbox_action.button(self.btnbox_action.Cancel).hide()

    def restore_settings(self, ini_name):
        """
        Technique structured from the code from: "https://stackoverflow.com
        /questions/23279125/python-pyqt4-functions-to-save-and-restore-ui-
        widget-values"

        :param ini_name: Name/path of the .ini file (Ex. pySecMaster_gui.ini)
        """

        settings = QtCore.QSettings(ini_name, QtCore.QSettings.IniFormat)

        for name, obj in inspect.getmembers(self):
            if isinstance(obj, QtGui.QComboBox):
                name = obj.objectName()
                value = str(settings.value(name))   # .toString())

                if value == "":
                    continue

                # Get the corresponding index for specified string in combobox
                index = obj.findText(value)
                # Check if the value exists, otherwise add it to the combobox
                if index == -1:
                    obj.insertItems(0, [value])
                    index = obj.findText(value)
                    obj.setCurrentIndex(index)
                else:
                    obj.setCurrentIndex(index)

            elif isinstance(obj, QtGui.QLineEdit):
                name = obj.objectName()
                value = str(settings.value(name))
                obj.setText(value)

            elif isinstance(obj, QtGui.QSpinBox):
                name = obj.objectName()
                value = int(settings.value(name))
                obj.setValue(value)

            elif isinstance(obj, QtGui.QCheckBox):
                name = obj.objectName()
                value = settings.value(name)
                if value:
                    obj.setChecked(value)   # setCheckState enables tristate

    def save_settings(self, ini_name):
        """
        Technique structured from the code from: "https://stackoverflow.com
        /questions/23279125/python-pyqt4-functions-to-save-and-restore-ui-
        widget-values"

        :param ini_name: Name of the .ini file (Ex. pysecmaster.ini)
        :return:
        """

        settings = QtCore.QSettings(ini_name, QtCore.QSettings.IniFormat)

        # For child in ui.children():  # works like getmembers, but because it
        # traverses the hierarchy, you would have to call the method recursively
        # to traverse down the tree.

        for name, obj in inspect.getmembers(self):
            if isinstance(obj, QtGui.QComboBox):
                name = obj.objectName()
                text = obj.currentText()
                settings.setValue(name, text)

            elif isinstance(obj, QtGui.QLineEdit):
                name = obj.objectName()
                value = obj.text()
                settings.setValue(name, value)

            elif isinstance(obj, QtGui.QSpinBox):
                name = obj.objectName()
                value = obj.value()
                settings.setValue(name, value)

            elif isinstance(obj, QtGui.QCheckBox):
                name = obj.objectName()
                state = obj.checkState()
                settings.setValue(name, state)

    def select_dir(self):
        """
        Opens a PyQt folder search. If a folder is selected, it will
        populate the db_dir text editor box.

        DEPRECIATED
        """

        db_dir = QtGui.QFileDialog.getExistingDirectory(self,
                                                        'Select Directory')
        if db_dir:
            self.lineedit_dbdir.setText(db_dir)

    def select_restore(self):
        """
        Opens a PyQt file search. If a file is selected, it will populate
        the gui settings with the values from the selected ini file.
        """

        file = QtGui.QFileDialog.getOpenFileName(self, 'Select Saved Settings',
                                                 '', 'INI (*.ini)')
        if file:
            self.restore_settings(file)

    def txtbrwsr_details_toggle(self):

        mw_size = [self.size().width(), self.size().height()]

        if self.txtbrwsr_details.isVisible():

            mw_size[1] -= self.txtbrwsr_details.size().height()
            self.txtbrwsr_details.hide()

            # Resize the main window
            while self.size().height() > mw_size[1]:
                QtGui.QApplication.sendPostedEvents()
                self.resize(mw_size[0], mw_size[1])

        else:
            self.txtbrwsr_details.show()

    def worker_finished(self):

        # Enable the 'Ok' button and change the Stop button back to Cancel
        self.btnbox_action.button(self.btnbox_action.Ok).setEnabled(True)
        # Hide the 'Stop' button and show the 'Cancel' button
        self.btnbox_action.button(self.btnbox_action.Abort).hide()
        self.btnbox_action.button(self.btnbox_action.Cancel).show()

        # ToDo: Figure out why none of these kill the thread...
        # Safely shut down the thread
        # self.thread_worker.quit()
        self.thread_worker.terminate()
        # self.thread_worker.wait()

        print('Current process has been halted.')


class Worker(QtCore.QObject):
    finished = QtCore.pyqtSignal()
    dataReady = QtCore.pyqtSignal(str)

    @QtCore.pyqtSlot(dict)
    def pysecmaster(self, settings_dict):
        """
        Calls the functions that operate the pySecMaster. Emits signals back to
        the main gui for further processing, using the dataReady process.

        :param settings_dict: Dictionary of all parameters to be passed back
        to the pySecMaster.py functions.
        """

        self.dataReady.emit('Building the pySecMaster in the %s database '
                            'located at host %s\n' %
                            (settings_dict['database_options']['database'],
                             settings_dict['database_options']['host']))

        maintenance(database_options=settings_dict['database_options'],
                    quandl_key=settings_dict['quandl_key'],
                    quandl_ticker_source=settings_dict['quandl_ticker_source'],
                    database_list=settings_dict['quandl_db_list'],
                    threads=settings_dict['threads'],
                    quandl_update_range=settings_dict['quandl_update_range'],
                    csidata_update_range=settings_dict['google_fin_update_range'],
                    symbology_sources=settings_dict['symbology_sources'])
        data_download(database_options=settings_dict['database_options'],
                      quandl_key=settings_dict['quandl_key'],
                      download_list=settings_dict['download_list'],
                      threads=settings_dict['threads'],
                      verbose=True)

        self.dataReady.emit('Finished running the pySecMaster process\n')
        self.finished.emit()


class StdoutQueue(object):
    """
    This is a queue that acts like the default system standard output (stdout)
    """

    def __init__(self, queue):
        self.queue = queue

    def write(self, string):
        self.queue.put(string)

    def flush(self):
        sys.__stdout__.flush()


class Receiver(QtCore.QObject):
    """
    A QObject (to be run in a QThread) that sits waiting for data to come
    through a Queue.Queue(). It blocks until data is available, and once it's
    received something from the queue, it sends it to the "MainThread" by
    emitting a Qt Signal.
    """

    signal = QtCore.pyqtSignal(str)

    def __init__(self, queue, *args, **kwargs):
        QtCore.QObject.__init__(self, *args, **kwargs)
        self.queue = queue

    @QtCore.pyqtSlot()
    def run(self):
        while True:
            text = self.queue.get()
            self.signal.emit(text)


def main():

    # Create Queue and redirect sys.stdout to this queue
    queue = Queue()
    sys.stdout = StdoutQueue(queue)

    # Start the main GUI class
    app = QtGui.QApplication(sys.argv)
    form = MainWindow()
    form.show()

    # Create thread that will listen for new strings in the queue. Upon new
    #   items, Receiver will emit a signal, which will be sent to the
    #   onDataReady method in the MainWindow class. The onDataReady method
    #   will add the string to the text editor in the GUI.
    thread = QtCore.QThread()
    receiver = Receiver(queue)
    receiver.signal.connect(form.onDataReady)
    receiver.moveToThread(thread)
    thread.started.connect(receiver.run)
    thread.start()

    sys.exit(app.exec_())

if __name__ == '__main__':

    main()