#
#  Aura Engine
#
#  Playout Daemon for autoradio project
#
#
#  Copyright (C) 2020 David Trattnig <david.trattnig@subsquare.at>
#
#  This file is part of engine.
#
#  engine is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  any later version.
#
#  engine is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with engine. If not, see <http://www.gnu.org/licenses/>.
#

# Meta
__version__ = '0.0.1'
__license__ = "GNU General Public License (GPL) Version 3"
__version_info__ = (0, 0, 1)
__author__ = 'David Trattnig <david.trattnig@subsquare.at>'


import os, os.path
import logging
import random

from accessify import private, protected
from modules.base.simpleutil import SimpleUtil
from modules.communication.mail import AuraMailer


class FallbackManager:
    """
    Handles all types of fallbacks in case there is an outage
    for the regular radio programme.

    Attributes:
        config (AuraConfig):        The engine configuration
        logger (AuraLogger):        The logger
        mail   (AuraMailer):        Mail service
        scheduler (AuraScheduler):  The scheduler
        fallback_history (Dict):    Holds a 24h history of played, local tracks to avoid re-play
        last_fallback (Integer):    Timestamp, when the last local file fallback was played
        is_processing (Boolean):    Flag to avoid race-conditions, as Liquidsoap sends plenty of requests at once
    """

    config = None
    logger = None
    mailer = None
    scheduler = None
    fallback_history = {}
    last_fallback = 0
    is_processing = False


    def __init__(self, config, logger, scheduler):
        """
        Constructor

        Args:
            config (AuraConfig):    Holds the engine configuration
        """
        self.config = config
        self.mailer = AuraMailer(self.config)
        self.scheduler = scheduler
        self.logger = logger


    #
    #   PUBLIC METHODS
    #


    def get_fallback_for(self, fallbackname):
        """
        Retrieves a random fallback audio source for any of the types:
            - timeslot/schedule
            - show
            - station
        
        Args:
            fallbackname (String):      Fallback type

        Returns:
            (String):                   Absolute path to the file
        """
        file = ""
        media_type = "PLAYLIST"
        active_schedule, active_playlist = self.scheduler.get_active_playlist()

        # Block access to avoid race-conditions
        if self.is_processing:
            return None
        else:
            self.is_processing = True

        # Get fallback track(s) by fallback-type
        if fallbackname == "timeslot":
            file = self.get_playlist_items(active_schedule, "schedule_fallback")

        elif fallbackname == "show":
            file = self.get_playlist_items(active_schedule, "show_fallback")

        elif fallbackname == "station":
            file = self.get_playlist_items(active_schedule, "station_fallback")

            if not file:
                media_type = "TRACK"
                file = self.get_random_local_track()

                if not file:
                    self.logger.critical("Got no file for station fallback! Playing default test track, to play anything at all.")
                    file = "../../testing/content/ernie_mayne_sugar.mp3"
                    media_type = "DEFAULT TRACK"
        else:
            file = ""
            self.logger.critical("Should set next fallback file for " + fallbackname + ", but this fallback is unknown!")

        if file:
            # Send admin email to notify about the fallback state
            if not active_playlist:
                active_playlist = "n/a"
            msg = "AURA ENGINE %s FALLBACK DETECTED!\n\n" % fallbackname
            msg += "Expected, active Schedule: %s \n" % active_schedule
            msg += "Expected, active Playlist: %s \n\n" % active_playlist
            msg += "Providing FALLBACK-%s for %s '%s'\n\n" % (media_type, fallbackname, file)
            msg += "Please review the schedules or contact your Aura Engine administrator."
            self.mailer.send_admin_mail("CRITICAL - Detected fallback for %s" % fallbackname, msg)
            self.logger.warn("Providing fallback %s: '%s'. Sent admin email about fallback state" % (media_type, file))

        self.is_processing = False
        return file



    def fallback_has_started(self, artist, title):
        """
        Called when a fallback track has actually started playing
        """
        self.logger.info("Now playing: fallback track '%s - %s'." % (artist, title))


    #
    #   PRIVATE METHODS
    #


    def get_playlist_items(self, schedule, fallback_key):
        """
        Retrieves the list of tracks from a playlist defined by `fallback_key`.
        """
        playlist_files = ""

        if hasattr(schedule, fallback_key):
            playlist = getattr(schedule, fallback_key)
            if len(playlist) > 0:
                playlist = playlist[0]
                if playlist and playlist.entries:
                    for entry in playlist.entries:
                        playlist_files += entry.filename + "\n"

        return playlist_files



    def get_random_local_track(self):
        """
        Retrieves a random audio track from the local station-fallback directory.

        Returns:
            (String):   Absolute path to an audio file
        """
        dir = self.config.fallback_music_folder
        files = os.listdir(dir)
        audio_files = list(filter(lambda f: self.is_audio_file(dir, f), files))
        
        if not dir or not audio_files:
            self.logger.error("Folder 'fallback_music_folder = %s' is empty!" % dir)
            return None

        # If last played fallback is > 24 hours ago, ignore play history
        # This should save used memory if the engine runs for a long time
        if self.last_fallback < SimpleUtil.timestamp() - (60*60*24):
            self.fallback_history = {}
        self.last_fallback = SimpleUtil.timestamp()
        
        # Retrieve files which haven't been played yet
        history = set(self.fallback_history.keys())
        left_audio_files = list(set(audio_files).difference(history))

        # If nothing left, clear history and start with all files again
        if not len(left_audio_files):
            self.fallback_history = {}
            left_audio_files = audio_files

        # Select random track from directory
        i = random.randint(0, len(left_audio_files)-1)
        file = os.path.join(dir, left_audio_files[i])

        # Store track in history, to avoid playing it multiple times
        if file:
            self.fallback_history[left_audio_files[i]] = SimpleUtil.timestamp()

        return file



    def is_audio_file(self, dir, file):
        """
        Checks if the passed file is an audio file i.e. has a file-extension
        known for audio files.

        Args:
            (File): file:   the file object.

        Returns:
            (Boolean):      True, if it's an audio file.
        """
        audio_extensions = [".wav", ".flac", ".mp3", ".ogg"]
        ext = os.path.splitext(file)[1]
        abs_path = os.path.join(dir, file)

        if os.path.isfile(abs_path):
            if any(ext in s for s in audio_extensions):
                return True
        return False