#!/usr/bin/env python

from log import log
# import argparse
import sys
# import threading

from datetime import datetime, timedelta
# from typing import Callable, Dict, List, Optional, Tuple

from PyQt5.QtCore import QDate, QEvent, Qt, QTime, QTimer
from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import (
    QApplication,
    QDialog,
    QFileDialog,
    QInputDialog,
    QLabel,
    #     QLineEdit,
    QListWidgetItem,
    QMainWindow,
    #     QMessageBox,
)
#
from dbconfig import engine, Session
import helpers
import music
#
from models import (
    Base,
    Playdates,
    PlaylistRows,
    Playlists,
    Settings,
    Tracks
)
from playlists import PlaylistTab
from sqlalchemy.orm.exc import DetachedInstanceError
from ui.dlg_search_database_ui import Ui_Dialog             # type: ignore
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist   # type: ignore
from ui.downloadcsv_ui import Ui_DateSelect                 # type: ignore
from config import Config
from ui.main_window_ui import Ui_MainWindow                 # type: ignore
# from utilities import create_track_from_file, update_db


class TrackData:
    def __init__(self, track):
        self.id = track.id
        self.title = track.title
        self.artist = track.artist
        self.duration = track.duration
        self.start_gap = track.start_gap
        self.fade_at = track.fade_at
        self.silence_at = track.silence_at
        self.path = track.path
        self.mtime = track.mtime


class Window(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None) -> None:
        super().__init__(parent)
        self.setupUi(self)

        self.timer: QTimer = QTimer()
        self.even_tick: bool = True
        self.playing: bool = False

        self.music: music.Music = music.Music()
        self.current_track: Optional[TrackData] = None
        self.current_track_playlist_tab: Optional[PlaylistTab] = None
        self.next_track: Optional[TrackData] = None
        self.next_track_playlist_tab: Optional[PlaylistTab] = None
        self.previous_track: Optional[TrackData] = None
        self.previous_track_position: Optional[int] = None

        self.set_main_window_size()
        self.lblSumPlaytime = QLabel("")
        self.statusbar.addPermanentWidget(self.lblSumPlaytime)
#         self.txtSearch = QLineEdit()
#         self.statusbar.addWidget(self.txtSearch)
#         self.txtSearch.setHidden(True)
        self.hide_played_tracks = False

        self.visible_playlist_tab: Callable[[], PlaylistTab] = \
            self.tabPlaylist.currentWidget

        self.load_last_playlists()
        self.enable_play_next_controls()
        self.timer.start(Config.TIMER_MS)
        self.connect_signals_slots()

#
#     @staticmethod
#     def print_current_database():
#         with Session() as session:
#             db = session.bind.engine.url.database
#             print(f"{db=}")
#

    def clear_selection(self) -> None:
        """ Clear selected row"""

        if self.visible_playlist_tab():
            self.visible_playlist_tab().clear_selection()

    def closeEvent(self, event: QEvent) -> None:
        """Handle attempt to close main window"""

        # Don't allow window to close when a track is playing
        if self.music.player and self.music.player.is_playing():
            event.ignore()
            helpers.show_warning(
                "Track playing",
                "Can't close application while track is playing")
        else:
            with Session() as session:
                record = Settings.get_int_settings(
                    session, "mainwindow_height")
                if record.f_int != self.height():
                    record.update(session, {'f_int': self.height()})

                record = Settings.get_int_settings(session, "mainwindow_width")
                if record.f_int != self.width():
                    record.update(session, {'f_int': self.width()})

                record = Settings.get_int_settings(session, "mainwindow_x")
                if record.f_int != self.x():
                    record.update(session, {'f_int': self.x()})

                record = Settings.get_int_settings(session, "mainwindow_y")
                if record.f_int != self.y():
                    record.update(session, {'f_int': self.y()})

                # Save splitter settings
                splitter_sizes = self.splitter.sizes()
                assert len(splitter_sizes) == 2
                splitter_top, splitter_bottom = splitter_sizes

                record = Settings.get_int_settings(session, "splitter_top")
                if record.f_int != splitter_top:
                    record.update(session, {'f_int': splitter_top})

                record = Settings.get_int_settings(session, "splitter_bottom")
                if record.f_int != splitter_bottom:
                    record.update(session, {'f_int': splitter_bottom})

            event.accept()

    def connect_signals_slots(self) -> None:
        self.actionInsertSectionHeader.triggered.connect(self.insert_header)
        self.action_Clear_selection.triggered.connect(self.clear_selection)
        self.actionClosePlaylist.triggered.connect(self.close_playlist_tab)
        self.actionDownload_CSV_of_played_tracks.triggered.connect(
            self.download_played_tracks)
        self.actionEnable_controls.triggered.connect(
            self.enable_play_next_controls)
        self.actionExport_playlist.triggered.connect(self.export_playlist_tab)
