#
# Aura Engine (https://gitlab.servus.at/aura/engine)
#
# Copyright (C) 2017-2020 - The Aura Engine Team.

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program 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 Affero General Public License for more details.

# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.



import logging

from enum                   import Enum
from datetime               import timedelta

from src.base.config        import AuraConfig
from src.base.utils         import SimpleUtil as SU
from src.core.resources     import ResourceClass, ResourceUtil
from src.core.channels      import Channel
from src.core.control       import EngineExecutor
from src.scheduling.models  import DB


class FallbackType(Enum):
    """
    Types of fallbacks.

        NONE:       No fallback active, default playout
        SCHEDULE:   The first played when some default playlist fails
        SHOW:       The second played when the timeslot fallback fails
        STATION:    The last played when everything else fails
    """
    NONE        = { "id": 0, "name": "default", "channels": [ Channel.QUEUE_A, Channel.QUEUE_B ] }
    SCHEDULE    = { "id": 1, "name": "schedule", "channels": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B ] }   
    SHOW        = { "id": 2, "name": "show", "channels": [ Channel.FALLBACK_QUEUE_A, Channel.FALLBACK_QUEUE_B ] }
    STATION     = { "id": 3, "name": "station", "channels": [ Channel.FALLBACK_STATION_FOLDER, Channel.FALLBACK_STATION_PLAYLIST ] }

    @property
    def id(self):
        return self.value["id"]

    @property
    def channels(self):
        return self.value["channels"]

    def __str__(self):
        return str(self.value["name"])



