__copyright__ = "Copyright 2014 Davide Olianas (ubuntupk@gmail.com)."

__license__ = """This file is part of poliBeePsync.
poliBeePsync is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
any later version.

poliBeePsync 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with poliBeePsync. If not, see <http://www.gnu.org/licenses/>.
"""

from polibeepsync import filesettings
from requests import ConnectionError, Timeout
from appdirs import user_config_dir, user_data_dir
import os
import pickle
import sys
from polibeepsync.common import User, InvalidLoginError
from PySide.QtCore import (QThread, QObject, Signal, QAbstractTableModel,
                           QModelIndex, Qt, Slot, QTimer, QLocale, QPoint)
from PySide.QtGui import (QApplication, QWidget, QTextCursor,
                          QMenu, QAction, QFileDialog,
                          QVBoxLayout, QLabel, QSystemTrayIcon,
                          qApp, QDialog)

from polibeepsync.ui_resizable import Ui_Form
import re
import logging
import sys

LEVELS = {'debug': logging.DEBUG,
          'info': logging.INFO,
          'warning': logging.WARNING,
          'error': logging.ERROR,
          'critical': logging.CRITICAL,
}

if len(sys.argv) > 1:
    level_name = sys.argv[1]
    level = LEVELS.get(level_name, logging.NOTSET)
    logging.basicConfig(level=level)


def read(*names, **kwargs):
    with open(
            os.path.join(os.path.dirname(__file__), *names),
            encoding=kwargs.get("encoding", "utf8")
    ) as fp:
        return fp.read()


def find_version(*file_paths):
    version_file = read(*file_paths)
    version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]",
                              version_file, re.M)
    if version_match:
        return version_match.group(1)
    raise RuntimeError("Unable to find version string.")


__version__ = find_version("__init__.py")


class MySignal(QObject):
    sig = Signal(str)


class CoursesSignal(QObject):
    sig = Signal(list)


class DownloadThread(QThread):
    def __init__(self, user, topdir, parent=None):
        QThread.__init__(self, parent)
        self.exiting = False
        self.course_finished = MySignal()
        self.signal_error = MySignal()
        self.user = user
        self.topdir = topdir
        self.dumpuser = MySignal()

    def run(self):
        while self.exiting == False:
            for course in self.user.available_courses:
                subdir = course.save_folder_name
                if course.sync == True:
                    try:
                        self.user.update_course_files(course)
                        outdir = os.path.join(self.topdir, subdir)
                        os.makedirs(outdir, exist_ok=True)
                        rootdir = self.user.find_files_and_folders(
                            course.documents_url, 'rootfolder')
                        self.user.save_files(rootdir, outdir)
                        self.dumpuser.sig.emit('User object changed')
                        text = "Synced files for {}".format(course.name)
                        self.course_finished.sig.emit(text)
                    except Exception as err:
                        self.signal_error.sig.emit(str(err))
                        logging.critical(str(err))
            self.exiting = True


class LoginThread(QThread):
    def __init__(self, user, parent=None):
        QThread.__init__(self, parent)
        self.exiting = False
        self.signal_ok = MySignal()
        self.signal_error = MySignal()
        self.user = user

    def run(self):
        while self.exiting == False:
            try:
                self.user.logout()
                self.user.login()
                if self.user.logged == True:
                    self.exiting = True
                    self.signal_ok.sig.emit('Successful login.')
            except IndexError:
                logging.critical(str(err))
                self.exiting = True
                self.signal_error.sig.emit('Already logged-in.')
            except InvalidLoginError:
                self.user.logout()
                self.exiting = True
                self.signal_error.sig.emit('Login failed.')
                logging.critical(str(err))
            except ConnectionError as err:
                self.user.logout()
                self.exiting = True
                self.signal_error.sig.emit('I can\'t connect to the server.'
                                           ' Is the Internet connection working?')
                logging.critical(str(err))
            except Timeout as err:
                self.user.logout()
                self.exiting = True
                self.signal_error.sig.emit("The timeout time has been reached."
                                           " Is the Internet connection working?")
                logging.critical(str(err))

            except Exception as err:
                self.user.logout()
                self.exiting = True
                self.signal_error.sig.emit("An error occurred.")
                logging.critical(str(err))