#         self.actionImport.triggered.connect(self.import_track)
        self.actionFade.triggered.connect(self.fade)
#         self.actionMoveSelected.triggered.connect(self.move_selected)
        self.actionNewPlaylist.triggered.connect(self.create_playlist)
        self.actionOpenPlaylist.triggered.connect(self.open_playlist)
        self.actionPlay_next.triggered.connect(self.play_next)
#         self.actionSearch.triggered.connect(self.search_playlist)
        self.actionInsertTrack.triggered.connect(self.insert_track)
        self.actionSelect_next_track.triggered.connect(self.select_next_row)
#         self.actionSelect_played_tracks.triggered.connect(self.select_played)
        self.actionSelect_previous_track.triggered.connect(
            self.select_previous_row)
#         self.actionSelect_unplayed_tracks.triggered.connect(
#             self.select_unplayed)
        self.actionSetNext.triggered.connect(
            lambda: self.tabPlaylist.currentWidget().set_selected_as_next())
        self.actionSkipToNext.triggered.connect(self.play_next)
        self.actionStop.triggered.connect(self.stop)
        self.btnDrop3db.clicked.connect(self.drop3db)
        self.btnHidePlayed.clicked.connect(self.hide_played)
        self.btnFade.clicked.connect(self.fade)
        self.btnStop.clicked.connect(self.stop)
        self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
