import copy
import datetime
import random
import traceback
from collections import deque
from itertools import islice

import langdetect

from utils.custom_exceptions import ExtractionError, CheckError
from .entry import URLPlaylistEntry
from .event_emitter import EventEmitter


class Playlist(EventEmitter):
    """
        A playlist is manages the list of songs that will be played.
    """

    def __init__(self, bot):
        super().__init__()
        self.bot = bot
        self.loop = bot.loop
        self.downloader = bot.downloader
        self.entries = deque()
        self.repeat_entries = deque()
        self.repeat = False

    def __iter__(self):
        return iter(self.entries)

    def shuffle(self):
        random.shuffle(self.entries)

    def clear(self):
        self.entries.clear()

    @property
    def count(self):
        return len(self.entries)

    async def add_entry(self, song_url, message, **meta):
        """
            Validates and adds a song_url to be played. This does not start the download of the song.

            Returns the entry & the position it is in the queue.

            :param message: the message that triggered this function.
            :param song_url: The song url to add to the playlist.
            :param meta: Any additional metadata to add to the playlist entry.
        """
        try:
            info = await self.downloader.extract_info(self.loop, song_url, download=False)
        except Exception as e:
            raise ExtractionError('Could not extract information from {}\n\n{}'.format(song_url, e))
        if not info:
            raise ExtractionError('Could not extract information from %s' % song_url)
        if info.get('display_id', None) == 'live':
            raise CheckError('Live')
        overtime = meta.get('overtime', False)
        shuffle = meta.get('shuffle', False)
        entry = None

        # if info.get('_type', None) == 'playlist' and len(info['entries']) == 1:
        #    raise WrongEntryTypeError("SearchError", True, info.get('webpage_url', None) or info.get('url', None))
        if 'entries' in info:
            entries = info['entries']
        else:
            entries = [info]
        if shuffle:
            random.shuffle(entries)
        omitted = 0
        for i in entries:
            if i.get('duration', 0) > 3700 and not overtime:
                omitted += 1
                continue
            is_gay = False
            try:
                is_gay = await self.loop.run_in_executor(self.bot.thread_pool, langdetect.detect, i.get('title'))
                is_gay = is_gay in 'ar'
            except:
                pass
            if is_gay:
                raise CheckError('GayServer')
            entry = URLPlaylistEntry(
                self,
                i,
                message,
                self.downloader.ytdl.prepare_filename(i),
                **meta
            )
            self._add_entry(entry)

        if omitted:
            return {
                'omitted': omitted,
                'entry_msg': f'The song {str(entry)} was omitted due to duration'
                }
        return {'omitted': omitted, 'entry_msg': entry}

    async def import_from(self, playlist_url, message, **meta):
        """
            Imports the songs from `playlist_url` and queues them to be played.

            Returns a list of `entries` that have been enqueued.

            :param message: the message obj
            :param playlist_url: The playlist url to be cut into individual urls and added to the playlist
            :param meta: Any additional metadata to add to the playlist entry
        """
        position = len(self.entries) + 1
        entry_list = []

        try:
            info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False)
        except Exception as e:
            raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))

        if not info:
            raise ExtractionError('Could not extract information from %s' % playlist_url)

        # Once again, the generic extractor fucks things up.

        baditems = 0
        for items in info['entries']:
            if items:
                try:
                    entry = URLPlaylistEntry(
                        self,
                        items,
                        message,
                        self.downloader.ytdl.prepare_filename(info),
                        **meta
                    )

                    self._add_entry(entry)
                    entry_list.append(entry)
                except:
                    baditems += 1
                    # Once I know more about what's happening here I can add a proper message
                    traceback.print_exc()
                    print(items)
                    print("Could not add item")
            else:
                baditems += 1

        if baditems:
            print("Skipped %s bad entries" % baditems)

        return entry_list, position

    async def async_process_playlist(self, playlist_url, **meta):
        """
            Processes youtube playlists links from `playlist_url` in a questionable, async fashion.

            :param playlist_url: The playlist url to be cut into individual urls and added to the playlist
            :param meta: Any additional metadata to add to the playlist entry
        """

        try:
            info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False, process=False)
        except Exception as e:
            raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))

        if not info:
            raise ExtractionError('Could not extract information from %s' % playlist_url)

        entries = info.get('entries')
        if not entries:
            return await self.add_entry(playlist_url, **meta)

        if meta.get('shuffle'):
            entries = [*info['entries']]
            random.shuffle(entries)
            meta.pop('shuffle')

        baditems = 0
        omitted = 0
        for entry_data in entries:
            if entry_data:
                song_url = None
                if info['extractor'] == 'youtube:playlist':
                    baseurl = info['webpage_url'].split('playlist?list=')[0]
                    song_url = baseurl + 'watch?v=%s' % entry_data['id']
                elif info['extractor'] == 'soundcloud:set':
                    song_url = entry_data['url']

                try:
                    info_dict = await self.add_entry(song_url, **meta)
                    _omitted, entry_msg = info_dict.values()
                    omitted += _omitted
                except ExtractionError:
                    baditems += 1
                except Exception as e:
                    baditems += 1
                    print("There was an error adding the song {}: {}: {}\n".format(
                        entry_data['id'], e.__class__.__name__, e))
            else:
                baditems += 1

        if baditems:
            print("Skipped %s bad entries" % baditems)

        if omitted > 0:
            return {
                'omitted': omitted,
                'entry_msg': f'{len(entries) - omitted} songs. {omitted} songs were omitted due to duration'
                }
        return {
            'omitted': omitted,
            'entry_msg': f'**{info.get("title", "gay playlist")}** with {len(entries)} songs'
        }

    async def async_process_sc_bc_playlist(self, playlist_url, **meta):
        """
            Processes soundcloud set and bancdamp album links from `playlist_url` in a questionable, async fashion.

            :param playlist_url: The playlist url to be cut into individual urls and added to the playlist
            :param meta: Any additional metadata to add to the playlist entry
        """

        try:
            info = await self.downloader.safe_extract_info(self.loop, playlist_url, download=False, process=False)
        except Exception as e:
            raise ExtractionError('Could not extract information from {}\n\n{}'.format(playlist_url, e))

        if not info:
            raise ExtractionError('Could not extract information from %s' % playlist_url)

        gooditems = []
        baditems = 0
        for entry_data in info['entries']:
            if entry_data:
                song_url = entry_data['url']

                try:
                    entry, elen = await self.add_entry(song_url, **meta)
                    gooditems.append(entry)
                except ExtractionError:
                    baditems += 1
                except Exception as e:
                    baditems += 1
                    print("There was an error adding the song {}: {}: {}\n".format(
                        entry_data['id'], e.__class__.__name__, e))
            else:
                baditems += 1

        if baditems:
            print("Skipped %s bad entries" % baditems)

        return gooditems

    def _add_entry(self, entry):
        self.entries.append(entry)
        self.repeat_entries.append(entry)
        self.emit('entry-added', playlist=self, entry=entry)

        if self.peek() is entry:
            entry.get_ready_future()

    async def get_next_entry(self, predownload_next=True):
        """
            A coroutine which will return the next song or None if no songs left to play.

            Additionally, if predownload_next is set to True, it will attempt to download the next
            song to be played - so that it's ready by the time we get to it.
        """
        if not self.entries:
            if self.repeat:
                self.entries = copy.copy(self.repeat_entries)
            else:
                return None

        entry = self.entries.popleft()

        if predownload_next:
            next_entry = self.peek()
            if next_entry:
                next_entry.get_ready_future()

        return await entry.get_ready_future()

    def peek(self):
        """
            Returns the next entry that should be scheduled to be played.
        """
        if self.entries:
            return self.entries[0]

    async def estimate_time_until(self, position, player):
        """
            (very) Roughly estimates the time till the queue will 'position'
        """
        estimated_time = sum([e.duration for e in islice(self.entries, position - 1)])

        # When the player plays a song, it eats the first playlist item, so we just have to add the time back
        if not player.is_stopped and player.current_entry:
            estimated_time += player.current_entry.duration - player.progress

        return datetime.timedelta(seconds=estimated_time)

    def count_for_user(self, user):
        return sum(1 for e in self.entries if e.meta.get('author', None) == user)
