# -*- coding: utf-8 -*-
# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
"""All functionality that requires Kodi imports"""

from __future__ import absolute_import, division, unicode_literals
from contextlib import contextmanager
from sys import version_info

import xbmc
import xbmcaddon
import xbmcplugin
from utils import from_unicode, to_unicode

ADDON = xbmcaddon.Addon()
DEFAULT_CACHE_DIR = 'cache'

SORT_METHODS = dict(
    # date=xbmcplugin.SORT_METHOD_DATE,
    dateadded=xbmcplugin.SORT_METHOD_DATEADDED,
    duration=xbmcplugin.SORT_METHOD_DURATION,
    episode=xbmcplugin.SORT_METHOD_EPISODE,
    # genre=xbmcplugin.SORT_METHOD_GENRE,
    # label=xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE,
    label=xbmcplugin.SORT_METHOD_LABEL,
    title=xbmcplugin.SORT_METHOD_TITLE,
    # none=xbmcplugin.SORT_METHOD_UNSORTED,
    # FIXME: We would like to be able to sort by unprefixed title (ignore date/episode prefix)
    # title=xbmcplugin.SORT_METHOD_TITLE_IGNORE_THE,
    unsorted=xbmcplugin.SORT_METHOD_UNSORTED,
)

WEEKDAY_LONG = {
    '0': xbmc.getLocalizedString(17),
    '1': xbmc.getLocalizedString(11),
    '2': xbmc.getLocalizedString(12),
    '3': xbmc.getLocalizedString(13),
    '4': xbmc.getLocalizedString(14),
    '5': xbmc.getLocalizedString(15),
    '6': xbmc.getLocalizedString(16),
}

MONTH_LONG = {
    '01': xbmc.getLocalizedString(21),
    '02': xbmc.getLocalizedString(22),
    '03': xbmc.getLocalizedString(23),
    '04': xbmc.getLocalizedString(24),
    '05': xbmc.getLocalizedString(25),
    '06': xbmc.getLocalizedString(26),
    '07': xbmc.getLocalizedString(27),
    '08': xbmc.getLocalizedString(28),
    '09': xbmc.getLocalizedString(29),
    '10': xbmc.getLocalizedString(30),
    '11': xbmc.getLocalizedString(31),
    '12': xbmc.getLocalizedString(32),
}

WEEKDAY_SHORT = {
    '0': xbmc.getLocalizedString(47),
    '1': xbmc.getLocalizedString(41),
    '2': xbmc.getLocalizedString(42),
    '3': xbmc.getLocalizedString(43),
    '4': xbmc.getLocalizedString(44),
    '5': xbmc.getLocalizedString(45),
    '6': xbmc.getLocalizedString(46),
}

MONTH_SHORT = {
    '01': xbmc.getLocalizedString(51),
    '02': xbmc.getLocalizedString(52),
    '03': xbmc.getLocalizedString(53),
    '04': xbmc.getLocalizedString(54),
    '05': xbmc.getLocalizedString(55),
    '06': xbmc.getLocalizedString(56),
    '07': xbmc.getLocalizedString(57),
    '08': xbmc.getLocalizedString(58),
    '09': xbmc.getLocalizedString(59),
    '10': xbmc.getLocalizedString(60),
    '11': xbmc.getLocalizedString(61),
    '12': xbmc.getLocalizedString(62),
}


class SafeDict(dict):
    """A safe dictionary implementation that does not break down on missing keys"""
    def __missing__(self, key):
        """Replace missing keys with the original placeholder"""
        return '{' + key + '}'


def addon_icon():
    """Return add-on icon"""
    return get_addon_info('icon')


def addon_id():
    """Return add-on ID"""
    return get_addon_info('id')


def addon_fanart():
    """Return add-on fanart"""
    return get_addon_info('fanart')


def addon_name():
    """Return add-on name"""
    return get_addon_info('name')


def addon_path():
    """Return add-on path"""
    return get_addon_info('path')


def addon_profile():
    """Return add-on profile"""
    return to_unicode(xbmc.translatePath(ADDON.getAddonInfo('profile')))


def url_for(name, *args, **kwargs):
    """Wrapper for routing.url_for() to lookup by name"""
    import addon
    return addon.plugin.url_for(getattr(addon, name), *args, **kwargs)


