# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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
# (at your option) any later version.
#
# gPodder 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 this program.  If not, see <http://www.gnu.org/licenses/>.
#

# gpodder.gtkui.desktop.sync - Glue code between GTK+ UI and sync module
# Thomas Perl <thp@gpodder.org>; 2009-09-05 (based on code from gui.py)
# Ported to gPodder 3 by Joseph Wickremasinghe in June 2012

import logging

import gpodder
from gpodder import sync, util
from gpodder.deviceplaylist import gPodderDevicePlaylist

_ = gpodder.gettext


logger = logging.getLogger(__name__)


class gPodderSyncUI(object):
    # download list states
    (DL_ONEOFF, DL_ADDING_TASKS, DL_ADDED_TASKS) = list(range(3))

    def __init__(self, config, notification, parent_window,
                 show_confirmation,
                 show_preferences,
                 channels,
                 download_status_model,
                 download_queue_manager,
                 set_download_list_state,
                 commit_changes_to_database,
                 delete_episode_list,
                 select_episodes_to_delete,
                 mount_volume_for_file):
        self.device = None

        self._config = config
        self.notification = notification
        self.parent_window = parent_window
        self.show_confirmation = show_confirmation

        self.show_preferences = show_preferences
        self.channels = channels
        self.download_status_model = download_status_model
        self.download_queue_manager = download_queue_manager
        self.set_download_list_state = set_download_list_state
        self.commit_changes_to_database = commit_changes_to_database
        self.delete_episode_list = delete_episode_list
        self.select_episodes_to_delete = select_episodes_to_delete
        self.mount_volume_for_file = mount_volume_for_file

    def _filter_sync_episodes(self, channels, only_downloaded=False):
        """Return a list of episodes for device synchronization.

        If only_downloaded is True, this will skip episodes that
        have not been downloaded yet and podcasts that are marked
        as "Do not synchronize to my device".
        """
        episodes = []
        for channel in channels:
            if only_downloaded or not channel.sync_to_mp3_player:
                logger.info('Skipping channel: %s', channel.title)
                continue

            for episode in channel.get_all_episodes():
                if (episode.was_downloaded(and_exists=True)
                        or not only_downloaded):
                    episodes.append(episode)
        return episodes

    def _show_message_unconfigured(self):
        title = _('No device configured')
        message = _('Please set up your device in the preferences dialog.')
        if self.show_confirmation(message, title):
            self.show_preferences(self.parent_window, None)

    def _show_message_cannot_open(self):
        title = _('Cannot open device')
        message = _('Please check logs and the settings in the preferences dialog.')
        self.notification(message, title, important=True)

    def on_synchronize_episodes(self, channels, episodes=None, force_played=True, done_callback=None):
        device = sync.open_device(self)

        if device is None:
            self._show_message_unconfigured()
            if done_callback:
                done_callback()
            return

        try:
            if not device.open():
                self._show_message_cannot_open()
                if done_callback:
                    done_callback()
                return
            else:
                # Only set if device is configured and opened successfully
                self.device = device
        except Exception as err:
            logger.error('opening destination %s failed with %s',
                device.destination.get_uri(), err.message)
            self._show_message_cannot_open()
            if done_callback:
                done_callback()
            return

        if episodes is None:
            force_played = False
            episodes = self._filter_sync_episodes(channels)

        def check_free_space():
            # "Will we add this episode to the device?"
            def will_add(episode):
                # If already on-device, it won't take up any space
                if device.episode_on_device(episode):
                    return False

                # Might not be synced if it's played already
                if (not force_played
                        and self._config.device_sync.skip_played_episodes):
                    return False

                # In all other cases, we expect the episode to be
                # synchronized to the device, so "answer" positive
                return True

            # "What is the file size of this episode?"
            def file_size(episode):
                filename = episode.local_filename(create=False)
                if filename is None:
                    return 0
                return util.calculate_size(str(filename))

            # Calculate total size of sync and free space on device
            total_size = sum(file_size(e) for e in episodes if will_add(e))
            free_space = max(device.get_free_space(), 0)

            if total_size > free_space:
                title = _('Not enough space left on device')
                message = (_('Additional free space required: %(required_space)s\nDo you want to continue?') %
               {'required_space': util.format_filesize(total_size - free_space)})
                if not self.show_confirmation(message, title):
                    device.cancel()
                    device.close()
                    return

            # enable updating of UI
            self.set_download_list_state(gPodderSyncUI.DL_ONEOFF)

            """Update device playlists
            General approach is as follows:

            When a episode is downloaded and synched, it is added to the
            standard playlist for that podcast which is then written to
            the device.

            After the user has played that episode on their device, they
            can delete that episode from their device.

            At the next sync, gPodder will then compare the standard
            podcast-specific playlists on the device (as written by
            gPodder during the last sync), with the episodes on the
            device.If there is an episode referenced in the playlist
            that is no longer on the device, gPodder will assume that
            the episode has already been synced and subsequently deleted
            from the device, and will hence mark that episode as deleted
            in gPodder. If there are no playlists, nothing is deleted.

            At the next sync, the playlists will be refreshed based on
            the downloaded, undeleted episodes in gPodder, and the
            cycle begins again...
            """

            def resume_sync(episode_urls, channel_urls, progress):
                if progress is not None:
                    progress.on_finished()

                # rest of sync process should continue here
                self.commit_changes_to_database()
                for current_channel in self.channels:
                    # only sync those channels marked for syncing
                    if (self._config.device_sync.device_type == 'filesystem'
                            and current_channel.sync_to_mp3_player
                            and self._config.device_sync.playlists.create):

                        # get playlist object
                        playlist = gPodderDevicePlaylist(self._config,
                                                         current_channel.title)
                        # need to refresh episode list so that
                        # deleted episodes aren't included in playlists
                        episodes_for_playlist = sorted(current_channel.get_episodes(gpodder.STATE_DOWNLOADED),
                                                       key=lambda ep: ep.published)
                        # don't add played episodes to playlist if skip_played_episodes is True
                        if self._config.device_sync.skip_played_episodes:
                            episodes_for_playlist = [ep for ep in episodes_for_playlist if ep.is_new]
                        playlist.write_m3u(episodes_for_playlist)

                # enable updating of UI, but mark it as tasks being added so that a
                # adding a single task that completes immediately doesn't turn off the
                # ui updates again
                self.set_download_list_state(gPodderSyncUI.DL_ADDING_TASKS)

                if (self._config.device_sync.device_type == 'filesystem' and self._config.device_sync.playlists.create):
                    title = _('Update successful')
                    message = _('The playlist on your MP3 player has been updated.')
                    self.notification(message, title)

                # called from the main thread to complete adding tasks
                def add_downloads_complete():
                    self.set_download_list_state(gPodderSyncUI.DL_ADDED_TASKS)

                # Finally start the synchronization process
                @util.run_in_background
                def sync_thread_func():
                    device.add_sync_tasks(episodes, force_played=force_played,
                                          done_callback=done_callback)
                    util.idle_add(add_downloads_complete)
                return

            if self._config.device_sync.playlists.create:
                try:
                    episodes_to_delete = []
                    if self._config.device_sync.playlists.two_way_sync:
                        for current_channel in self.channels:
                            # only include channels that are included in the sync
                            if current_channel.sync_to_mp3_player:
                                # get playlist object
                                playlist = gPodderDevicePlaylist(self._config, current_channel.title)
                                # get episodes to be written to playlist
                                episodes_for_playlist = sorted(current_channel.get_episodes(gpodder.STATE_DOWNLOADED),
                                                               key=lambda ep: ep.published)
                                episode_keys = list(map(playlist.get_absolute_filename_for_playlist,
                                                        episodes_for_playlist))

                                episode_dict = dict(list(zip(episode_keys, episodes_for_playlist)))

                                # then get episodes in playlist (if it exists) already on device
                                episodes_in_playlists = playlist.read_m3u()
                                # if playlist doesn't exist (yet) episodes_in_playlist will be empty
                                if episodes_in_playlists:
                                    for episode_filename in episodes_in_playlists:
                                        if ((not self._config.device_sync.playlists.use_absolute_path
                                        and not playlist.playlist_folder.resolve_relative_path(episode_filename).query_exists())
                                        or (self._config.device_sync.playlists.use_absolute_path
                                        and not playlist.mountpoint.resolve_relative_path(episode_filename).query_exists())):
                                            # episode was synced but no longer on device
                                            # i.e. must have been deleted by user, so delete from gpodder
                                            try:
                                                episodes_to_delete.append(episode_dict[episode_filename])
                                            except KeyError:
                                                logger.warning('Episode %s, removed from device has already been deleted from gpodder',
                                                            episode_filename)
                    # delete all episodes from gpodder (will prompt user)

                    # not using playlists to delete
                    def auto_delete_callback(episodes):

                        if not episodes:
                            # episodes were deleted on device
                            # but user decided not to delete them from gpodder
                            # so jump straight to sync
                            logger.info('Starting sync - no episodes selected for deletion')
                            resume_sync([], [], None)
                        else:
                            # episodes need to be deleted from gpodder
                            for episode_to_delete in episodes:
                                logger.info("Deleting episode %s",
                                            episode_to_delete.title)

                            logger.info('Will start sync - after deleting episodes')
                            self.delete_episode_list(episodes, False, resume_sync)

                        return

                    if episodes_to_delete:
                        columns = (
                            ('markup_delete_episodes', None, None, _('Episode')),
                        )

                        self.select_episodes_to_delete(
                            self.parent_window,
                            title=_('Episodes have been deleted on device'),
                            instructions='Select the episodes you want to delete:',
                            episodes=episodes_to_delete,
                            selected=[True, ] * len(episodes_to_delete),
                            columns=columns,
                            callback=auto_delete_callback,
                            _config=self._config)
                    else:
                        logger.warning("Starting sync - no episodes to delete")
                        resume_sync([], [], None)

                except IOError as ioe:
                    title = _('Error writing playlist files')
                    message = _(str(ioe))
                    self.notification(message, title)
            else:
                logger.info('Not creating playlists - starting sync')
                resume_sync([], [], None)

        # This function is used to remove files from the device
        def cleanup_episodes():
            # 'skip_played_episodes' must be used or else all the
            # played tracks will be copied then immediately deleted
            if (self._config.device_sync.delete_deleted_episodes
                or (self._config.device_sync.delete_played_episodes
                    and self._config.device_sync.skip_played_episodes)):
                all_episodes = self._filter_sync_episodes(
                    channels, only_downloaded=False)
                for local_episode in all_episodes:
                    episode = device.episode_on_device(local_episode)
                    if episode is None:
                        continue

                    if local_episode.state == gpodder.STATE_DELETED:
                        logger.info('Removing episode from device: %s',
                                    episode.title)
                        device.remove_track(episode)

            # When this is done, start the callback in the UI code
            util.idle_add(check_free_space)

        # This will run the following chain of actions:
        #  1. Remove old episodes (in worker thread)
        #  2. Check for free space (in UI thread)
        #  3. Sync the device (in UI thread)
        util.run_in_background(cleanup_episodes)