#         self.txtSearch.returnPressed.connect(self.search_playlist_return)
#         self.txtSearch.textChanged.connect(self.search_playlist_update)
#
        self.timer.timeout.connect(self.tick)

    def close_playlist_tab(self) -> None:
        """
        Close active playlist tab, called by menu item
        """

        self.close_tab(self.tabPlaylist.currentIndex())

    def close_tab(self, tab_index: int) -> None:
        """
        Close active playlist tab unless it holds the curren or next track.
        Called from close_playlist_tab() or by clicking close button on tab.
        """

        # Don't close current track playlist
        if self.tabPlaylist.widget(tab_index) == (
                self.current_track_playlist_tab):
            self.statusbar.showMessage(
                "Can't close current track playlist", 5000)
            return

        # Don't close next track playlist
        if self.tabPlaylist.widget(tab_index) == self.next_track_playlist_tab:
            self.statusbar.showMessage(
                "Can't close next track playlist", 5000)
            return

        # Close playlist and remove tab
        self.tabPlaylist.widget(tab_index).close()
        self.tabPlaylist.removeTab(tab_index)

    def insert_header(self) -> None:
        """Show dialog box to enter header text and add to playlist"""

        try:
            playlist_tab = self.visible_playlist_tab()
        except AttributeError:
            # Just return if there's no visible playlist tab
            return

        # Get header text
        dlg: QInputDialog = QInputDialog(self)
        dlg.setInputMode(QInputDialog.TextInput)
        dlg.setLabelText("Header text:")
        dlg.resize(500, 100)
        ok = dlg.exec()
        if ok:
            with Session() as session:
                playlist_tab.insert_header(session, dlg.textValue())

    def create_playlist(self) -> None:
        """Create new playlist"""

        dlg = QInputDialog(self)
        dlg.setInputMode(QInputDialog.TextInput)
        dlg.setLabelText("Playlist name:")
        dlg.resize(500, 100)
        ok = dlg.exec()
        if ok:
            with Session() as session:
                playlist = Playlists(session, dlg.textValue())
                self.create_playlist_tab(session, playlist)

    def create_playlist_tab(self, session: Session,
                            playlist: Playlists) -> None:
        """
        Take the passed playlist database object, create a playlist tab and
        add tab to display.
        """

        playlist_tab = PlaylistTab(
            musicmuster=self, session=session, playlist_id=playlist.id)
        idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
        self.tabPlaylist.setCurrentIndex(idx)

    def disable_play_next_controls(self) -> None:
        """
        Disable "play next" keyboard controls
        """

        self.actionPlay_next.setEnabled(False)
        self.statusbar.showMessage("Play controls: Disabled", 0)

    def download_played_tracks(self) -> None:
        """Download a CSV of played tracks"""

        dlg = DownloadCSV(self)
        if dlg.exec():
            start_dt = dlg.ui.dateTimeEdit.dateTime().toPyDateTime()
            # Get output filename
            pathspec = QFileDialog.getSaveFileName(
                self, 'Save CSV of tracks played',
                directory="/tmp/playlist.csv",
                filter="CSV files (*.csv)"
            )
            if not pathspec:
                return

            path = pathspec[0]
            if not path.endswith(".csv"):
                path += ".csv"

            with open(path, "w") as f:
                with Session() as session:
                    for playdate in Playdates.played_after(session, start_dt):
                        f.write(
                            f"{playdate.track.artist},{playdate.track.title}\n"
                        )

    def drop3db(self) -> None:
        """Drop music level by 3db if button checked"""

        if self.btnDrop3db.isChecked():
            self.music.set_volume(Config.VOLUME_VLC_DROP3db, set_default=False)
        else:
            self.music.set_volume(Config.VOLUME_VLC_DEFAULT, set_default=False)

    def enable_play_next_controls(self) -> None:
        """
        Enable "play next" keyboard controls
        """

        self.actionPlay_next.setEnabled(True)
        self.statusbar.showMessage("Play controls: Enabled", 0)

    def end_of_track_actions(self) -> None:
        """
        Clean up after track played

        Actions required:
         - Set flag to say we're not playing a track
         - Reset current track
         - Tell playlist_tab track has finished
         - Reset current playlist_tab
         - Reset clocks
         - Update headers
         - Enable controls
        """

        # Set flag to say we're not playing a track so that tick()
        # doesn't see player=None and kick off end-of-track actions
        self.playing = False

        # Reset current track
        if self.current_track:
            self.previous_track = self.current_track
            self.current_track = None

        # Tell playlist_tab track has finished and
        # reset current playlist_tab
        if self.current_track_playlist_tab:
            self.current_track_playlist_tab.play_stopped()
            self.current_track_playlist_tab = None

        # Reset clocks
        self.frame_fade.setStyleSheet("")
        self.frame_silent.setStyleSheet("")
        self.label_elapsed_timer.setText("00:00")
        self.label_end_timer.setText("00:00")
        self.label_fade_length.setText("0:00")
        self.label_fade_timer.setText("00:00")
        self.label_silent_timer.setText("00:00")
        self.label_track_length.setText("0:00")
        self.label_start_time.setText("00:00:00")
        self.label_end_time.setText("00:00:00")

        # Update headers
        self.update_headers()

        # Enable controls
        self.enable_play_next_controls()

    def export_playlist_tab(self) -> None:
        """Export the current playlist to an m3u file"""

        if not self.visible_playlist_tab():
            return

        playlist_id = self.visible_playlist_tab().playlist_id

        with Session() as session:

            # Get output filename
            playlist = session.get(Playlists, playlist_id)
            pathspec = QFileDialog.getSaveFileName(
                self, 'Save Playlist',
                directory=f"{playlist.name}.m3u",
                filter="M3U files (*.m3u);;All files (*.*)"
            )
            if not pathspec:
                return
            path = pathspec[0]
            if not path.endswith(".m3u"):
                path += ".m3u"

            # Get list of track rows for this playlist
            plrs = PlaylistRows.get_rows_with_tracks(session, playlist_id)
            with open(path, "w") as f:
                # Required directive on first line
                f.write("#EXTM3U\n")
                for track in [a.track for a in plrs]:
                    f.write(
                        "#EXTINF:"
                        f"{int(track.duration / 1000)},"
                        f"{track.title} - "
                        f"{track.artist}"
                        "\n"
                        f"{track.path}"
                        "\n"
                    )

    def fade(self) -> None:
        """Fade currently playing track"""

        self.stop_playing(fade=True)

    def hide_played(self):
        """Toggle hide played tracks"""

        if self.hide_played_tracks:
            self.hide_played_tracks = False
            self.btnHidePlayed.setText("Hide played")
        else:
            self.hide_played_tracks = True
            self.btnHidePlayed.setText("Show played")

        # Update all displayed playlists
        with Session() as session:
            for i in range(self.tabPlaylist.count()):
                self.tabPlaylist.widget(i).update_display(session)