def show_listing(list_items, category=None, sort='unsorted', ascending=True, content=None, cache=None, selected=None):
    """Show a virtual directory in Kodi"""
    from xbmcgui import ListItem
    from addon import plugin

    set_property('container.url', 'plugin://' + addon_id() + plugin.path)
    xbmcplugin.setPluginFanart(handle=plugin.handle, image=from_unicode(addon_fanart()))

    usemenucaching = get_setting_bool('usemenucaching', default=True)
    if cache is None:
        cache = usemenucaching
    elif usemenucaching is False:
        cache = False

    if content:
        # content is one of: files, songs, artists, albums, movies, tvshows, episodes, musicvideos
        xbmcplugin.setContent(plugin.handle, content=content)

    # Jump through hoops to get a stable breadcrumbs implementation
    category_label = ''
    if category:
        if not content:
            category_label = 'VRT NU / '
        if plugin.path.startswith(('/favorites/', '/resumepoints/')):
            category_label += localize(30428) + ' / '  # My
        if isinstance(category, int):
            category_label += localize(category)
        else:
            category_label += category
    elif not content:
        category_label = 'VRT NU'
    xbmcplugin.setPluginCategory(handle=plugin.handle, category=category_label)

    # FIXME: Since there is no way to influence descending order, we force it here
    if not ascending:
        sort = 'unsorted'

    # NOTE: When showing tvshow listings and 'showoneoff' was set, force 'unsorted'
    if get_setting_bool('showoneoff', default=True) and sort == 'label' and content == 'tvshows':
        sort = 'unsorted'

    # Add all sort methods to GUI (start with preferred)
    xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[sort])
    for key in sorted(SORT_METHODS):
        if key != sort:
            xbmcplugin.addSortMethod(handle=plugin.handle, sortMethod=SORT_METHODS[key])

    # FIXME: This does not appear to be working, we have to order it ourselves
#    xbmcplugin.setProperty(handle=plugin.handle, key='sort.ascending', value='true' if ascending else 'false')
#    if ascending:
#        xbmcplugin.setProperty(handle=plugin.handle, key='sort.order', value=str(SORT_METHODS[sort]))
#    else:
#        # NOTE: When descending, use unsorted
#        xbmcplugin.setProperty(handle=plugin.handle, key='sort.order', value=str(SORT_METHODS['unsorted']))

    listing = []
    showfanart = get_setting_bool('showfanart', default=True)
    for title_item in list_items:
        # Three options:
        #  - item is a virtual directory/folder (not playable, path)
        #  - item is a playable file (playable, path)
        #  - item is non-actionable item (not playable, no path)
        is_folder = bool(not title_item.is_playable and title_item.path)
        is_playable = bool(title_item.is_playable and title_item.path)

        list_item = ListItem(label=title_item.label)

        prop_dict = dict(
            IsInternetStream='true' if is_playable else 'false',
            IsPlayable='true' if is_playable else 'false',
            IsFolder='false' if is_folder else 'true',
        )
        if title_item.prop_dict:
            title_item.prop_dict.update(prop_dict)
        else:
            title_item.prop_dict = prop_dict
        # NOTE: The setProperties method is new in Kodi18
        try:
            list_item.setProperties(title_item.prop_dict)
        except AttributeError:
            for key, value in list(title_item.prop_dict.items()):
                list_item.setProperty(key=key, value=str(value))

        # FIXME: The setIsFolder method is new in Kodi18, so we cannot use it just yet
        # list_item.setIsFolder(is_folder)

        if showfanart:
            # Add add-on fanart when fanart is missing
            if not title_item.art_dict:
                title_item.art_dict = dict(fanart=addon_fanart())
            elif not title_item.art_dict.get('fanart'):
                title_item.art_dict.update(fanart=addon_fanart())
            list_item.setArt(title_item.art_dict)

        if title_item.info_dict:
            # type is one of: video, music, pictures, game
            list_item.setInfo(type='video', infoLabels=title_item.info_dict)

        if title_item.stream_dict:
            # type is one of: video, audio, subtitle
            list_item.addStreamInfo('video', title_item.stream_dict)

        if title_item.context_menu:
            list_item.addContextMenuItems(title_item.context_menu)

        url = None
        if title_item.path:
            url = title_item.path

        listing.append((url, list_item, is_folder))

    # Jump to specific item
    if selected is not None:
        pass
#        from xbmcgui import getCurrentWindowId, Window
#        wnd = Window(getCurrentWindowId())
#        wnd.getControl(wnd.getFocusId()).selectItem(selected)

    succeeded = xbmcplugin.addDirectoryItems(plugin.handle, listing, len(listing))
    xbmcplugin.endOfDirectory(plugin.handle, succeeded, updateListing=False, cacheToDisc=cache)