class RefreshCoursesThread(QThread):
    def __init__(self, user, parent=None):
        QThread.__init__(self, parent)
        self.exiting = False
        self.refreshed = MySignal()
        self.dumpuser = MySignal()
        self.newcourses = CoursesSignal()
        self.removable = CoursesSignal()
        self.user = user

    def run(self):
        while self.exiting == False:
            most_recent = self.user.get_online_courses()
            last = self.user.available_courses
            new = most_recent - last
            removable = last - most_recent
            if len(removable) > 0:
                self.refreshed.sig.emit('The following courses have'
                                        ' been removed because they '
                                        'aren\'t available online: {}'.format(
                    removable))
            if len(new) > 0:
                for course in new:
                    course.save_folder_name = course.simplify_name(course.name)
                    self.refreshed.sig.emit('A new course '
                                            'was found: {}'.format(course))
            if len(new) == 0:
                self.refreshed.sig.emit('No new courses found.')
            self.user.sync_available_courses(most_recent)
            self.dumpuser.sig.emit('User object changed')
            self.newcourses.sig.emit(new)
            self.removable.sig.emit(removable)
            self.exiting = True
            # nel main thread chaimare dumpUser()
            # e emettere segnale che passa new e removable

            # for course in new:
            # self.courses_model.insertRows(0, 1, course)
            #for course in removable:
            #    index = self.courses_model.courses.index(course)
            #    self.courses_model.removeRows(index, 1)


