# -*- 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/>.
#


#
#  config.py -- gPodder Configuration Manager
#  Thomas Perl <thp@perli.net>   2007-11-02
#


import atexit
import logging
import os
import time

import gpodder
from gpodder import jsonconfig, util

_ = gpodder.gettext

defaults = {
    # External applications used for playback
    'player': {
        'audio': 'default',
        'video': 'default',
    },

    # gpodder.net settings
    'mygpo': {
        'enabled': False,
        'server': 'gpodder.net',
        'username': '',
        'password': '',
        'device': {
            'uid': util.get_hostname(),
            'type': 'desktop',
            'caption': _('gPodder on %s') % util.get_hostname(),
        },
    },

    # Various limits (downloading, updating, etc..)
    'limit': {
        'bandwidth': {
            'enabled': False,
            'kbps': 500.0,  # maximum kB/s per download
        },
        'downloads': {
            'enabled': True,
            'concurrent': 1,
            'concurrent_max': 16,
        },
        'episodes': 200,  # max episodes per feed
    },

    # Behavior of downloads
    'downloads': {
        'chronological_order': True,  # download older episodes first
    },

    # Automatic feed updates, download removal and retry on download timeout
    'auto': {
        'update': {
            'enabled': False,
            'frequency': 20,  # minutes
        },

        'cleanup': {
            'days': 7,
            'played': False,
            'unplayed': False,
            'unfinished': True,
        },

        'retries': 3,  # number of retries when downloads time out
    },

    'check_connection': True,

    # Software updates from gpodder.org
    'software_update': {
        'check_on_startup': True,  # check for updates on start
        'last_check': 0,  # unix timestamp of last update check
        'interval': 5,  # interval (in days) to check for updates
    },

    'ui': {
        # Settings for the Command-Line Interface
        'cli': {
            'colors': True,
        },

        # Settings for the Gtk UI
        'gtk': {
            'state': {
                'main_window': {
                    'width': 700,
                    'height': 500,
                    'x': -1, 'y': -1, 'maximized': False,

                    'paned_position': 200,
                    'episode_list_size': 200,

                    'episode_column_sort_id': 0,
                    'episode_column_sort_order': False,
                    'episode_column_order': [],
                },
                'podcastdirectory': {
                    'width': -1,
                    'height': -1,
                    'x': -1, 'y': -1, 'maximized': False,
                },
                'preferences': {
                    'width': -1,
                    'height': -1,
                    'x': -1, 'y': -1, 'maximized': False,
                },
                'config_editor': {
                    'width': -1,
                    'height': -1,
                    'x': -1, 'y': -1, 'maximized': False,
                },
                'channel_editor': {
                    'width': -1,
                    'height': -1,
                    'x': -1, 'y': -1, 'maximized': False,
                },
                'episode_selector': {
                    'width': 600,
                    'height': 400,
                    'x': -1, 'y': -1, 'maximized': False,
                },
                'episode_window': {
                    'width': 500,
                    'height': 400,
                    'x': -1, 'y': -1, 'maximized': False,
                },
                'export_to_local_folder': {
                    'width': 500,
                    'height': 400,
                    'x': -1, 'y': -1, 'maximized': False,
                }
            },

            'toolbar': False,
            'new_episodes': 'show',  # ignore, show, queue, download
            'only_added_are_new': False,  # Only just added episodes are considered new after an update
            'live_search_delay': 200,
            'search_always_visible': False,
            'find_as_you_type': True,

            'podcast_list': {
                'view_mode': 1,
                'hide_empty': False,
                'all_episodes': True,
                'sections': True,
            },

            'episode_list': {
                'view_mode': 1,
                'always_show_new': True,
                'trim_title_prefix': True,
                'descriptions': True,
                'show_released_time': False,
                'right_align_released_column': False,
                'ctrl_click_to_sort': False,
                'columns': int('110', 2),  # bitfield of visible columns
            },

            'download_list': {
                'remove_finished': True,
            },

            'html_shownotes': True,  # enable webkit renderer
            'color_scheme': None,  # system, light or dark. Initialized in app.py
        },
    },

    # Synchronization with portable devices (MP3 players, etc..)
    'device_sync': {
        'device_type': 'none',  # Possible values: 'none', 'filesystem', 'ipod'
        'device_folder': '/media',

        'one_folder_per_podcast': True,
        'skip_played_episodes': True,
        'delete_played_episodes': False,
        'delete_deleted_episodes': False,

        'max_filename_length': 120,

        'compare_episode_filesize': True,

        'custom_sync_name': '{episode.sortdate}_{episode.title}',
        'custom_sync_name_enabled': False,

        'after_sync': {
            'mark_episodes_played': False,
            'delete_episodes': False,
            'sync_disks': False,
        },
        'playlists': {
            'create': True,
            'two_way_sync': False,
            'use_absolute_path': True,
            'folder': 'Playlists',
            'extension': 'm3u',
        }

    },

    'youtube': {
        'preferred_fmt_id': 18,  # default fmt_id (see fallbacks in youtube.py)
        'preferred_fmt_ids': [],  # for advanced uses (custom fallback sequence)
        'preferred_hls_fmt_id': 93,  # default fmt_id (see fallbacks in youtube.py)
        'preferred_hls_fmt_ids': [],  # for advanced uses (custom fallback sequence)
    },

    'vimeo': {
        'fileformat': '720p',  # preferred file format (see vimeo.py)
    },

    'network': {
        'use_proxy': False,
        'proxy_type': 'socks5h',  # Possible values: socks5h (routes dns through the proxy), socks5, http
        'proxy_hostname': '127.0.0.1',
        'proxy_port': '8123',
        'proxy_use_username_password': False,
        'proxy_username': '',
        'proxy_password': '',
    },

    'extensions': {
        'enabled': [],
    },
    'sendto': {
        'custom_file_format': '{episode.title}',
        'custom_file_format_enabled': False,
    },
    'path': {
        'alternate': '',
    },
}