#
#     def import_track(self) -> None:
#         """Import track file"""
#
#         dlg = QFileDialog()
#         dlg.setFileMode(QFileDialog.ExistingFiles)
#         dlg.setViewMode(QFileDialog.Detail)
#         dlg.setDirectory(Config.IMPORT_DESTINATION)
#         dlg.setNameFilter("Music files (*.flac *.mp3)")
#
#         if dlg.exec_():
#             with Session() as session:
#                 txt: str = ""
#                 new_tracks = []
#                 for fname in dlg.selectedFiles():
#                     tags = helpers.get_tags(fname)
#                     new_tracks.append((fname, tags))
#                     title = tags['title']
#                     artist = tags['artist']
#                     possible_matches = Tracks.search_titles(session, title)
#                     if possible_matches:
#                         txt += 'Similar to new track '
#                         txt += f'"{title}" by "{artist} ({fname})":\n\n'
#                         for track in possible_matches:
#                             txt += f' "{track.title}" by {track.artist}'
#                             txt += f' ({track.path})\n'
#                         txt += "\n"
#                 # Check whether to proceed if there were potential matches
#                 if txt:
#                     txt += "Proceed with import?"
#                     result = QMessageBox.question(self,
#                                                   "Possible duplicates",
#                                                   txt,
#                                                   QMessageBox.Ok,
#                                                   QMessageBox.Cancel
#                                                   )
#                     if result == QMessageBox.Cancel:
#                         return
#
#                 # Import in separate thread
#                 thread = threading.Thread(target=self._import_tracks,
#                                           args=(new_tracks,))
#                 thread.start()
#
#     def _import_tracks(self, tracks: list):
#         """
#         Import passed files. Don't use parent session as that may be invalid
#         by the time we need it.
#         """
#
#         with Session() as session:
#             for (fname, tags) in tracks:
#                 track = create_track_from_file(session, fname, tags=tags)
#                 # Add to playlist on screen
#                 # If we don't specify "repaint=False", playlist will
#                 # also be saved to database
#                 self.visible_playlist_tab().insert_track(session, track)

    def load_last_playlists(self) -> None:
        """Load the playlists that were open when the last session closed"""

        with Session() as session:
            for playlist in Playlists.get_open(session):
                self.create_playlist_tab(session, playlist)
                playlist.mark_open(session)

    def move_selected(self) -> None:
        """
        Move selected rows to another playlist

        Actions required:
        - identify destination playlist
        - update playlist for the rows in the database
        - remove them from the display
        - update destination playlist display if loaded
        """

        # Identify destination playlist
        with Session() as session:
            visible_tab = self.visible_playlist_tab()
            source_playlist = visible_tab.playlist_id
            playlists = []
            for playlist in Playlists.get_all(session):
                if playlist.id == source_playlist:
                    continue
                else:
                    playlists.append(playlist)

            # Get destination playlist id
            dlg = SelectPlaylistDialog(self, playlists=playlists,
                                       session=session)
            dlg.exec()
            if not dlg.playlist:
                return
            destination_playlist = dlg.playlist

            # Update playlist for the rows in the database
            plr_ids = visible_tab.get_selected_playlistrow_ids()
            PlaylistRows.move_to_playlist(
                session, plr_ids, destination_playlist.id
            )

            # Remove moved rows from display
            visible_tab.remove_selected_rows()

            # Update destination playlist_tab if visible (if not visible, it
            # will be re-populated when it is opened)
            destination_visible_playlist_tab = None
            for tab in range(self.tabPlaylist.count()):
                # Non-playlist tabs won't have a 'playlist_id' attribute
                if not hasattr(self.tabPlaylist.widget(tab), 'playlist_id'):
                    continue
                if self.tabPlaylist.widget(tab).playlist_id == dlg.playlist.id:
                    destination_visible_playlist_tab = (
                        self.tabPlaylist.widget(tab))
                    break
            if destination_visible_playlist_tab:
                destination_visible_playlist_tab.populate(
                    session, dlg.playlist.id)

    def play_next(self) -> None:
        """
        Play next track.

        Actions required:
        - If there is no next track set, return.
        - If there's currently a track playing, fade it.
        - Move next track to current track.
        - Ensure playlist tabs are the correct colour
        - Restore volume if -3dB active
        - Play (new) current track.
        - Tell database to record it as played
        - Tell playlist track is now playing
        - Note that track is now playing
        - Disable play next controls
        - Update headers
        - Update clocks
        """

        # If there is no next track set, return.
        if not self.next_track:
            log.debug("musicmuster.play_next(): no next track selected")
            return

        with Session() as session:
            # If there's currently a track playing, fade it.
            self.stop_playing(fade=True)

            # Move next track to current track.
            self.current_track = self.next_track
            self.next_track = None

            # Ensure playlist tabs are the correct colour
            # If current track on different playlist_tab to last, reset
            # last track playlist_tab colour
            if self.current_track_playlist_tab != self.next_track_playlist_tab:
                self.set_tab_colour(self.current_track_playlist_tab,
                                    QColor(Config.COLOUR_NORMAL_TAB))
            # # Update record of current track playlist_tab
            self.current_track_playlist_tab = self.next_track_playlist_tab
            self.next_track_playlist_tab = None
            # Set current track playlist_tab colour
            self.set_tab_colour(self.current_track_playlist_tab,
                                QColor(Config.COLOUR_CURRENT_TAB))

            # Restore volume if -3dB active
            if self.btnDrop3db.isChecked():
                self.btnDrop3db.setChecked(False)

            # Play (new) current track
            start_at = datetime.now()
            self.music.play(self.current_track.path)

            # Tell database to record it as played
            Playdates(session, self.current_track.id)

            # Tell playlist track is now playing
            self.current_track_playlist_tab.play_started(session)

            # Note that track is now playing
            self.playing = True

            # Disable play next controls
            self.disable_play_next_controls()

            # Update headers
            self.update_headers()

            # Update clocks
            self.label_track_length.setText(
                helpers.ms_to_mmss(self.current_track.duration)
            )
            fade_at = self.current_track.fade_at
            silence_at = self.current_track.silence_at
            length = self.current_track.duration
            self.label_fade_length.setText(
                helpers.ms_to_mmss(silence_at - fade_at))
            self.label_start_time.setText(
                start_at.strftime(Config.TRACK_TIME_FORMAT))
            end_at = start_at + timedelta(
                milliseconds=self.current_track.duration)
            self.label_end_time.setText(
                end_at.strftime(Config.TRACK_TIME_FORMAT))

    def insert_track(self) -> None:
        """Show dialog box to select and add track from database"""

        with Session() as session:
            dlg = DbDialog(self, session)
            dlg.exec()
