#!/usr/bin/env python

# inherent python libraries
from datetime import datetime, timezone, timedelta
from configparser import ConfigParser
from glob import glob
from functools import partial
import sys, os, math

# installs
from PyQt5.QtWidgets import (QApplication, QMainWindow, QDialog, QWidget, QVBoxLayout, QHBoxLayout, 
    QGroupBox, QLabel, QFileDialog, QToolButton, QLineEdit, QStyle, QSlider, QInputDialog, QMessageBox,
    QGridLayout, QCheckBox, QAction, QScrollArea, QComboBox, QPushButton)
from PyQt5.QtCore import Qt, QSize, QUrl, QCoreApplication
from PyQt5.QtGui import QMovie, QIntValidator, QIcon
from PyQt5.QtMultimedia import QMediaContent, QMediaPlayer
from PyQt5.QtMultimediaWidgets import QVideoWidget
from PyQt5 import uic
from google.cloud import translate_v2 as translate
from telethon import TelegramClient

# project files
sys.path.append(os.path.normpath(os.path.abspath(__file__) + (os.sep + os.pardir) * 2))
from Scraper import Scraper, EndRange, NoMedia, XPostThrowaway
import resources.images

# utc offset to get from QDateTimeEdit to UTC
#   (E.g. Belarus is UTC+3 so utcoffset of -3 is used here)
utcoffset = -3

# get path to resources folder
resource_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "resources")

# Telegram Scraper folder
tel_scrape_path = os.path.join(os.path.expanduser("~"), "Documents", "Thesis")

if not os.path.isdir(tel_scrape_path):
    os.mkdir(tel_scrape_path)