def play(stream, video=None):
    """Create a virtual directory listing to play its only item"""
    try:  # Python 3
        from urllib.parse import unquote
    except ImportError:  # Python 2
        from urllib2 import unquote

    from xbmcgui import ListItem
    from addon import plugin

    play_item = ListItem(path=stream.stream_url)
    if video and hasattr(video, 'info_dict'):
        play_item.setProperty('subtitle', video.label)
        play_item.setArt(video.art_dict)
        play_item.setInfo(
            type='video',
            infoLabels=video.info_dict
        )
    play_item.setProperty('inputstream.adaptive.max_bandwidth', str(get_max_bandwidth() * 1000))
    play_item.setProperty('network.bandwidth', str(get_max_bandwidth() * 1000))
    if stream.stream_url is not None and stream.use_inputstream_adaptive:
        if kodi_version_major() < 19:
            play_item.setProperty('inputstreamaddon', 'inputstream.adaptive')
        else:
            play_item.setProperty('inputstream', 'inputstream.adaptive')
        play_item.setProperty('inputstream.adaptive.manifest_type', 'mpd')
        play_item.setContentLookup(False)
        play_item.setMimeType('application/dash+xml')
        if stream.license_key is not None:
            import inputstreamhelper
            is_helper = inputstreamhelper.Helper('mpd', drm='com.widevine.alpha')
            if is_helper.check_inputstream():
                play_item.setProperty('inputstream.adaptive.license_type', 'com.widevine.alpha')
                play_item.setProperty('inputstream.adaptive.license_key', stream.license_key)

    subtitles_visible = get_setting_bool('showsubtitles', default=True)
    # Separate subtitle url for hls-streams
    if subtitles_visible and stream.subtitle_url is not None:
        log(2, 'Subtitle URL: {url}', url=unquote(stream.subtitle_url))
        play_item.setSubtitles([stream.subtitle_url])

    log(1, 'Play: {url}', url=unquote(stream.stream_url))
    xbmcplugin.setResolvedUrl(plugin.handle, bool(stream.stream_url), listitem=play_item)

    while not xbmc.Player().isPlaying() and not xbmc.Monitor().abortRequested():
        xbmc.sleep(100)
    xbmc.Player().showSubtitles(subtitles_visible)


def get_search_string(search_string=None):
    """Ask the user for a search string"""
    keyboard = xbmc.Keyboard(search_string, localize(30134))
    keyboard.doModal()
    if keyboard.isConfirmed():
        search_string = to_unicode(keyboard.getText())
    return search_string


def ok_dialog(heading='', message=''):
    """Show Kodi's OK dialog"""
    from xbmcgui import Dialog
    if not heading:
        heading = addon_name()
    if kodi_version_major() < 19:
        return Dialog().ok(heading=heading, line1=message)
    return Dialog().ok(heading=heading, message=message)


def notification(heading='', message='', icon='info', time=4000):
    """Show a Kodi notification"""
    from xbmcgui import Dialog
    if not heading:
        heading = addon_name()
    if not icon:
        icon = addon_icon()
    Dialog().notification(heading=heading, message=message, icon=icon, time=time)


def multiselect(heading='', options=None, autoclose=0, preselect=None, use_details=False):
    """Show a Kodi multi-select dialog"""
    from xbmcgui import Dialog
    if not heading:
        heading = addon_name()
    return Dialog().multiselect(heading=heading, options=options, autoclose=autoclose, preselect=preselect, useDetails=use_details)


def set_locale():
    """Load the proper locale for date strings, only once"""
    if hasattr(set_locale, 'cached'):
        return getattr(set_locale, 'cached')
    from locale import Error, LC_ALL, setlocale
    locale_lang = get_global_setting('locale.language').split('.')[-1]
    locale_lang = locale_lang[:-2] + locale_lang[-2:].upper()
    # NOTE: setlocale() only works if the platform supports the Kodi configured locale
    try:
        setlocale(LC_ALL, locale_lang)
    except (Error, ValueError) as exc:
        if locale_lang != 'en_GB':
            log(3, "Your system does not support locale '{locale}': {error}", locale=locale_lang, error=exc)
            set_locale.cached = False
            return False
    set_locale.cached = True
    return True


def localize(string_id, **kwargs):
    """Return the translated string from the .po language files, optionally translating variables"""
    if kwargs:
        from string import Formatter
        return Formatter().vformat(ADDON.getLocalizedString(string_id), (), SafeDict(**kwargs))
    return ADDON.getLocalizedString(string_id)


def localize_time(time):
    """Localize time format"""
    time_format = xbmc.getRegion('time')

    # Fix a bug in Kodi v18.5 and older causing double hours
    # https://github.com/xbmc/xbmc/pull/17380
    time_format = time_format.replace('%H%H:', '%H:')

    # Strip off seconds
    time_format = time_format.replace(':%S', '')

    return time.strftime(time_format)


def localize_date(date, strftime):
    """Return a localized date, even if the system does not support your locale"""
    has_locale = set_locale()
    # When locale is supported, return original format
    if has_locale:
        return date.strftime(strftime)
    # When locale is unsupported, translate weekday and month
    if '%A' in strftime:
        strftime = strftime.replace('%A', WEEKDAY_LONG[date.strftime('%w')])
    elif '%a' in strftime:
        strftime = strftime.replace('%a', WEEKDAY_SHORT[date.strftime('%w')])
    if '%B' in strftime:
        strftime = strftime.replace('%B', MONTH_LONG[date.strftime('%m')])
    elif '%b' in strftime:
        strftime = strftime.replace('%b', MONTH_SHORT[date.strftime('%m')])
    return date.strftime(strftime)