#
#     def search_playlist(self):
#         """Show text box to search playlist"""
#
#         self.disable_play_next_controls()
#         self.txtSearch.setHidden(False)
#         self.txtSearch.setFocus()
#
#     def search_playlist_return(self):
#         """Close off search box when return pressed"""
#
#         self.txtSearch.setText("")
#         self.txtSearch.setHidden(True)
#         self.enable_play_next_controls()
#         self.visible_playlist_tab().set_filter("")
#
#     def search_playlist_update(self):
#         """Update search when search string changes"""
#
#         self.visible_playlist_tab().set_filter(self.txtSearch.text())

    def open_playlist(self):
        with Session() as session:
            playlists = Playlists.get_closed(session)
            dlg = SelectPlaylistDialog(self, playlists=playlists,
                                       session=session)
            dlg.exec()
            playlist = dlg.playlist
            if playlist:
                playlist.mark_open(session)
                self.create_playlist_tab(session, playlist)

    def select_next_row(self) -> None:
        """Select next or first row in playlist"""

        self.visible_playlist_tab().select_next_row()
#
#     def select_played(self) -> None:
#         """Select all played tracks in playlist"""
#
#         self.visible_playlist_tab().select_played_tracks()
#
    def select_previous_row(self) -> None:
        """Select previous or first row in playlist"""

        self.visible_playlist_tab().select_previous_row()