class Settings_window(QDialog):
    """A widget to hold information about the scraper's settings"""

    def __init__(self, *args, **kwargs):
        """Constructor"""

        super().__init__(*args, **kwargs)

        # load ui
        uic.loadUi(os.path.join(resource_path, "settings_window.ui"), self)

        self.x_posts_area.setWidgetResizable(True)
        # turn off scroll bar on horizontal
        self.x_posts_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        # setup scroll area layout
        self.x_post_layout = QVBoxLayout()
        self.chnls_grp = QGroupBox()
        self.chnls_grp.setLayout(self.x_post_layout)
        self.x_posts_area.setWidget(self.chnls_grp)

        # setup entries in scroll area
        self.excludes = []
        self.parse()

        # make sure minimum size is set (to stop horizontal scrolling)
        self.chnls_grp.setMinimumWidth(self.x_post_layout.itemAt(0).widget().minimumSizeHint().width())
        self.x_posts_area.setMinimumWidth(self.chnls_grp.minimumSizeHint().width())

        # get db info fields
        self.db_path = self.db_group.findChild(QLineEdit, "db_loc")
        self.db_edit = self.db_group.findChild(QToolButton, "db_edit")
        # connect db edit button
        self.db_edit.clicked.connect(self.edit_db_loc)

        # if a session file exists, use it
        if os.path.isfile(os.path.join(tel_scrape_path, "session.ini")):
            cf = ConfigParser()
            cf.read(os.path.join(tel_scrape_path, "session.ini"))

            # set channel name
            self.chnl.setText("t.me/"+cf["Scraper"]["chnl"])

            # set date range
            start = datetime.fromisoformat(cf["Scraper"]["start"])
            end = datetime.fromisoformat(cf["Scraper"]["end"])
            self.date_from.setDateTime(start)
            self.date_to.setDateTime(end)

            # set database location
            self.db_path.setText(cf["Settings"]["db_loc"])

            # set save media button
            self.media_sv.setChecked(cf["Settings"]["sv_media"] == "True")

            # set excludes
            excludes = cf["Settings"]["excludes"].split("|")
            # add excludes to widget list
            for excl in excludes:
                if excl != "":
                    # make a new widget
                    widge = QWidget()
                    # load ui
                    uic.loadUi(os.path.join(resource_path, "x_post_item.ui"), widge)
                    # connect delete button
                    rmv = lambda : (self.x_post_layout.removeWidget(widge),
                                    widge.setParent(None),
                                    self.parse())
                    # set text
                    widge.chnl_nm.setText(excl)
                    # lambda function for parsing after text edit
                    chng = lambda : self.parse()
                    widge.dlt_btn.clicked.connect(rmv)
                    # connect editingFinished in qlabel
                    widge.chnl_nm.editingFinished.connect(chng)
                    # add it to the scroll area
                    self.x_post_layout.addWidget(widge)
            # parse excludes
            self.parse()
        else:
            # check if there are any databases in default location
            dbs = glob(os.path.join(tel_scrape_path, "*.db"))
            if len(dbs) > 0: self.db_path.setText(dbs[0])
            else: self.db_path.setText(os.path.join(tel_scrape_path, "Project1.db"))

    def parse(self):
        """A method to parse all the cross post items"""

        self.excludes = []
        removes = []
        for idx in range(0, self.x_post_layout.count()):
            # get widget
            widge = self.x_post_layout.itemAt(idx).widget()
            # if we find a blank channel, add it to the list of widgets to remove
            if widge.chnl_nm.text() == "": removes.insert(0, idx)
            else:
                # if channel name doesn't start with t.me/, prepend it
                if not widge.chnl_nm.text().startswith("t.me/"):
                    widge.chnl_nm.setText("t.me/{}".format(widge.chnl_nm.text()))
                # add to list to exclude
                self.excludes.append(widge.chnl_nm.text())

        # if last field is not blank, make it so
        if len(removes) == 0 or removes[-1] != self.x_post_layout.count() -1:
            # make a new widget
            widge = QWidget()
            # load ui
            uic.loadUi(os.path.join(resource_path, "x_post_item.ui"), widge)
            # connect delete button
            rmv = lambda : (self.x_post_layout.removeWidget(widge),
                            widge.setParent(None),
                            self.parse())
            # lambda function for parsing after text edit
            chng = lambda : self.parse()
            widge.dlt_btn.clicked.connect(rmv)
            # connect editingFinished in qlabel
            widge.chnl_nm.editingFinished.connect(chng)
            # add it to the scroll area
            self.x_post_layout.addWidget(widge)
        # otherwise, don't remove last element
        else: removes.pop(-1)

        # remove blank elements. Remove is decreasing, so we don't need
        #   to worry about indexes changing as we remove elements
        for idx in removes:
            w = self.x_post_layout.itemAt(idx).widget()
            # disconnect slots
            w.dlt_btn.clicked.disconnect()
            w.chnl_nm.editingFinished.disconnect()
            # disconnect from parent
            w.setParent(None)
            # clear from layout
            self.x_post_layout.removeWidget(w)

    def edit_db_loc(self):
        """Method that's called when the edit db button is pressed"""

        filedialog = QFileDialog(self)
        # set to select save file
        filedialog.setAcceptMode(QFileDialog.AcceptOpen)
        # set filter to only show .db files
        filedialog.setNameFilters(["Databases (*.db)", "All files (*.*)"])
        filedialog.selectNameFilter("Databases (*.db)")
        # set default directory
        filedialog.setDirectory(tel_scrape_path)
        
        # if window was accepted
        if (filedialog.exec()):
            # pull filename
            filename = filedialog.selectedFiles()[0]
            self.db_path.setText(filename)

    def keyPressEvent(self, evt):
        """Override keyPressEvent to not close dialog box w/ enter"""

        # if key was enter or return, return
        if(evt.key() == Qt.Key_Enter or evt.key() == Qt.Key_Return): return
        # otherwise, behave normally
        super().keyPressEvent(evt)

