#!/usr/bin/env python3

import json
import os
import sys
from datetime import datetime
from fnmatch import fnmatch

import dottorrent
import humanfriendly
from PyQt5 import QtCore, QtGui, QtWidgets

from dottorrentGUI import Ui_AboutDialog, Ui_MainWindow, __version__


PROGRAM_NAME = "dottorrent-gui"
PROGRAM_NAME_VERSION = "{} {}".format(PROGRAM_NAME, __version__)
CREATOR = "dottorrent-gui/{} (https://github.com/kz26/dottorrent-gui)".format(
    __version__)

PIECE_SIZES = [None] + [2 ** i for i in range(14, 27)]

if getattr(sys, 'frozen', False):
    _basedir = sys._MEIPASS
else:
    _basedir = os.path.dirname(__file__)


class CreateTorrentQThread(QtCore.QThread):

    progress_update = QtCore.pyqtSignal(str, int, int)
    onError = QtCore.pyqtSignal(str)

    def __init__(self, torrent, save_path):
        super().__init__()
        self.torrent = torrent
        self.save_path = save_path

    def run(self):
        def progress_callback(*args):
            self.progress_update.emit(*args)
            return self.isInterruptionRequested()

        self.torrent.creation_date = datetime.now()
        self.torrent.created_by = CREATOR
        try:
            self.success = self.torrent.generate(callback=progress_callback)
        except Exception as exc:
            self.onError.emit(str(exc))
            return
        if self.success:
            with open(self.save_path, 'wb') as f:
                self.torrent.save(f)


class CreateTorrentBatchQThread(QtCore.QThread):

    progress_update = QtCore.pyqtSignal(str, int, int)
    onError = QtCore.pyqtSignal(str)

    def __init__(self, path, exclude, save_dir, trackers, web_seeds,
                 private, source, comment, include_md5):
        super().__init__()
        self.path = path
        self.exclude = exclude
        self.save_dir = save_dir
        self.trackers = trackers
        self.web_seeds = web_seeds
        self.private = private
        self.source = source
        self.comment = comment
        self.include_md5 = include_md5

    def run(self):
        def callback(*args):
            return self.isInterruptionRequested()

        entries = os.listdir(self.path)
        for i, p in enumerate(entries):
            if any(fnmatch(p, ex) for ex in self.exclude):
                continue
            p = os.path.join(self.path, p)
            if not dottorrent.is_hidden_file(p):
                sfn = os.path.split(p)[1] + '.torrent'
                self.progress_update.emit(sfn, i, len(entries))
                t = dottorrent.Torrent(
                    p,
                    exclude=self.exclude,
                    trackers=self.trackers,
                    web_seeds=self.web_seeds,
                    private=self.private,
                    source=self.source,
                    comment=self.comment,
                    include_md5=self.include_md5,
                    creation_date=datetime.now(),
                    created_by=CREATOR
                )
                try:
                    self.success = t.generate(callback=callback)
                # ignore empty inputs
                except dottorrent.exceptions.EmptyInputException:
                    continue
                except Exception as exc:
                    self.onError.emit(str(exc))
                    return
                if self.isInterruptionRequested():
                    return
                if self.success:
                    with open(os.path.join(self.save_dir, sfn), 'wb') as f:
                        t.save(f)