logger = logging.getLogger(__name__)

# Global variable for network proxies. Updated when the network proxy in the config changes
_proxies = None


def get_network_proxy_observer(config):
    """Return an observer function inside a closure containing given config instance."""

    def get_proxies_from_config(config):
        proxies = None
        if config.network.use_proxy:
            protocol = config.network.proxy_type
            user_pass = ""
            if config.network.proxy_use_username_password:
                user_pass = f"{config.network.proxy_username}:{config.network.proxy_password}@"
            proxy_url = f"{protocol}://{user_pass}{config.network.proxy_hostname}:{config.network.proxy_port}"
            proxies = {"http": proxy_url, "https": proxy_url}
        logger.debug(f"config observer returning proxies: {proxies}")
        return proxies

    def network_proxy_observer(name, old_value, new_value):
        global _proxies
        if name.startswith("network."):
            _proxies = get_proxies_from_config(config)

    return network_proxy_observer


def config_value_to_string(config_value):
    config_type = type(config_value)

    if config_type == list:
        return ','.join(map(config_value_to_string, config_value))
    elif config_type in (str, str):
        return config_value
    else:
        return str(config_value)


def string_to_config_value(new_value, old_value):
    config_type = type(old_value)

    if config_type == list:
        return [_f for _f in [x.strip() for x in new_value.split(',')] if _f]
    elif config_type == bool:
        return (new_value.strip().lower() in ('1', 'true'))
    else:
        return config_type(new_value)