class Filter_window(QDialog):
    """A class to handle filters"""

    def __init__(self, main_window, *args, **kwargs):
        """Constructor"""

        super().__init__(*args, **kwargs)

        # load ui
        uic.loadUi(os.path.join(resource_path, "Filter_window.ui"), self)
        
        # get hook to main widget so we can get scraper
        self.main = main_window

        # hold filter tags
        self.tags = []
        # hold tag widgets
        self.tag_widgs = {}

        # set up scroll area
        self.scroll_area = self.findChildren(QScrollArea)[0]
        self.filters = QGroupBox()
        self.scroll_area.setWidget(self.filters)
        self.filters.setLayout(QVBoxLayout())

        self.add_section = QWidget()
        self.add_section.setLayout(QHBoxLayout())
        self.add_section_tag = QComboBox()
        self.add_section_btn = QPushButton("add")
        self.add_section_btn.clicked.connect(self.add_tag)
        self.add_section.layout().addWidget(self.add_section_tag)
        self.add_section.layout().addWidget(self.add_section_btn)
        self.add_section.layout().setStretch(0, 5)
        self.add_section.layout().setStretch(1, 1)

        self.filters.layout().addWidget(self.add_section)

        # populate self
        self.parse_tags()

    def parse_tags(self):
        """A method to setup the tag box"""

        # if there's no scraper, erase the filter
        if self.main.scraper is None:
            for nm, widg in self.tag_widgs.items():
                widg.setParent(None)
            self.tag_widgs = {}
            return

        # otherwise, delete the tags that need to be deleted
        for nm, widg in self.tag_widgs.copy().items():
            if nm not in self.main.scraper.tags:
                _ = self.tag_widgs.pop(nm)
                widg.setParent(None)

        self.add_section_tag.clear()
        for tag, val in self.main.scraper.tags.items():
            self.add_section_tag.addItem(tag)

    def add_tag(self):
        """Method to add a tag"""

        tag = self.add_section_tag.currentText()
        if tag not in self.tag_widgs:
            self.tag_widgs[tag] = QWidget()
            uic.loadUi(os.path.join(resource_path, "filter_tag.ui"), self.tag_widgs[tag])
            self.tag_widgs[tag].tag_str.setText(tag)
            self.tag_widgs[tag].trash.clicked.connect(partial(self.remove_filter, tag))
            self.filters.layout().insertWidget(0, self.tag_widgs[tag])

    def remove_filter(self, tag):
        """Method to remove the filter on <tag>"""

        if not tag in self.tag_widgs: return

        widg = self.tag_widgs.pop(tag)
        widg.setParent(None)

    def exec_(self, *args):
        """Catch execute to copy tags, widgets and then revert/keep changes
            depending on cancel/ok button"""

        self.parse_tags()

        tags = {nm : widg.tag_val.currentIndex() for nm, widg in self.tag_widgs.items()}

        ret = super().exec_(*args)

        # if accepted, continue
        if ret:
            self.main.load_msg()
            return ret
        # otherwise, reset widgets
        else:
            for tag, widg in self.tag_widgs.copy().items():
                _ = self.tag_widgs.pop(tag)
                widg.setParent(None)
            for tag, val in tags.items():
                self.tag_widgs[tag] = QWidget()
                uic.loadUi(os.path.join(resource_path, "filter_tag.ui"), self.tag_widgs[tag])
                self.tag_widgs[tag].tag_str.setText(tag)
                self.tag_widgs[tag].tag_val.setCurrentIndex(val)
                self.tag_widgs[tag].trash.clicked.connect(partial(self.remove_filter, tag))
                self.filters.layout().insertWidget(0, self.tag_widgs[tag]) 

    def get_filters(self):
        """Returns a dictionary with filter"""

        if len(self.tag_widgs) > 0:
            return {tag:bool(widg.tag_val.currentIndex()) for tag, widg in self.tag_widgs.items()}
        else: return None

class Media_Player(QWidget):
    """A widget to create a media player
    
    modified from example at https://pythonprogramminglanguage.com/pyqt5-video-widget/
    """

    def __init__(self, *args, **kwargs):
        """Constructor"""

        super().__init__(*args, **kwargs)

        self.mediaPlayer = QMediaPlayer(None, QMediaPlayer.VideoSurface)

        self.videoWidget = QVideoWidget()

        self.playButton = QToolButton()
        self.playButton.setEnabled(False)
        self.playButton.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
        self.playButton.clicked.connect(self.play)

        self.positionSlider = QSlider(Qt.Horizontal)
        self.positionSlider.setRange(0, 0)
        self.positionSlider.sliderMoved.connect(self.setPosition)

        self.controlLayout = QHBoxLayout()
        self.controlLayout.setContentsMargins(0, 0, 0, 0)
        self.controlLayout.addWidget(self.playButton)
        self.controlLayout.addWidget(self.positionSlider)

        layout = QVBoxLayout()
        layout.addWidget(self.videoWidget)
        layout.addLayout(self.controlLayout)

        self.setLayout(layout)

        self.mediaPlayer.setVideoOutput(self.videoWidget)
        self.mediaPlayer.stateChanged.connect(self.mediaStateChanged)
        self.mediaPlayer.positionChanged.connect(self.positionChanged)
        self.mediaPlayer.durationChanged.connect(self.durationChanged)

    def openFile(self, path):
        """Opens the media file at the given path"""

        if path != '':
            self.mediaPlayer.setMedia(
                    QMediaContent(QUrl.fromLocalFile(path)))
            self.playButton.setEnabled(True)
        else: raise ValueError("Path can't be empty.")

    def play(self):
        """Plays/pauses the currently loaded video"""

        if self.mediaPlayer.state() == QMediaPlayer.PlayingState:
            self.mediaPlayer.pause()
        else:
            self.mediaPlayer.play()

    def mediaStateChanged(self, state):
        """Sets play/pause button depending on current video state"""

        if self.mediaPlayer.state() == QMediaPlayer.PlayingState:
            self.playButton.setIcon(
                    self.style().standardIcon(QStyle.SP_MediaPause))
        else:
            self.playButton.setIcon(
                    self.style().standardIcon(QStyle.SP_MediaPlay))

    def positionChanged(self, position):
        """Set slider for current position"""

        self.positionSlider.setValue(position)

    def durationChanged(self, duration):
        """Set slider for current position"""

        self.positionSlider.setRange(0, duration)

    def setPosition(self, position):
        """Set current position in video"""

        self.mediaPlayer.setPosition(position)

