import time
from collections import deque
from threading import Thread, Timer, Event


from PyQt5.QtCore import pyqtSignal, QObject
from path import Path

from core import db, file_utilities
from core.exceptions import NotSupportedFile
from core.file_utilities import is_media_file_ext, hash_file
from core.logging_ import Logger
from core.models import Track
from core.repositories import MusicFolderRepository, TrackRepository
from core.vlc_ import vlc

logger = Logger.get()


class AlreadyIndexed(Exception):
    pass


class Emitter(QObject):
    filesIndexed = pyqtSignal(list)
    musicFolderStatusChanged = pyqtSignal(int)


class Indexer(Thread):
    DELAY = 2

    def __init__(self):
        Thread.__init__(self)
        self.stopped = Event()
        self.emitter = Emitter()
        self.timer = Timer(self.DELAY, self.act)

    def run(self):
        logger.info('** indexation thread started **')
        while not self.stopped.wait(self.DELAY):
            # logger.debug("... indexation")
            self.act()

    def act(self):
        # Initialize
        session = db.Session()
        music_folder_repo = MusicFolderRepository(session)
        track_repo = TrackRepository(session)

        # Get current data
        music_folders = music_folder_repo.get_all()
        tracks = track_repo.get_all()

        # Index existing
        index = {t.path: t for t in tracks}
        buffer = deque()

        # -- Walk through music folders
        # Put new files in buffer
        for music_folder in music_folders:
            music_folder_path = Path(music_folder.path)

            # music folder cant be found
            if not music_folder_path.exists():
                if music_folder.status == music_folder.STATUS_FOUND:
                    music_folder.status = music_folder.STATUS_UNAVAILABLE
                    music_folder_repo.commit()
                    self.emitter.musicFolderStatusChanged.emit(music_folder.id)
                continue

            # music folder found
            if music_folder.status != music_folder.STATUS_FOUND:
                music_folder.status = music_folder.STATUS_FOUND
                music_folder_repo.commit()
                self.emitter.musicFolderStatusChanged.emit(music_folder.id)

            # walk files
            for filename in music_folder_path.walkfiles():
                # filename already seen
                if filename in buffer:
                    continue
                # new file
                if filename not in index and is_media_file_ext(filename.ext):
                    buffer.append(filename)
                # file already in db
                elif filename in index:
                    track = index[filename]
                    if track.status in (Track.STATUS_UNAVAILABLE, Track.STATUS_UNKNOWN):
                        buffer.append(track.id)
                    del index[filename]

        # Put missing files in buffer
        for filename, track in index.items():
            if track.id in buffer:
                continue

            filename = Path(filename)
            if not filename.exists() and track.status != Track.STATUS_UNAVAILABLE:
                buffer.append(track.id)

        # Index buffered tracks
        # NB: the tracks are treated from the end to the beginning, so missing files are treated before the new ones
        tracks = []
        while buffer:
            filename_or_id = buffer.pop()
            try:
                track = self.index(track_repo, filename_or_id, tracks)
                tracks.append(track)
            except AlreadyIndexed:
                pass
            except (FileNotFoundError, NotSupportedFile) as e:
                logger.warning("Error during indexation: %s" % e)
                continue
            except IndexError:
                break

        # Finalize
        if tracks:
            for track in tracks:
                if track.id is None:
                    track_repo.create(track)
            track_repo.commit()
            self.emitter.filesIndexed.emit(tracks)
            logger.info(f"{len(tracks)} tracks indexed")

    @staticmethod
    def index(track_repo, filename_or_track_id, previously_indexed=None):
        """ index a media file from the filesystem or a track id """

        previously_indexed = previously_indexed or []

        if type(filename_or_track_id) is int:
            track = track_repo.get_by_id(filename_or_track_id)
            filename = Path(track.path)
            track_hash = track.hash

            if not filename.exists() and track.status != Track.STATUS_UNAVAILABLE:
                logger.debug('Index - missing: %s' % filename)
                track.status = Track.STATUS_UNAVAILABLE
                return track
        else:
            filename = Path(filename_or_track_id)
            if not filename.exists():
                raise FileNotFoundError(f"File not found: {filename}")
            if not is_media_file_ext(filename.ext):
                raise NotSupportedFile(f"File's extension {filename.ext} is not supported")

            track_hash = hash_file(filename)

            if any(t.hash == track_hash for t in previously_indexed):
                raise AlreadyIndexed(f"File already indexed")

            track = track_repo.get_by_hash(track_hash)
            if not track:
                track = Track()
            elif track.status == Track.STATUS_FOUND:
                raise AlreadyIndexed(f"File already indexed")

        vlc_media = vlc.Media(filename)
        vlc_media.parse()

        title = vlc_media.get_meta(vlc.Meta.Title)
        if not title or title == '(null)' or title == filename.name:
            title = filename.stripext().name
        track.title = title.strip()
        track.format = filename.ext
        track.artist = (vlc_media.get_meta(vlc.Meta.AlbumArtist) or
                        vlc_media.get_meta(vlc.Meta.Artist)).strip()
        track.album = vlc_media.get_meta(vlc.Meta.Album).strip()
        track.track_num = vlc_media.get_meta(vlc.Meta.TrackNumber)
        track.duration = vlc_media.get_duration() // 1000
        track.note = ""
        track.status = Track.STATUS_FOUND
        track.path = filename
        track.hash = track_hash

        return track

    def stop(self):
        self.stopped.set()
        while self.is_alive():
            time.sleep(0.1)
        logger.info('** indexation thread stopped **')


if __name__ == '__main__':
    indexer = Indexer()
    indexer.start()
    try:
        indexer.join()
    except KeyboardInterrupt:
        indexer.stop()