#
#     def select_unplayed(self) -> None:
#         """Select all unplayed tracks in playlist"""
#
#         self.visible_playlist_tab().select_unplayed_tracks()

    def set_main_window_size(self) -> None:
        """Set size of window from database"""

        with Session() as session:
            record = Settings.get_int_settings(session, "mainwindow_x")
            x = record.f_int or 1
            record = Settings.get_int_settings(session, "mainwindow_y")
            y = record.f_int or 1
            record = Settings.get_int_settings(session, "mainwindow_width")
            width = record.f_int or 1599
            record = Settings.get_int_settings(session, "mainwindow_height")
            height = record.f_int or 981
            self.setGeometry(x, y, width, height)
            record = Settings.get_int_settings(session, "splitter_top")
            splitter_top = record.f_int or 256
            record = Settings.get_int_settings(session, "splitter_bottom")
            splitter_bottom = record.f_int or 256
            self.splitter.setSizes([splitter_top, splitter_bottom])
        return

    def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None:
        """
        Find the tab containing the widget and set the text colour
        """

        idx = self.tabPlaylist.indexOf(widget)
        self.tabPlaylist.tabBar().setTabTextColor(idx, colour)

    def stop(self) -> None:
        """Stop playing immediately"""

        self.stop_playing(fade=False)

    def stop_playing(self, fade=True) -> None:
        """
        Stop playing current track

        Actions required:
        - Return if not playing
        - Stop/fade track
        - Reset playlist_tab colour
        - Run end-of-track actions
        """

        # Return if not playing
        if not self.playing:
            return

        # Stop/fade track
        self.previous_track_position = self.music.get_position()
        if fade:
            self.music.fade()
        else:
            self.music.stop()

        # Reset playlist_tab colour
        if self.current_track_playlist_tab == self.next_track_playlist_tab:
            self.set_tab_colour(self.current_track_playlist_tab,
                                QColor(Config.COLOUR_NEXT_TAB))
        else:
            self.set_tab_colour(self.current_track_playlist_tab,
                                QColor(Config.COLOUR_NORMAL_TAB))

        # Run end-of-track actions
        self.end_of_track_actions()

    def this_is_the_next_track(self, session: Session,
                               playlist_tab: PlaylistTab,
                               track: Tracks) -> None:
        """
        This is notification from a playlist tab that it holds the next
        track to be played.

        Actions required:
         - Clear next track if on other tab
         - Reset tab colour if on other tab
         - Note next playlist tab
         - Set next playlist_tab tab colour
         - Note next track
         - Update headers
         - Populate ‘info’ tabs

        """

        # Clear next track if on another tab
        if self.next_track_playlist_tab != playlist_tab:
            # We need to reset the ex-next-track playlist
            if self.next_track_playlist_tab:
                self.next_track_playlist_tab.clear_next(session)

                # Reset tab colour if on other tab
                if (self.next_track_playlist_tab !=
                        self.current_track_playlist_tab):
                    self.set_tab_colour(
                        self.next_track_playlist_tab,
                        QColor(Config.COLOUR_NORMAL_TAB))

            # Note next playlist tab
            self.next_track_playlist_tab = playlist_tab

        # Set next playlist_tab tab colour if it isn't the
        # currently-playing tab
        if (self.next_track_playlist_tab !=
                self.current_track_playlist_tab):
            self.set_tab_colour(
                self.next_track_playlist_tab,
                QColor(Config.COLOUR_NEXT_TAB))

        # Note next track
        self.next_track = TrackData(track)

        # Update headers
        self.update_headers()

        # Populate 'info' tabs
        self.tabInfolist.open_tab(track.title)

    def tick(self) -> None:
        """
        Carry out clock tick actions.

        The Time of Day clock is updated every tick (500ms).

        All other timers are updated every second. As the timer displays
        have a one-second resolution, updating every 500ms can result in
        some timers updating and then, 500ms later, other timers
        updating. That looks odd.

        Actions required:
        - Update TOD clock
        - If track is playing:
            update track clocks time and colours
        - Else:
            run stop_track
        """

        # Update TOD clock
        self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT))

        self.even_tick = not self.even_tick
        if not self.even_tick:
            return
        if not self.playing:
            return

        # If track is playing, update track clocks time and colours
        if self.music.player and self.music.player.is_playing():
            playtime = self.music.get_playtime()
            time_to_fade = (self.current_track.fade_at - playtime)
            time_to_silence = (
                    self.current_track.silence_at - playtime)
            time_to_end = (self.current_track.duration - playtime)

            # Elapsed time
            self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))

            # Time to fade
            self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))

            # If silent in the next 5 seconds, put warning colour on
            # time to silence box and enable play controls
            if time_to_silence <= 5500:
                self.frame_silent.setStyleSheet(
                    f"background: {Config.COLOUR_ENDING_TIMER}"
                )
                self.enable_play_next_controls()
            # Set warning colour on time to silence box when fade starts
            elif time_to_fade <= 500:
                self.frame_silent.setStyleSheet(
                    f"background: {Config.COLOUR_WARNING_TIMER}"
                )
            # Five seconds before fade starts, set warning colour on
            # time to silence box and enable play controls
            elif time_to_fade <= 5500:
                self.frame_fade.setStyleSheet(
                    f"background: {Config.COLOUR_WARNING_TIMER}"
                )
                self.enable_play_next_controls()
            else:
                self.frame_silent.setStyleSheet("")
                self.frame_fade.setStyleSheet("")

            self.label_silent_timer.setText(
                helpers.ms_to_mmss(time_to_silence)
            )

            # Time to end
            self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))

        else:
            if self.playing:
                self.stop_playing()

    def update_headers(self) -> None:
        """
        Update last / current / next track headers
        """

        try:
            self.hdrPreviousTrack.setText(
                f"{self.previous_track.title} - {self.previous_track.artist}")
        except AttributeError:
            self.hdrPreviousTrack.setText("")

        try:
            self.hdrCurrentTrack.setText(
                f"{self.current_track.title} - {self.current_track.artist}")
        except AttributeError:
            self.hdrCurrentTrack.setText("")

        try:
            self.hdrNextTrack.setText(
                f"{self.next_track.title} - {self.next_track.artist}"
            )
        except AttributeError:
            self.hdrNextTrack.setText("")