def localize_datelong(date):
    """Return a localized long date string"""
    return localize_date(date, xbmc.getRegion('datelong'))


def localize_from_data(name, data):
    """Return a localized name string from a Dutch data object"""
    # Return if Kodi language is Dutch
    if get_global_setting('locale.language') == 'resource.language.nl_nl':
        return name
    return next((localize(item.get('msgctxt')) for item in data if item.get('name') == name), name)


def get_setting(key, default=None):
    """Get an add-on setting as string"""
    try:
        value = to_unicode(ADDON.getSetting(key))
    except RuntimeError:  # Occurs when the add-on is disabled
        return default
    if value == '' and default is not None:
        return default
    return value


def get_setting_bool(key, default=None):
    """Get an add-on setting as boolean"""
    try:
        return ADDON.getSettingBool(key)
    except (AttributeError, TypeError):  # On Krypton or older, or when not a boolean
        value = get_setting(key, default)
        if value not in ('false', 'true'):
            return default
        return bool(value == 'true')
    except RuntimeError:  # Occurs when the add-on is disabled
        return default


def get_setting_int(key, default=None):
    """Get an add-on setting as integer"""
    try:
        return ADDON.getSettingInt(key)
    except (AttributeError, TypeError):  # On Krypton or older, or when not an integer
        value = get_setting(key, default)
        try:
            return int(value)
        except ValueError:
            return default
    except RuntimeError:  # Occurs when the add-on is disabled
        return default


def get_setting_float(key, default=None):
    """Get an add-on setting"""
    try:
        return ADDON.getSettingNumber(key)
    except (AttributeError, TypeError):  # On Krypton or older, or when not a float
        value = get_setting(key, default)
        try:
            return float(value)
        except ValueError:
            return default
    except RuntimeError:  # Occurs when the add-on is disabled
        return default


def set_setting(key, value):
    """Set an add-on setting"""
    return ADDON.setSetting(key, from_unicode(str(value)))


def set_setting_bool(key, value):
    """Set an add-on setting as boolean"""
    try:
        return ADDON.setSettingBool(key, value)
    except (AttributeError, TypeError):  # On Krypton or older, or when not a boolean
        if value in ['false', 'true']:
            return set_setting(key, value)
        if value:
            return set_setting(key, 'true')
        return set_setting(key, 'false')


def set_setting_int(key, value):
    """Set an add-on setting as integer"""
    try:
        return ADDON.setSettingInt(key, value)
    except (AttributeError, TypeError):  # On Krypton or older, or when not an integer
        return set_setting(key, value)


def set_setting_float(key, value):
    """Set an add-on setting"""
    try:
        return ADDON.setSettingNumber(key, value)
    except (AttributeError, TypeError):  # On Krypton or older, or when not a float
        return set_setting(key, value)


def open_settings():
    """Open the add-in settings window, shows Credentials"""
    ADDON.openSettings()


def get_global_setting(key):
    """Get a Kodi setting"""
    result = jsonrpc(method='Settings.GetSettingValue', params=dict(setting=key))
    return result.get('result', {}).get('value')


def get_advanced_setting(key, default=None):
    """Get a setting from advancedsettings.xml"""
    as_path = xbmc.translatePath('special://masterprofile/advancedsettings.xml')
    if not exists(as_path):
        return default
    from xml.etree.ElementTree import parse, ParseError
    try:
        as_root = parse(as_path).getroot()
    except ParseError:
        return default
    value = as_root.find(key)
    if value is not None:
        if value.text is None:
            return default
        return value.text
    return default


def get_advanced_setting_int(key, default=0):
    """Get a setting from advancedsettings.xml as an integer"""
    if not isinstance(default, int):
        default = 0
    setting = get_advanced_setting(key, default)
    if not isinstance(setting, int):
        setting = int(setting.strip()) if setting.strip().isdigit() else default
    return setting


def get_property(key, default=None, window_id=10000):
    """Get a Window property"""
    from xbmcgui import Window
    value = to_unicode(Window(window_id).getProperty(key))
    if value == '' and default is not None:
        return default
    return value


def set_property(key, value, window_id=10000):
    """Set a Window property"""
    from xbmcgui import Window
    return Window(window_id).setProperty(key, from_unicode(value))


def clear_property(key, window_id=10000):
    """Clear a Window property"""
    from xbmcgui import Window
    return Window(window_id).clearProperty(key)


def notify(sender, message, data):
    """Send a notification to Kodi using JSON RPC"""
    result = jsonrpc(method='JSONRPC.NotifyAll', params=dict(
        sender=sender,
        message=message,
        data=data,
    ))
    if result.get('result') != 'OK':
        log_error('Failed to send notification: {error}', error=result.get('error').get('message'))
        return False
    log(2, 'Succesfully sent notification')
    return True