class DottorrentGUI(Ui_MainWindow):

    def setupUi(self, MainWindow):
        super().setupUi(MainWindow)

        self.torrent = None
        self.MainWindow = MainWindow

        self.actionImportProfile.triggered.connect(self.import_profile)
        self.actionExportProfile.triggered.connect(self.export_profile)
        self.actionAbout.triggered.connect(self.showAboutDialog)
        self.actionQuit.triggered.connect(self.MainWindow.close)

        self.fileRadioButton.toggled.connect(self.inputModeToggle)
        self.fileRadioButton.setChecked(True)
        self.directoryRadioButton.toggled.connect(self.inputModeToggle)

        self.browseButton.clicked.connect(self.browseInput)
        self.batchModeCheckBox.stateChanged.connect(self.batchModeChanged)

        self.inputEdit.dragEnterEvent = self.inputDragEnterEvent
        self.inputEdit.dropEvent = self.inputDropEvent
        self.pasteButton.clicked.connect(self.pasteInput)

        self.pieceCountLabel.hide()
        self.pieceSizeComboBox.addItem('Auto')
        for x in PIECE_SIZES[1:]:
            self.pieceSizeComboBox.addItem(
                humanfriendly.format_size(x, binary=True))

        self.pieceSizeComboBox.currentIndexChanged.connect(
            self.pieceSizeChanged)

        self.privateTorrentCheckBox.stateChanged.connect(
            self.privateTorrentChanged)

        self.commentEdit.textEdited.connect(
            self.commentEdited)

        self.sourceEdit.textEdited.connect(
            self.sourceEdited)

        self.md5CheckBox.stateChanged.connect(
            self.md5Changed)

        self.progressBar.hide()
        self.createButton.setEnabled(False)
        self.createButton.clicked.connect(self.createButtonClicked)
        self.cancelButton.hide()
        self.cancelButton.clicked.connect(self.cancel_creation)
        self.resetButton.clicked.connect(self.reset)

        self._statusBarMsg('Ready')

    def getSettings(self):
        portable_fn = PROGRAM_NAME + '.ini'
        portable_fn = os.path.join(_basedir, portable_fn)
        if os.path.exists(portable_fn):
            return QtCore.QSettings(
                portable_fn,
                QtCore.QSettings.IniFormat
            )
        return QtCore.QSettings(
            QtCore.QSettings.IniFormat,
            QtCore.QSettings.UserScope,
            PROGRAM_NAME,
            PROGRAM_NAME
        )

    def loadSettings(self):
        settings = self.getSettings()
        if settings.value('input/mode') == 'directory':
            self.directoryRadioButton.setChecked(True)
        batch_mode = bool(int(settings.value('input/batch_mode') or 0))
        self.batchModeCheckBox.setChecked(batch_mode)
        exclude = settings.value('input/exclude')
        if exclude:
            self.excludeEdit.setPlainText(exclude)
        trackers = settings.value('seeding/trackers')
        if trackers:
            self.trackerEdit.setPlainText(trackers)
        web_seeds = settings.value('seeding/web_seeds')
        if web_seeds:
            self.webSeedEdit.setPlainText(web_seeds)
        private = bool(int(settings.value('options/private') or 0))
        self.privateTorrentCheckBox.setChecked(private)
        source = settings.value('options/source')
        if source:
            self.sourceEdit.setText(source)
        compute_md5 = bool(int(settings.value('options/compute_md5') or 0))
        if compute_md5:
            self.md5CheckBox.setChecked(compute_md5)
        mainwindow_size = settings.value("geometry/size")
        if mainwindow_size:
            self.MainWindow.resize(mainwindow_size)
        mainwindow_position = settings.value("geometry/position")
        if mainwindow_position:
            self.MainWindow.move(mainwindow_position)
        self.last_input_dir = settings.value('history/last_input_dir') or None
        self.last_output_dir = settings.value(
            'history/last_output_dir') or None

    def saveSettings(self):
        settings = self.getSettings()
        settings.setValue('input/mode', self.inputMode)
        settings.setValue('input/batch_mode', int(self.batchModeCheckBox.isChecked()))
        settings.setValue('input/exclude', self.excludeEdit.toPlainText())
        settings.setValue('seeding/trackers', self.trackerEdit.toPlainText())
        settings.setValue('seeding/web_seeds', self.webSeedEdit.toPlainText())
        settings.setValue('options/private',
                          int(self.privateTorrentCheckBox.isChecked()))
        settings.setValue('options/source', self.sourceEdit.text())
        settings.setValue('options/compute_md5', int(self.md5CheckBox.isChecked()))
        settings.setValue('geometry/size', self.MainWindow.size())
        settings.setValue('geometry/position', self.MainWindow.pos())
        if self.last_input_dir:
            settings.setValue('history/last_input_dir', self.last_input_dir)
        if self.last_output_dir:
            settings.setValue('history/last_output_dir', self.last_output_dir)

    def _statusBarMsg(self, msg):
        self.MainWindow.statusBar().showMessage(msg)

    def _showError(self, msg):
        errdlg = QtWidgets.QErrorMessage()
        errdlg.setWindowTitle('Error')
        errdlg.showMessage(msg)
        errdlg.exec_()

    def showAboutDialog(self):
        qdlg = QtWidgets.QDialog()
        ad = Ui_AboutDialog()
        ad.setupUi(qdlg)
        ad.programVersionLabel.setText("version {}".format(__version__))
        ad.dtVersionLabel.setText("(dottorrent {})".format(
            dottorrent.__version__))
        qdlg.exec_()

    def inputModeToggle(self):
        if self.fileRadioButton.isChecked():
            self.inputMode = 'file'
            self.batchModeCheckBox.setEnabled(False)
            self.batchModeCheckBox.hide()
        else:
            self.inputMode = 'directory'
            self.batchModeCheckBox.setEnabled(True)
            self.batchModeCheckBox.show()
        self.inputEdit.setText('')

    def browseInput(self):
        qfd = QtWidgets.QFileDialog(self.MainWindow)
        if self.last_input_dir and os.path.exists(self.last_input_dir):
            qfd.setDirectory(self.last_input_dir)
        if self.inputMode == 'file':
            qfd.setWindowTitle('Select file')
            qfd.setFileMode(QtWidgets.QFileDialog.ExistingFile)
        else:
            qfd.setWindowTitle('Select directory')
            qfd.setFileMode(QtWidgets.QFileDialog.Directory)
        if qfd.exec_():
            fn = qfd.selectedFiles()[0]
            self.inputEdit.setText(fn)
            self.last_input_dir = os.path.split(fn)[0]
            self.initializeTorrent()

    def injectInputPath(self, path):
        if os.path.exists(path):
            if os.path.isfile(path):
                self.fileRadioButton.setChecked(True)
                self.inputMode = 'file'
                self.batchModeCheckBox.setCheckState(QtCore.Qt.Unchecked)
                self.batchModeCheckBox.setEnabled(False)
                self.batchModeCheckBox.hide()
            else:
                self.directoryRadioButton.setChecked(True)
                self.inputMode = 'directory'
                self.batchModeCheckBox.setEnabled(True)
                self.batchModeCheckBox.show()
            self.inputEdit.setText(path)
            self.last_input_dir = os.path.split(path)[0]
            self.initializeTorrent()

    def inputDragEnterEvent(self, event):
        if event.mimeData().hasUrls():
            urls = event.mimeData().urls()
            if len(urls) == 1 and urls[0].isLocalFile():
                event.accept()
                return
        event.ignore()

    def inputDropEvent(self, event):
        path = event.mimeData().urls()[0].toLocalFile()
        self.injectInputPath(path)

    def pasteInput(self):
        mimeData = self.clipboard().mimeData()
        if mimeData.hasText():
            path = mimeData.text().strip("'\"")
            self.injectInputPath(path)

    def batchModeChanged(self, state):
        if state == QtCore.Qt.Checked:
            self.pieceSizeComboBox.setCurrentIndex(0)
            self.pieceSizeComboBox.setEnabled(False)
            self.pieceCountLabel.hide()
        else:
            self.pieceSizeComboBox.setEnabled(True)
            if self.torrent:
                self.pieceCountLabel.show()

    def initializeTorrent(self):
        self.torrent = dottorrent.Torrent(self.inputEdit.text())
        try:
            t_info = self.torrent.get_info()
        except Exception as e:
            self.torrent = None
            self._showError(str(e))
            return
        ptail = os.path.split(self.torrent.path)[1]
        if self.inputMode == 'file':
            self._statusBarMsg(
                "{}: {}".format(ptail, humanfriendly.format_size(
                    t_info[0], binary=True)))
        else:
            self._statusBarMsg(
                "{}: {} files, {}".format(
                    ptail, t_info[1], humanfriendly.format_size(
                        t_info[0], binary=True)))
        self.pieceSizeComboBox.setCurrentIndex(0)
        self.updatePieceCountLabel(t_info[2], t_info[3])
        self.pieceCountLabel.show()
        self.createButton.setEnabled(True)

    def commentEdited(self, comment):
        if getattr(self, 'torrent', None):
            self.torrent.comment = comment

    def sourceEdited(self, source):
        if getattr(self, 'torrent', None):
            self.torrent.source = source

    def pieceSizeChanged(self, index):
        if getattr(self, 'torrent', None):
            self.torrent.piece_size = PIECE_SIZES[index]
            t_info = self.torrent.get_info()
            self.updatePieceCountLabel(t_info[2], t_info[3])

    def updatePieceCountLabel(self, ps, pc):
        ps = humanfriendly.format_size(ps, binary=True)
        self.pieceCountLabel.setText("{} pieces @ {} each".format(pc, ps))

    def privateTorrentChanged(self, state):
        if getattr(self, 'torrent', None):
            self.torrent.private = (state == QtCore.Qt.Checked)

    def md5Changed(self, state):
        if getattr(self, 'torrent', None):
            self.torrent.include_md5 = (state == QtCore.Qt.Checked)

    def createButtonClicked(self):
        self.torrent.exclude = self.excludeEdit.toPlainText().strip().splitlines()
        # Validate trackers and web seed URLs
        trackers = self.trackerEdit.toPlainText().strip().split()
        web_seeds = self.webSeedEdit.toPlainText().strip().split()
        try:
            self.torrent.trackers = trackers
            self.torrent.web_seeds = web_seeds
        except Exception as e:
            self._showError(str(e))
            return
        self.torrent.private = self.privateTorrentCheckBox.isChecked()
        self.torrent.comment = self.commentEdit.text() or None
        self.torrent.source = self.sourceEdit.text() or None
        self.torrent.include_md5 = self.md5CheckBox.isChecked()
        if self.inputMode == 'directory' and self.batchModeCheckBox.isChecked():
            self.createTorrentBatch()
        else:
            self.createTorrent()

    def createTorrent(self):
        if os.path.isfile(self.inputEdit.text()):
            save_fn = os.path.splitext(
                os.path.split(self.inputEdit.text())[1])[0] + '.torrent'
        else:
            save_fn = self.inputEdit.text().split(os.sep)[-1] + '.torrent'
        if self.last_output_dir and os.path.exists(self.last_output_dir):
            save_fn = os.path.join(self.last_output_dir, save_fn)
        fn = QtWidgets.QFileDialog.getSaveFileName(
            self.MainWindow, 'Save torrent', save_fn,
            filter=('Torrent file (*.torrent)'))[0]
        if fn:
            self.last_output_dir = os.path.split(fn)[0]
            self.creation_thread = CreateTorrentQThread(
                self.torrent,
                fn)
            self.creation_thread.started.connect(
                self.creation_started)
            self.creation_thread.progress_update.connect(
                self._progress_update)
            self.creation_thread.finished.connect(
                self.creation_finished)
            self.creation_thread.onError.connect(
                self._showError)
            self.creation_thread.start()

    def createTorrentBatch(self):
        save_dir = QtWidgets.QFileDialog.getExistingDirectory(
            self.MainWindow, 'Select output directory', self.last_output_dir)
        if save_dir:
            self.last_output_dir = save_dir
            trackers = self.trackerEdit.toPlainText().strip().split()
            web_seeds = self.webSeedEdit.toPlainText().strip().split()
            self.creation_thread = CreateTorrentBatchQThread(
                path=self.inputEdit.text(),
                exclude=self.excludeEdit.toPlainText().strip().splitlines(),
                save_dir=save_dir,
                trackers=trackers,
                web_seeds=web_seeds,
                private=self.privateTorrentCheckBox.isChecked(),
                source=self.sourceEdit.text(),
                comment=self.commentEdit.text(),
                include_md5=self.md5CheckBox.isChecked(),
            )
            self.creation_thread.started.connect(
                self.creation_started)
            self.creation_thread.progress_update.connect(
                self._progress_update_batch)
            self.creation_thread.finished.connect(
                self.creation_finished)
            self.creation_thread.onError.connect(
                self._showError)
            self.creation_thread.start()

    def cancel_creation(self):
        self.creation_thread.requestInterruption()

    def _progress_update(self, fn, pc, pt):
        fn = os.path.split(fn)[1]
        msg = "{} ({}/{})".format(fn, pc, pt)
        self.updateProgress(msg, int(round(100 * pc / pt)))

    def _progress_update_batch(self, fn, tc, tt):
        msg = "({}/{}) {}".format(tc, tt, fn)
        self.updateProgress(msg, int(round(100 * tc / tt)))

    def updateProgress(self, statusMsg, pv):
        self._statusBarMsg(statusMsg)
        self.progressBar.setValue(pv)

    def creation_started(self):
        self.inputGroupBox.setEnabled(False)
        self.seedingGroupBox.setEnabled(False)
        self.optionGroupBox.setEnabled(False)
        self.progressBar.show()
        self.createButton.hide()
        self.cancelButton.show()
        self.resetButton.setEnabled(False)

    def creation_finished(self):
        self.inputGroupBox.setEnabled(True)
        self.seedingGroupBox.setEnabled(True)
        self.optionGroupBox.setEnabled(True)
        self.progressBar.hide()
        self.createButton.show()
        self.cancelButton.hide()
        self.resetButton.setEnabled(True)
        if self.creation_thread.success:
            self._statusBarMsg('Finished')
        else:
            self._statusBarMsg('Canceled')
        self.creation_thread = None

    def export_profile(self):

        fn = QtWidgets.QFileDialog.getSaveFileName(
            self.MainWindow, 'Save profile', self.last_output_dir,
            filter=('JSON configuration file (*.json)'))[0]
        if fn:
            exclude = self.excludeEdit.toPlainText().strip().splitlines()
            trackers = self.trackerEdit.toPlainText().strip().split()
            web_seeds = self.webSeedEdit.toPlainText().strip().split()
            private = self.privateTorrentCheckBox.isChecked()
            compute_md5 = self.md5CheckBox.isChecked()
            source = self.sourceEdit.text()
            data = {
                'exclude': exclude,
                'trackers': trackers,
                'web_seeds': web_seeds,
                'private': private,
                'compute_md5': compute_md5,
                'source': source
            }
            with open(fn, 'w') as f:
                json.dump(data, f, indent=4, sort_keys=True)
            self._statusBarMsg("Profile saved to " + fn)

    def import_profile(self):
        fn = QtWidgets.QFileDialog.getOpenFileName(
            self.MainWindow, 'Open profile', self.last_input_dir,
            filter=('JSON configuration file (*.json)'))[0]
        if fn:
            with open(fn) as f:
                data = json.load(f)
            exclude = data.get('exclude', [])
            trackers = data.get('trackers', [])
            web_seeds = data.get('web_seeds', [])
            private = data.get('private', False)
            compute_md5 = data.get('compute_md5', False)
            source = data.get('source', '')
            try:
                self.excludeEdit.setPlainText(os.linesep.join(exclude))
                self.trackerEdit.setPlainText(os.linesep.join(trackers))
                self.webSeedEdit.setPlainText(os.linesep.join(web_seeds))
                self.privateTorrentCheckBox.setChecked(private)
                self.md5CheckBox.setChecked(compute_md5)
                self.sourceEdit.setText(source)
            except Exception as e:
                self._showError(str(e))
                return
            self._statusBarMsg("Profile {} loaded".format(
                os.path.split(fn)[1]))

    def reset(self):
        self._statusBarMsg('')
        self.createButton.setEnabled(False)
        self.fileRadioButton.setChecked(True)
        self.batchModeCheckBox.setChecked(False)
        self.inputEdit.setText(None)
        self.excludeEdit.setPlainText(None)
        self.trackerEdit.setPlainText(None)
        self.webSeedEdit.setPlainText(None)
        self.pieceSizeComboBox.setCurrentIndex(0)
        self.pieceCountLabel.hide()
        self.commentEdit.setText(None)
        self.privateTorrentCheckBox.setChecked(False)
        self.md5CheckBox.setChecked(False)
        self.sourceEdit.setText(None)
        self.torrent = None
        self._statusBarMsg('Ready')


def main():
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = DottorrentGUI()
    ui.setupUi(MainWindow)

    MainWindow.setWindowTitle(PROGRAM_NAME_VERSION)

    ui.loadSettings()
    ui.clipboard = app.clipboard
    app.aboutToQuit.connect(lambda: ui.saveSettings())
    MainWindow.show()
    sys.exit(app.exec_())


if __name__ == "__main__":
    main()