class CoursesListModel(QAbstractTableModel):
    def __init__(self, courses):
        QAbstractTableModel.__init__(self)
        # il mio è un mapping, mentre con tableview viene più comodo avere indici
        self.courses = list(courses)

    def rowCount(self, parent=QModelIndex()):
        return len(self.courses)

    def columnCount(self, parent=QModelIndex()):
        return 3

    def insertRows(self, position, rows, newcourse, parent=QModelIndex()):
        self.beginInsertRows(parent, position, position + rows - 1)
        for row in range(rows):
            self.courses.insert(position, newcourse)
        self.endInsertRows()
        return True

    def removeRows(self, position, rows, parent=QModelIndex()):
        self.beginRemoveRows(parent, position, position + rows - 1)
        for row in range(rows):
            del self.courses[position]
        self.endRemoveRows()
        return True

    def flags(self, index):
        if index.column() == 2:
            flags = Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable
            return flags
        elif index.column() == 1:
            flags = Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable \
                    | Qt.ItemIsUserCheckable
            return flags
        else:
            return Qt.ItemIsEnabled

    def setData(self, index, value, role=Qt.EditRole):
        if role == Qt.EditRole:
            if index.column() == 2:
                other_names = [elem.save_folder_name for elem in self.courses]
                if value not in other_names:
                    self.courses[index.row()].save_folder_name = value
                    self.dataChanged.emit(index, index)
                return True
            elif index.column() == 1:
                self.courses[index.row()].sync = value
                self.dataChanged.emit(index, index)
                return True
        return False


    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            if index.column() == 0:
                return self.courses[index.row()].name
            if index.column() == 1:
                return self.courses[index.row()].sync
            if index.column() == 2:
                return self.courses[index.row()].save_folder_name
        elif role == Qt.CheckStateRole:
            return None

    def headerData(self, col, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            if col == 0:
                return "Name"
            elif col == 1:
                return "Sync"
            elif col == 2:
                return "Save as"


class MainWindow(QWidget, Ui_Form):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.appname = "poliBeePsync"
        self.settings_fname = 'pbs-settings.ini'
        self.data_fname = 'pbs.data'
        self.setupUi(self)
        self.w = QWidget()
        self.createTray()
        self.about.clicked.connect(self.about_box)
        self.timer = QTimer(self)

        self.load_settings()
        self.load_data()

        self.timer.timeout.connect(self.syncfiles)
        self.timer.start(1000 * 60 * int(self.settings['UpdateEvery']))

        self.loginthread = LoginThread(self.user)
        self.loginthread.signal_error.sig.connect(self.myStream_message)
        self.loginthread.signal_error.sig.connect(self.loginstatus)
        self.loginthread.signal_ok.sig.connect(self.myStream_message)
        self.loginthread.signal_ok.sig.connect(self.loginstatus)

        self.refreshcoursesthread = RefreshCoursesThread(self.user)
        self.refreshcoursesthread.dumpuser.sig.connect(self.dumpUser)
        self.refreshcoursesthread.newcourses.sig.connect(self.addtocoursesview)
        self.refreshcoursesthread.newcourses.sig.connect(self.syncnewcourses)
        self.refreshcoursesthread.refreshed.sig.connect(self.myStream_message)
        self.refreshcoursesthread.removable.sig.connect(self.rmfromcoursesview)

        self.downloadthread = DownloadThread(self.user,
                                             self.settings['RootFolder'])
        self.downloadthread.dumpuser.sig.connect(self.dumpUser)
        self.downloadthread.course_finished.sig.connect(self.myStream_message)
        self.downloadthread.signal_error.sig.connect(self.myStream_message)

        self.userCode.setText(str(self.user.username))
        self.userCode.textEdited.connect(self.setusercode)
        self.password.setText(self.user.password)
        self.password.textEdited.connect(self.setpassword)
        self.trylogin.clicked.connect(self.testlogin)

        self.courses_model = CoursesListModel(self.user.available_courses)
        self.coursesView.setModel(self.courses_model)
        self.refreshCourses.clicked.connect(self.refreshcourses)
        self.courses_model.dataChanged.connect(self.dumpUser)
        self.syncNow.clicked.connect(self.syncfiles)

        if self.settings['SyncNewCourses'] == str(True):
            self.sync_new = Qt.Checked
        else:
            self.sync_new = Qt.Unchecked

        # if self.settings['NotifyNewCourses'] == str(True):
        # self.notify_new = Qt.Checked
        #else:
        #    self.notify_new = Qt.Unchecked

        self.rootfolder.setText(self.settings['RootFolder'])
        self.rootfolder.textChanged.connect(self.rootfolderslot)

        #self.notifyNewCourses.setCheckState(self.notify_new)

        #self.notifyNewCourses.stateChanged.connect(self.notifynew)


        self.addSyncNewCourses.setCheckState(self.sync_new)
        self.addSyncNewCourses.stateChanged.connect(self.syncnewslot)

        self.timerMinutes.setValue(int(self.settings['UpdateEvery']))
        self.timerMinutes.valueChanged.connect(self.updateminuteslot)
        #self.timerMinutes.valueChanged.connect(STUFF)

        self.changeRootFolder.clicked.connect(self.chooserootdir)

    def syncnewcourses(self, newlist):
        if self.settings['SyncNewCourses'] == 'True':
            for elem in newlist:
                elem.sync = True


    def load_settings(self):
        for path in [user_config_dir(self.appname),
                     user_data_dir(self.appname)]:
            try:
                os.makedirs(path, exist_ok=True)
            except OSError as err:
                logging.critical(str(err))
                if not os.path.isdir(path):
                    self.myStream_message(str(err))
        self.settings_path = os.path.join(user_config_dir(self.appname),
                                          self.settings_fname)
        defaults = {
            'UpdateEvery': '60',
            'RootFolder': os.path.join(os.path.expanduser('~'), self.appname),
            'SyncNewCourses': 'False'
        }
        self.settings = filesettings.settingsFromFile(self.settings_path,
                                                      defaults)

    def load_data(self):
        try:
            with open(os.path.join(user_data_dir(self.appname),
                                   self.data_fname), 'rb') as f:
                self.user = pickle.load(f)
                self.myStream_message("Data has been loaded successfully.")
        except FileNotFoundError as err:
            logging.critical(str(err))
            self.user = User('', '')
            complete_message = str(err) + " ".join([
                "\nThis error means that no data can be found in the predefined",
                "directory. Ignore this if you're using poliBeePsync for "
                "the first time."])
            self.myStream_message(complete_message)
        except Exception as err:
            logging.critical(str(err))
            self.user = User('', '')
            self.myStream_message(str(err))

    def loginstatus(self, status):
        self.login_attempt.setText(status)

    # @Slot(int)
    # def notifynew(self, state):
    #    if state == 2:
    #        self.settings['NotifyNewCourses'] = 'True'
    #    else:
    #        self.settings['NotifyNewCourses'] = 'False'
    #    filesettings.settingsToFile(self.settings, self.settings_path)

    @Slot(int)
    def syncnewslot(self, state):
        if state == 2:
            self.settings['SyncNewCourses'] = 'True'
        else:
            self.settings['SyncNewCourses'] = 'False'
        filesettings.settingsToFile(self.settings, self.settings_path)

    @Slot(int)
    def updateminuteslot(self, minutes):
        self.settings['UpdateEvery'] = str(minutes)
        filesettings.settingsToFile(self.settings, self.settings_path)
        self.timer.start(1000 * 60 * int(self.settings['UpdateEvery']))

    @Slot(str)
    def rootfolderslot(self, path):
        self.settings['RootFolder'] = path
        filesettings.settingsToFile(self.settings, self.settings_path)


    def chooserootdir(self):
        currentdir = self.settings['RootFolder']
        flags = QFileDialog.DontResolveSymlinks | QFileDialog.ShowDirsOnly
        newroot = QFileDialog.getExistingDirectory(None,
                                                   "Open Directory",
                                                   currentdir, flags)
        if newroot != "" and str(newroot) != currentdir:
            self.settings['RootFolder'] = str(newroot)
            filesettings.settingsToFile(self.settings, self.settings_path)
            self.rootfolder.setText(newroot)
            # we delete the already present downloadthread and recreate it
            # because otherwise it uses the old download folder. I don't know
            # if there's a cleaner approach
            del self.downloadthread
            self.downloadthread = DownloadThread(self.user,
                                                 self.settings['RootFolder'])
            self.downloadthread.dumpuser.sig.connect(self.dumpUser)
            self.downloadthread.course_finished.sig.connect(
                self.myStream_message)
            self.downloadthread.signal_error.sig.connect(self.myStream_message)


    def setusercode(self):
        newcode = self.userCode.text()
        self.user.username = newcode
        try:
            self.dumpUser()
            if len(newcode) == 8:
                self.myStream_message("User code changed to {}."
                                      .format(newcode))
        except Exception as err:
            self.myStream_message(str(err))
            logging.critical(str(err))


    def setpassword(self):
        newpass = self.password.text()
        self.user.password = newpass
        try:
            self.dumpUser()
            self.myStream_message("Password changed.")
        except Exception as err:
            self.myStream_message(str(err))
            logging.critical(str(err))

    def testlogin(self):
        if not self.loginthread.isRunning():
            self.loginthread.exiting = False
            self.loginthread.start()
            self.login_attempt.setStyleSheet("color: rgba(0, 0, 0, 255);")
            self.login_attempt.setText("Logging in, please wait.")

    def addtocoursesview(self, addlist):
        for elem in addlist:
            self.courses_model.insertRows(0, 1, elem)

    def rmfromcoursesview(self, removelist):
        for elem in removelist:
            index = self.courses_model.courses.index(elem)
            self.courses_model.removeRows(index, 1)


    def dumpUser(self):
        # we don't use the message...
        with open(os.path.join(user_data_dir(self.appname),
                               self.data_fname), 'wb') as f:
            pickle.dump(self.user, f)

    def refreshcourses(self):
        if not self.loginthread.isRunning():
            self.loginthread.exiting = False
            self.loginthread.signal_ok.sig.connect(self.do_refreshcourses)
            self.loginthread.start()

    def do_refreshcourses(self):
        self.loginthread.signal_ok.sig.disconnect(self.do_refreshcourses)
        if not self.refreshcoursesthread.isRunning():
            self.refreshcoursesthread.exiting = False
            self.refreshcoursesthread.start()

    @Slot()
    def syncfiles(self):
        self.refreshcoursesthread.finished.connect(self.do_syncfiles)
        self.refreshcourses()

    def do_syncfiles(self):
        self.refreshcoursesthread.finished.disconnect(self.do_syncfiles)
        if not self.downloadthread.isRunning():
            self.downloadthread.exiting = False
            self.downloadthread.start()


    @Slot(str)
    def myStream_message(self, message):
        self.status.moveCursor(QTextCursor.End)
        self.status.insertPlainText(message + "\n\n")

    def createTray(self):
        restoreAction = QAction("&Restore", self, triggered=self.showNormal)
        quitAction = QAction("&Quit", self, triggered=qApp.quit)
        trayIconMenu = QMenu()
        trayIconMenu.addAction(restoreAction)
        trayIconMenu.addAction(quitAction)
        trayIcon = QSystemTrayIcon(self.icon, self.w)
        trayIcon.setContextMenu(trayIconMenu)
        trayIcon.show()

    def closeEvent(self, event):
        self.hide()
        event.ignore()

    def about_box(self):
        Dialog = QDialog()
        Dialog.setObjectName("Dialog")
        Dialog.resize(379, 161)
        Dialog.setWindowIcon(self.icon)
        verticalLayout = QVBoxLayout(Dialog)
        verticalLayout.setObjectName("verticalLayout")
        label = QLabel(Dialog)
        label.setTextFormat(Qt.RichText)
        label.setOpenExternalLinks(True)
        label.setLocale(QLocale(QLocale.English, QLocale.UnitedStates))
        label.setScaledContents(True)
        label.setWordWrap(True)
        label.setObjectName("label")
        verticalLayout.addWidget(label)
        text = "<html><head/><body><p>poliBeePsync version {}.</p><p>poliBeePsync is a program written by Davide Olianas, released under GNU GPLv3+.</p><p><br/></p><p>More information is available on the <a href=\"http://www.davideolianas.com/polibeepsync\"><span style=\" text-decoration: underline; color:#0000ff;\">official website</span></a>.</p></body></html>".format(
            __version__)
        Dialog.setWindowTitle(
            QApplication.translate("Dialog", "About poliBeePsync", None,
                                   QApplication.UnicodeUTF8))
        label.setText(QApplication.translate("Dialog", text, None,
                                             QApplication.UnicodeUTF8))
        Dialog.exec_()


def main():
    app = QApplication(sys.argv)
    frame = MainWindow()
    frame.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