class FallbackManager:
    """
    Handles all types of fallbacks in case there is an outage or missing schedules
    for the radio programme.
    """    
    config = None
    logger = None
    engine = None
    state = None
    

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

        Args:
            scheduler (Scheduler):  The scheduler
        """
        self.config = AuraConfig.config()
        self.logger = logging.getLogger("AuraEngine")
        self.engine = engine
        self.state = {            
            "fallback_type": FallbackType.NONE,
            "previous_fallback_type": None, 
            "timeslot": None
        }


    #
    #   EVENTS
    #


    def on_timeslot_start(self, timeslot=None):
        """
        Some new timeslot has just started.
        """
        self.state["timeslot"] = timeslot


    def on_timeslot_end(self, timeslot):
        """
        The timeslot has ended and the state is updated. The method ensures that any intermediate state 
        update doesn't get overwritten.
        """
        if self.state["timeslot"] == timeslot:
            self.state["timeslot"] = None


    def on_play(self, entry):
        """
        Event Handler which is called by the engine when some entry is actually playing. 

        Args:
            source (String):    The `PlaylistEntry` object
        """     
        content_class = ResourceUtil.get_content_class(entry.get_content_type())
        if content_class == ResourceClass.FILE:
            # Files are handled by "on_metadata" called via Liquidsoap
            return

        self.update_fallback_state(entry.channel)


    def on_metadata(self, data):
        """
        Event called by the soundsystem implementation (i.e. Liquidsoap) when some entry is actually playing. 
        This does not include live or stream sources, since they ain't have metadata and are triggered from 
        engine core (see `on_play(..)`).

        Args:
            data (dict):    A collection of metadata related to the current track
        """
        channel = data.get("source")
        fallback_type = self.update_fallback_state(channel)

        # If we turned into a fallback state we issue an event
        if fallback_type is not FallbackType.NONE:
            # Only trigger the event the upon first state change
            if fallback_type != self.state.get("previous_fallback_type"):
                timeslot = self.state["timeslot"]
                if timeslot:
                    DB.session.merge(timeslot)
                self.engine.event_dispatcher.on_fallback_active(timeslot, fallback_type)



    #
    #   METHODS
    #


    def update_fallback_state(self, channel):
        """
        Update the current and previously active fallback state.

        Returns:
            (FallbackType): The current fallback
        """
        fallback_type = self.type_for_channel(channel)
        self.state["previous_fallback_type"] = self.state["fallback_type"]
        self.state["fallback_type"] = fallback_type
        return fallback_type


    def type_for_channel(self, source):
        """
        Retrieves the matching fallback type for the given source.
        """
        if source in [str(i) for i in FallbackType.SCHEDULE.channels]:
            return FallbackType.SCHEDULE
        if source in [str(i) for i in FallbackType.SHOW.channels]:
            return FallbackType.SHOW
        if source in [str(i) for i in FallbackType.STATION.channels]:
            return FallbackType.STATION
        return FallbackType.NONE


    def queue_fallback_playlist(self, timeslot):
        """
        Evaluates the scheduled fallback and queues it using a timed thread.
        """
        (fallback_type, playlist) = self.get_fallback_playlist(timeslot)

        if playlist:
            self.logger.info(f"Resolved {fallback_type.value} fallback")         
            return FallbackCommand(timeslot, playlist.entries)
        else:
            msg = f"There is no timeslot- or show-fallback defined for timeslot#{timeslot.timeslot_id}. "
            msg += f"The station fallback will be used automatically."
            self.logger.info(msg)



    def resolve_playlist(self, timeslot):
        """
        Retrieves the currently planned (fallback) playlist. If a normal playlist is available,
        this one is returned. In case of station fallback no playlist is returned.

        Args:
            timeslot (Timeslot)
        
        Returns:
            (FallbackType, Playlist)
        """
        fallback_type = None
        planned_playlist = self.engine.scheduler.programme.get_current_playlist(timeslot)

        if planned_playlist:        
            fallback_type = FallbackType.NONE
        else:
            (fallback_type, planned_playlist) = self.get_fallback_playlist(timeslot)            

        return (fallback_type, planned_playlist)



    def get_fallback_playlist(self, timeslot):
        """
        Retrieves the playlist to be used in a fallback scenario.

        Args: 
            timeslot (Timeslot)

        Returns:
            (Playlist) 
        """        
        playlist = None
        fallback_type = FallbackType.STATION

        if self.validate_playlist(timeslot, "schedule_fallback"):
            playlist = timeslot.schedule_fallback
            fallback_type = FallbackType.SCHEDULE
        elif self.validate_playlist(timeslot, "show_fallback"):
            playlist = timeslot.show_fallback
            fallback_type = FallbackType.SHOW

        return (fallback_type, playlist)



    def validate_playlist(self, timeslot, playlist_type):
        """
        Checks if a playlist is valid for play-out.

        Following checks are done for all playlists:

            - has one or more entries

        Fallback playlists should either:
                
            - have filesystem entries only
            - reference a recording of a previous playout of a show (also filesystem)
        
        Otherwise, if a fallback playlist contains Live or Stream entries,
        the exact playout behaviour can hardly be predicted.
        """
        playlist = getattr(timeslot, playlist_type)
        if playlist \
            and playlist.entries \
            and len(playlist.entries) > 0:

            # Default playlist
            if playlist_type == "playlist":
                return True

            # Fallback playlist
            elif playlist.entries:
                is_fs_only = True
                for entry in playlist.entries:
                    if entry.get_content_type() not in ResourceClass.FILE.types:
                        self.logger.error(SU.red("Fallback playlist of type '%s' contains not only file-system entries! \
                            Skipping fallback level..." % playlist_type))
                        is_fs_only = False
                        break                
                return is_fs_only

        return False






class FallbackCommand(EngineExecutor):
    """
    Command composition for executing timed scheduling and unscheduling of fallback playlists.

    Based on the `timeslot.start_date` and `timeslot.end_date` two `EngineExecutor commands
    are created.
    """


    def __init__(self, timeslot, entries):
        """
        Constructor

        Args:
            timeslot (Timeslot):    The timeslot any fallback entries should be scheduled for
            entries (List):         List of entries to be scheduled as fallback
        """        
        from src.core.engine import Engine

        def do_play(entries):
            self.logger.info(SU.cyan(f"=== start_fallback_playlist('{entries}') ==="))
            Engine.get_instance().player.start_fallback_playlist(entries)

        def do_stop():
            self.logger.info(SU.cyan("=== stop_fallback_playlist() ==="))
            Engine.get_instance().player.stop_fallback_playlist()

        # Start fade-out 50% before the end of the timeslot for a more smooth transition
        end_time_offset = int(float(AuraConfig.config().get("fade_out_time")) / 2 * 1000 * -1)
        end_time = SU.timestamp(timeslot.timeslot_end + timedelta(milliseconds=end_time_offset))
        self.logger.info(f"Starting fade-out of scheduled fallback with an offset of {end_time_offset} milliseconds at {end_time}")
        super().__init__("FALLBACK", None, timeslot.start_unix, do_play, entries)
        EngineExecutor("FALLBACK", self, end_time, do_stop, None)