def get_playerid():
    """Get current playerid"""
    result = dict()
    while not result.get('result'):
        result = jsonrpc(method='Player.GetActivePlayers')
    return result.get('result', [{}])[0].get('playerid')


def get_max_bandwidth():
    """Get the max bandwidth based on Kodi and add-on settings"""
    vrtnu_max_bandwidth = get_setting_int('max_bandwidth', default=0)
    global_max_bandwidth = int(get_global_setting('network.bandwidth'))
    if vrtnu_max_bandwidth != 0 and global_max_bandwidth != 0:
        return min(vrtnu_max_bandwidth, global_max_bandwidth)
    if vrtnu_max_bandwidth != 0:
        return vrtnu_max_bandwidth
    if global_max_bandwidth != 0:
        return global_max_bandwidth
    return 0


def has_socks():
    """Test if socks is installed, and use a static variable to remember"""
    if hasattr(has_socks, 'cached'):
        return getattr(has_socks, 'cached')
    try:
        import socks  # noqa: F401; pylint: disable=unused-variable,unused-import
    except ImportError:
        has_socks.cached = False
        return None  # Detect if this is the first run
    has_socks.cached = True
    return True


def get_proxies():
    """Return a usable proxies dictionary from Kodi proxy settings"""
    usehttpproxy = get_global_setting('network.usehttpproxy')
    if usehttpproxy is not True:
        return None

    try:
        httpproxytype = int(get_global_setting('network.httpproxytype'))
    except ValueError:
        httpproxytype = 0

    socks_supported = has_socks()
    if httpproxytype != 0 and not socks_supported:
        # Only open the dialog the first time (to avoid multiple popups)
        if socks_supported is None:
            ok_dialog('', localize(30966))  # Requires PySocks
        return None

    proxy_types = ['http', 'socks4', 'socks4a', 'socks5', 'socks5h']

    proxy = dict(
        scheme=proxy_types[httpproxytype] if 0 <= httpproxytype < 5 else 'http',
        server=get_global_setting('network.httpproxyserver'),
        port=get_global_setting('network.httpproxyport'),
        username=get_global_setting('network.httpproxyusername'),
        password=get_global_setting('network.httpproxypassword'),
    )

    if proxy.get('username') and proxy.get('password') and proxy.get('server') and proxy.get('port'):
        proxy_address = '{scheme}://{username}:{password}@{server}:{port}'.format(**proxy)
    elif proxy.get('username') and proxy.get('server') and proxy.get('port'):
        proxy_address = '{scheme}://{username}@{server}:{port}'.format(**proxy)
    elif proxy.get('server') and proxy.get('port'):
        proxy_address = '{scheme}://{server}:{port}'.format(**proxy)
    elif proxy.get('server'):
        proxy_address = '{scheme}://{server}'.format(**proxy)
    else:
        return None

    return dict(http=proxy_address, https=proxy_address)


def get_cond_visibility(condition):
    """Test a condition in XBMC"""
    return xbmc.getCondVisibility(condition)


def has_inputstream_adaptive():
    """Whether InputStream Adaptive is installed and enabled in add-on settings"""
    return get_setting_bool('useinputstreamadaptive', default=True) and has_addon('inputstream.adaptive')


def has_addon(name):
    """Checks if add-on is installed and enabled"""
    if kodi_version_major() < 19:
        return xbmc.getCondVisibility('System.HasAddon(%s)' % name) == 1
    return xbmc.getCondVisibility('System.AddonIsEnabled(%s)' % name) == 1


def has_credentials():
    """Whether the add-on has credentials filled in"""
    return bool(get_setting('username') and get_setting('password'))


def kodi_version():
    """Returns full Kodi version as string"""
    return xbmc.getInfoLabel('System.BuildVersion').split(' ')[0]


def kodi_version_major():
    """Returns major Kodi version as integer"""
    return int(kodi_version().split('.')[0])


def can_play_drm():
    """Whether this Kodi can do DRM using InputStream Adaptive"""
    return get_setting_bool('usedrm', default=True) and get_setting_bool('useinputstreamadaptive', default=True) and supports_drm()


def supports_drm():
    """Whether this Kodi version supports DRM decryption using InputStream Adaptive"""
    return kodi_version_major() > 17


COLOUR_THEMES = dict(
    dark=dict(highlighted='yellow', availability='blue', geoblocked='red', greyedout='gray'),
    light=dict(highlighted='brown', availability='darkblue', geoblocked='darkred', greyedout='darkgray'),
    custom=dict(
        highlighted=get_setting('colour_highlighted'),
        availability=get_setting('colour_availability'),
        geoblocked=get_setting('colour_geoblocked'),
        greyedout=get_setting('colour_greyedout')
    )
)


def themecolour(kind):
    """Get current theme color by kind (highlighted, availability, geoblocked, greyedout)"""
    theme = get_setting('colour_theme', 'dark')
    color = COLOUR_THEMES.get(theme).get(kind, COLOUR_THEMES.get('dark').get(kind))
    return color