class DbDialog(QDialog):
    """Select track from database"""

    def __init__(self, parent: QMainWindow, session: Session) -> None:
        """Subclassed QDialog to manage track selection"""

        super().__init__(parent)
        self.session = session
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        self.ui.btnAdd.clicked.connect(self.add_selected)
        self.ui.btnAddClose.clicked.connect(self.add_selected_and_close)
        self.ui.btnClose.clicked.connect(self.close)
        self.ui.matchList.itemDoubleClicked.connect(self.double_click)
        self.ui.matchList.itemSelectionChanged.connect(self.selection_changed)
        self.ui.radioTitle.toggled.connect(self.title_artist_toggle)
        self.ui.searchString.textEdited.connect(self.chars_typed)

        record = Settings.get_int_settings(self.session, "dbdialog_width")
        width = record.f_int or 800
        record = Settings.get_int_settings(self.session, "dbdialog_height")
        height = record.f_int or 600
        self.resize(width, height)

    def __del__(self) -> None:
        """Save dialog size and position"""

        record = Settings.get_int_settings(self.session, "dbdialog_height")
        if record.f_int != self.height():
            record.update(self.session, {'f_int': self.height()})

        record = Settings.get_int_settings(self.session, "dbdialog_width")
        if record.f_int != self.width():
            record.update(self.session, {'f_int': self.width()})

    def add_selected(self) -> None:
        """Handle Add button"""

        if not self.ui.matchList.selectedItems():
            return

        item = self.ui.matchList.currentItem()
        track = item.data(Qt.UserRole)
        self.add_track(track)

    def add_selected_and_close(self) -> None:
        """Handle Add and Close button"""

        self.add_selected()
        self.close()

    def add_track(self, track: Tracks) -> None:
        """Add passed track to playlist on screen"""

        self.parent().visible_playlist_tab().insert_track(self.session, track)
        # Commit session to get correct row numbers if more tracks added
        self.session.commit()
        # Select search text to make it easier for next search
        self.select_searchtext()

    def chars_typed(self, s: str) -> None:
        """Handle text typed in search box"""

        self.ui.matchList.clear()
        if len(s) > 1:
            if self.ui.radioTitle.isChecked():
                matches = Tracks.search_titles(self.session, s)
            else:
                matches = Tracks.search_artists(self.session, s)
            if matches:
                for track in matches:
                    last_played = Playdates.last_played(self.session, track.id)
                    t = QListWidgetItem()
                    t.setText(
                        f"{track.title} - {track.artist} "
                        f"[{helpers.ms_to_mmss(track.duration)}] "
                        f"({helpers.get_relative_date(last_played)})"
                    )
                    t.setData(Qt.UserRole, track)
                    self.ui.matchList.addItem(t)

    def double_click(self, entry: QListWidgetItem) -> None:
        """Add items that are double-clicked"""

        track = entry.data(Qt.UserRole)
        self.add_track(track)
        # Select search text to make it easier for next search
        self.select_searchtext()

    def select_searchtext(self) -> None:
        """Select the searchbox"""

        self.ui.searchString.selectAll()
        self.ui.searchString.setFocus()

    def selection_changed(self) -> None:
        """Display selected track path in dialog box"""

        if not self.ui.matchList.selectedItems():
            return

        item = self.ui.matchList.currentItem()
        track = item.data(Qt.UserRole)
        self.ui.dbPath.setText(track.path)
     
    def title_artist_toggle(self) -> None:
        """
        Handle switching between searching for artists and searching for
        titles
        """

        # Logic is handled already in chars_typed(), so just call that.
        self.chars_typed(self.ui.searchString.text())