class Tag_Area(QWidget):
    """A widget to handle the tag area"""

    def __init__(self, main, *args, **kwargs):
        """Constructor
        
        Args:
            main = the main widget
        """

        super().__init__(*args, **kwargs)

        uic.loadUi(os.path.join(resource_path, "tag_area.ui"), self)

        self.main = main

        # setup tag area
        self.add_tag_btn.clicked.connect(self.add)
        self.chkboxs = []
        self.setup()

    def setup(self, clear = False):
        """A method to setup the tag area with the current scraper's tags"""

        if self.main.scraper is None:
            self.setEnabled(False)
            return
        else:
            self.setEnabled(True)

        # create a new layout
        new_layout = QGridLayout()
        
        # row/column starting positions
        row = 0
        column = 0

        # get the size of one column unit (1/5th of the scroll area width)
        col_u = self.frameSize().width() / 5

        # set spacing to a known number
        new_layout.setHorizontalSpacing(10)

        for tg in self.chkboxs:
            tg.setParent(None)

        if not clear:
            self.chkboxs = []
            # iterate through tags, adding them to layout
            for tag in self.main.scraper.tags:
                # make checkbox with correct status
                self.chkboxs.append(QCheckBox(tag))
                self.chkboxs[-1].setChecked(self.main.scraper.tags[tag])
                #self.chkboxs[-1].toggled.connect(lambda : print("yay bandaids?"))
                # figure out how many column units this tag should take up (factoring in spacing)
                col_span = max(1, math.ceil((self.chkboxs[-1].sizeHint().width()) / col_u))
                if col_span > 2:
                    # check if with spacing, we fit in one fewer column
                    if self.chkboxs[-1].sizeHint().width() <= ((col_span - 1) * col_u + (col_span - 2) * 10):
                        col_span -= 1
                # decide if column span should push this tag to a new row
                if col_span + column > 5 and column != 0:
                    column = 0
                    row += 1
                # add tag
                new_layout.addWidget(self.chkboxs[-1], row, column, 1, col_span)
                # calculate row/column start for next tag
                column = (min(column + col_span, 5)) % 5
                row = row + 1 if column == 0 else row
        else:
            self.setEnabled(False)

        # add add button
        new_layout.addWidget(self.add_tag_btn, row, column)

        # set old layout to temporary widget to re-parent it
        QWidget().setLayout(self.layout())

        # set new layout
        self.setLayout(new_layout) 

    def add(self):
        """A mthod to add a tag to the scraper's current tag list"""

        # make dialog to get relevant info
        dialog = QDialog()
        uic.loadUi(os.path.join(resource_path, "add_tag.ui"), dialog)
        
        # display dialog
        accepted = dialog.exec()

        # if dialog was accepted, add tag
        if accepted:
            try:
                # add tag to scraper
                self.main.scraper.add_tag(dialog.tag_nm.text(), dialog.tag_def_val.currentText() == "True")
                # redo tags in box
                self.setup()
            except Exception as e:
                warning = QMessageBox()
                warning.setText(str(e))
                warning.setIcon(QMessageBox.Warning)
                #warning.setTitle("Tag error")
                warning.exec()
        # otherwise, do nothing
        else: pass