def colour(text):
    """Convert stub color bbcode into colors from the settings"""
    theme = get_setting('colour_theme', 'dark')
    text = text.format(**COLOUR_THEMES.get(theme))
    return text


def get_cache_path(cache_file, cache_dir=DEFAULT_CACHE_DIR):
    """Return a specified cache path"""
    import os
    cache_dir = get_cache_dir(cache_dir)
    return os.path.join(cache_dir, cache_file)


def get_cache_dir(cache_dir=DEFAULT_CACHE_DIR):
    """Create and return a specified cache directory"""
    import os
    cache_dir = os.path.join(addon_profile(), cache_dir, '')
    return cache_dir


def get_addon_info(key):
    """Return addon information"""
    return to_unicode(ADDON.getAddonInfo(key))


def listdir(path):
    """Return all files in a directory (using xbmcvfs)"""
    from xbmcvfs import listdir as vfslistdir
    return vfslistdir(path)


def mkdir(path):
    """Create a directory (using xbmcvfs)"""
    from xbmcvfs import mkdir as vfsmkdir
    log(3, "Create directory '{path}'.", path=path)
    return vfsmkdir(path)


def mkdirs(path):
    """Create directory including parents (using xbmcvfs)"""
    from xbmcvfs import mkdirs as vfsmkdirs
    log(3, "Recursively create directory '{path}'.", path=path)
    return vfsmkdirs(path)


def exists(path):
    """Whether the path exists (using xbmcvfs)"""
    from xbmcvfs import exists as vfsexists
    return vfsexists(path)


@contextmanager
def open_file(path, flags='r'):
    """Open a file (using xbmcvfs)"""
    from xbmcvfs import File
    fdesc = File(path, flags)
    yield fdesc
    fdesc.close()


def stat_file(path):
    """Return information about a file (using xbmcvfs)"""
    from xbmcvfs import Stat
    return Stat(path)


def delete(path):
    """Remove a file (using xbmcvfs)"""
    from xbmcvfs import delete as vfsdelete
    log(3, "Delete file '{path}'.", path=path)
    return vfsdelete(path)


def delete_cached_thumbnail(url):
    """Remove a cached thumbnail from Kodi in an attempt to get a realtime live screenshot"""
    # Get texture
    result = jsonrpc(method='Textures.GetTextures', params=dict(
        filter=dict(
            field='url',
            operator='is',
            value=url,
        ),
    ))
    if result.get('result', {}).get('textures') is None:
        log_error('URL {url} not found in texture cache', url=url)
        return False

    texture_id = next((texture.get('textureid') for texture in result.get('result').get('textures')), None)
    if not texture_id:
        log_error('URL {url} not found in texture cache', url=url)
        return False
    log(2, 'found texture_id {id} for url {url} in texture cache', id=texture_id, url=url)

    # Remove texture
    result = jsonrpc(method='Textures.RemoveTexture', params=dict(textureid=texture_id))
    if result.get('result') != 'OK':
        log_error('failed to remove {url} from texture cache: {error}', url=url, error=result.get('error', {}).get('message'))
        return False

    log(2, 'succesfully removed {url} from texture cache', url=url)
    return True


def input_down():
    """Move the cursor down"""
    jsonrpc(method='Input.Down')


def current_container_url():
    """Get current container plugin:// url"""
    url = xbmc.getInfoLabel('Container.FolderPath')
    if url == '':
        return None
    return url


def container_refresh(url=None):
    """Refresh the current container or (re)load a container by URL"""
    if url:
        log(3, 'Execute: Container.Refresh({url})', url=url)
        xbmc.executebuiltin('Container.Refresh({url})'.format(url=url))
    else:
        log(3, 'Execute: Container.Refresh')
        xbmc.executebuiltin('Container.Refresh')


def container_update(url):
    """Update the current container while respecting the path history."""
    if url:
        log(3, 'Execute: Container.Update({url})', url=url)
        xbmc.executebuiltin('Container.Update({url})'.format(url=url))
    else:
        # URL is a mandatory argument for Container.Update, use Container.Refresh instead
        container_refresh()


def container_reload(url=None):
    """Only update container if the play action was initiated from it"""
    if url is None:
        url = get_property('container.url')
    if current_container_url() != url:
        return
    container_update(url)


def execute_builtin(builtin):
    """Run an internal Kodi builtin"""
    xbmc.executebuiltin(builtin)


def end_of_directory():
    """Close a virtual directory, required to avoid a waiting Kodi"""
    from addon import plugin
    xbmcplugin.endOfDirectory(handle=plugin.handle, succeeded=False, updateListing=False, cacheToDisc=False)