class Config(object):
    # Number of seconds after which settings are auto-saved
    WRITE_TO_DISK_TIMEOUT = 60

    def __init__(self, filename='gpodder.json'):
        self.__json_config = jsonconfig.JsonConfig(default=defaults,
                on_key_changed=self._on_key_changed)
        self.__save_thread = None
        self.__filename = filename
        self.__observers = []

        self.load()
        self.migrate_defaults()

        # If there is no configuration file, we create one here (bug 1511)
        if not os.path.exists(self.__filename):
            self.save()

        atexit.register(self.__atexit)

        if self.path.alternate != '':
            os.environ['PATH'] += os.pathsep + self.path.alternate
            logger.info('Appending alternate PATH: %s' % self.path.alternate)

    def register_defaults(self, defaults):
        """Register default configuration options (e.g. for extensions).

        This function takes a dictionary that will be merged into the
        current configuration if the keys don't yet exist. This can
        be used to add a default configuration for extension modules.
        """
        self.__json_config._merge_keys(defaults)

    def add_observer(self, callback):
        """Add a callback function as observer.

        This callback will be called when a setting changes. It should
        have this signature:

            observer(name, old_value, new_value)

        The "name" is the setting name, the "old_value" is
        the value that has been overwritten with "new_value".
        """
        if callback not in self.__observers:
            self.__observers.append(callback)
        else:
            logger.warning('Observer already added: %s', repr(callback))

    def remove_observer(self, callback):
        """Remove an observer previously added to this object."""
        if callback in self.__observers:
            self.__observers.remove(callback)
        else:
            logger.warning('Observer not added: %s', repr(callback))

    def all_keys(self):
        return self.__json_config._keys_iter()

    def schedule_save(self):
        if self.__save_thread is None:
            self.__save_thread = util.run_in_background(self.save_thread_proc, True)

    def save_thread_proc(self):
        time.sleep(self.WRITE_TO_DISK_TIMEOUT)
        if self.__save_thread is not None:
            self.save()

    def __atexit(self):
        if self.__save_thread is not None:
            self.save()

    def save(self, filename=None):
        if filename is None:
            filename = self.__filename

        logger.info('Flushing settings to disk')

        try:
            # revoke unix group/world permissions (this has no effect under windows)
            umask = os.umask(0o077)
            with open(filename + '.tmp', 'wt') as fp:
                fp.write(repr(self.__json_config))
            util.atomic_rename(filename + '.tmp', filename)
        except:
            logger.error('Cannot write settings to %s', filename)
            util.delete_file(filename + '.tmp')
            raise
        finally:
            os.umask(umask)

        self.__save_thread = None

    def load(self, filename=None):
        if filename is not None:
            self.__filename = filename

        if os.path.exists(self.__filename):
            try:
                with open(self.__filename, 'rt') as f:
                    data = f.read()
                new_keys_added = self.__json_config._restore(data)
            except:
                logger.warning('Cannot parse config file: %s',
                        self.__filename, exc_info=True)
                new_keys_added = False

            if new_keys_added:
                logger.info('New default keys added - saving config.')
                self.save()

    def toggle_flag(self, name):
        setattr(self, name, not getattr(self, name))

    def update_field(self, name, new_value):
        """Update a config field, converting strings to the right types."""
        old_value = self._lookup(name)
        new_value = string_to_config_value(new_value, old_value)
        setattr(self, name, new_value)
        return True

    def _on_key_changed(self, name, old_value, value):
        if 'ui.gtk.state' not in name:
            # Only log non-UI state changes
            logger.debug('%s: %s -> %s', name, old_value, value)
        for observer in self.__observers:
            try:
                observer(name, old_value, value)
            except Exception as exception:
                logger.error('Error while calling observer %r: %s',
                        observer, exception, exc_info=True)

        self.schedule_save()

    def __getattr__(self, name):
        return getattr(self.__json_config, name)

    def __setattr__(self, name, value):
        if name.startswith('_'):
            object.__setattr__(self, name, value)
            return

        setattr(self.__json_config, name, value)

    def migrate_defaults(self):
        """Change default values in config."""
        if self.device_sync.max_filename_length == 999:
            logger.debug("setting config.device_sync.max_filename_length=120"
                         " (999 is bad for NTFS and ext{2-4})")
            self.device_sync.max_filename_length = 120

    def clamp_range(self, name, minval, maxval):
        value = getattr(self, name)
        if value < minval:
            setattr(self, name, minval)
            return True
        if value > maxval:
            setattr(self, name, maxval)
            return True
        return False