class Main(QMainWindow):

    def __init__(self):
        """Constructor"""

        super().__init__()

        self.media_idx = 0
        self.unscaled_img = None

        # load in ui
        uic.loadUi(os.path.join(resource_path, "Main_window.ui"), self)

        # set window title
        self.setWindowTitle("Telegram tagger")

        # instantiate scraper
        self.scraper = None
        # instantiate google translate helper
        self.translator = translate.Client()

        # add Qlabels to scroll areas for text
        self.orig_msg = QLabel("No messages for this channel in this date range.")
        self.orig_msg.setWordWrap(True)
        self.orig_msg.setAlignment(Qt.AlignTop)
        self.orig_msg.setMargin(10)
        self.orig_scroll.setWidget(self.orig_msg)
        self.trans_msg = QLabel("No messages for this channel in this date range.")
        self.trans_msg.setWordWrap(True)
        self.trans_msg.setAlignment(Qt.AlignTop)
        self.trans_msg.setMargin(10)
        self.trans_scroll.setWidget(self.trans_msg)

        # connect next and prev buttons for media
        self.prev_media_btn.clicked.connect(lambda _: self.load_media(self.media_idx-1))
        self.next_media_btn.clicked.connect(lambda _: self.load_media(self.media_idx+1))

        # connect next and prev buttons for message
        self.filter = Filter_window(main_window = self)

        self.prev_msg_btn.clicked.connect(lambda _: (self.set_tags(), self.scraper.prev(filter = self.filter.get_filters()), self.load_msg()))
        self.next_msg_btn.clicked.connect(lambda _: (self.set_tags(), self.scraper.next(filter = self.filter.get_filters()), self.load_msg()))

        self.prev_msg_btn.setEnabled(False)
        self.next_msg_btn.setEnabled(False)

        # connect msg id field
        self.msg_id.setValidator(QIntValidator(bottom=0))
        self.msg_id.editingFinished.connect(self.jump_to_id)

        # connect comment field
        self.comment_box.textChanged.connect(self.parse_comment)

        # disable msg id box and comment box if necessary
        if self.scraper is None:
            self.msg_id.setEnabled(False)
            self.comment_box.setEnabled(False)
        else:
            self.msg_id.setEnabled(True)
            self.comment_box.setEnabled(True)

        self.tag_area = Tag_Area(self)
        self.tag_scroll_area.setWidget(self.tag_area)

        # if no session file (used for telegram authentication) is present,
        #   get one
        if not os.path.isfile(os.path.join(tel_scrape_path, "scraper.session")):
            num, accepted = QInputDialog.getText(self, "Tagger setup", "Phone number (with country code): ", QLineEdit.Normal, "")
            # if number wasn't submitted, exit
            if not accepted: sys.exit()

            # otherwise send request to phone number
            cf = ConfigParser()
            cf.read(os.path.join(tel_scrape_path, ".Telegram_info.ini"))
            
            client = TelegramClient("scraper", cf["Thesis"]["api_id"], cf["Thesis"]["api_hash"]) 
            client.connect()
            client.send_code_request(num)

            # get code from user
            code, accepted = QInputDialog.getText(self, "Tagger setup", "Enter code sent through Telegram: ", QLineEdit.Normal, "")
            # if number wasn't submitted, exit
            if not accepted: sys.exit()

            # authenticate
            client.sign_in(num, code)

            # now there should be a scraper.session file in the pwd
            if not os.path.isfile("scraper.session"): raise FileNotFoundError("No session file?")
            # move it to this program's directory
            os.rename("scraper.session", os.path.join(tel_scrape_path, "scraper.session"))

        # open settings window
        self.settings = Settings_window()
        # only need to show settings if we don't have already
        if not os.path.isfile(os.path.join(tel_scrape_path, "session.ini")):
            ret = self.settings.exec_()
            # if settings weren't confirmed, close
            if not ret: sys.exit()

            ok = False

            while not ok:
                # try to make scraper
                try:
                    self.make_scraper()
                    ok = True
                except AssertionError:
                    err = QMessageBox()
                    err.showMessage("\tDates invalid. Please be sure to check\nthat Start date is earlier than end date and that\nEnd date is at latest the current date.")
                    _ = err._exec()
                    # redisplay settings window  
                    ret = self.settings.exec_()
                    # if settings weren't confirmed, close
                    if not ret: sys.exit()
        # otherwise create a scraper
        else:
            ok = False

            while not ok:
                # try to make scraper
                try:
                    self.make_scraper()
                    ok = True
                except AssertionError:
                    err = QMessageBox()
                    err.showMessage("\tDates invalid. Please be sure to check\nthat Start date is earlier than end date and that\nEnd date is at latest the current date.")
                    _ = err._exec()
                    # redisplay settings window  
                    ret = self.settings.exec_()
                    # if settings weren't confirmed, close
                    if not ret: sys.exit()

        # setup menu bar
        taggermenu = self.menuBar().addMenu("Tagger")

        settings_act = QAction(QIcon(":icons/settings"), "Settings", self)
        settings_act.triggered.connect(self.open_settings)
        taggermenu.addAction(settings_act)

        filter_act = QAction(QIcon(":icons/filter"), "Filters", self)
        filter_act.triggered.connect(lambda _ : self.filter.exec_())
        taggermenu.addAction(filter_act)

        # show GUI
        self.show()

        # set channel info if session file exists
        if os.path.isfile(os.path.join(tel_scrape_path, "session.ini")):
            cf = ConfigParser()
            cf.read(os.path.join(tel_scrape_path, "session.ini"))

            # set id
            self.scraper.get_msg_by_id(int(cf["Scraper"]["msg_id"]))
            # load message
            self.load_msg()
            # load media
            try: self.load_media(self.scraper.media.index(int(cf["Scraper"]["media_id"])))
            except: pass
        else:
            # placeholder is tiny for some reason at start so we load
            #   media after showing window to scale correctly
            self.load_media(0)

    def open_settings(self):
        """Open the settings window and change the scraper if necessary"""

        ret = self.settings.exec_()
        # if settings weren't confirmed, close
        #if not ret: sys.exit()

        ok = False

        while not ok:
            # try to make scraper
            try:
                self.make_scraper()
                ok = True
            except AssertionError:
                err = QMessageBox()
                err.showMessage("\tDates invalid. Please be sure to check\nthat Start date is earlier than end date and that\nEnd date is at latest the current date.")
                _ = err._exec()
                # redisplay settings window  
                ret = self.settings.exec_()
                # if settings weren't confirmed, close
                if not ret: sys.exit()

    def parse_comment(self):
        """Limits the comment box to 255 characters"""

        # if text is too long, truncate it
        if len(self.comment_box.toPlainText()) > 255:
            # otherwise, truncate to 255 characters
            self.comment_box.setPlainText(self.comment_box.toPlainText()[:255])
            # put cursor at end of text
            curs = self.comment_box.textCursor()
            curs.setPosition(255)
            self.comment_box.setTextCursor(curs)

        # update message's comment field (which will be committed 
        #   when message is changed or when scraper is cleaned up)
        if not self.scraper is None:
            self.scraper.comment = self.comment_box.toPlainText()

    def jump_to_id(self, id=None):
        """Jumps to the given id, or to the id in the msg_id box if id is None

        Args:
            id = the id to jump to, or use msg_id line edit if None
        """

        if id is None:
            id = int(self.msg_id.text())

        if id < 0: raise ValueError("ID must be positive")

        if self.scraper is None: raise AttributeError("No active scraper.")

        try:
            self.set_tags()
            self.scraper.get_msg_by_id(id, False)
            self.load_msg()
            self.msg_id.setStyleSheet("")
            self.msg_id.setToolTip("")
        except EndRange:
            self.msg_id.setText(str(self.scraper.msg_id))
            self.msg_id.setStyleSheet("background-color:red;")
            self.msg_id.setToolTip("ID {} not in date range.".format(id)) 
        except XPostThrowaway as e:
            self.msg_id.setText(str(self.scraper.msg_id))
            self.msg_id.setStyleSheet("background-color:red;")
            self.msg_id.setToolTip("ID {} is {}.".format(id, str(e).lower())) 

    def make_scraper(self):
        """Make a scraper"""
        
        # convert dates to datetimes in utc
        start = self.settings.date_from.dateTime()
        start = datetime(year = start.date().year(), month = start.date().month(), day = start.date().day(),
            hour = start.time().hour(), minute = start.time().minute(), second = start.time().second(),
            tzinfo=timezone.utc) + timedelta(hours = utcoffset)
        end = self.settings.date_to.dateTime()
        end = datetime(year = end.date().year(), month=end.date().month(), day=end.date().day(),
            hour = end.time().hour(), minute = end.time().minute(), second = end.time().second(),
            tzinfo=timezone.utc) + timedelta(hours = utcoffset)
        # validate dates
        assert end <= datetime.now().astimezone(timezone.utc)
        assert start <= end

        # if this describes the current scraper, do nothing
        if self.scraper is not None:
            if self.scraper.start == start and self.scraper.end == end:
                if self.scraper.chnl.username == self.settings.chnl.text().split("/")[-1]: return

        # try to make scraper
        try: 
            # cleanup old scraper
            if self.scraper is not None: 
                self.set_tags()
                self.scraper._cleanup()
                self.scraper = None
            self.scraper = Scraper(chnl = self.settings.chnl.text(), start = start, end = end,
                db = self.settings.db_path.text(), x_post_excl = self.settings.excludes,
                dwnld_media = self.settings.media_sv.isChecked())
            self.orig_msg.setToolTip("")
            self.orig_msg.setStyleSheet("")
        except ValueError:
            self.orig_msg.setStyleSheet("background-color:red;")
            self.orig_msg.setToolTip("No messages for this channel in\nthe given date range.")
            self.orig_msg.setText("")
        except Exception as e:
            self.orig_msg.setStyleSheet("background-color:red;")
            self.orig_msg.setToolTip("No messages for this channel in\nthe given date range.") 
        finally:
            if self.scraper is None:
                self.msg_id.setEnabled(False)
                self.comment_box.setEnabled(False)
            else:
                self.msg_id.setEnabled(True)
                self.comment_box.setEnabled(True)

        self.load_msg()

    def set_tags(self):
        """Pulls tags from tag area and loads them into scraper"""

        for bx in self.tag_area.findChildren(QCheckBox):
            self.scraper.tags[bx.text()] = bx.isChecked()

    def load_msg(self):
        """Loads the message currently held by
            the scraper"""

        # clear messages if there's no valid scraper
        if self.scraper is None:
            self.orig_msg.setText("No messages for this channel in that date range")
            self.trans_msg.setText("No messages for this channel in that date range")
            self.msg_id.setText("")
            self.prev_msg_btn.setEnabled(False)
            self.next_msg_btn.setEnabled(False)
            self.date.setText("---")
            self.xpost.setText("---")
            self.reply.setText("---")
            self.load_media("clear")
            self.tag_area.setup(clear = True)
            self.comment_box.setPlainText("")
            return

        # clear messages if there's no valid message in filter
        filt = self.filter.get_filters()
        if filt is not None:
            if not all([self.scraper.tags[tag] == val for tag, val in filt.items() if tag in self.scraper.tags]):
                try: self.scraper.next(filt)
                except:
                    try: self.scraper.prev(filt)
                    except:
                        self.orig_msg.setText("No messages for this channel in that date range with the given filters")
                        self.trans_msg.setText("No messages for this channel in that date range with the given filters")
                        self.msg_id.setText("")
                        self.prev_msg_btn.setEnabled(False)
                        self.next_msg_btn.setEnabled(False)
                        self.date.setText("---")
                        self.xpost.setText("---")
                        self.reply.setText("---")
                        self.load_media("clear")
                        self.tag_area.setup(clear = True)
                        self.comment_box.setPlainText("")
                        return 

        # load tags
        self.tag_area.setup()

        # otherwise set original text to message text
        self.orig_msg.setText(self.scraper.msg)

        # and try to translate
        try:
            trans = self.translator.translate(self.scraper.msg, target_language='en', format_="text")
            self.trans_msg.setText(trans["translatedText"])
        except Exception:
            self.trans_msg.setText("Error occurred with translation. Check internet connection.")

        # set message id box
        self.msg_id.setText("{}".format(self.scraper.msg_id))

        self.unscaled_img = None

        # disable next/prev message and media buttons if necessary
        try:
            self.scraper.prev(filter=self.filter.get_filters())
            self.prev_msg_btn.setEnabled(True)
        except EndRange:
            self.prev_msg_btn.setEnabled(False)
        try:
            self.scraper.next(filter=self.filter.get_filters())
        except: pass

        try:
            self.scraper.next(filter=self.filter.get_filters())
            self.next_msg_btn.setEnabled(True)
        except EndRange:
            self.next_msg_btn.setEnabled(False)
        try:
            self.scraper.prev(filter=self.filter.get_filters())
        except: pass

        # reset style sheet to get rid of any errors previously indicated
        self.msg_id.setStyleSheet("")
        self.msg_id.setToolTip("")

        # set date
        self.date.setText(f"{self.scraper.msg_dat:%B %d, %Y}")

        # set xpost if this is a crosspost
        self.xpost.setText(self.scraper.fwd if (self.scraper.fwd is not None and len(self.scraper.fwd) > 0) else "---")

        # set reply link if this is a crosspost
        self.reply.setText(f'<a href="t.me/{self.scraper.reply_channel}/{self.scraper.reply_msg_id}>{self.scraper.reply_channel}</a>' if self.scraper.reply_channel is not None else "---")

        # load comment field
        try: self.comment_box.setPlainText(self.scraper.comment)
        except: self.comment_box.setPlainText("")

        self.load_media(self.scraper.media[0] if self.scraper.media is not None and len(self.scraper.media) > 0 else 0)

    def load_media(self, idx):
        """Loads the media at the given idx from the scraper
            
        Args:
            idx = the index of the id in self.scraper.media to load.
                idx will be modded by the length of self.scraper.media
                so that media is loaded cyclicly by just incrementing
                idx (the caller doesn't have to keep track of when to
                reset the counter
        """

        try:
            if self.unscaled_img is None or self.media_idx != idx:
                try:
                    self.media_f = self.scraper.get_media(self.scraper.media[idx%len(self.scraper.media)]) 
                    assert idx != "clear"
                except:
                    self.media_f = ":/placeholder/no_img.jpg"

                if self.media_f.endswith((".jpg", ".png", ".gif")):
                    # if previous media was movie, make sure it's paused
                    try:
                        if self.media_type == "movie":
                            if self.media_box.currentWidget().mediaPlayer.state() == QMediaPlayer.PlayingState: 
                                self.media_box.currentWidget().mediaPlayer.pause()
                    except: pass
                    self.media_type = "label"
                    # set media box to qlabel widget
                    self.media_box.setCurrentIndex(0)
                    # make a QMovie to get unscaled frame
                    _ = QMovie(self.media_f)
                    _.jumpToFrame(0)
                    self.unscaled_img = _.currentImage()
                else:
                    self.media_type = "movie"
                    self.media_box.setCurrentIndex(1)
                    try:
                        self.media_box.currentWidget().openFile(self.media_f)
                    # if there was an error, display placeholder image
                    except Exception:
                        self.media_f = ":/placeholder/no_img.jpg"
                        self.media_type = "label"
                        # set media box to qlabel widget
                        self.media_box.setCurrentIndex(0)
                        # make a QMovie to get unscaled frame
                        _ = QMovie(self.media_f)
                        _.jumpToFrame(0)
                        self.unscaled_img = _.currentImage()
            # video player will resize and scale itself correctly so we only have
            #   to worry about size if in "label" mode
            if self.media_type == "label":
                # get label
                label = self.media_box.currentWidget().layout().itemAt(0).widget()
                # get aspect ratio
                scaled_size = self.unscaled_img.scaled(label.width(),
                            label.height(), Qt.KeepAspectRatio).size()

                # load new movie (scaling doesn't work once we jump to frame)
                movie = QMovie(self.media_f)
                # scale down
                movie.setScaledSize(scaled_size)
                label.setMovie(movie)
                movie.start()
                # set minimum size so we can scale back down
                label.setMinimumSize(QSize(1,1))
            
            self.media_idx = idx

            # disable buttons
            if idx == "clear" or self.scraper is None or len(self.scraper.media) <= 1:
                self.prev_media_btn.setEnabled(False)
                self.next_media_btn.setEnabled(False)
            else:
                self.prev_media_btn.setEnabled(True)
                self.next_media_btn.setEnabled(True)

            # set media id
            try:
                assert idx != "clear"
                self.media_id.setText("{}".format(self.scraper.media[idx%len(self.scraper.media)]))
            except:
                self.media_id.setText("---")
        except: pass

    def resizeEvent(self, *args, **kwargs):
        """Implement resize event so we can reload image when window is made bigger"""

        super().resizeEvent(*args, **kwargs)

        self.load_media(self.media_idx)

    def closeEvent(self, e):
        """Catch close"""

        try: self.set_tags()
        except: pass

        try: self.scraper._cleanup()
        except: pass

        try:
            cf = ConfigParser()
            # store settings info
            cf["Settings"] = {"db_loc":self.settings.db_path.text(),
                              "excludes":"|".join(self.settings.excludes),
                              "sv_media":self.settings.media_sv.isChecked()}
            # store info about current scraper if there is one
            if self.scraper is not None:
                cf["Scraper"] = {"start":self.scraper.start - timedelta(hours = utcoffset),
                                 "end":self.scraper.end - timedelta(hours = utcoffset),
                                 "chnl":self.scraper.chnl.username,
                                 "msg_id":self.scraper.msg_id,
                                 "media_id":self.media_id.text()}

            # write config file
            with open(os.path.join(tel_scrape_path, "session.ini"), "w") as f:
                cf.write(f)
        except Exception as exc: print(exc)

        super().closeEvent(e)

if __name__ == "__main__":

    app = QApplication(sys.argv)
    QCoreApplication.setAttribute(Qt.AA_DontUseNativeMenuBar)
    widge = Main()
    sys.exit(app.exec_())