def wait_for_resumepoints():
    """Wait for resumepoints to be updated"""
    update = get_property('vrtnu_resumepoints')
    if update == 'busy':
        import time
        timeout = time.time() + 5  # 5 seconds timeout
        log(3, 'Resumepoint update is busy, wait')
        while update != 'ready':
            if time.time() > timeout:  # Exit loop in case something goes wrong
                break
            xbmc.sleep(50)
            update = get_property('vrtnu_resumepoints')
        set_property('vrtnu_resumepoints', None)
        log(3, 'Resumepoint update is ready, continue')
        return True
    return False


def log(level=1, message='', **kwargs):
    """Log info messages to Kodi"""
    debug_logging = get_global_setting('debug.showloginfo')  # Returns a boolean
    max_log_level = get_setting_int('max_log_level', default=0)
    if not debug_logging and not (level <= max_log_level and max_log_level != 0):
        return
    if kwargs:
        from string import Formatter
        message = Formatter().vformat(message, (), SafeDict(**kwargs))
    message = '[{addon}] {message}'.format(addon=addon_id(), message=message)
    xbmc.log(from_unicode(message), level % 3 if debug_logging else 2)


def log_access(argv):
    """Log addon access"""
    log(1, 'Access: {url}{qs}', url=argv[0], qs=argv[2] if len(argv) > 2 else '')


def log_error(message, **kwargs):
    """Log error messages to Kodi"""
    if kwargs:
        from string import Formatter
        message = Formatter().vformat(message, (), SafeDict(**kwargs))
    message = '[{addon}] {message}'.format(addon=addon_id(), message=message)
    xbmc.log(from_unicode(message), 4)


def jsonrpc(*args, **kwargs):
    """Perform JSONRPC calls"""
    from json import dumps, loads

    # We do not accept both args and kwargs
    if args and kwargs:
        log_error('Wrong use of jsonrpc()')
        return None

    # Process a list of actions
    if args:
        for (idx, cmd) in enumerate(args):
            if cmd.get('id') is None:
                cmd.update(id=idx)
            if cmd.get('jsonrpc') is None:
                cmd.update(jsonrpc='2.0')
        return loads(xbmc.executeJSONRPC(dumps(args)))

    # Process a single action
    if kwargs.get('id') is None:
        kwargs.update(id=0)
    if kwargs.get('jsonrpc') is None:
        kwargs.update(jsonrpc='2.0')
    return loads(xbmc.executeJSONRPC(dumps(kwargs)))


def human_delta(seconds):
    """Return a human-readable representation of the TTL"""
    from math import floor
    days = int(floor(seconds / (24 * 60 * 60)))
    seconds = seconds % (24 * 60 * 60)
    hours = int(floor(seconds / (60 * 60)))
    seconds = seconds % (60 * 60)
    if days:
        return '%d day%s and %d hour%s' % (days, 's' if days != 1 else '', hours, 's' if hours != 1 else '')
    minutes = int(floor(seconds / 60))
    seconds = seconds % 60
    if hours:
        return '%d hour%s and %d minute%s' % (hours, 's' if hours != 1 else '', minutes, 's' if minutes != 1 else '')
    if minutes:
        return '%d minute%s and %d second%s' % (minutes, 's' if minutes != 1 else '', seconds, 's' if seconds != 1 else '')
    return '%d second%s' % (seconds, 's' if seconds != 1 else '')


def get_cache(cache_file, ttl=None, cache_dir=DEFAULT_CACHE_DIR):  # pylint: disable=redefined-outer-name
    """Get the content from cache, if it is still fresh"""
    if not get_setting_bool('usehttpcaching', default=True):
        return None

    fullpath = get_cache_path(cache_file, cache_dir)
    if not exists(fullpath):
        return None

    if ttl is not None:
        from time import localtime, mktime
        mtime = stat_file(fullpath).st_mtime()
        now = mktime(localtime())
        if now >= mtime + ttl:
            return None

#    if ttl is None:
#        log(3, "Cache '{path}' is forced from cache.", path=path)
#    else:
#        log(3, "Cache '{path}' is fresh, expires in {time}.", path=path, time=human_delta(mtime + ttl - now))
    with open_file(fullpath, 'r') as fdesc:
        json = get_json_data(fdesc)

    if json is None:
        return None

    if ttl is None and isinstance(json, dict):
        expiration_date = json.get('expirationDate', None)
        if expiration_date:
            from datetime import datetime
            import dateutil.parser
            import dateutil.tz
            now = datetime.now(dateutil.tz.tzlocal())
            exp = dateutil.parser.parse(expiration_date)
            if exp <= now:
                log(2, "Cache expired: '{path}'", path=fullpath)
                return None
    log(2, "Got item from cache '{path}'", path=fullpath)
    return json