class DownloadCSV(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.ui = Ui_DateSelect()
        self.ui.setupUi(self)
        self.ui.dateTimeEdit.setDate(QDate.currentDate())
        self.ui.dateTimeEdit.setTime(QTime(19, 59, 0))
        self.ui.buttonBox.accepted.connect(self.accept)
        self.ui.buttonBox.rejected.connect(self.reject)


class SelectPlaylistDialog(QDialog):
    def __init__(self, parent=None, playlists=None, session=None):
        super().__init__(parent)

        if playlists is None:
            return
        self.ui = Ui_dlgSelectPlaylist()
        self.ui.setupUi(self)
        self.ui.lstPlaylists.itemDoubleClicked.connect(self.list_doubleclick)
        self.ui.buttonBox.accepted.connect(self.open)
        self.ui.buttonBox.rejected.connect(self.close)
        self.session = session
        self.playlist = None
        self.plid = None

        record = Settings.get_int_settings(
            self.session, "select_playlist_dialog_width")
        width = record.f_int or 800
        record = Settings.get_int_settings(
            self.session, "select_playlist_dialog_height")
        height = record.f_int or 600
        self.resize(width, height)

        for playlist in playlists:
            p = QListWidgetItem()
            p.setText(playlist.name)
            p.setData(Qt.UserRole, playlist)
            self.ui.lstPlaylists.addItem(p)

    def __del__(self):  # review
        record = Settings.get_int_settings(
            self.session, "select_playlist_dialog_height")
        if record.f_int != self.height():
            record.update(self.session, {'f_int': self.height()})

        record = Settings.get_int_settings(
            self.session, "select_playlist_dialog_width")
        if record.f_int != self.width():
            record.update(self.session, {'f_int': self.width()})

    def list_doubleclick(self, entry):  # review
        self.playlist = entry.data(Qt.UserRole)
        self.accept()

    def open(self):     # review
        if self.ui.lstPlaylists.selectedItems():
            item = self.ui.lstPlaylists.currentItem()
            self.playlist = item.data(Qt.UserRole)
            self.accept()


if __name__ == "__main__":
    #     p = argparse.ArgumentParser()
    #     # Only allow at most one option to be specified
    #     group = p.add_mutually_exclusive_group()
    #     group.add_argument('-u', '--update',
    #                        action="store_true", dest="update",
    #                        default=False, help="Update database")
    #     # group.add_argument('-f', '--full-update',
    #     #                    action="store_true", dest="full_update",
    #     #                    default=False, help="Update database")
    #     # group.add_argument('-i', '--import', dest="fname", help="Input file")
    #     args = p.parse_args()
    #
    #     # Run as required
    #     if args.update:
    #         log.debug("Updating database")
    #         with Session() as session:
    #             update_db(session)
    #     # elif args.full_update:
    #     #     log.debug("Full update of database")
    #     #     with Session() as session:
    #     #         full_update_db(session)
    #     else:
    #         # Normal run
    try:
        Base.metadata.create_all(engine)
        app = QApplication(sys.argv)
        win = Window()
        win.show()
        sys.exit(app.exec())
    except Exception:
        msg = "Unhandled Exception caught by musicmuster.main()"
        log.exception(msg, exc_info=True, stack_info=True)