def update_cache(cache_file, data, cache_dir=DEFAULT_CACHE_DIR):
    """Update the cache, if necessary"""
    if not get_setting_bool('usehttpcaching', default=True):
        return

    fullpath = get_cache_path(cache_file, cache_dir)
    if not exists(fullpath):
        # Create cache directory if missing
        directory = get_cache_dir(cache_dir)
        if not exists(directory):
            mkdirs(directory)
        write_cache(fullpath, data)
        return

    with open_file(fullpath, 'r') as fdesc:
        cache = fdesc.read()

    # Avoid writes if possible (i.e. SD cards)
    if cache == data:
        update_timestamp(fullpath)
        return

    write_cache(fullpath, data)


def write_cache(fullpath, data):
    """Write data to cache"""
    log(3, "Write cache '{path}'.", path=fullpath)
    with open_file(fullpath, 'w') as fdesc:
        fdesc.write(data)


def update_timestamp(fullpath):
    """Update a file's timestamp"""
    from os import utime
    log(3, "Cache '{path}' has not changed, updating mtime only.", path=fullpath)
    utime(fullpath, None)


def ttl(kind='direct'):
    """Return the HTTP cache ttl in seconds based on kind of relation"""
    if kind == 'direct':
        return get_setting_int('httpcachettldirect', default=5) * 60
    if kind == 'indirect':
        return get_setting_int('httpcachettlindirect', default=60) * 60
    return 5 * 60


def get_json_data(response, fail=None):
    """Return json object from HTTP response"""
    from json import load, loads
    try:
        if (3, 0, 0) <= version_info < (3, 6, 0):  # the JSON object must be str, not 'bytes'
            return loads(to_unicode(response.read()))
        return load(response)
    except TypeError as exc:  # 'NoneType' object is not callable
        log_error('JSON TypeError: {exc}', exc=exc)
        return fail
    except ValueError as exc:  # No JSON object could be decoded
        log_error('JSON ValueError: {exc}', exc=exc)
        return fail


def get_url_json(url, cache=None, headers=None, data=None, fail=None):
    """Return HTTP data"""
    try:  # Python 3
        from urllib.error import HTTPError
        from urllib.parse import unquote
        from urllib.request import urlopen, Request
    except ImportError:  # Python 2
        from urllib2 import HTTPError, unquote, urlopen, Request

    if headers is None:
        headers = dict()
    req = Request(url, headers=headers)

    if data is not None:
        log(2, 'URL post: {url}', url=unquote(url))
        log(2, 'URL post data: {data}', data=data)
        req.data = data
    else:
        log(2, 'URL get: {url}', url=unquote(url))
    try:
        json_data = get_json_data(urlopen(req), fail=fail)
    except HTTPError as exc:
        if hasattr(req, 'selector'):  # Python 3.4+
            url_length = len(req.selector)
        else:  # Python 2.7
            url_length = len(req.get_selector())
        if exc.code == 400 and 7600 <= url_length <= 8192:
            ok_dialog(heading='HTTP Error 400', message=localize(30967))
            log_error('HTTP Error 400: Probably exceeded maximum url length: '
                      'VRT Search API url has a length of {length} characters.', length=url_length)
            return fail
        if exc.code == 413 and url_length > 8192:
            ok_dialog(heading='HTTP Error 413', message=localize(30967))
            log_error('HTTP Error 413: Exceeded maximum url length: '
                      'VRT Search API url has a length of {length} characters.', length=url_length)
            return fail
        if exc.code == 431:
            ok_dialog(heading='HTTP Error 431', message=localize(30967))
            log_error('HTTP Error 431: Request header fields too large: '
                      'VRT Search API url has a length of {length} characters.', length=url_length)
            return fail
        json_data = get_json_data(exc, fail=fail)
        if json_data is None:
            raise
    else:
        if cache:
            from json import dumps
            update_cache(cache, dumps(json_data))
    return json_data


def delete_cache(cache_file, cache_dir=DEFAULT_CACHE_DIR):
    """Delete a cached file"""
    path = get_cache_path(cache_file, cache_dir)
    if exists(path):
        delete(path)


def get_cached_url_json(url, cache, headers=None, ttl=None, fail=None):  # pylint: disable=redefined-outer-name
    """Return data from cache, if any, else make an HTTP request"""
    # Get api data from cache if it is fresh
    json_data = get_cache(cache, ttl=ttl)
    if json_data is not None:
        return json_data
    return get_url_json(url, cache=cache, headers=headers, fail=fail)


def refresh_caches(cache_file=None):
    """Invalidate the needed caches and refresh container"""
    files = ['favorites.json', 'oneoff.json', 'resume_points.json']
    if cache_file and cache_file not in files:
        files.append(cache_file)
    invalidate_caches(*files)
    container_refresh()
    notification(message=localize(30981))


def invalidate_caches(*caches):
    """Invalidate multiple cache files"""
    import fnmatch
    _, files = listdir(get_cache_dir())
    # Invalidate caches related to menu list refreshes
    removes = set()
    for expr in caches:
        removes.update(fnmatch.filter(files, expr))
    for filename in removes:
        delete(get_cache_path(filename))